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 
17 WV_LINK(UniIniGen);
18 
19 
20 static IUniConfGen *creator(WvStringParm s, IObject*)
21 {
22  return new UniIniGen(s);
23 }
24 
25 WvMoniker<IUniConfGen> UniIniGenMoniker("ini", creator);
26 
27 
28 /***** UniIniGen *****/
29 
30 UniIniGen::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 
39 void 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 
50 UniIniGen::~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
213 bool 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
248 bool 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).
357 static 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 
396 static 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 
416 static 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 
450 static 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 
485 void 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 }
WvString::edit
char * edit()
make the string editable, and return a non-const (char*)
Definition: wvstring.h:397
WvStringMask
A class used to provide a masked lookup for characters in a string.
Definition: wvstringmask.h:18
wvtcl_getword
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.
Definition: wvtclstring.cc:359
WvErrorBase::geterr
virtual int geterr() const
If isok() is false, return the system error number corresponding to the error, -1 for a special error...
Definition: wverror.h:48
WvFdStream::isok
virtual bool isok() const
return true if the stream is actually usable right now
Definition: wvfdstream.cc:134
UniIniGen::commit
virtual void commit()
Commits any changes.
Definition: uniinigen.cc:294
WvStream::blocking_getline
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
UniIniGen::set
virtual void set(const UniConfKey &key, WvStringParm value)
Stores a string value for a key into the registry.
Definition: uniinigen.cc:39
wvtclstring.h
WvFile
WvFile implements a stream connected to a file or Unix device.
Definition: wvfile.h:28
wvtcl_unescape
WvString wvtcl_unescape(WvStringParm s)
tcl-unescape a string.
Definition: wvtclstring.cc:204
WvBufBase< unsigned char >::getstr
WvString getstr()
Returns the entire buffer as a null-terminated WvString.
Definition: wvbuffer.cc:17
UniConfKey::prepend
void prepend(const UniConfKey &other)
Prepends a path to this path.
Definition: uniconfkey.cc:152
WvBufBase< unsigned char >::putstr
void putstr(WvStringParm str)
Copies a WvString into the buffer, excluding the null-terminator.
Definition: wvbuffer.cc:11
UniIniGen
Loads and saves ".ini"-style files similar to those used by Windows, but adapted to represent keys an...
Definition: uniinigen.h:25
UniTempGen::root
UniConfValueTree * root
Definition: unitempgen.h:25
WvFdStream::getrfd
int getrfd() const
Returns the Unix file descriptor for reading from this stream.
Definition: wvfdstream.h:63
UniConfTree::compare
bool compare(const Sub *other, const Comparator &comparator)
Compares this tree with another using the specified comparator function.
Definition: uniconftree.h:124
trim_string
char * trim_string(char *string)
Trims whitespace from the beginning and end of the character string, including carriage return / line...
Definition: strutils.cc:59
UniIniGen::refresh
virtual bool refresh()
Refreshes information about a key recursively.
Definition: uniinigen.cc:55
WvConstStringBuffer
A raw memory read-only buffer backed by a constant WvString.
Definition: wvbuf.h:241
UniConfGen::unhold_delta
void unhold_delta()
Resumes notifications when each hold_delta() has been matched.
Definition: uniconfgen.cc:38
WvStream::seterr
virtual void seterr(int _errnum)
Override seterr() from WvError so that it auto-closes the stream.
Definition: wvstream.cc:451
WvString
WvString is an implementation of a simple and efficient printable-string class.
Definition: wvstring.h:329
IUniConfGen
An abstract data container that backs a UniConf tree.
Definition: uniconfgen.h:39
UniHashTreeBase::key
const UniConfKey & key() const
Returns the key field.
Definition: unihashtree.h:40
UniConfValueTree::value
const WvString & value() const
Returns the value field.
Definition: uniconftree.h:163
UniConfTree::visit
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).
Definition: uniconftree.h:108
WvFastString::isnull
bool isnull() const
returns true if this string is null
Definition: wvstring.h:290
wvtcl_escape
WvString wvtcl_escape(WvStringParm s, const WvStringMask &nasties=WVTCL_NASTY_SPACES)
tcl-escape a string.
Definition: wvtclstring.cc:128
UniTempGen::dirty
bool dirty
Definition: unitempgen.h:26
UniHashTreeBase::Iter
Definition: unihashtree.h:78
UniConfKey
Represents a UniConf key which is a path in a hierarchy structured much like the traditional Unix fil...
Definition: uniconfkey.h:38
WvMoniker
A type-safe version of WvMonikerBase that lets you provide create functions for object types other th...
Definition: wvmoniker.h:61
UniIniGen::UniIniGen
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
UniConfKey::isempty
bool isempty() const
Returns true if this path has zero segments (also known as root).
Definition: uniconfkey.h:264
UniTempGen::set
virtual void set(const UniConfKey &key, WvStringParm value)
Stores a string value for a key into the registry.
Definition: unitempgen.cc:57
UniConfGen::delta
void delta(const UniConfKey &key, WvStringParm value)
Call this when a key's value or children have possibly changed.
Definition: uniconfgen.cc:77
IObject
Definition: IObject.h:65
UniConfKey::EMPTY
static UniConfKey EMPTY
Definition: uniconfkey.h:171
WvStream
Unified support for streams, that is, sequences of bytes that may or may not be ready for read/write ...
Definition: wvstream.h:24
UniTempGen::commit
virtual void commit()
Commits any changes.
Definition: unitempgen.cc:185
WvString::unique
WvString & unique()
make the buf and str pointers owned only by this WvString.
Definition: wvstring.cc:306
UniConfTree::fullkey
UniConfKey fullkey(const Sub *ancestor=NULL) const
Returns full path of this node relative to an ancestor.
Definition: uniconftree.h:55
WvFdStream::close
virtual void close()
Closes the file descriptors.
Definition: wvfdstream.cc:117
UniTempGen::refresh
virtual bool refresh()
Refreshes information about a key recursively.
Definition: unitempgen.cc:191
WvFdStream::getwfd
int getwfd() const
Returns the Unix file descriptor for writing to this stream.
Definition: wvfdstream.h:70
WvDynBufBase< unsigned char >
UniConfValueTree
A plain UniConfTree that holds keys and values.
Definition: uniconftree.h:152
WvBufBaseCommonImpl::used
size_t used() const
Returns the number of elements in the buffer currently available for reading.
Definition: wvbufbase.h:92
UniHashTreeBase::haschildren
bool haschildren() const
Returns true if the node has children.
Definition: unihashtree.cc:114
UniConfGen::hold_delta
void hold_delta()
Pauses notifications until matched with a call to unhold_delta().
Definition: uniconfgen.cc:32
UniTempGen
A UniConf generator that stores keys in memory.
Definition: unitempgen.h:20
WvBufBase< unsigned char >::strchr
size_t strchr(int ch)
Returns the number of characters that would have to be read to find the first instance of the charact...
Definition: wvbuffer.cc:46