1/*
2   +----------------------------------------------------------------------+
3   | PHP Version 5                                                        |
4   +----------------------------------------------------------------------+
5   | Copyright (c) 1997-2014 The PHP Group                                |
6   +----------------------------------------------------------------------+
7   | This source file is subject to version 3.01 of the PHP license,      |
8   | that is bundled with this package in the file LICENSE, and is        |
9   | available through the world-wide-web at the following url:           |
10   | http://www.php.net/license/3_01.txt                                  |
11   | If you did not receive a copy of the PHP license and are unable to   |
12   | obtain it through the world-wide-web, please send a note to          |
13   | license@php.net so we can mail you a copy immediately.               |
14   +----------------------------------------------------------------------+
15   | Author: Marcus Boerger <helly@php.net>                               |
16   +----------------------------------------------------------------------+
17 */
18
19/* $Id: 8c771beba4c19d8d610cee34342a0ee22806914c $ */
20
21#ifdef HAVE_CONFIG_H
22#include "config.h"
23#endif
24
25#include "php.h"
26#include "php_globals.h"
27
28#include <stdlib.h>
29#include <string.h>
30#include <errno.h>
31#if HAVE_UNISTD_H
32#include <unistd.h>
33#endif
34
35#include "inifile.h"
36
37/* ret = -1 means that database was opened for read-only
38 * ret = 0  success
39 * ret = 1  key already exists - nothing done
40 */
41
42/* {{{ inifile_version */
43char *inifile_version()
44{
45    return "1.0, $Id: 8c771beba4c19d8d610cee34342a0ee22806914c $";
46}
47/* }}} */
48
49/* {{{ inifile_free_key */
50void inifile_key_free(key_type *key)
51{
52    if (key->group) {
53        efree(key->group);
54    }
55    if (key->name) {
56        efree(key->name);
57    }
58    memset(key, 0, sizeof(key_type));
59}
60/* }}} */
61
62/* {{{ inifile_free_val */
63void inifile_val_free(val_type *val)
64{
65    if (val->value) {
66        efree(val->value);
67    }
68    memset(val, 0, sizeof(val_type));
69}
70/* }}} */
71
72/* {{{ inifile_free_val */
73void inifile_line_free(line_type *ln)
74{
75    inifile_key_free(&ln->key);
76    inifile_val_free(&ln->val);
77    ln->pos = 0;
78}
79/* }}} */
80
81/* {{{ inifile_alloc */
82inifile * inifile_alloc(php_stream *fp, int readonly, int persistent TSRMLS_DC)
83{
84    inifile *dba;
85
86    if (!readonly) {
87        if (!php_stream_truncate_supported(fp)) {
88            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Can't truncate this stream");
89            return NULL;
90        }
91    }
92
93    dba = pemalloc(sizeof(inifile), persistent);
94    memset(dba, 0, sizeof(inifile));
95    dba->fp = fp;
96    dba->readonly = readonly;
97    return dba;
98}
99/* }}} */
100
101/* {{{ inifile_free */
102void inifile_free(inifile *dba, int persistent)
103{
104    if (dba) {
105        inifile_line_free(&dba->curr);
106        inifile_line_free(&dba->next);
107        pefree(dba, persistent);
108    }
109}
110/* }}} */
111
112/* {{{ inifile_key_split */
113key_type inifile_key_split(const char *group_name)
114{
115    key_type key;
116    char *name;
117
118    if (group_name[0] == '[' && (name = strchr(group_name, ']')) != NULL) {
119        key.group = estrndup(group_name+1, name - (group_name + 1));
120        key.name = estrdup(name+1);
121    } else {
122        key.group = estrdup("");
123        key.name = estrdup(group_name);
124    }
125    return key;
126}
127/* }}} */
128
129/* {{{ inifile_key_string */
130char * inifile_key_string(const key_type *key)
131{
132    if (key->group && *key->group) {
133        char *result;
134        spprintf(&result, 0, "[%s]%s", key->group, key->name ? key->name : "");
135        return result;
136    } else if (key->name) {
137        return estrdup(key->name);
138    } else {
139        return NULL;
140    }
141}
142/* }}} */
143
144/* {{{ etrim */
145static char *etrim(const char *str)
146{
147    char *val;
148    size_t l;
149
150    if (!str) {
151        return NULL;
152    }
153    val = (char*)str;
154    while (*val && strchr(" \t\r\n", *val)) {
155        val++;
156    }
157    l = strlen(val);
158    while (l && (strchr(" \t\r\n", val[l-1]))) {
159        l--;
160    }
161    return estrndup(val, l);
162}
163/* }}} */
164
165/* {{{ inifile_findkey
166 */
167static int inifile_read(inifile *dba, line_type *ln TSRMLS_DC) {
168    char *fline;
169    char *pos;
170
171    inifile_val_free(&ln->val);
172    while ((fline = php_stream_gets(dba->fp, NULL, 0)) != NULL) {
173        if (fline) {
174            if (fline[0] == '[') {
175                /* A value name cannot start with '['
176                 * So either we find a ']' or we found an error
177                 */
178                pos = strchr(fline+1, ']');
179                if (pos) {
180                    *pos = '\0';
181                    inifile_key_free(&ln->key);
182                    ln->key.group = etrim(fline+1);
183                    ln->key.name = estrdup("");
184                    ln->pos = php_stream_tell(dba->fp);
185                    efree(fline);
186                    return 1;
187                } else {
188                    efree(fline);
189                    continue;
190                }
191            } else {
192                pos = strchr(fline, '=');
193                if (pos) {
194                    *pos = '\0';
195                    /* keep group or make empty if not existent */
196                    if (!ln->key.group) {
197                        ln->key.group = estrdup("");
198                    }
199                    if (ln->key.name) {
200                        efree(ln->key.name);
201                    }
202                    ln->key.name = etrim(fline);
203                    ln->val.value = etrim(pos+1);
204                    ln->pos = php_stream_tell(dba->fp);
205                    efree(fline);
206                    return 1;
207                } else {
208                    /* simply ignore lines without '='
209                     * those should be comments
210                     */
211                     efree(fline);
212                     continue;
213                }
214            }
215        }
216    }
217    inifile_line_free(ln);
218    return 0;
219}
220/* }}} */
221
222/* {{{ inifile_key_cmp */
223/* 0 = EQUAL
224 * 1 = GROUP-EQUAL,NAME-DIFFERENT
225 * 2 = DIFFERENT
226 */
227static int inifile_key_cmp(const key_type *k1, const key_type *k2 TSRMLS_DC)
228{
229    assert(k1->group && k1->name && k2->group && k2->name);
230
231    if (!strcasecmp(k1->group, k2->group)) {
232        if (!strcasecmp(k1->name, k2->name)) {
233            return 0;
234        } else {
235            return 1;
236        }
237    } else {
238        return 2;
239    }
240}
241/* }}} */
242
243/* {{{ inifile_fetch
244 */
245val_type inifile_fetch(inifile *dba, const key_type *key, int skip TSRMLS_DC) {
246    line_type ln = {{NULL,NULL},{NULL}};
247    val_type val;
248    int res, grp_eq = 0;
249
250    if (skip == -1 && dba->next.key.group && dba->next.key.name && !inifile_key_cmp(&dba->next.key, key TSRMLS_CC)) {
251        /* we got position already from last fetch */
252        php_stream_seek(dba->fp, dba->next.pos, SEEK_SET);
253    } else {
254        /* specific instance or not same key -> restart search */
255        /* the slow way: restart and seacrch */
256        php_stream_rewind(dba->fp);
257        inifile_line_free(&dba->next);
258    }
259    if (skip == -1) {
260        skip = 0;
261    }
262    while(inifile_read(dba, &ln TSRMLS_CC)) {
263        if (!(res=inifile_key_cmp(&ln.key, key TSRMLS_CC))) {
264            if (!skip) {
265                val.value = estrdup(ln.val.value ? ln.val.value : "");
266                /* allow faster access by updating key read into next */
267                inifile_line_free(&dba->next);
268                dba->next = ln;
269                dba->next.pos = php_stream_tell(dba->fp);
270                return val;
271            }
272            skip--;
273        } else if (res == 1) {
274            grp_eq = 1;
275        } else if (grp_eq) {
276            /* we are leaving group now: that means we cannot find the key */
277            break;
278        }
279    }
280    inifile_line_free(&ln);
281    dba->next.pos = php_stream_tell(dba->fp);
282    return ln.val;
283}
284/* }}} */
285
286/* {{{ inifile_firstkey
287 */
288int inifile_firstkey(inifile *dba TSRMLS_DC) {
289    inifile_line_free(&dba->curr);
290    dba->curr.pos = 0;
291    return inifile_nextkey(dba TSRMLS_CC);
292}
293/* }}} */
294
295/* {{{ inifile_nextkey
296 */
297int inifile_nextkey(inifile *dba TSRMLS_DC) {
298    line_type ln = {{NULL,NULL},{NULL}};
299
300    /*inifile_line_free(&dba->next); ??? */
301    php_stream_seek(dba->fp, dba->curr.pos, SEEK_SET);
302    ln.key.group = estrdup(dba->curr.key.group ? dba->curr.key.group : "");
303    inifile_read(dba, &ln TSRMLS_CC);
304    inifile_line_free(&dba->curr);
305    dba->curr = ln;
306    return ln.key.group || ln.key.name;
307}
308/* }}} */
309
310/* {{{ inifile_truncate
311 */
312static int inifile_truncate(inifile *dba, size_t size TSRMLS_DC)
313{
314    int res;
315
316    if ((res=php_stream_truncate_set_size(dba->fp, size)) != 0) {
317        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error in ftruncate: %d", res);
318        return FAILURE;
319    }
320    php_stream_seek(dba->fp, size, SEEK_SET);
321    return SUCCESS;
322}
323/* }}} */
324
325/* {{{ inifile_find_group
326 * if found pos_grp_start points to "[group_name]"
327 */
328static int inifile_find_group(inifile *dba, const key_type *key, size_t *pos_grp_start TSRMLS_DC)
329{
330    int ret = FAILURE;
331
332    php_stream_flush(dba->fp);
333    php_stream_seek(dba->fp, 0, SEEK_SET);
334    inifile_line_free(&dba->curr);
335    inifile_line_free(&dba->next);
336
337    if (key->group && strlen(key->group)) {
338        int res;
339        line_type ln = {{NULL,NULL},{NULL}};
340
341        res = 1;
342        while(inifile_read(dba, &ln TSRMLS_CC)) {
343            if ((res=inifile_key_cmp(&ln.key, key TSRMLS_CC)) < 2) {
344                ret = SUCCESS;
345                break;
346            }
347            *pos_grp_start = php_stream_tell(dba->fp);
348        }
349        inifile_line_free(&ln);
350    } else {
351        *pos_grp_start = 0;
352        ret = SUCCESS;
353    }
354    if (ret == FAILURE) {
355        *pos_grp_start = php_stream_tell(dba->fp);
356    }
357    return ret;
358}
359/* }}} */
360
361/* {{{ inifile_next_group
362 * only valid after a call to inifile_find_group
363 * if any next group is found pos_grp_start points to "[group_name]" or whitespace before that
364 */
365static int inifile_next_group(inifile *dba, const key_type *key, size_t *pos_grp_start TSRMLS_DC)
366{
367    int ret = FAILURE;
368    line_type ln = {{NULL,NULL},{NULL}};
369
370    *pos_grp_start = php_stream_tell(dba->fp);
371    ln.key.group = estrdup(key->group);
372    while(inifile_read(dba, &ln TSRMLS_CC)) {
373        if (inifile_key_cmp(&ln.key, key TSRMLS_CC) == 2) {
374            ret = SUCCESS;
375            break;
376        }
377        *pos_grp_start = php_stream_tell(dba->fp);
378    }
379    inifile_line_free(&ln);
380    return ret;
381}
382/* }}} */
383
384/* {{{ inifile_copy_to
385 */
386static int inifile_copy_to(inifile *dba, size_t pos_start, size_t pos_end, inifile **ini_copy TSRMLS_DC)
387{
388    php_stream *fp;
389
390    if (pos_start == pos_end) {
391        *ini_copy = NULL;
392        return SUCCESS;
393    }
394    if ((fp = php_stream_temp_create(0, 64 * 1024)) == NULL) {
395        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not create temporary stream");
396        *ini_copy = NULL;
397        return FAILURE;
398    }
399
400    if ((*ini_copy = inifile_alloc(fp, 1, 0 TSRMLS_CC)) == NULL) {
401        /* writes error */
402        return FAILURE;
403    }
404    php_stream_seek(dba->fp, pos_start, SEEK_SET);
405    if (!php_stream_copy_to_stream_ex(dba->fp, fp, pos_end - pos_start, NULL)) {
406        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not copy group [%zu - %zu] to temporary stream", pos_start, pos_end);
407        return FAILURE;
408    }
409    return SUCCESS;
410}
411/* }}} */
412
413/* {{{ inifile_filter
414 * copy from to dba while ignoring key name (group must equal)
415 */
416static int inifile_filter(inifile *dba, inifile *from, const key_type *key TSRMLS_DC)
417{
418    size_t pos_start = 0, pos_next = 0, pos_curr;
419    int ret = SUCCESS;
420    line_type ln = {{NULL,NULL},{NULL}};
421
422    php_stream_seek(from->fp, 0, SEEK_SET);
423    php_stream_seek(dba->fp, 0, SEEK_END);
424    while(inifile_read(from, &ln TSRMLS_CC)) {
425        switch(inifile_key_cmp(&ln.key, key TSRMLS_CC)) {
426        case 0:
427            pos_curr = php_stream_tell(from->fp);
428            if (pos_start != pos_next) {
429                php_stream_seek(from->fp, pos_start, SEEK_SET);
430                if (!php_stream_copy_to_stream_ex(from->fp, dba->fp, pos_next - pos_start, NULL)) {
431                    php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not copy [%zu - %zu] from temporary stream", pos_next, pos_start);
432                    ret = FAILURE;
433                }
434                php_stream_seek(from->fp, pos_curr, SEEK_SET);
435            }
436            pos_next = pos_start = pos_curr;
437            break;
438        case 1:
439            pos_next = php_stream_tell(from->fp);
440            break;
441        case 2:
442            /* the function is meant to process only entries from same group */
443            assert(0);
444            break;
445        }
446    }
447    if (pos_start != pos_next) {
448        php_stream_seek(from->fp, pos_start, SEEK_SET);
449        if (!php_stream_copy_to_stream_ex(from->fp, dba->fp, pos_next - pos_start, NULL)) {
450            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not copy [%zu - %zu] from temporary stream", pos_next, pos_start);
451            ret = FAILURE;
452        }
453    }
454    inifile_line_free(&ln);
455    return ret;
456}
457/* }}} */
458
459/* {{{ inifile_delete_replace_append
460 */
461static int inifile_delete_replace_append(inifile *dba, const key_type *key, const val_type *value, int append TSRMLS_DC)
462{
463    size_t pos_grp_start=0, pos_grp_next;
464    inifile *ini_tmp = NULL;
465    php_stream *fp_tmp = NULL;
466    int ret;
467
468    /* 1) Search group start
469     * 2) Search next group
470     * 3) If not append: Copy group to ini_tmp
471     * 4) Open temp_stream and copy remainder
472     * 5) Truncate stream
473     * 6) If not append AND key.name given: Filtered copy back from ini_tmp
474     *    to stream. Otherwise the user wanted to delete the group.
475     * 7) Append value if given
476     * 8) Append temporary stream
477     */
478
479    assert(!append || (key->name && value)); /* missuse */
480
481    /* 1 - 3 */
482    inifile_find_group(dba, key, &pos_grp_start TSRMLS_CC);
483    inifile_next_group(dba, key, &pos_grp_next TSRMLS_CC);
484    if (append) {
485        ret = SUCCESS;
486    } else {
487        ret = inifile_copy_to(dba, pos_grp_start, pos_grp_next, &ini_tmp TSRMLS_CC);
488    }
489
490    /* 4 */
491    if (ret == SUCCESS) {
492        fp_tmp = php_stream_temp_create(0, 64 * 1024);
493        if (!fp_tmp) {
494            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not create temporary stream");
495            ret = FAILURE;
496        } else {
497            php_stream_seek(dba->fp, 0, SEEK_END);
498            if (pos_grp_next != (size_t)php_stream_tell(dba->fp)) {
499                php_stream_seek(dba->fp, pos_grp_next, SEEK_SET);
500                if (!php_stream_copy_to_stream_ex(dba->fp, fp_tmp, PHP_STREAM_COPY_ALL, NULL)) {
501                    php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not copy remainder to temporary stream");
502                    ret = FAILURE;
503                }
504            }
505        }
506    }
507
508    /* 5 */
509    if (ret == SUCCESS) {
510        if (!value || (key->name && strlen(key->name))) {
511            ret = inifile_truncate(dba, append ? pos_grp_next : pos_grp_start TSRMLS_CC); /* writes error on fail */
512        }
513    }
514
515    if (ret == SUCCESS) {
516        if (key->name && strlen(key->name)) {
517            /* 6 */
518            if (!append && ini_tmp) {
519                ret = inifile_filter(dba, ini_tmp, key TSRMLS_CC);
520            }
521
522            /* 7 */
523            /* important: do not query ret==SUCCESS again: inifile_filter might fail but
524             * however next operation must be done.
525             */
526            if (value) {
527                if (pos_grp_start == pos_grp_next && key->group && strlen(key->group)) {
528                    php_stream_printf(dba->fp TSRMLS_CC, "[%s]\n", key->group);
529                }
530                php_stream_printf(dba->fp TSRMLS_CC, "%s=%s\n", key->name, value->value ? value->value : "");
531            }
532        }
533
534        /* 8 */
535        /* important: do not query ret==SUCCESS again: inifile_filter might fail but
536         * however next operation must be done.
537         */
538        if (fp_tmp && php_stream_tell(fp_tmp)) {
539            php_stream_seek(fp_tmp, 0, SEEK_SET);
540            php_stream_seek(dba->fp, 0, SEEK_END);
541            if (!php_stream_copy_to_stream_ex(fp_tmp, dba->fp, PHP_STREAM_COPY_ALL, NULL)) {
542                php_error_docref(NULL TSRMLS_CC, E_RECOVERABLE_ERROR, "Could not copy from temporary stream - ini file truncated");
543                ret = FAILURE;
544            }
545        }
546    }
547
548    if (ini_tmp) {
549        php_stream_close(ini_tmp->fp);
550        inifile_free(ini_tmp, 0);
551    }
552    if (fp_tmp) {
553        php_stream_close(fp_tmp);
554    }
555    php_stream_flush(dba->fp);
556    php_stream_seek(dba->fp, 0, SEEK_SET);
557
558    return ret;
559}
560/* }}} */
561
562/* {{{ inifile_delete
563 */
564int inifile_delete(inifile *dba, const key_type *key TSRMLS_DC)
565{
566    return inifile_delete_replace_append(dba, key, NULL, 0 TSRMLS_CC);
567}
568/* }}} */
569
570/* {{{ inifile_relace
571 */
572int inifile_replace(inifile *dba, const key_type *key, const val_type *value TSRMLS_DC)
573{
574    return inifile_delete_replace_append(dba, key, value, 0 TSRMLS_CC);
575}
576/* }}} */
577
578/* {{{ inifile_append
579 */
580int inifile_append(inifile *dba, const key_type *key, const val_type *value TSRMLS_DC)
581{
582    return inifile_delete_replace_append(dba, key, value, 1 TSRMLS_CC);
583}
584/* }}} */
585
586/*
587 * Local variables:
588 * tab-width: 4
589 * c-basic-offset: 4
590 * End:
591 * vim600: sw=4 ts=4 fdm=marker
592 * vim<600: sw=4 ts=4
593 */
594