E-MailRelay
gmapfile.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2023 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 "gprocess.h"
27#include "gdatetime.h"
28#include "gdate.h"
29#include "gtime.h"
30#include "gfile.h"
31#include "glog.h"
32#include "gassert.h"
33#include <algorithm> // std::find
34#include <iterator>
35#include <stdexcept>
36#include <array>
37
39= default;
40
41G::MapFile::MapFile( const Path & path , string_view kind ) :
42 m_kind(sv_to_string(kind))
43{
44 if( !path.empty() )
45 {
46 m_path = path ;
47 readFrom( path , kind ) ;
48 }
49}
50
51G::MapFile::MapFile( std::istream & stream )
52{
53 readFrom( stream ) ;
54}
55
57 m_map(map)
58{
59 m_keys.reserve( m_map.size() ) ;
60 for( auto & p : m_map )
61 m_keys.push_back( p.first ) ;
62}
63
65{
66 for( auto p = map.begin() ; p != map.end() ; )
67 {
68 const std::string & key = (*p).first ;
69 if( !(*p).second.isOff() )
70 {
71 std::string value = (*p).second.isOn() ? sv_to_string(yes) : map.value(key) ;
72 add( key , value ) ;
73 }
74 while( p != map.end() && (*p).first == key ) // since we used OptionMap::value() to get them all
75 ++p ;
76 }
77}
78
79void G::MapFile::readFrom( const Path & path , string_view kind )
80{
81 std::ifstream stream ;
82 File::open( stream , path , File::Text() ) ;
83 if( !stream.good() )
84 throw readError( path , sv_to_string(kind) ) ;
85 G_LOG( "MapFile::read: reading [" << path.str() << "]" ) ;
86 readFrom( stream ) ;
87 if( stream.bad() ) // eg. EISDIR
88 throw readError( path , sv_to_string(kind) ) ;
89}
90
91void G::MapFile::readFrom( std::istream & stream )
92{
93 std::string line ;
94 while( stream.good() )
95 {
96 Str::readLine( stream , line ) ;
97 Str::trimRight( line , "\r"_sv ) ;
98 if( line.empty() )
99 continue ;
100 if( !stream )
101 break ;
102 if( ignore(line) )
103 continue ;
104
105 // no escaping here -- just strip quotes if the value starts and ends with them
106
107 G::string_view line_sv( line ) ;
108 StringTokenView t( line_sv , " =\t"_sv ) ;
109 if( !t.valid() )
110 continue ;
111
112 string_view key = t() ;
113 auto pos = line.find( key.data() , 0U , key.size() ) + key.size() ;
114 string_view value = Str::tailView( line , pos ) ;
115 value = Str::trimLeftView( value , " =\t"_sv ) ;
116 value = Str::trimRightView( value , Str::ws() ) ;
117 if( value.size() >= 2U && value.at(0U) == '"' && value.at(value.size()-1U) == '"' )
118 value = value.substr(1U,value.length()-2U) ;
119
120 add( key , value ) ;
121 }
122}
123
124bool G::MapFile::ignore( const std::string & line ) const
125{
126 std::string::size_type pos_interesting = line.find_first_not_of(" \t\r#") ;
127 if( pos_interesting == std::string::npos )
128 return true ;
129
130 std::string::size_type pos_hash = line.find('#') ;
131 return pos_hash != std::string::npos && pos_hash < pos_interesting ;
132}
133
134void G::MapFile::check( const Path & path , string_view kind )
135{
136 MapFile tmp ;
137 tmp.readFrom( path , kind ) ;
138}
139
140void G::MapFile::log( const std::string & prefix_in ) const
141{
142 std::string prefix = prefix_in.empty() ? std::string() : ( prefix_in + ": " ) ;
143 for( const auto & key : m_keys )
144 {
145 auto p = find( key ) ;
146 if( p == m_map.end() ) continue ;
147 std::string value = (*p).second ;
148 G_LOG( "MapFile::item: " << prefix << key << "=[" <<
149 ( Str::ifind(key,"password") == std::string::npos ?
150 Str::printable(value) :
151 std::string("<not-logged>")
152 ) << "]" ) ;
153 }
154}
155
156void G::MapFile::writeItem( std::ostream & stream , string_view key ) const
157{
158 auto p = find( key ) ;
159 if( p == m_map.end() )
160 writeItem( stream , key , {} ) ;
161 else
162 writeItem( stream , key , (*p).second ) ;
163}
164
165void G::MapFile::writeItem( std::ostream & stream , string_view key , string_view value )
166{
167 const char * qq = value.find(' ') == std::string::npos ? "" : "\"" ;
168 stream << key << "=" << qq << value << qq << "\n" ;
169}
170
171std::string G::MapFile::quote( const std::string & s )
172{
173 return s.find_first_of(" \t") == std::string::npos ? s : ("\""+s+"\"") ;
174}
175
176void G::MapFile::editInto( const Path & path , bool make_backup ,
177 bool allow_read_error , bool allow_write_error ) const
178{
179 List lines = read( path , m_kind , allow_read_error ) ;
180 commentOut( lines ) ;
181 replace( lines ) ;
182 if( make_backup ) backup( path ) ;
183 save( path , lines , allow_write_error ) ;
184}
185
186G::MapFile::List G::MapFile::read( const Path & path , string_view kind , bool allow_read_error ) const
187{
188 List line_list ;
189 std::ifstream file_in ;
190 File::open( file_in , path , File::Text() ) ;
191 if( !file_in.good() && !allow_read_error )
192 throw readError( path , kind ) ;
193 while( file_in.good() )
194 {
195 std::string line = Str::readLineFrom( file_in ) ;
196 Str::trimRight( line , "\r"_sv ) ;
197 if( !file_in ) break ;
198 line_list.push_back( line ) ;
199 }
200 return line_list ;
201}
202
203void G::MapFile::commentOut( List & line_list ) const
204{
205 for( auto & line : line_list )
206 {
207 if( line.empty() || line.at(0U) == '#' )
208 continue ;
209 line.insert( 0U , 1U , '#' ) ;
210 }
211}
212
213void G::MapFile::replace( List & line_list ) const
214{
215 for( const auto & map_item : m_map )
216 {
217 bool found = false ;
218 for( auto & line : line_list )
219 {
220 if( line.empty() ) continue ;
221 G::string_view line_sv( line ) ;
222 StringTokenView t( line_sv , " \r\n\t=#"_sv ) ;
223 if( !t ) continue ;
224 if( G::Str::match( map_item.first , t() ) )
225 {
226 const std::string & value = map_item.second ;
227 line = Str::trimmed( std::string(map_item.first).append(1U,' ').append(quote(value)) , Str::ws() ) ;
228 found = true ;
229 break ;
230 }
231 }
232
233 if( !found )
234 {
235 const std::string & value = map_item.second ;
236 line_list.push_back( Str::trimmed( std::string(map_item.first).append(1U,' ').append(quote(value)) , Str::ws() ) ) ;
237 }
238 }
239}
240
241void G::MapFile::backup( const Path & path )
242{
243 // ignore errors
244 BrokenDownTime now = SystemTime::now().local() ;
245 std::string timestamp = Date(now).str(Date::Format::yyyy_mm_dd) + Time(now).hhmmss() ;
246 Path backup( path.dirname() , path.basename() + "." + timestamp ) ;
247 Process::Umask umask( Process::Umask::Mode::Tightest ) ;
248 File::copy( path , backup , std::nothrow ) ;
249}
250
251void G::MapFile::save( const Path & path , List & line_list , bool allow_write_error )
252{
253 std::ofstream file_out ;
254 File::open( file_out , path , File::Text() ) ;
255 std::copy( line_list.begin() , line_list.end() , std::ostream_iterator<std::string>(file_out,"\n") ) ;
256 file_out.close() ;
257 if( file_out.fail() && !allow_write_error )
258 throw writeError( path ) ;
259}
260
261bool G::MapFile::booleanValue( string_view key , bool default_ ) const
262{
263 auto p = find( key ) ;
264 if( p == m_map.end() )
265 {
266 return default_ ;
267 }
268 else if( (*p).second.empty() )
269 {
270 return true ;
271 }
272 else
273 {
274 return Str::isPositive( (*p).second ) ;
275 }
276}
277
278std::string G::MapFile::value( string_view key , string_view default_ ) const
279{
280 auto p = find( key ) ;
281 return ( p == m_map.end() || (*p).second.empty() ) ? sv_to_string(default_) : (*p).second ;
282}
283
284std::string G::MapFile::mandatoryValue( string_view key ) const
285{
286 if( find(key) == m_map.end() )
287 throw missingValueError( m_path , m_kind , sv_to_string(key) ) ;
288 return value( key ) ;
289}
290
292{
293 return { expand(value(key,default_.str())) } ;
294}
295
297{
298 return { expand(mandatoryValue(key)) } ;
299}
300
301G::Path G::MapFile::pathValue( string_view key , const Path & default_ ) const
302{
303 return { value(key,default_.str()) } ;
304}
305
307{
308 return { mandatoryValue(key) } ;
309}
310
311unsigned int G::MapFile::numericValue( string_view key , unsigned int default_ ) const
312{
313 return Str::toUInt( value(key,{}) , default_ ) ;
314}
315
317{
318 auto p = find( key ) ;
319 if( p != m_map.end() )
320 {
321 m_map.erase( p ) ;
322 G_ASSERT( std::find(m_keys.begin(),m_keys.end(),key) != m_keys.end() ) ;
323 m_keys.erase( std::find(m_keys.begin(),m_keys.end(),key) ) ;
324 }
325}
326
327std::string G::MapFile::expand( string_view value_in ) const
328{
329 std::string value = sv_to_string(value_in) ;
330 expand_( value ) ;
331 return value ;
332}
333
334namespace G
335{
336 namespace MapFileImp
337 {
338 std::size_t find_single( std::string & s , char c , std::size_t start_pos )
339 {
340 std::array<char,2U> cc {{ c , '\0' }} ;
341 std::size_t pos = start_pos ;
342 for(;;)
343 {
344 pos = s.find( &cc[0] , pos ) ;
345 if( pos == std::string::npos )
346 {
347 break ; // not found
348 }
349 else if( (pos+1U) < s.length() && s.at(pos+1U) == c )
350 {
351 s.erase( pos , 1U ) ;
352 if( (pos+1U) == s.length() )
353 {
354 pos = std::string::npos ;
355 break ;
356 }
357 pos++ ;
358 }
359 else
360 {
361 break ; // found
362 }
363 }
364 return pos ;
365 }
366 }
367}
368
369bool G::MapFile::expand_( std::string & value ) const
370{
371 bool changed = false ;
372 std::size_t start = 0U ;
373 std::size_t end = 0U ;
374 std::size_t const npos = std::string::npos ;
375 while( end < value.length() )
376 {
377 start = MapFileImp::find_single( value , '%' , end ) ;
378 if( start == npos ) break ;
379 end = value.find( '%' , start+1U ) ;
380 if( end == npos ) break ;
381 end++ ;
382 std::string key = value.substr( start+1U , end-start-2U ) ;
383 auto p = find( key ) ;
384 if( p != m_map.end() )
385 {
386 std::size_t old = end - start ;
387 std::size_t new_ = (*p).second.length() ;
388 value.replace( start , old , (*p).second ) ;
389 end += new_ ;
390 end -= old ;
391 changed = true ;
392 }
393 }
394 return changed ;
395}
396
397void G::MapFile::add( string_view key_in , string_view value , bool clear )
398{
399 std::string key = sv_to_string( key_in ) ;
400 if( find(key) == m_map.end() )
401 {
402 m_keys.push_back( key ) ;
403 m_map[key] = sv_to_string(value) ;
404 }
405 else if( clear )
406 {
407 m_map[key] = sv_to_string(value) ;
408 }
409 else
410 {
411 m_map[key].append( 1U , ',' ) ;
412 m_map[key].append( value.data() , value.size() ) ;
413 }
414}
415
416G::StringMap::iterator G::MapFile::find( string_view key )
417{
418 return m_map.find( sv_to_string(key) ) ; // or c++14 'generic associative lookup' of string_view
419}
420
421G::StringMap::const_iterator G::MapFile::find( string_view key ) const
422{
423 return m_map.find( sv_to_string(key) ) ; // or c++14 'generic associative lookup' of string_view
424}
425
427{
428 return find( key ) != m_map.end() ;
429}
430
432{
433 return m_map ;
434}
435
437{
438 return m_keys ;
439}
440
441std::string G::MapFile::ekind( string_view kind )
442{
443 return kind.empty() ? std::string("map") : sv_to_string(kind) ;
444}
445
446std::string G::MapFile::epath( const Path & path_in )
447{
448 return path_in.empty() ? std::string() : (" ["+path_in.str()+"]") ;
449}
450
451G::MapFile::Error G::MapFile::readError( const Path & path , string_view kind )
452{
453 std::string description = "cannot read " + ekind(kind) + " file" + epath(path) ;
454 return Error( description ) ;
455}
456
457G::MapFile::Error G::MapFile::writeError( const Path & path , string_view kind )
458{
459 return Error( std::string("cannot create ").append(ekind(kind)).append(" file ").append(epath(path)) ) ;
460}
461
462G::MapFile::Error G::MapFile::missingValueError( const Path & path , const std::string & kind ,
463 const std::string & key )
464{
465 return Error( std::string("no item [").append(key).append("] in ").append(ekind(kind)).append(" file ").append(epath(path)) ) ;
466}
467
std::string str(const char *fmt) const
Returns the formatted date, with the same restrictions as format().
Definition: gdatetime.cpp:259
An overload discriminator for G::File::open().
Definition: gfile.h:63
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:55
static bool copy(const Path &from, const Path &to, std::nothrow_t)
Copies a file. Returns false on error.
Definition: gfile.cpp:79
A class for reading, writing and editing key=value files, supporting variable expansion of percent-ke...
Definition: gmapfile.h:57
static void check(const Path &, string_view kind={})
Throws if the file is invalid.
Definition: gmapfile.cpp:134
void writeItem(std::ostream &, string_view key) const
Writes a single item from this map to the stream.
Definition: gmapfile.cpp:156
std::string value(string_view key, string_view default_={}) const
Returns a string value from the map.
Definition: gmapfile.cpp:278
bool booleanValue(string_view key, bool default_) const
Returns a boolean value from the map.
Definition: gmapfile.cpp:261
void editInto(const Path &path, bool make_backup, bool allow_read_error, bool allow_write_error) const
Edits an existing file so that its contents reflect this map.
Definition: gmapfile.cpp:176
void add(string_view key, string_view value, bool clear=false)
Adds or updates a single item in the map.
Definition: gmapfile.cpp:397
unsigned int numericValue(string_view key, unsigned int default_) const
Returns a numeric value from the map.
Definition: gmapfile.cpp:311
const StringMap & map() const
Returns a reference to the internal map.
Definition: gmapfile.cpp:431
bool contains(string_view key) const
Returns true if the map contains the given key.
Definition: gmapfile.cpp:426
std::string expand(string_view value) const
Does one-pass variable substitution for the given string.
Definition: gmapfile.cpp:327
Path expandedPathValue(string_view key) const
Returns a mandatory path value from the map with expand().
Definition: gmapfile.cpp:296
void remove(string_view key)
Removes a value (if it exists).
Definition: gmapfile.cpp:316
MapFile()
Constructor for an empty map.
Path pathValue(string_view key) const
Returns a mandatory path value from the map.
Definition: gmapfile.cpp:306
const StringArray & keys() const
Returns a reference to the internal ordered list of keys.
Definition: gmapfile.cpp:436
void log(const std::string &prefix={}) const
Logs the contents.
Definition: gmapfile.cpp:140
A multimap-like container for command-line options and their values.
Definition: goptionmap.h:43
A Path object represents a file system path.
Definition: gpath.h:73
std::string str() const
Returns the path string.
Definition: gpath.h:224
bool empty() const noexcept
Returns true if size() is zero.
Definition: gpath.h:212
static std::size_t ifind(string_view s, string_view key)
Returns the position of the key in 's' using a seven-bit case-insensitive search.
Definition: gstr.cpp:1436
static bool match(string_view, string_view) noexcept
Returns true if the two strings are the same.
Definition: gstr.cpp:1395
static string_view tailView(string_view in, std::size_t pos, string_view default_={}) noexcept
Like tail() but returning a view into the input string.
Definition: gstr.cpp:1340
static unsigned int toUInt(string_view s)
Converts string 's' to an unsigned int.
Definition: gstr.cpp:651
static std::istream & readLine(std::istream &stream, std::string &result, 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:961
static std::string readLineFrom(std::istream &stream, string_view eol={})
Reads a line from the stream using the given line terminator.
Definition: gstr.cpp:954
static std::string & trimRight(std::string &s, 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 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:916
static bool isPositive(string_view) noexcept
Returns true if the string has a positive meaning, such as "1", "true", "yes".
Definition: gstr.cpp:1379
static std::string trimmed(const std::string &s, string_view ws)
Returns a trim()med version of s.
Definition: gstr.cpp:343
static string_view trimLeftView(string_view, 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 string_view ws() noexcept
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1268
static string_view trimRightView(string_view sv, 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 SystemTime now()
Factory function for the current time.
Definition: gdatetime.cpp:328
BrokenDownTime local() const
Returns the locale-dependent local broken-down time.
Definition: gdatetime.cpp:356
A class like c++17's std::string_view.
Definition: gstringview.h:51
Low-level classes.
Definition: garg.h:30
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:60