Implemented crossplatform+$HOME configuration file management.
[udpgate.git] / src / configuration.c
1 /* $Id$
2  * UDP Gateway persistent configuration
3  * Copyright (C) 2004 Jan Kratochvil <project-udpgate@jankratochvil.net>
4  * 
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; exactly version 2 of June 1991 is required
8  * 
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  * 
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17  */
18
19
20 #include "config.h"
21
22 #include <glib/gmessages.h>
23 #include <glib/ghash.h>
24 #include <glib/gstring.h>
25 #include <glib/gmem.h>
26 #include <stdio.h>
27 #include <errno.h>
28 #include <ctype.h>
29 #include <glib/gstrfuncs.h>
30 #include <string.h>
31
32 #include "configuration.h"      /* self */
33 #include "configuration-pathname.h"
34 #include "network.h"
35 #include "main.h"
36
37
38 /* OK, FIXME: GConf would be nice.
39  * Unfortunately the fully-static build does not support GConf process spawn at
40  * all.
41  */
42
43
44 /* Config: */
45 #define LOCATION_LINK "/proc/self/exe"  /* for Linux kernel */
46
47
48 static GHashTable *configuration_hash_new(void)
49 {
50         return g_hash_table_new_full(g_str_hash,g_str_equal,
51                         (GDestroyNotify)g_free, /* key_destroy_func; of g_strdup() strings */
52                         (GDestroyNotify)g_free);        /* value_destroy_func; of g_strdup() strings */
53 }
54
55 static GHashTable *configuration_comments_hash_new(void)
56 {
57 static GHashTable *hash;
58
59         if (!hash) {
60                 hash=g_hash_table_new(g_str_hash,g_str_equal);
61                 g_hash_table_insert(hash,"PORT",_("Local UDP port"));
62                 g_hash_table_insert(hash,"LOCATION",_("Binary location pathname"));
63                 }
64         return hash;
65 }
66
67 static gboolean configuration_file_write(const gchar *file_content)
68 {
69 FILE *f;
70 const gchar *pathname;
71
72         g_return_val_if_fail(file_content!=NULL,FALSE);
73
74         if (!(pathname=configuration_pathname()))
75                 return FALSE;
76         if (!(f=fopen(pathname,"w"))) {
77                 g_warning(_("Error write opening configuration file \"%s\": %m"),pathname);
78                 return FALSE;
79                 }
80         if (fputs(file_content,f)<0) {
81                 g_warning(_("Error writing configuration file \"%s\": %m"),pathname);
82                 fclose(f);      /* errors ignored */
83                 return FALSE;
84                 }
85         if (fclose(f))
86                 g_warning(_("Error closing configuration file \"%s\": %m"),pathname);
87         return TRUE;
88 }
89
90 struct configuration_hash_readwrite_hash_flush_foreach_param {
91         GString *gstring;
92         GHashTable *hash_flushed;
93         gboolean modified;
94         };
95 static void configuration_hash_readwrite_hash_flush_foreach
96                 (const gchar *key,const gchar *value,struct configuration_hash_readwrite_hash_flush_foreach_param *param)
97 {
98 const gchar *comment;
99
100         g_return_if_fail(key!=NULL);
101         g_return_if_fail(value!=NULL);
102         g_return_if_fail(param!=NULL);
103         g_return_if_fail(param->gstring!=NULL);
104         g_return_if_fail(param->hash_flushed!=NULL);
105
106         /* Already written out? */
107         if (g_hash_table_lookup(param->hash_flushed,key))
108                 return;
109         if (!(comment=g_hash_table_lookup(configuration_comments_hash_new(),key)))
110                 comment=_("*** UNDOCUMENTED ***");
111         g_string_append(param->gstring,udpgate_printf_alloca("# %s: %s\n%s=%s\n",key,comment,key,value));
112         param->modified=TRUE;
113         if (optarg_verbose)
114                 g_message(_("Appended configuration variable name \"%s\", value \"%s\""),key,value);
115 }
116
117 /* hash_flush==NULL => read info and return new (GHashTable *)
118  * hash_flush!=NULL => update configuration file by this (GHashTable *)
119  */
120 /* FIXME: File locking! */
121 static GHashTable *configuration_hash_readwrite(GHashTable *hash_flush)
122 {
123 FILE *f;
124 char line[LINE_MAX];
125 int lineno;
126 GHashTable *hash_fill;
127 /* Which 'keys' of 'hash_flush' were already written. 'key' must be g_free()able.
128  * Any 'value' is ignored. 'value' must be !=NULL. 'value' must be g_free()able.
129  */
130 GHashTable *hash_flushed;
131 GString *gstring;
132 gboolean modified=FALSE;        /* 'gstring' contains modified value */
133 gboolean already_written=FALSE;
134 const gchar *pathname;
135
136         if (!(pathname=configuration_pathname()))
137                 return NULL;
138 open_retry:
139         if (!(f=fopen(pathname,(!hash_flush ? "r" : "rw")))) {
140                 if (errno!=ENOENT)
141                         g_warning(_("Error r/o opening configuration file \"%s\": %m"),pathname);
142                 if (!hash_flush || already_written)
143                         return NULL;
144                 else {
145                         already_written=TRUE;
146                         if (!configuration_file_write(_(
147 "# Configuration file for UDPGate - see its man page udpgate(1).\n"
148 "\n"
149                                         )))
150                                 return NULL;
151                         goto open_retry;
152                         }
153                 }
154         if (!hash_flush) {
155                 hash_fill=configuration_hash_new();
156                 hash_flushed=NULL;
157                 gstring=NULL;
158                 }
159         else {
160                 hash_fill=NULL;
161                 hash_flushed=configuration_hash_new();
162                 gstring=g_string_new(NULL);
163                 }
164         lineno=0;
165         while (errno=0,fgets(line,sizeof(line),f)) {
166 char *s;
167 char *varname_start,*varname_stop,varname_stop_orig;
168 char *varcontent_start,*varcontent_stop,varcontent_stop_orig;
169 const gchar *value;
170
171                 lineno++;
172                 s=line;
173                 varname_stop=NULL;
174                 varcontent_stop=NULL;
175                 /* Parse: ^\s*([[:alnum:]])\s*=\s*(\S*)\s*$
176                  */
177                 while (*s && isspace(*s)) s++;
178                 if (!*s || *s=='#') {
179 err_append:
180                         if (gstring)
181                                 g_string_append(gstring,line);
182                         continue;
183                         }
184                 varname_start=s;
185                 while (*s && isalnum(*s)) s++;
186                 varname_stop=s;
187                 varname_stop_orig=*varname_stop;
188                 while (*s && isspace(*s)) s++;
189                 if (*s++!='=') {
190 err_line:
191                         if (varname_stop)
192                                 *varname_stop=varname_stop_orig;
193                         if (varcontent_stop)
194                                 *varcontent_stop=varcontent_stop_orig;
195                         g_warning(_("Error parsing line %d of the configuration file \"%s\": %s"),lineno,pathname,line);
196                         goto err_append;
197                         }
198                 while (*s && isspace(*s)) s++;
199                 varcontent_start=s;
200                 if (!*s)
201                         goto err_line;
202                 while (*s && !isspace(*s)) s++;
203                 varcontent_stop=s;
204                 varcontent_stop_orig=*varcontent_stop;
205                 while (*s && isspace(*s)) s++;
206                 if (*s && *s!='#')
207                         goto err_line;
208
209                 if (!*varname_start || !*varcontent_start)
210                         goto err_line;
211
212                 *varname_stop='\0';
213                 *varcontent_stop='\0';
214                 if (optarg_verbose)
215                         g_message(_("Parsed configuration variable name \"%s\", value \"%s\""),varname_start,varcontent_start);
216                 if (hash_fill)
217                         g_hash_table_insert(
218                                         hash_fill,      /* hash_table */
219                                         g_strdup(varname_start),        /* key */
220                                         g_strdup(varcontent_start));    /* value */
221                 if (hash_flush && (value=g_hash_table_lookup(hash_flush,varname_start))) {
222 const char *line_new=udpgate_printf_alloca("%s=%s\n",varname_start,value);
223
224                         *varname_stop=varname_stop_orig;
225                         *varcontent_stop=varcontent_stop_orig;
226                         if (strcmp(line,line_new))
227                                 modified=TRUE;
228                         *varname_stop=0;
229                         *varcontent_stop=0;
230                         g_string_append(gstring,line_new);
231                         g_hash_table_insert(hash_flushed,g_strdup(varname_start),g_strdup(varcontent_start));
232                         }
233                 else
234                         goto err_append;
235                 }
236         if (errno) {
237                 g_warning(_("Error reading line from the configuration file \"%s\": %s"),pathname,strerror(errno));
238                 if (hash_fill)
239                         g_hash_table_destroy(hash_fill);
240                 fclose(f);      /* errors ignored */
241                 return NULL;
242                 }
243         if (fclose(f))
244                 g_warning(_("Error closing configuration file \"%s\": %m"),pathname);
245         if (hash_flushed) {
246 struct configuration_hash_readwrite_hash_flush_foreach_param param;
247
248                 /* Append variable names not yet present in the file */
249                 param.gstring=gstring;
250                 param.hash_flushed=hash_flushed;
251                 param.modified=modified;
252                 g_hash_table_foreach(hash_flush,(GHFunc)configuration_hash_readwrite_hash_flush_foreach,&param);
253                 g_hash_table_destroy(hash_flushed);
254                 hash_flushed=NULL;
255                 modified=param.modified;
256                 }
257         if (gstring) {
258                 if (modified)
259                         configuration_file_write(gstring->str); /* errors ignored */
260                 g_string_free(gstring,
261                                 TRUE);  /* free_segment */
262                 gstring=NULL;
263                 }
264         return hash_fill;
265 }
266
267 static void configuration_read_hash_foreach(const gchar *key,const gchar *value,gpointer user_data)
268 {
269         g_return_if_fail(key!=NULL);
270         g_return_if_fail(value!=NULL);
271
272         /**/ if (!strcmp(key,"PORT"))
273                 optarg_port_set_string(value);  
274         else if (!strcmp(key,"LOCATION"))
275                 /* nop */;
276         else
277                 g_warning(_("Unknown configuration key \"%s\" with value \"%s\" found in the file \"%s\""),
278                                 key,value,configuration_pathname());
279 }
280
281 gboolean configuration_read(void)
282 {
283 GHashTable *hash;
284
285         if (!(hash=configuration_hash_readwrite(NULL)))
286                 return FALSE;
287         g_hash_table_foreach(hash,(GHFunc)configuration_read_hash_foreach,NULL);
288         g_hash_table_destroy(hash);
289         return TRUE;
290 }
291
292 static void location_insert(GHashTable *hash)
293 {
294 gchar buf[LINE_MAX];
295 int got;
296
297         g_return_if_fail(hash!=NULL);
298
299         /* FIXME: Support also argv[0] as a fallback. */
300         got=readlink(LOCATION_LINK,buf,sizeof(buf)-1);
301         if (got<=0 || got>=(int)(sizeof(buf)-1))
302                 return;
303         buf[got]='\0';
304         g_hash_table_insert(hash,"LOCATION",g_strdup(buf));
305 }
306
307 gboolean configuration_write(void)
308 {
309 GHashTable *hash;
310
311         hash=g_hash_table_new_full(g_str_hash,g_str_equal,
312                         (GDestroyNotify)NULL,   /* key_destroy_func */
313                         (GDestroyNotify)g_free);        /* value_destroy_func; of g_strdup() strings */
314         g_hash_table_insert(hash,"PORT",g_strdup_printf("%d",(int)optarg_port));
315         location_insert(hash);
316         configuration_hash_readwrite(hash);     /* FIXME: errors ignored */
317         g_hash_table_destroy(hash);
318         return TRUE;
319 }