E-MailRelay
gmapfile.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2024 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gmapfile.cpp
19///
20
21#include "gdef.h"
22#include "gmapfile.h"
23#include "gstr.h"
24#include "gstringtoken.h"
25#include "gpath.h"
26#include "gfile.h"
27#include "gcodepage.h"
28#include "glog.h"
29#include "gassert.h"
30#include <algorithm> // std::find
31#include <iterator>
32#include <stdexcept>
33#include <array>
34
36= default;
37
38G::MapFile::MapFile( const Path & path , std::string_view kind ) :
39 m_path(path) ,
40 m_kind(sv_to_string(kind))
41{
42 if( !m_path.empty() )
43 readFromFile( path , kind , true ) ;
44}
45
46G::MapFile::MapFile( const Path & path , std::string_view kind , std::nothrow_t ) :
47 m_path(path) ,
48 m_kind(sv_to_string(kind))
49{
50 if( !m_path.empty() )
51 readFromFile( path , kind , false ) ;
52}
53
54G::MapFile::MapFile( std::istream & stream )
55{
56 readFromStream( stream ) ;
57}
58
60 m_map(map)
61{
62 if( !m_map.empty() )
63 m_keys.reserve( m_map.size() ) ;
64 for( auto & p : m_map )
65 m_keys.push_back( p.first ) ;
66}
67
68G::MapFile::MapFile( const OptionMap & map , std::string_view yes )
69{
70 for( auto p = map.begin() ; p != map.end() ; )
71 {
72 const std::string & key = (*p).first ;
73 if( !(*p).second.isOff() )
74 {
75 std::string value = (*p).second.isOn() ? sv_to_string(yes) : map.value(key) ;
76 add( key , value ) ;
77 }
78 while( p != map.end() && (*p).first == key ) // since we used OptionMap::value() to get them all
79 ++p ;
80 }
81}
82
83void G::MapFile::readFromFile( const Path & path , std::string_view kind , bool do_throw )
84{
85 std::ifstream stream ;
86 File::open( stream , path , File::Text() ) ;
87 if( !stream.good() )
88 {
89 if( !do_throw ) return ;
90 throw readError( path , kind ) ;
91 }
92
93 readFromStream( stream ) ;
94
95 if( stream.bad() && do_throw ) // eg. EISDIR
96 throw readError( path , sv_to_string(kind) ) ;
97}
98
99G::MapFile::List G::MapFile::readLines( const Path & path , std::string_view kind , bool do_throw ) const
100{
101 std::ifstream stream ;
102 File::open( stream , path , File::Text() ) ;
103 if( !stream.good() )
104 {
105 if( !do_throw ) return {} ;
106 throw readError( path , kind ) ;
107 }
108
109 List line_list ; // all lines including blanks and comments
110 std::string line ;
111 while( stream.good() && Str::readLine(stream,line) )
112 line_list.push_back( line ) ;
113 return line_list ;
114}
115
116void G::MapFile::readFromStream( std::istream & stream )
117{
118 std::string line ;
119 while( stream.good() )
120 {
121 Str::readLine( stream , line ) ;
122 Str::trimRight( line , std::string_view("\r",1U) ) ;
123 if( line.empty() )
124 continue ;
125 if( !stream )
126 break ;
127
128 if( !valued(line) )
129 continue ;
130
131 auto pair = split( line ) ;
132 if( !pair.first.empty() )
133 add( pair.first , pair.second ) ;
134 }
135}
136
137std::pair<std::string_view,std::string_view> G::MapFile::split( std::string_view line )
138{
139 StringTokenView t( line , std::string_view(" =\t",3U) ) ;
140 if( !t.valid() )
141 return {{},{}} ;
142
143 std::string_view key = t() ;
144 auto pos = line.find( key.data() , 0U , key.size() ) + key.size() ;
145 std::string_view value = Str::tailView( line , pos ) ;
146 value = Str::trimLeftView( value , std::string_view(" =\t",3U) ) ;
147 value = Str::trimRightView( value , Str::ws() ) ;
148
149 // strip simple quotes -- no escaping
150 if( value.size() >= 2U && value.at(0U) == '"' && value.at(value.size()-1U) == '"' )
151 value = value.substr( 1U , value.length() - 2U ) ;
152
153 return {key,value} ;
154}
155
156std::string G::MapFile::join( std::string_view key , std::string_view value )
157{
158 std::string line = sv_to_string(key).append(1U,' ').append(quote(sv_to_string(value))) ;
159 return Str::trimmed( line , Str::ws() ) ;
160}
161
162std::string G::MapFile::quote( const std::string & s )
163{
164 return s.find_first_of(" \t") == std::string::npos ? s : ("\""+s+"\"") ;
165}
166
167bool G::MapFile::valued( const std::string & line )
168{
169 auto pos_letter = line.find_first_not_of( " \t\r#" ) ;
170 auto pos_hash = line.find( '#' ) ;
171 if( pos_letter == std::string::npos )
172 return false ; // no value if just # and ws
173 else if( pos_hash == std::string::npos )
174 return true ; // value if only letters
175 else
176 return pos_hash >= pos_letter ; // value if comment comes later
177}
178
179bool G::MapFile::commentedOut( const std::string & line )
180{
181 auto pos_letter = line.find_first_not_of( " \t\r#" ) ;
182 auto pos_hash = line.find( '#' ) ;
183 if( pos_letter == std::string::npos )
184 return false ; // not commented-out if just # and ws
185 else if( pos_hash == std::string::npos )
186 return false ; // not commented-out if only letters
187 else
188 return pos_hash == 0U && pos_letter == 1U ; // commented-out if hash then letter
189}
190
191void G::MapFile::log( const std::string & prefix_in ) const
192{
193 std::string prefix = prefix_in.empty() ? std::string() : ( prefix_in + ": " ) ;
194 for( const auto & key : m_keys )
195 {
196 auto p = find( key ) ;
197 if( p == m_map.end() ) continue ;
198 std::string value = (*p).second ;
199 G_LOG( "MapFile::item: " << prefix << key << "=[" <<
200 ( Str::ifind(key,"password") == std::string::npos ?
201 Str::printable(value) :
202 std::string("<not-logged>")
203 ) << "]" ) ;
204 }
205}
206
207void G::MapFile::writeItem( std::ostream & stream , std::string_view key ) const
208{
209 auto p = find( key ) ;
210 if( p == m_map.end() )
211 writeItem( stream , key , {} ) ;
212 else
213 writeItem( stream , key , (*p).second ) ;
214}
215
216void G::MapFile::writeItem( std::ostream & stream , std::string_view key , std::string_view value )
217{
218 const char * qq = value.find(' ') == std::string::npos ? "" : "\"" ;
219 stream << key << "=" << qq << value << qq << "\n" ;
220}
221
222G::Path G::MapFile::editInto( const Path & path , bool make_backup , bool do_throw ) const
223{
224 // read the file
225 List lines = readLines( path , m_kind , do_throw ) ;
226 List old_lines ;
227 if( make_backup )
228 old_lines = lines ;
229
230 // mark lines that are values that we can change, including lines that
231 // might be commented-out values
232 std::for_each( lines.begin() , lines.end() , [](std::string & line_){
233 if( valued(line_) ) line_.insert(0U,1U,'\0') ;
234 else if( commentedOut(line_) ) line_.at(0U) = '\0';
235 } ) ;
236
237 // re-write lines that match each item in the map
238 for( const auto & map_item : m_map )
239 {
240 bool found = false ;
241 for( auto & line : lines )
242 {
243 if( !line.empty() && line[0] == '\0' )
244 {
245 auto pair = split( std::string_view(line).substr(1U) ) ;
246 if( !pair.first.empty() && pair.first == map_item.first )
247 {
248 line = join( pair.first , map_item.second ) ;
249 found = true ;
250 break ;
251 }
252 }
253 }
254 if( !found )
255 {
256 lines.push_back( join(map_item.first,map_item.second) ) ;
257 }
258 }
259
260 // comment-out lines we could have re-written but didn't
261 std::for_each( lines.begin() , lines.end() , [](std::string & line_){
262 if( !line_.empty() && line_[0U] == '\0' ) line_[0] = '#';} ) ;
263
264 // optionally make a backup if there have been changes
265 Path backup_path ;
266 if( make_backup && lines != old_lines )
267 backup_path = File::backup( path , std::nothrow ) ;
268
269 // write the lines back to the file
270 std::ofstream file_out ;
271 File::open( file_out , path , File::Text() ) ;
272 std::copy( lines.begin() , lines.end() , std::ostream_iterator<std::string>(file_out,"\n") ) ;
273 file_out.close() ;
274 if( file_out.fail() && do_throw )
275 throw writeError( path ) ;
276
277 return backup_path ;
278}
279
280bool G::MapFile::booleanValue( std::string_view key , bool default_ ) const
281{
282 auto p = find( key ) ;
283 if( p == m_map.end() )
284 {
285 return default_ ;
286 }
287 else if( (*p).second.empty() )
288 {
289 return true ;
290 }
291 else
292 {
293 return Str::isPositive( (*p).second ) ;
294 }
295}
296
297std::string G::MapFile::value( std::string_view key , std::string_view default_ ) const
298{
299 auto p = find( key ) ;
300 return ( p == m_map.end() || (*p).second.empty() ) ? sv_to_string(default_) : (*p).second ;
301}
302
303bool G::MapFile::valueContains( std::string_view key , std::string_view token , std::string_view default_ ) const
304{
305 const std::string s = value( key , default_ ) ;
306 const std::string_view sv = s ;
307 for( StringTokenView t(sv,",",1U) ; t ; ++t )
308 {
309 if( t() == token )
310 return true ;
311 }
312 return false ;
313}
314
315std::string G::MapFile::mandatoryValue( std::string_view key ) const
316{
317 if( find(key) == m_map.end() )
318 throw missingValueError( m_path , m_kind , sv_to_string(key) ) ;
319 return value( key ) ;
320}
321
322G::Path G::MapFile::expandedPathValue( std::string_view key , const Path & default_ ) const
323{
324 return toPath( expand(value(key,default_.str())) ) ;
325}
326
327G::Path G::MapFile::expandedPathValue( std::string_view key ) const
328{
329 return toPath( expand(mandatoryValue(key)) ) ;
330}
331
332G::Path G::MapFile::pathValue( std::string_view key , const Path & default_ ) const
333{
334 return toPath( value(key,default_.str()) ) ;
335}
336
337G::Path G::MapFile::pathValue( std::string_view key ) const
338{
339 return toPath( mandatoryValue(key) ) ;
340}
341
342G::Path G::MapFile::toPath( std::string_view path_in )
343{
344 // (temporary backwards compatibility in case the file is ansi-encoded)
345 Path path1( path_in ) ;
346 Path path2( CodePage::fromCodePageAnsi(path_in) ) ;
347 if( is_windows() && !File::isDirectory(path1,std::nothrow) && File::isDirectory(path2,std::nothrow) )
348 return path2 ;
349 return path1 ;
350}
351
352unsigned int G::MapFile::numericValue( std::string_view key , unsigned int default_ ) const
353{
354 return Str::toUInt( value(key,{}) , default_ ) ;
355}
356
357bool G::MapFile::remove( std::string_view key )
358{
359 auto p = find( key ) ;
360 if( p != m_map.end() )
361 {
362 m_map.erase( p ) ;
363 G_ASSERT( std::find(m_keys.begin(),m_keys.end(),key) != m_keys.end() ) ;
364 m_keys.erase( std::find(m_keys.begin(),m_keys.end(),key) ) ;
365 return true ;
366 }
367 else
368 {
369 return false ;
370 }
371}
372
373std::string G::MapFile::expand( std::string_view value_in ) const
374{
375 std::string value = sv_to_string(value_in) ;
376 expand_( value ) ;
377 return value ;
378}
379
380namespace G
381{
382 namespace MapFileImp
383 {
384 std::size_t find_single( std::string & s , char c , std::size_t start_pos )
385 {
386 std::array<char,2U> cc {{ c , '\0' }} ;
387 std::size_t pos = start_pos ;
388 for(;;)
389 {
390 pos = s.find( &cc[0] , pos ) ;
391 if( pos == std::string::npos )
392 {
393 break ; // not found
394 }
395 else if( (pos+1U) < s.length() && s.at(pos+1U) == c )
396 {
397 s.erase( pos , 1U ) ;
398 if( (pos+1U) == s.length() )
399 {
400 pos = std::string::npos ;
401 break ;
402 }
403 pos++ ;
404 }
405 else
406 {
407 break ; // found
408 }
409 }
410 return pos ;
411 }
412 }
413}
414
415bool G::MapFile::expand_( std::string & value ) const
416{
417 bool changed = false ;
418 std::size_t start = 0U ;
419 std::size_t end = 0U ;
420 std::size_t const npos = std::string::npos ;
421 while( end < value.length() )
422 {
423 start = MapFileImp::find_single( value , '%' , end ) ;
424 if( start == npos ) break ;
425 end = value.find( '%' , start+1U ) ;
426 if( end == npos ) break ;
427 end++ ;
428 std::string key = value.substr( start+1U , end-start-2U ) ;
429 auto p = find( key ) ;
430 if( p != m_map.end() )
431 {
432 std::size_t old = end - start ;
433 std::size_t new_ = (*p).second.length() ;
434 value.replace( start , old , (*p).second ) ;
435 end += new_ ;
436 end -= old ;
437 changed = true ;
438 }
439 }
440 return changed ;
441}
442
443void G::MapFile::add( std::string_view key , std::string_view value , bool clear )
444{
445 auto p = find( key ) ;
446 if( p == m_map.end() )
447 {
448 m_keys.push_back( sv_to_string(key) ) ;
449 m_map[sv_to_string(key)] = sv_to_string(value) ;
450 }
451 else if( clear )
452 {
453 m_map[sv_to_string(key)] = sv_to_string(value) ;
454 }
455 else
456 {
457 (*p).second.append(1U,',').append( value.data() , value.size() ) ;
458 }
459}
460
461bool G::MapFile::update( std::string_view key , std::string_view value )
462{
463 auto p = find( key ) ;
464 if( p == m_map.end() )
465 {
466 return false ;
467 }
468 else
469 {
470 (*p).second = sv_to_string(value) ;
471 return true ;
472 }
473}
474
475G::StringMap::iterator G::MapFile::find( std::string_view key )
476{
477 return m_map.find( sv_to_string(key) ) ; // or c++14 'generic associative lookup' of std::string_view
478}
479
480G::StringMap::const_iterator G::MapFile::find( std::string_view key ) const
481{
482 return m_map.find( sv_to_string(key) ) ; // or c++14 'generic associative lookup' of std::string_view
483}
484
485bool G::MapFile::contains( std::string_view key ) const
486{
487 return find( key ) != m_map.end() ;
488}
489
491{
492 return m_map ;
493}
494
496{
497 return m_keys ;
498}
499
500std::string G::MapFile::strkind( std::string_view kind )
501{
502 return kind.empty() ? std::string("map") : sv_to_string(kind) ;
503}
504
505std::string G::MapFile::strpath( const Path & path_in )
506{
507 return path_in.empty() ? std::string() : (" ["+path_in.str()+"]") ;
508}
509
510G::MapFile::Error G::MapFile::readError( const Path & path , std::string_view kind )
511{
512 std::string description = "cannot read " + strkind(kind) + " file" + strpath(path) ;
513 return Error( description ) ;
514}
515
516G::MapFile::Error G::MapFile::writeError( const Path & path , std::string_view kind )
517{
518 return Error( std::string("cannot create ").append(strkind(kind)).append(" file ").append(strpath(path)) ) ;
519}
520
521G::MapFile::Error G::MapFile::missingValueError( const Path & path , const std::string & kind ,
522 const std::string & key )
523{
524 return Error( std::string("no item [").append(key).append("] in ").append(strkind(kind)).append(" file ").append(strpath(path)) ) ;
525}
526
An overload discriminator for G::File::open().
Definition: gfile.h:64
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:56
static bool isDirectory(const Path &path, std::nothrow_t)
Returns true if the path exists() and is a directory.
Definition: gfile.cpp:179
static Path backup(const Path &from, std::nothrow_t)
Creates a backup copy of the given file in the same directory and with a lightly-mangled filename.
Definition: gfile.cpp:322
bool remove(std::string_view key)
Removes a value (if it exists).
Definition: gmapfile.cpp:357
unsigned int numericValue(std::string_view key, unsigned int default_) const
Returns a numeric value from the map.
Definition: gmapfile.cpp:352
Path expandedPathValue(std::string_view key) const
Returns a mandatory path value from the map with expand().
Definition: gmapfile.cpp:327
std::string value(std::string_view key, std::string_view default_={}) const
Returns a string value from the map.
Definition: gmapfile.cpp:297
bool valueContains(std::string_view key, std::string_view token, std::string_view default_={}) const
Returns true if value(key,default_) contains the given comma-separated token.
Definition: gmapfile.cpp:303
bool update(std::string_view key, std::string_view value)
Updates an existing value.
Definition: gmapfile.cpp:461
std::string expand(std::string_view value) const
Does one-pass variable substitution for the given string.
Definition: gmapfile.cpp:373
bool booleanValue(std::string_view key, bool default_) const
Returns a boolean value from the map.
Definition: gmapfile.cpp:280
const StringMap & map() const
Returns a reference to the internal map.
Definition: gmapfile.cpp:490
Path editInto(const Path &path, bool make_backup, bool do_throw=true) const
Edits an existing file so that its contents reflect this map.
Definition: gmapfile.cpp:222
void add(std::string_view key, std::string_view value, bool clear=false)
Adds or updates a single item in the map.
Definition: gmapfile.cpp:443
bool contains(std::string_view key) const
Returns true if the map contains the given key.
Definition: gmapfile.cpp:485
MapFile()
Constructor for an empty map.
const StringArray & keys() const
Returns a reference to the internal ordered list of keys.
Definition: gmapfile.cpp:495
void writeItem(std::ostream &, std::string_view key) const
Writes a single item from this map to the stream.
Definition: gmapfile.cpp:207
void log(const std::string &prefix={}) const
Logs the contents.
Definition: gmapfile.cpp:191
Path pathValue(std::string_view key) const
Returns a mandatory path value from the map.
Definition: gmapfile.cpp:337
A multimap-like container for command-line options and their values.
Definition: goptionmap.h:44
A Path object represents a file system path.
Definition: gpath.h:82
std::string str() const
Returns the path string.
Definition: gpath.h:243
bool empty() const noexcept
Returns true if the path is empty.
Definition: gpath.h:237
static std::string & trimRight(std::string &s, std::string_view ws, std::size_t limit=0U)
Trims the rhs of s, taking off up to 'limit' of the 'ws' characters.
Definition: gstr.cpp:313
static std::string_view trimLeftView(std::string_view, std::string_view ws, std::size_t limit=0U) noexcept
Trims the lhs of s, taking off up to 'limit' of the 'ws' characters.
Definition: gstr.cpp:300
static std::string_view trimRightView(std::string_view sv, std::string_view ws, std::size_t limit=0U) noexcept
Trims the rhs of s, taking off up to 'limit' of the 'ws' characters.
Definition: gstr.cpp:325
static bool isPositive(std::string_view) noexcept
Returns true if the string has a positive meaning, such as "1", "true", "yes".
Definition: gstr.cpp:1376
static std::istream & readLine(std::istream &stream, std::string &result, std::string_view eol={}, bool pre_erase_result=true, std::size_t limit=0U)
Reads a line from the stream using the given line terminator, which may be multi-character.
Definition: gstr.cpp:958
static std::string printable(const std::string &in, char escape='\\')
Returns a printable representation of the given input string, using chacter code ranges 0x20 to 0x7e ...
Definition: gstr.cpp:913
static std::size_t ifind(std::string_view s, std::string_view key)
Returns the position of the key in 's' using a seven-bit case-insensitive search.
Definition: gstr.cpp:1433
static std::string_view tailView(std::string_view in, std::size_t pos, std::string_view default_={}) noexcept
Like tail() but returning a view into the input string.
Definition: gstr.cpp:1337
static unsigned int toUInt(std::string_view s)
Converts string 's' to an unsigned int.
Definition: gstr.cpp:648
static std::string_view ws() noexcept
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1265
static std::string trimmed(const std::string &s, std::string_view ws)
Returns a trim()med version of s.
Definition: gstr.cpp:343
A zero-copy string token iterator where the token separators are runs of whitespace characters,...
Definition: gstringtoken.h:54
std::string fromCodePageAnsi(std::string_view)
Converts from the active OEM codepage (see GetACP(), 1252 on unix) to UTF-8.
Definition: gcodepage.cpp:257
Low-level classes.
Definition: garg.h:36
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstringarray.h:30
std::map< std::string, std::string > StringMap
A std::map of std::strings.
Definition: gstringmap.h:30
Exception class for G::MapFile.
Definition: gmapfile.h:63