# ini.cpp

Go to the documentation of this file.

    00001 /* $Id$ */
    00002
    00003 /*
    00004  * This file is part of OpenTTD.
    00005  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
    00006  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    00007  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
    00008  */
    00009
    00012 #include "stdafx.h"
    00013 #include "core/alloc_func.hpp"
    00014 #include "core/mem_func.hpp"
    00015 #include "debug.h"
    00016 #include "ini_type.h"
    00017 #include "string_func.h"
    00018 #include "fileio_func.h"
    00019
    00020 #if (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 199309L) || (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE >= 500)
    00021 # define WITH_FDATASYNC
    00022 # include <unistd.h>
    00023 #endif
    00024
    00025 #ifdef WIN32
    00026 # include <shellapi.h>
    00027 #endif
    00028
    00029 IniItem::IniItem(IniGroup *parent, const char *name, size_t len) : next(NULL), value(NULL), comment(NULL)
    00030 {
    00031   if (len == 0) len = strlen(name);
    00032
    00033   this->name = strndup(name, len);
    00034   *parent->last_item = this;
    00035   parent->last_item = &this->next;
    00036 }
    00037
    00038 IniItem::~IniItem()
    00039 {
    00040   free(this->name);
    00041   free(this->value);
    00042   free(this->comment);
    00043
    00044   delete this->next;
    00045 }
    00046
    00047 void IniItem::SetValue(const char *value)
    00048 {
    00049   free(this->value);
    00050   this->value = strdup(value);
    00051 }
    00052
    00053 IniGroup::IniGroup(IniFile *parent, const char *name, size_t len) : next(NULL), type(IGT_VARIABLES), item(NULL), comment(NULL)
    00054 {
    00055   if (len == 0) len = strlen(name);
    00056
    00057   this->name = strndup(name, len);
    00058   this->last_item = &this->item;
    00059   *parent->last_group = this;
    00060   parent->last_group = &this->next;
    00061
    00062   if (parent->list_group_names == NULL) return;
    00063
    00064   for (uint i = 0; parent->list_group_names[i] != NULL; i++) {
    00065     if (strcmp(this->name, parent->list_group_names[i]) == 0) {
    00066       this->type = IGT_LIST;
    00067       return;
    00068     }
    00069   }
    00070 }
    00071
    00072 IniGroup::~IniGroup()
    00073 {
    00074   free(this->name);
    00075   free(this->comment);
    00076
    00077   delete this->item;
    00078   delete this->next;
    00079 }
    00080
    00081 IniItem *IniGroup::GetItem(const char *name, bool create)
    00082 {
    00083   for (IniItem *item = this->item; item != NULL; item = item->next) {
    00084     if (strcmp(item->name, name) == 0) return item;
    00085   }
    00086
    00087   if (!create) return NULL;
    00088
    00089   /* otherwise make a new one */
    00090   return new IniItem(this, name, strlen(name));
    00091 }
    00092
    00093 void IniGroup::Clear()
    00094 {
    00095   delete this->item;
    00096   this->item = NULL;
    00097   this->last_item = &this->item;
    00098 }
    00099
    00100 IniFile::IniFile(const char * const *list_group_names) : group(NULL), comment(NULL), list_group_names(list_group_names)
    00101 {
    00102   this->last_group = &this->group;
    00103 }
    00104
    00105 IniFile::~IniFile()
    00106 {
    00107   free(this->comment);
    00108   delete this->group;
    00109 }
    00110
    00111 IniGroup *IniFile::GetGroup(const char *name, size_t len)
    00112 {
    00113   if (len == 0) len = strlen(name);
    00114
    00115   /* does it exist already? */
    00116   for (IniGroup *group = this->group; group != NULL; group = group->next) {
    00117     if (!memcmp(group->name, name, len) && group->name[len] == 0) {
    00118       return group;
    00119     }
    00120   }
    00121
    00122   /* otherwise make a new one */
    00123   IniGroup *group = new IniGroup(this, name, len);
    00124   group->comment = strdup("\n");
    00125   return group;
    00126 }
    00127
    00128 void IniFile::RemoveGroup(const char *name)
    00129 {
    00130   size_t len = strlen(name);
    00131   IniGroup *prev = NULL;
    00132   IniGroup *group;
    00133
    00134   /* does it exist already? */
    00135   for (group = this->group; group != NULL; prev = group, group = group->next) {
    00136     if (memcmp(group->name, name, len) == 0) {
    00137       break;
    00138     }
    00139   }
    00140
    00141   if (group == NULL) return;
    00142
    00143   if (prev != NULL) {
    00144     prev->next = prev->next->next;
    00145     if (this->last_group == &group->next) this->last_group = &prev->next;
    00146   } else {
    00147     this->group = this->group->next;
    00148     if (this->last_group == &group->next) this->last_group = &this->group;
    00149   }
    00150
    00151   group->next = NULL;
    00152   delete group;
    00153 }
    00154
    00155 void IniFile::LoadFromDisk(const char *filename)
    00156 {
    00157   assert(this->last_group == &this->group);
    00158
    00159   char buffer[1024];
    00160   IniGroup *group = NULL;
    00161
    00162   char *comment = NULL;
    00163   uint comment_size = 0;
    00164   uint comment_alloc = 0;
    00165
    00166   size_t end;
    00167   /*
    00168    * Now we are going to open a file that contains no more than simple
    00169    * plain text. That would raise the question: "why open the file as
    00170    * if it is a binary file?". That's simple... Microsoft, in all
    00171    * their greatness and wisdom decided it would be useful if ftell
    00172    * is aware of '\r\n' and "sees" that as a single character. The
    00173    * easiest way to test for that situation is by searching for '\n'
    00174    * and decrease the value every time you encounter a '\n'. This will
    00175    * thus also make ftell "see" the '\r' when it is not there, so the
    00176    * result of ftell will be highly unreliable. So to work around this
    00177    * marvel of wisdom we have to open in as a binary file.
    00178    */
    00179   FILE *in = FioFOpenFile(filename, "rb", DATA_DIR, &end);
    00180   if (in == NULL) return;
    00181
    00182   end += ftell(in);
    00183
    00184   /* for each line in the file */
    00185   while ((size_t)ftell(in) < end && fgets(buffer, sizeof(buffer), in)) {
    00186     char c, *s;
    00187     /* trim whitespace from the left side */
    00188     for (s = buffer; *s == ' ' || *s == '\t'; s++) {}
    00189
    00190     /* trim whitespace from right side. */
    00191     char *e = s + strlen(s);
    00192     while (e > s && ((c = e[-1]) == '\n' || c == '\r' || c == ' ' || c == '\t')) e--;
    00193     *e = '\0';
    00194
    00195     /* skip comments and empty lines */
    00196     if (*s == '#' || *s == ';' || *s == '\0') {
    00197       uint ns = comment_size + (e - s + 1);
    00198       uint a = comment_alloc;
    00199       /* add to comment */
    00200       if (ns > a) {
    00201         a = max(a, 128U);
    00202         do a *= 2; while (a < ns);
    00203         comment = ReallocT(comment, comment_alloc = a);
    00204       }
    00205       uint pos = comment_size;
    00206       comment_size += (e - s + 1);
    00207       comment[pos + e - s] = '\n'; // comment newline
    00208       memcpy(comment + pos, s, e - s); // copy comment contents
    00209       continue;
    00210     }
    00211
    00212     /* it's a group? */
    00213     if (s[0] == '[') {
    00214       if (e[-1] != ']') {
    00215         ShowInfoF("ini: invalid group name '%s'", buffer);
    00216       } else {
    00217         e--;
    00218       }
    00219       s++; // skip [
    00220       group = new IniGroup(this, s, e - s);
    00221       if (comment_size) {
    00222         group->comment = strndup(comment, comment_size);
    00223         comment_size = 0;
    00224       }
    00225     } else if (group) {
    00226       char *t;
    00227       /* find end of keyname */
    00228       if (*s == '\"') {
    00229         s++;
    00230         for (t = s; *t != '\0' && *t != '\"'; t++) {}
    00231         if (*t == '\"') *t = ' ';
    00232       } else {
    00233         for (t = s; *t != '\0' && *t != '=' && *t != '\t' && *t != ' '; t++) {}
    00234       }
    00235
    00236       /* it's an item in an existing group */
    00237       IniItem *item = new IniItem(group, s, t - s);
    00238       if (comment_size) {
    00239         item->comment = strndup(comment, comment_size);
    00240         comment_size = 0;
    00241       }
    00242
    00243       /* find start of parameter */
    00244       while (*t == '=' || *t == ' ' || *t == '\t') t++;
    00245
    00246       bool quoted = (*t == '\"');
    00247       /* remove starting quotation marks */
    00248       if (*t == '\"') t++;
    00249       /* remove ending quotation marks */
    00250       e = t + strlen(t);
    00251       if (e > t && e[-1] == '\"') e--;
    00252       *e = '\0';
    00253
    00254       /* If the value was not quoted and empty, it must be NULL */
    00255       item->value = (!quoted && e == t) ? NULL : strndup(t, e - t);
    00256     } else {
    00257       /* it's an orphan item */
    00258       ShowInfoF("ini: '%s' outside of group", buffer);
    00259     }
    00260   }
    00261
    00262   if (comment_size > 0) {
    00263     this->comment = strndup(comment, comment_size);
    00264     comment_size = 0;
    00265   }
    00266
    00267   free(comment);
    00268   fclose(in);
    00269 }
    00270
    00271 bool IniFile::SaveToDisk(const char *filename)
    00272 {
    00273   /*
    00274    * First write the configuration to a (temporary) file and then rename
    00275    * that file. This to prevent that when OpenTTD crashes during the save
    00276    * you end up with a truncated configuration file.
    00277    */
    00278   char file_new[MAX_PATH];
    00279
    00280   strecpy(file_new, filename, lastof(file_new));
    00281   strecat(file_new, ".new", lastof(file_new));
    00282   FILE *f = fopen(file_new, "w");
    00283   if (f == NULL) return false;
    00284
    00285   for (const IniGroup *group = this->group; group != NULL; group = group->next) {
    00286     if (group->comment) fputs(group->comment, f);
    00287     fprintf(f, "[%s]\n", group->name);
    00288     for (const IniItem *item = group->item; item != NULL; item = item->next) {
    00289       if (item->comment != NULL) fputs(item->comment, f);
    00290
    00291       /* protect item->name with quotes if needed */
    00292       if (strchr(item->name, ' ') != NULL ||
    00293           item->name[0] == '[') {
    00294         fprintf(f, "\"%s\"", item->name);
    00295       } else {
    00296         fprintf(f, "%s", item->name);
    00297       }
    00298
    00299       fprintf(f, " = %s\n", item->value == NULL ? "" : item->value);
    00300     }
    00301   }
    00302   if (this->comment) fputs(this->comment, f);
    00303
    00304 /*
    00305  * POSIX (and friends) do not guarantee that when a file is closed it is
    00306  * flushed to the disk. So we manually flush it do disk if we have the
    00307  * APIs to do so. We only need to flush the data as the metadata itself
    00308  * (modification date etc.) is not important to us; only the real data is.
    00309  */
    00310 #ifdef WITH_FDATASYNC
    00311   int ret = fdatasync(fileno(f));
    00312   fclose(f);
    00313   if (ret != 0) return false;
    00314 #else
    00315   fclose(f);
    00316 #endif
    00317
    00318 #if defined(WIN32) || defined(WIN64)
    00319   /* Allocate space for one more \0 character. */
    00320   TCHAR tfilename[MAX_PATH + 1], tfile_new[MAX_PATH + 1];
    00321   _tcsncpy(tfilename, OTTD2FS(filename), MAX_PATH);
    00322   _tcsncpy(tfile_new, OTTD2FS(file_new), MAX_PATH);
    00323   /* SHFileOperation wants a double '\0' terminated string. */
    00324   tfilename[MAX_PATH - 1] = '\0';
    00325   tfile_new[MAX_PATH - 1] = '\0';
    00326   tfilename[_tcslen(tfilename) + 1] = '\0';
    00327   tfile_new[_tcslen(tfile_new) + 1] = '\0';
    00328
    00329   /* Rename file without any user confirmation. */
    00330   SHFILEOPSTRUCT shfopt;
    00331   MemSetT(&shfopt, 0);
    00332   shfopt.wFunc  = FO_MOVE;
    00333   shfopt.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI | FOF_SILENT;
    00334   shfopt.pFrom  = tfile_new;
    00335   shfopt.pTo    = tfilename;
    00336   SHFileOperation(&shfopt);
    00337 #else
    00338   rename(file_new, filename);
    00339 #endif
    00340
    00341   return true;
    00342 }