/* * Worldvisions Weaver Software: * Copyright (C) 1997-2002 Net Integration Technologies, Inc. * * A generator for .ini files. */ #include "uniinigen.h" #include "strutils.h" #include "unitempgen.h" #include "wvfile.h" #include "wvmoniker.h" #include "wvstringmask.h" #include "wvtclstring.h" #include #include "wvlinkerhack.h" WV_LINK(UniIniGen); static IUniConfGen *creator(WvStringParm s, IObject*) { return new UniIniGen(s); } WvMoniker UniIniGenMoniker("ini", creator); /***** UniIniGen *****/ UniIniGen::UniIniGen(WvStringParm _filename, int _create_mode, UniIniGen::SaveCallback _save_cb) : filename(_filename), create_mode(_create_mode), log(_filename), save_cb(_save_cb) { // Create the root, since this generator can't handle it not existing. UniTempGen::set(UniConfKey::EMPTY, WvString::empty); memset(&old_st, 0, sizeof(old_st)); } void UniIniGen::set(const UniConfKey &key, WvStringParm value) { UniTempGen::set(key, value); // Re-create the root, since this generator can't handle it not existing. if (value.isnull() && key.isempty()) UniTempGen::set(UniConfKey::EMPTY, WvString::empty); } UniIniGen::~UniIniGen() { } bool UniIniGen::refresh() { WvFile file(filename, O_RDONLY); #ifndef _WIN32 struct stat statbuf; if (file.isok() && fstat(file.getrfd(), &statbuf) == -1) { log(WvLog::Warning, "Can't stat '%s': %s\n", filename, strerror(errno)); file.close(); } if (file.isok() && (statbuf.st_mode & S_ISVTX)) { file.close(); file.seterr(EAGAIN); } if (file.isok() // guarantes statbuf is valid from above && statbuf.st_ctime == old_st.st_ctime && statbuf.st_dev == old_st.st_dev && statbuf.st_ino == old_st.st_ino && statbuf.st_blocks == old_st.st_blocks && statbuf.st_size == old_st.st_size) { log(WvLog::Debug3, "refresh: file hasn't changed; do nothing.\n"); return true; } memcpy(&old_st, &statbuf, sizeof(statbuf)); #endif if (!file.isok()) { log(WvLog::Warning, "Can't open '%s' for reading: %s\n" "...starting with blank configuration.\n", filename, file.errstr()); return false; } // loop over all Tcl words in the file UniTempGen *newgen = new UniTempGen(); newgen->set(UniConfKey::EMPTY, WvString::empty); UniConfKey section; WvDynBuf buf; while (buf.used() || file.isok()) { if (file.isok()) { // read entire lines to ensure that we get whole values char *line = file.blocking_getline(-1); if (line) { buf.putstr(line); buf.put('\n'); // this was auto-stripped by getline() } } WvString word; while (!(word = wvtcl_getword(buf, WVTCL_NASTY_NEWLINES, false)).isnull()) { //log(WvLog::Info, "LINE: '%s'\n", word); char *str = trim_string(word.edit()); int len = strlen(str); if (len == 0) continue; // blank line if (str[0] == '#') { // a comment line. FIXME: we drop it completely! //log(WvLog::Debug5, "Comment: \"%s\"\n", str + 1); continue; } if (str[0] == '[' && str[len - 1] == ']') { // a section name str[len - 1] = '\0'; WvString name(wvtcl_unescape(trim_string(str + 1))); section = UniConfKey(name); //log(WvLog::Debug5, "Refresh section: \"%s\"\n", section); continue; } // we possibly have a key = value line WvConstStringBuffer line(word); static const WvStringMask nasty_equals("="); WvString name = wvtcl_getword(line, nasty_equals, false); if (!name.isnull() && line.used()) { name = wvtcl_unescape(trim_string(name.edit())); if (!!name) { UniConfKey key(name); key.prepend(section); WvString value = line.getstr(); assert(*value == '='); value = wvtcl_unescape(trim_string(value.edit() + 1)); newgen->set(key, value.unique()); //log(WvLog::Debug5, "Refresh: (\"%s\", \"%s\")\n", // key, value); continue; } } // if we get here, the line was tcl-decoded but not useful. log(WvLog::Warning, "Ignoring malformed input line: \"%s\"\n", word); } if (buf.used() && !file.isok()) { // EOF and some of the data still hasn't been used. Weird. // Let's remove a line of data and try again. size_t offset = buf.strchr('\n'); assert(offset); // the last thing we put() is *always* a newline! WvString line1(trim_string(buf.getstr(offset).edit())); if (!!line1) // not just whitespace log(WvLog::Warning, "XXX Ignoring malformed input line: \"%s\"\n", line1); } } if (file.geterr()) { log(WvLog::Warning, "Error reading from config file: %s\n", file.errstr()); WVRELEASE(newgen); return false; } // switch the trees and send notifications hold_delta(); UniConfValueTree *oldtree = root; UniConfValueTree *newtree = newgen->root; root = newtree; newgen->root = NULL; dirty = false; oldtree->compare(newtree, wv::bind(&UniIniGen::refreshcomparator, this, _1, _2)); delete oldtree; unhold_delta(); WVRELEASE(newgen); UniTempGen::refresh(); return true; } // returns: true if a==b bool UniIniGen::refreshcomparator(const UniConfValueTree *a, const UniConfValueTree *b) { if (a) { if (b) { if (a->value() != b->value()) { // key changed delta(b->fullkey(), b->value()); // CHANGED return false; } return true; } else { // key removed // Issue notifications for every that is missing. a->visit(wv::bind(&UniIniGen::notify_deleted, this, _1, _2), NULL, false, true); return false; } } else // a didn't exist { assert(b); // key added delta(b->fullkey(), b->value()); // ADDED return false; } } #ifndef _WIN32 bool UniIniGen::commit_atomic(WvStringParm real_filename) { struct stat statbuf; if (lstat(real_filename, &statbuf) == -1) { if (errno != ENOENT) return false; } else if (!S_ISREG(statbuf.st_mode)) return false; WvString tmp_filename("%s.tmp%s", real_filename, getpid()); WvFile file(tmp_filename, O_WRONLY|O_TRUNC|O_CREAT, 0000); if (file.geterr()) { log(WvLog::Warning, "Can't write '%s': %s\n", tmp_filename, strerror(errno)); unlink(tmp_filename); file.close(); return false; } save(file, *root); // write the changes out to our temp file mode_t theumask = umask(0); umask(theumask); fchmod(file.getwfd(), create_mode & ~theumask); file.close(); if (file.geterr() || rename(tmp_filename, real_filename) == -1) { log(WvLog::Warning, "Can't write '%s': %s\n", filename, strerror(errno)); unlink(tmp_filename); return false; } return true; } #endif void UniIniGen::commit() { if (!dirty) return; UniTempGen::commit(); #ifdef _WIN32 // Windows doesn't support all that fancy stuff, just open the // file and be done with it WvFile file(filename, O_WRONLY|O_TRUNC|O_CREAT, create_mode); save(file, *root); // write the changes out to our file file.close(); if (file.geterr()) { log(WvLog::Warning, "Can't write '%s': %s\n", filename, file.errstr()); return; } #else WvString real_filename(filename); char resolved_path[PATH_MAX]; if (realpath(filename, resolved_path) != NULL) real_filename = resolved_path; if (!commit_atomic(real_filename)) { WvFile file(real_filename, O_WRONLY|O_TRUNC|O_CREAT, create_mode); struct stat statbuf; if (fstat(file.getwfd(), &statbuf) == -1) { log(WvLog::Warning, "Can't write '%s' ('%s'): %s\n", filename, real_filename, strerror(errno)); return; } fchmod(file.getwfd(), (statbuf.st_mode & 07777) | S_ISVTX); save(file, *root); if (!file.geterr()) { /* We only reset the sticky bit if all went well, but before * we close it, because we need the file descriptor. */ statbuf.st_mode = statbuf.st_mode & ~S_ISVTX; fchmod(file.getwfd(), statbuf.st_mode & 07777); } else log(WvLog::Warning, "Error writing '%s' ('%s'): %s\n", filename, real_filename, file.errstr()); } #endif dirty = false; } // may return false for strings that wvtcl_escape would escape anyway; this // may not escape tcl-invalid strings, but that's on purpose so we can keep // old-style .ini file compatibility (and wvtcl_getword() and friends can // still parse them anyway). static bool absolutely_needs_escape(WvStringParm s, const char *sepchars) { const char *cptr; int numbraces = 0; bool inescape = false, inspace = false; if (isspace((unsigned char)*s)) return true; // leading whitespace needs escaping for (cptr = s; *cptr; cptr++) { if (inescape) inescape = false; // fine else if (!numbraces && strchr(sepchars, *cptr)) return true; // one of the magic characters, and not escaped else if (*cptr == '\\') inescape = true; else if (*cptr == '{') numbraces++; else if (*cptr == '}') numbraces--; inspace = isspace((unsigned char)*cptr); if (numbraces < 0) // yikes! mismatched braces will need some help. return false; } if (inescape || inspace) return true; // terminating backslash or whitespace... evil. if (numbraces != 0) return true; // uneven number of braces, can't be good // otherwise, I guess we're safe. return false; } static void printsection(WvStream &file, const UniConfKey &key, UniIniGen::SaveCallback save_cb) { WvString s; static const WvStringMask nasties("\r\n[]"); if (absolutely_needs_escape(key, "\r\n[]")) s = wvtcl_escape(key, nasties); else s = key; // broken up for optimization, no temp wvstring created //file.print("\n[%s]\n", s); file.print("\n["); file.print(s); file.print("]\n"); if (!!save_cb) save_cb(); } static void printkey(WvStream &file, const UniConfKey &_key, WvStringParm _value, UniIniGen::SaveCallback save_cb) { WvString key, value; static const WvStringMask nasties("\r\n\t []=#"); if (absolutely_needs_escape(_key, "\r\n[]=#\"")) key = wvtcl_escape(_key, nasties); else if (_key == "") key = "/"; else key = _key; // value is more relaxed, since we don't use wvtcl_getword after we grab // the "key=" part of each line if (absolutely_needs_escape(_value, "\r\n")) value = wvtcl_escape(_value, WVTCL_NASTY_SPACES); else value = _value; // need to escape []#= in key only to distinguish a key/value // pair from a section name or comment and to delimit the value // broken up for optimization, no temp wvstring created //file.print("%s = %s\n", key, value); file.print(key); file.print(" = "); file.print(value); file.print("\n"); if (!!save_cb) save_cb(); } static void save_sect(WvStream &file, UniConfValueTree &toplevel, UniConfValueTree §, bool &printedsection, bool recursive, UniIniGen::SaveCallback save_cb) { UniConfValueTree::Iter it(sect); for (it.rewind(); it.next(); ) { UniConfValueTree &node = *it; // FIXME: we never print empty-string ("") keys, for compatibility // with WvConf. Example: set x/y = 1; delete x/y; now x = "", because // it couldn't be NULL while x/y existed, and nobody auto-deleted it // when x/y went away. Therefore we would try to write x = "" to the // config file, but that's not what WvConf would do. // // The correct fix would be to auto-delete x if the only reason it // exists is for x/y. But since that's hard, we'll just *never* // write lines for "" entries. Icky, but it works. if (!!node.value())// || !node.haschildren()) { if (!printedsection) { printsection(file, toplevel.fullkey(), save_cb); printedsection = true; } printkey(file, node.fullkey(&toplevel), node.value(), save_cb); } // print all children, if requested if (recursive && node.haschildren()) save_sect(file, toplevel, node, printedsection, recursive, save_cb); } } void UniIniGen::save(WvStream &file, UniConfValueTree &parent) { // parent might be NULL, so it really should be a pointer, not // a reference. Oh well... if (!&parent) return; if (parent.fullkey() == root->fullkey()) { // the root itself is a special case, since it's not in a section, // and it's never NULL (so we don't need to write it if it's just // blank) if (!!parent.value()) printkey(file, parent.key(), parent.value(), save_cb); } bool printedsection = false; save_sect(file, parent, parent, printedsection, false, save_cb); UniConfValueTree::Iter it(parent); for (it.rewind(); it.next(); ) { UniConfValueTree &node = *it; printedsection = false; save_sect(file, node, node, printedsection, true, save_cb); } }