uniinigen.cc

00001 /*
00002  * Worldvisions Weaver Software:
00003  *   Copyright (C) 1997-2002 Net Integration Technologies, Inc.
00004  * 
00005  * A generator for .ini files.
00006  */
00007 #include "uniinigen.h"
00008 #include "strutils.h"
00009 #include "unitempgen.h"
00010 #include "wvfile.h"
00011 #include "wvmoniker.h"
00012 #include "wvstringmask.h"
00013 #include "wvtclstring.h"
00014 #include <ctype.h>
00015 #include "wvlinkerhack.h"
00016 
00017 WV_LINK(UniIniGen);
00018 
00019 
00020 static IUniConfGen *creator(WvStringParm s)
00021 {
00022     return new UniIniGen(s);
00023 }
00024 
00025 WvMoniker<IUniConfGen> UniIniGenMoniker("ini", creator);
00026 
00027 
00028 /***** UniIniGen *****/
00029 
00030 UniIniGen::UniIniGen(WvStringParm _filename, int _create_mode, UniIniGen::SaveCallback _save_cb)
00031     : filename(_filename), create_mode(_create_mode), log(_filename), save_cb(_save_cb)
00032 {
00033     // Create the root, since this generator can't handle it not existing.
00034     UniTempGen::set(UniConfKey::EMPTY, WvString::empty);
00035     memset(&old_st, 0, sizeof(old_st));
00036 }
00037 
00038 
00039 void UniIniGen::set(const UniConfKey &key, WvStringParm value)
00040 {
00041     // Don't allow people to delete the root, since this generator can't
00042     // handle it not existing.
00043     if (!(value.isnull() && key.isempty()))
00044         UniTempGen::set(key, value);
00045 }
00046 
00047 
00048 UniIniGen::~UniIniGen()
00049 {
00050 }
00051 
00052 
00053 bool UniIniGen::refresh()
00054 {
00055     WvFile file(filename, O_RDONLY);
00056 
00057 #ifndef _WIN32
00058     struct stat statbuf;
00059     if (file.isok() && fstat(file.getrfd(), &statbuf) == -1)
00060     {
00061         log(WvLog::Warning, "Can't stat '%s': %s\n",
00062             filename, strerror(errno));
00063         file.close();
00064     }
00065 
00066     if (file.isok() && (statbuf.st_mode & S_ISVTX))
00067     {
00068         file.close();
00069         file.seterr(EAGAIN);
00070     }
00071     
00072     if (file.isok() // guarantes statbuf is valid from above
00073         && statbuf.st_ctime == old_st.st_ctime
00074         && statbuf.st_dev == old_st.st_dev
00075         && statbuf.st_ino == old_st.st_ino
00076         && statbuf.st_blocks == old_st.st_blocks
00077         && statbuf.st_size == old_st.st_size)
00078     {
00079         log(WvLog::Debug3, "refresh: file hasn't changed; do nothing.\n");
00080         return true;
00081     }
00082     memcpy(&old_st, &statbuf, sizeof(statbuf));
00083 #endif
00084 
00085     if (!file.isok())
00086     {
00087         log(WvLog::Warning, 
00088             "Can't open '%s' for reading: %s\n"
00089             "...starting with blank configuration.\n",
00090             filename, file.errstr());
00091         return false;
00092     }
00093     
00094     // loop over all Tcl words in the file
00095     UniTempGen *newgen = new UniTempGen();
00096     newgen->set(UniConfKey::EMPTY, WvString::empty);
00097     UniConfKey section;
00098     WvDynBuf buf;
00099     while (buf.used() || file.isok())
00100     {
00101         if (file.isok())
00102         {
00103             // read entire lines to ensure that we get whole values
00104             char *line = file.blocking_getline(-1);
00105             if (line)
00106             {
00107                 buf.putstr(line);
00108                 buf.put('\n'); // this was auto-stripped by getline()
00109             }
00110         }
00111 
00112         WvString word;
00113         while (!(word = wvtcl_getword(buf,
00114                                       WVTCL_NASTY_NEWLINES,
00115                                       false)).isnull())
00116         {
00117             //log(WvLog::Info, "LINE: '%s'\n", word);
00118             
00119             char *str = trim_string(word.edit());
00120             int len = strlen(str);
00121             if (len == 0) continue; // blank line
00122             
00123             if (str[0] == '#')
00124             {
00125                 // a comment line.  FIXME: we drop it completely!
00126                 //log(WvLog::Debug5, "Comment: \"%s\"\n", str + 1);
00127                 continue;
00128             }
00129             
00130             if (str[0] == '[' && str[len - 1] == ']')
00131             {
00132                 // a section name
00133                 str[len - 1] = '\0';
00134                 WvString name(wvtcl_unescape(trim_string(str + 1)));
00135                 section = UniConfKey(name);
00136                 //log(WvLog::Debug5, "Refresh section: \"%s\"\n", section);
00137                 continue;
00138             }
00139             
00140             // we possibly have a key = value line
00141             WvConstStringBuffer line(word);
00142             static const WvStringMask nasty_equals("=");
00143             WvString name = wvtcl_getword(line, nasty_equals, false);
00144             if (!name.isnull() && line.used())
00145             {
00146                 name = wvtcl_unescape(trim_string(name.edit()));
00147                 
00148                 if (!!name)
00149                 {
00150                     UniConfKey key(name);
00151                     key.prepend(section);
00152                     
00153                     WvString value = line.getstr();
00154                     assert(*value == '=');
00155                     value = wvtcl_unescape(trim_string(value.edit() + 1));
00156                     newgen->set(key, value.unique());
00157 
00158                     //log(WvLog::Debug5, "Refresh: (\"%s\", \"%s\")\n",
00159                     //    key, value);
00160                     continue;
00161                 }
00162             }
00163             
00164             // if we get here, the line was tcl-decoded but not useful.
00165             log(WvLog::Warning,
00166                 "Ignoring malformed input line: \"%s\"\n", word);
00167         }
00168         
00169         if (buf.used() && !file.isok())
00170         {
00171             // EOF and some of the data still hasn't been used.  Weird.
00172             // Let's remove a line of data and try again.
00173             size_t offset = buf.strchr('\n');
00174             assert(offset); // the last thing we put() is *always* a newline!
00175             WvString line1(trim_string(buf.getstr(offset).edit()));
00176             if (!!line1) // not just whitespace
00177                 log(WvLog::Warning,
00178                     "XXX Ignoring malformed input line: \"%s\"\n", line1);
00179         }
00180     }
00181 
00182     if (file.geterr())
00183     {
00184         log(WvLog::Warning, 
00185             "Error reading from config file: %s\n", file.errstr());
00186         WVRELEASE(newgen);
00187         return false;
00188     }
00189 
00190     // switch the trees and send notifications
00191     hold_delta();
00192     UniConfValueTree *oldtree = root;
00193     UniConfValueTree *newtree = newgen->root;
00194     root = newtree;
00195     newgen->root = NULL;
00196     dirty = false;
00197     oldtree->compare(newtree, UniConfValueTree::Comparator
00198             (this, &UniIniGen::refreshcomparator), NULL);
00199     
00200     delete oldtree;
00201     unhold_delta();
00202 
00203     WVRELEASE(newgen);
00204 
00205     UniTempGen::refresh();
00206     return true;
00207 }
00208 
00209 
00210 // returns: true if a==b
00211 bool UniIniGen::refreshcomparator(const UniConfValueTree *a,
00212     const UniConfValueTree *b, void *userdata)
00213 {
00214     if (a)
00215     {
00216         if (b)
00217         {
00218             if (a->value() != b->value())
00219             {
00220                 // key changed
00221                 delta(b->fullkey(), b->value()); // CHANGED
00222                 return false;
00223             }
00224             return true;
00225         }
00226         else
00227         {
00228             // key removed
00229             // Issue notifications for every that is missing.
00230             a->visit(UniConfValueTree::Visitor(this,
00231                 &UniIniGen::notify_deleted), NULL, false, true);
00232             return false;
00233         }
00234     }
00235     else // a didn't exist
00236     {
00237         assert(b);
00238         // key added
00239         delta(b->fullkey(), b->value()); // ADDED
00240         return false;
00241     }
00242 }
00243 
00244 
00245 #ifndef _WIN32
00246 bool UniIniGen::commit_atomic(WvStringParm real_filename)
00247 {
00248     struct stat statbuf;
00249 
00250     if (lstat(real_filename, &statbuf) == -1)
00251     {
00252         if (errno != ENOENT)
00253             return false;
00254     }
00255     else
00256         if (!S_ISREG(statbuf.st_mode))
00257             return false;
00258 
00259     WvString tmp_filename("%s.tmp%s", real_filename, getpid());
00260     WvFile file(tmp_filename, O_WRONLY|O_TRUNC|O_CREAT, 0000);
00261 
00262     if (file.geterr())
00263     {
00264         log(WvLog::Warning, "Can't write '%s': %s\n",
00265             tmp_filename, strerror(errno));
00266         unlink(tmp_filename);
00267         file.close();
00268         return false;
00269     }
00270 
00271     save(file, *root); // write the changes out to our temp file
00272 
00273     mode_t theumask = umask(0);
00274     umask(theumask);
00275     fchmod(file.getwfd(), create_mode & ~theumask);
00276 
00277     file.close();
00278 
00279     if (file.geterr() || rename(tmp_filename, real_filename) == -1)
00280     {
00281         log(WvLog::Warning, "Can't write '%s': %s\n",
00282             filename, strerror(errno));
00283         unlink(tmp_filename);
00284         return false;
00285     }
00286 
00287     return true;
00288 }
00289 #endif
00290 
00291 
00292 void UniIniGen::commit()
00293 {
00294     if (!dirty)
00295         return;
00296 
00297     UniTempGen::commit();
00298 
00299 #ifdef _WIN32
00300     // Windows doesn't support all that fancy stuff, just open the
00301     // file and be done with it
00302     WvFile file(filename, O_WRONLY|O_TRUNC|O_CREAT, create_mode);
00303     save(file, *root); // write the changes out to our file
00304     file.close();
00305     if (file.geterr())
00306     {
00307         log(WvLog::Warning, "Can't write '%s': %s\n",
00308             filename, file.errstr());
00309         return;
00310     }
00311 #else
00312     WvString real_filename(filename);
00313     char resolved_path[PATH_MAX];
00314 
00315     if (realpath(filename, resolved_path) != NULL)
00316         real_filename = resolved_path;
00317 
00318     if (!commit_atomic(real_filename))
00319     {
00320         WvFile file(real_filename, O_WRONLY|O_TRUNC|O_CREAT, create_mode);
00321         struct stat statbuf;
00322 
00323         if (fstat(file.getwfd(), &statbuf) == -1)
00324         {
00325             log(WvLog::Warning, "Can't write '%s' ('%s'): %s\n",
00326                 filename, real_filename, strerror(errno));
00327             return;
00328         }
00329 
00330         fchmod(file.getwfd(), (statbuf.st_mode & 07777) | S_ISVTX);
00331 
00332         save(file, *root);
00333     
00334         if (!file.geterr())
00335         {
00336             /* We only reset the sticky bit if all went well, but before
00337              * we close it, because we need the file descriptor. */
00338             statbuf.st_mode = statbuf.st_mode & ~S_ISVTX;
00339             fchmod(file.getwfd(), statbuf.st_mode & 07777);
00340         }
00341         else
00342             log(WvLog::Warning, "Error writing '%s' ('%s'): %s\n",
00343                 filename, real_filename, file.errstr());
00344     }
00345 #endif
00346 
00347     dirty = false;
00348 }
00349 
00350 
00351 // may return false for strings that wvtcl_escape would escape anyway; this
00352 // may not escape tcl-invalid strings, but that's on purpose so we can keep
00353 // old-style .ini file compatibility (and wvtcl_getword() and friends can
00354 // still parse them anyway).
00355 static bool absolutely_needs_escape(WvStringParm s, const char *sepchars)
00356 {
00357     const char *cptr;
00358     int numbraces = 0;
00359     bool inescape = false, inspace = false;
00360     
00361     if (isspace((unsigned char)*s))
00362         return true; // leading whitespace needs escaping
00363     
00364     for (cptr = s; *cptr; cptr++)
00365     {
00366         if (inescape)
00367             inescape = false; // fine
00368         else if (!numbraces && strchr(sepchars, *cptr))
00369             return true; // one of the magic characters, and not escaped
00370         else if (*cptr == '\\')
00371             inescape = true;
00372         else if (*cptr == '{')
00373             numbraces++;
00374         else if (*cptr == '}')
00375             numbraces--;
00376         
00377         inspace = isspace((unsigned char)*cptr);
00378         
00379         if (numbraces < 0) // yikes!  mismatched braces will need some help.
00380             return false; 
00381     }
00382     
00383     if (inescape || inspace)
00384         return true; // terminating backslash or whitespace... evil.
00385     
00386     if (numbraces != 0)
00387         return true; // uneven number of braces, can't be good
00388 
00389     // otherwise, I guess we're safe.
00390     return false;
00391 }
00392 
00393 
00394 static void printsection(WvStream &file, const UniConfKey &key, UniIniGen::SaveCallback save_cb)
00395 {
00396     WvString s;
00397     static const WvStringMask nasties("\r\n[]");
00398 
00399     if (absolutely_needs_escape(key, "\r\n[]"))
00400         s = wvtcl_escape(key, nasties);
00401     else
00402         s = key;
00403     // broken up for optimization, no temp wvstring created
00404     //file.print("\n[%s]\n", s);
00405     file.print("\n[");
00406     file.print(s);
00407     file.print("]\n");
00408 
00409     if (!!save_cb)
00410         save_cb();
00411 }
00412 
00413 
00414 static void printkey(WvStream &file, const UniConfKey &_key,
00415                      WvStringParm _value, UniIniGen::SaveCallback save_cb)
00416 {
00417     WvString key, value;
00418     static const WvStringMask nasties("\r\n\t []=#");
00419 
00420     if (absolutely_needs_escape(_key, "\r\n[]=#\""))
00421         key = wvtcl_escape(_key, nasties);
00422     else if (_key == "")
00423         key = "/";
00424     else
00425         key = _key;
00426     
00427     // value is more relaxed, since we don't use wvtcl_getword after we grab
00428     // the "key=" part of each line
00429     if (absolutely_needs_escape(_value, "\r\n"))
00430         value = wvtcl_escape(_value, WVTCL_NASTY_SPACES);
00431     else
00432         value = _value;
00433     
00434     // need to escape []#= in key only to distinguish a key/value
00435     // pair from a section name or comment and to delimit the value
00436     // broken up for optimization, no temp wvstring created
00437     //file.print("%s = %s\n", key, value);
00438     file.print(key);
00439     file.print(" = ");
00440     file.print(value);
00441     file.print("\n");
00442 
00443     if (!!save_cb)
00444         save_cb();
00445 }
00446 
00447 
00448 static void save_sect(WvStream &file, UniConfValueTree &toplevel,
00449                       UniConfValueTree &sect, bool &printedsection,
00450                       bool recursive, UniIniGen::SaveCallback save_cb)
00451 {
00452     UniConfValueTree::Iter it(sect);
00453     for (it.rewind(); it.next(); )
00454     {
00455         UniConfValueTree &node = *it;
00456         
00457         // FIXME: we never print empty-string ("") keys, for compatibility
00458         // with WvConf.  Example: set x/y = 1; delete x/y; now x = "", because
00459         // it couldn't be NULL while x/y existed, and nobody auto-deleted it
00460         // when x/y went away.  Therefore we would try to write x = "" to the
00461         // config file, but that's not what WvConf would do.
00462         // 
00463         // The correct fix would be to auto-delete x if the only reason it
00464         // exists is for x/y.  But since that's hard, we'll just *never*
00465         // write lines for "" entries.  Icky, but it works.
00466         if (!!node.value())// || !node.haschildren())
00467         {
00468             if (!printedsection)
00469             {
00470                 printsection(file, toplevel.fullkey(), save_cb);
00471                 printedsection = true;
00472             }
00473             printkey(file, node.fullkey(&toplevel), node.value(), save_cb);
00474         }
00475 
00476         // print all children, if requested
00477         if (recursive && node.haschildren())
00478             save_sect(file, toplevel, node, printedsection, recursive, save_cb);
00479     }
00480 }
00481 
00482 
00483 void UniIniGen::save(WvStream &file, UniConfValueTree &parent)
00484 {
00485     // parent might be NULL, so it really should be a pointer, not
00486     // a reference.  Oh well...
00487     if (!&parent) return;
00488     
00489     if (parent.fullkey() == root->fullkey())
00490     {
00491         // the root itself is a special case, since it's not in a section,
00492         // and it's never NULL (so we don't need to write it if it's just
00493         // blank)
00494         if (!!parent.value())
00495             printkey(file, parent.key(), parent.value(), save_cb);
00496     }
00497 
00498     bool printedsection = false;
00499     
00500     save_sect(file, parent, parent, printedsection, false, save_cb);
00501     
00502     UniConfValueTree::Iter it(parent);
00503     for (it.rewind(); it.next(); )
00504     {
00505         UniConfValueTree &node = *it;
00506         
00507         printedsection = false;
00508         save_sect(file, node, node, printedsection, true, save_cb);
00509     }
00510 }

Generated on Fri Oct 5 18:20:26 2007 for WvStreams by  doxygen 1.5.3