WvStreams
uniinigen.cc
1/*
2 * Worldvisions Weaver Software:
3 * Copyright (C) 1997-2002 Net Integration Technologies, Inc.
4 *
5 * A generator for .ini files.
6 */
7#include "uniinigen.h"
8#include "strutils.h"
9#include "unitempgen.h"
10#include "wvfile.h"
11#include "wvmoniker.h"
12#include "wvstringmask.h"
13#include "wvtclstring.h"
14#include <ctype.h>
15#include "wvlinkerhack.h"
16
17WV_LINK(UniIniGen);
18
19
20static IUniConfGen *creator(WvStringParm s, IObject*)
21{
22 return new UniIniGen(s);
23}
24
25WvMoniker<IUniConfGen> UniIniGenMoniker("ini", creator);
26
27
28/***** UniIniGen *****/
29
30UniIniGen::UniIniGen(WvStringParm _filename, int _create_mode, UniIniGen::SaveCallback _save_cb)
31 : filename(_filename), create_mode(_create_mode), log(_filename), save_cb(_save_cb)
32{
33 // Create the root, since this generator can't handle it not existing.
34 UniTempGen::set(UniConfKey::EMPTY, WvString::empty);
35 memset(&old_st, 0, sizeof(old_st));
36}
37
38
39void UniIniGen::set(const UniConfKey &key, WvStringParm value)
40{
41 UniTempGen::set(key, value);
42
43 // Re-create the root, since this generator can't handle it not existing.
44 if (value.isnull() && key.isempty())
45 UniTempGen::set(UniConfKey::EMPTY, WvString::empty);
46
47}
48
49
50UniIniGen::~UniIniGen()
51{
52}
53
54
56{
57 WvFile file(filename, O_RDONLY);
58
59#ifndef _WIN32
60 struct stat statbuf;
61 if (file.isok() && fstat(file.getrfd(), &statbuf) == -1)
62 {
63 log(WvLog::Warning, "Can't stat '%s': %s\n",
64 filename, strerror(errno));
65 file.close();
66 }
67
68 if (file.isok() && (statbuf.st_mode & S_ISVTX))
69 {
70 file.close();
71 file.seterr(EAGAIN);
72 }
73
74 if (file.isok() // guarantes statbuf is valid from above
75 && statbuf.st_ctime == old_st.st_ctime
76 && statbuf.st_dev == old_st.st_dev
77 && statbuf.st_ino == old_st.st_ino
78 && statbuf.st_blocks == old_st.st_blocks
79 && statbuf.st_size == old_st.st_size)
80 {
81 log(WvLog::Debug3, "refresh: file hasn't changed; do nothing.\n");
82 return true;
83 }
84 memcpy(&old_st, &statbuf, sizeof(statbuf));
85#endif
86
87 if (!file.isok())
88 {
89 log(WvLog::Warning,
90 "Can't open '%s' for reading: %s\n"
91 "...starting with blank configuration.\n",
92 filename, file.errstr());
93 return false;
94 }
95
96 // loop over all Tcl words in the file
97 UniTempGen *newgen = new UniTempGen();
98 newgen->set(UniConfKey::EMPTY, WvString::empty);
99 UniConfKey section;
100 WvDynBuf buf;
101 while (buf.used() || file.isok())
102 {
103 if (file.isok())
104 {
105 // read entire lines to ensure that we get whole values
106 char *line = file.blocking_getline(-1);
107 if (line)
108 {
109 buf.putstr(line);
110 buf.put('\n'); // this was auto-stripped by getline()
111 }
112 }
113
114 WvString word;
115 while (!(word = wvtcl_getword(buf,
116 WVTCL_NASTY_NEWLINES,
117 false)).isnull())
118 {
119 //log(WvLog::Info, "LINE: '%s'\n", word);
120
121 char *str = trim_string(word.edit());
122 int len = strlen(str);
123 if (len == 0) continue; // blank line
124
125 if (str[0] == '#')
126 {
127 // a comment line. FIXME: we drop it completely!
128 //log(WvLog::Debug5, "Comment: \"%s\"\n", str + 1);
129 continue;
130 }
131
132 if (str[0] == '[' && str[len - 1] == ']')
133 {
134 // a section name
135 str[len - 1] = '\0';
136 WvString name(wvtcl_unescape(trim_string(str + 1)));
137 section = UniConfKey(name);
138 //log(WvLog::Debug5, "Refresh section: \"%s\"\n", section);
139 continue;
140 }
141
142 // we possibly have a key = value line
143 WvConstStringBuffer line(word);
144 static const WvStringMask nasty_equals("=");
145 WvString name = wvtcl_getword(line, nasty_equals, false);
146 if (!name.isnull() && line.used())
147 {
148 name = wvtcl_unescape(trim_string(name.edit()));
149
150 if (!!name)
151 {
152 UniConfKey key(name);
153 key.prepend(section);
154
155 WvString value = line.getstr();
156 assert(*value == '=');
157 value = wvtcl_unescape(trim_string(value.edit() + 1));
158 newgen->set(key, value.unique());
159
160 //log(WvLog::Debug5, "Refresh: (\"%s\", \"%s\")\n",
161 // key, value);
162 continue;
163 }
164 }
165
166 // if we get here, the line was tcl-decoded but not useful.
167 log(WvLog::Warning,
168 "Ignoring malformed input line: \"%s\"\n", word);
169 }
170
171 if (buf.used() && !file.isok())
172 {
173 // EOF and some of the data still hasn't been used. Weird.
174 // Let's remove a line of data and try again.
175 size_t offset = buf.strchr('\n');
176 assert(offset); // the last thing we put() is *always* a newline!
177 WvString line1(trim_string(buf.getstr(offset).edit()));
178 if (!!line1) // not just whitespace
179 log(WvLog::Warning,
180 "XXX Ignoring malformed input line: \"%s\"\n", line1);
181 }
182 }
183
184 if (file.geterr())
185 {
186 log(WvLog::Warning,
187 "Error reading from config file: %s\n", file.errstr());
188 WVRELEASE(newgen);
189 return false;
190 }
191
192 // switch the trees and send notifications
193 hold_delta();
194 UniConfValueTree *oldtree = root;
195 UniConfValueTree *newtree = newgen->root;
196 root = newtree;
197 newgen->root = NULL;
198 dirty = false;
199 oldtree->compare(newtree, wv::bind(&UniIniGen::refreshcomparator, this,
200 _1, _2));
201
202 delete oldtree;
203 unhold_delta();
204
205 WVRELEASE(newgen);
206
208 return true;
209}
210
211
212// returns: true if a==b
213bool UniIniGen::refreshcomparator(const UniConfValueTree *a,
214 const UniConfValueTree *b)
215{
216 if (a)
217 {
218 if (b)
219 {
220 if (a->value() != b->value())
221 {
222 // key changed
223 delta(b->fullkey(), b->value()); // CHANGED
224 return false;
225 }
226 return true;
227 }
228 else
229 {
230 // key removed
231 // Issue notifications for every that is missing.
232 a->visit(wv::bind(&UniIniGen::notify_deleted, this, _1, _2),
233 NULL, false, true);
234 return false;
235 }
236 }
237 else // a didn't exist
238 {
239 assert(b);
240 // key added
241 delta(b->fullkey(), b->value()); // ADDED
242 return false;
243 }
244}
245
246
247#ifndef _WIN32
248bool UniIniGen::commit_atomic(WvStringParm real_filename)
249{
250 struct stat statbuf;
251
252 if (lstat(real_filename, &statbuf) == -1)
253 {
254 if (errno != ENOENT)
255 return false;
256 }
257 else
258 if (!S_ISREG(statbuf.st_mode))
259 return false;
260
261 WvString tmp_filename("%s.tmp%s", real_filename, getpid());
262 WvFile file(tmp_filename, O_WRONLY|O_TRUNC|O_CREAT, 0000);
263
264 if (file.geterr())
265 {
266 log(WvLog::Warning, "Can't write '%s': %s\n",
267 tmp_filename, strerror(errno));
268 unlink(tmp_filename);
269 file.close();
270 return false;
271 }
272
273 save(file, *root); // write the changes out to our temp file
274
275 mode_t theumask = umask(0);
276 umask(theumask);
277 fchmod(file.getwfd(), create_mode & ~theumask);
278
279 file.close();
280
281 if (file.geterr() || rename(tmp_filename, real_filename) == -1)
282 {
283 log(WvLog::Warning, "Can't write '%s': %s\n",
284 filename, strerror(errno));
285 unlink(tmp_filename);
286 return false;
287 }
288
289 return true;
290}
291#endif
292
293
295{
296 if (!dirty)
297 return;
298
300
301#ifdef _WIN32
302 // Windows doesn't support all that fancy stuff, just open the
303 // file and be done with it
304 WvFile file(filename, O_WRONLY|O_TRUNC|O_CREAT, create_mode);
305 save(file, *root); // write the changes out to our file
306 file.close();
307 if (file.geterr())
308 {
309 log(WvLog::Warning, "Can't write '%s': %s\n",
310 filename, file.errstr());
311 return;
312 }
313#else
314 WvString real_filename(filename);
315 char resolved_path[PATH_MAX];
316
317 if (realpath(filename, resolved_path) != NULL)
318 real_filename = resolved_path;
319
320 if (!commit_atomic(real_filename))
321 {
322 WvFile file(real_filename, O_WRONLY|O_TRUNC|O_CREAT, create_mode);
323 struct stat statbuf;
324
325 if (fstat(file.getwfd(), &statbuf) == -1)
326 {
327 log(WvLog::Warning, "Can't write '%s' ('%s'): %s\n",
328 filename, real_filename, strerror(errno));
329 return;
330 }
331
332 fchmod(file.getwfd(), (statbuf.st_mode & 07777) | S_ISVTX);
333
334 save(file, *root);
335
336 if (!file.geterr())
337 {
338 /* We only reset the sticky bit if all went well, but before
339 * we close it, because we need the file descriptor. */
340 statbuf.st_mode = statbuf.st_mode & ~S_ISVTX;
341 fchmod(file.getwfd(), statbuf.st_mode & 07777);
342 }
343 else
344 log(WvLog::Warning, "Error writing '%s' ('%s'): %s\n",
345 filename, real_filename, file.errstr());
346 }
347#endif
348
349 dirty = false;
350}
351
352
353// may return false for strings that wvtcl_escape would escape anyway; this
354// may not escape tcl-invalid strings, but that's on purpose so we can keep
355// old-style .ini file compatibility (and wvtcl_getword() and friends can
356// still parse them anyway).
357static bool absolutely_needs_escape(WvStringParm s, const char *sepchars)
358{
359 const char *cptr;
360 int numbraces = 0;
361 bool inescape = false, inspace = false;
362
363 if (isspace((unsigned char)*s))
364 return true; // leading whitespace needs escaping
365
366 for (cptr = s; *cptr; cptr++)
367 {
368 if (inescape)
369 inescape = false; // fine
370 else if (!numbraces && strchr(sepchars, *cptr))
371 return true; // one of the magic characters, and not escaped
372 else if (*cptr == '\\')
373 inescape = true;
374 else if (*cptr == '{')
375 numbraces++;
376 else if (*cptr == '}')
377 numbraces--;
378
379 inspace = isspace((unsigned char)*cptr);
380
381 if (numbraces < 0) // yikes! mismatched braces will need some help.
382 return false;
383 }
384
385 if (inescape || inspace)
386 return true; // terminating backslash or whitespace... evil.
387
388 if (numbraces != 0)
389 return true; // uneven number of braces, can't be good
390
391 // otherwise, I guess we're safe.
392 return false;
393}
394
395
396static void printsection(WvStream &file, const UniConfKey &key, UniIniGen::SaveCallback save_cb)
397{
398 WvString s;
399 static const WvStringMask nasties("\r\n[]");
400
401 if (absolutely_needs_escape(key, "\r\n[]"))
402 s = wvtcl_escape(key, nasties);
403 else
404 s = key;
405 // broken up for optimization, no temp wvstring created
406 //file.print("\n[%s]\n", s);
407 file.print("\n[");
408 file.print(s);
409 file.print("]\n");
410
411 if (!!save_cb)
412 save_cb();
413}
414
415
416static void printkey(WvStream &file, const UniConfKey &_key,
417 WvStringParm _value, UniIniGen::SaveCallback save_cb)
418{
419 WvString key, value;
420 static const WvStringMask nasties("\r\n\t []=#");
421
422 if (absolutely_needs_escape(_key, "\r\n[]=#\""))
423 key = wvtcl_escape(_key, nasties);
424 else if (_key == "")
425 key = "/";
426 else
427 key = _key;
428
429 // value is more relaxed, since we don't use wvtcl_getword after we grab
430 // the "key=" part of each line
431 if (absolutely_needs_escape(_value, "\r\n"))
432 value = wvtcl_escape(_value, WVTCL_NASTY_SPACES);
433 else
434 value = _value;
435
436 // need to escape []#= in key only to distinguish a key/value
437 // pair from a section name or comment and to delimit the value
438 // broken up for optimization, no temp wvstring created
439 //file.print("%s = %s\n", key, value);
440 file.print(key);
441 file.print(" = ");
442 file.print(value);
443 file.print("\n");
444
445 if (!!save_cb)
446 save_cb();
447}
448
449
450static void save_sect(WvStream &file, UniConfValueTree &toplevel,
451 UniConfValueTree &sect, bool &printedsection,
452 bool recursive, UniIniGen::SaveCallback save_cb)
453{
454 UniConfValueTree::Iter it(sect);
455 for (it.rewind(); it.next(); )
456 {
457 UniConfValueTree &node = *it;
458
459 // FIXME: we never print empty-string ("") keys, for compatibility
460 // with WvConf. Example: set x/y = 1; delete x/y; now x = "", because
461 // it couldn't be NULL while x/y existed, and nobody auto-deleted it
462 // when x/y went away. Therefore we would try to write x = "" to the
463 // config file, but that's not what WvConf would do.
464 //
465 // The correct fix would be to auto-delete x if the only reason it
466 // exists is for x/y. But since that's hard, we'll just *never*
467 // write lines for "" entries. Icky, but it works.
468 if (!!node.value())// || !node.haschildren())
469 {
470 if (!printedsection)
471 {
472 printsection(file, toplevel.fullkey(), save_cb);
473 printedsection = true;
474 }
475 printkey(file, node.fullkey(&toplevel), node.value(), save_cb);
476 }
477
478 // print all children, if requested
479 if (recursive && node.haschildren())
480 save_sect(file, toplevel, node, printedsection, recursive, save_cb);
481 }
482}
483
484
485void UniIniGen::save(WvStream &file, UniConfValueTree &parent)
486{
487 // parent might be NULL, so it really should be a pointer, not
488 // a reference. Oh well...
489 if (!&parent) return;
490
491 if (parent.fullkey() == root->fullkey())
492 {
493 // the root itself is a special case, since it's not in a section,
494 // and it's never NULL (so we don't need to write it if it's just
495 // blank)
496 if (!!parent.value())
497 printkey(file, parent.key(), parent.value(), save_cb);
498 }
499
500 bool printedsection = false;
501
502 save_sect(file, parent, parent, printedsection, false, save_cb);
503
504 UniConfValueTree::Iter it(parent);
505 for (it.rewind(); it.next(); )
506 {
507 UniConfValueTree &node = *it;
508
509 printedsection = false;
510 save_sect(file, node, node, printedsection, true, save_cb);
511 }
512}
The basic interface which is included by all other XPLC interfaces and objects.
An abstract data container that backs a UniConf tree.
void hold_delta()
Pauses notifications until matched with a call to unhold_delta().
Definition uniconfgen.cc:32
void unhold_delta()
Resumes notifications when each hold_delta() has been matched.
Definition uniconfgen.cc:38
void delta(const UniConfKey &key, WvStringParm value)
Call this when a key's value or children have possibly changed.
Definition uniconfgen.cc:77
Represents a UniConf key which is a path in a hierarchy structured much like the traditional Unix fil...
void prepend(const UniConfKey &other)
Prepends a path to this path.
bool isempty() const
Returns true if this path has zero segments (also known as root).
UniConfKey fullkey(const Sub *ancestor=NULL) const
Returns full path of this node relative to an ancestor.
void visit(const Visitor &visitor, void *userdata, bool preorder=true, bool postorder=false) const
Performs a traversal on this tree using the specified visitor function and traversal type(s).
bool compare(const Sub *other, const Comparator &comparator)
Compares this tree with another using the specified comparator function.
A plain UniConfTree that holds keys and values.
const WvString & value() const
Returns the value field.
const UniConfKey & key() const
Returns the key field.
bool haschildren() const
Returns true if the node has children.
Loads and saves ".ini"-style files similar to those used by Windows, but adapted to represent keys an...
virtual void set(const UniConfKey &key, WvStringParm value)
Stores a string value for a key into the registry.
Definition uniinigen.cc:39
UniIniGen(WvStringParm filename, int _create_mode=0666, SaveCallback _save_cb=SaveCallback())
Creates a generator which can load/modify/save a .ini file.
Definition uniinigen.cc:30
virtual void commit()
Commits any changes.
Definition uniinigen.cc:294
virtual bool refresh()
Refreshes information about a key recursively.
Definition uniinigen.cc:55
A UniConf generator that stores keys in memory.
virtual bool refresh()
Refreshes information about a key recursively.
virtual void commit()
Commits any changes.
virtual void set(const UniConfKey &key, WvStringParm value)
Stores a string value for a key into the registry.
Definition unitempgen.cc:57
void put(const T *data, size_t count)
Writes the specified number of elements from the specified storage location into the buffer at its ta...
size_t used() const
Returns the number of elements in the buffer currently available for reading.
A raw memory read-only buffer backed by a constant WvString.
virtual int geterr() const
If isok() is false, return the system error number corresponding to the error, -1 for a special error...
A WvFastString acts exactly like a WvString, but can take (const char *) strings without needing to a...
bool isnull() const
returns true if this string is null
int getrfd() const
Returns the Unix file descriptor for reading from this stream.
virtual bool isok() const
return true if the stream is actually usable right now
int getwfd() const
Returns the Unix file descriptor for writing to this stream.
virtual void close()
Closes the file descriptors.
WvFile implements a stream connected to a file or Unix device.
A type-safe version of WvMonikerBase that lets you provide create functions for object types other th...
Unified support for streams, that is, sequences of bytes that may or may not be ready for read/write ...
char * blocking_getline(time_t wait_msec, int separator='\n', int readahead=1024)
This is a version of getline() that allows you to block for more data to arrive.
Definition wvstream.cc:602
virtual void seterr(int _errnum)
Override seterr() from WvError so that it auto-closes the stream.
Definition wvstream.cc:451
A class used to provide a masked lookup for characters in a string.
WvString is an implementation of a simple and efficient printable-string class.
WvString & unique()
make the buf and str pointers owned only by this WvString.
Definition wvstring.cc:306
char * edit()
make the string editable, and return a non-const (char*)
char * trim_string(char *string)
Trims whitespace from the beginning and end of the character string, including carriage return / line...
Definition strutils.cc:59
WvString wvtcl_unescape(WvStringParm s)
tcl-unescape a string.
WvString wvtcl_escape(WvStringParm s, const WvStringMask &nasties=WVTCL_NASTY_SPACES)
tcl-escape a string.
WvString wvtcl_getword(WvBuf &buf, const WvStringMask &splitchars=WVTCL_SPLITCHARS, bool do_unescape=true)
Get a single tcl word from an input buffer, and return the rest of the buffer untouched.