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