dav/scfg.c

Thu, 28 Nov 2024 17:20:19 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Thu, 28 Nov 2024 17:20:19 +0100
changeset 849
c949c6ab5346
parent 816
839fefbdedc7
permissions
-rw-r--r--

fix crash in printxmldoc, fixes #514

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2019 Olaf Wintermann. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <libidav/utils.h>
#include <cx/hash_map.h>
#include <cx/utils.h>
#include <cx/linked_list.h>
#include <cx/printf.h>

#include "scfg.h"
#include "config.h"

#define xstreq(a,b) xmlStrEqual(BAD_CAST a, BAD_CAST b)


#define print_error(lineno, ...) \
    do {\
        fprintf(stderr, "Error (sync.xml line %u): ", lineno); \
        fprintf(stderr, __VA_ARGS__); \
        fprintf(stderr, "Abort.\n"); \
    } while(0);
#define print_warning(lineno, ...) \
    do {\
        fprintf(stderr, "Warning (sync.xml line %u): ", lineno); \
        fprintf(stderr, __VA_ARGS__); \
    } while(0);

#ifdef _WIN32
#define ENV_HOME getenv("USERPROFILE")
#else
#define ENV_HOME getenv("HOME")
#endif /* _WIN32 */

static CxMap *directories;

CxIterator scfg_directory_iterator() {
    return cxMapIteratorValues(directories);
}

static int create_default_sync_config(char *file) {
    FILE *out = fopen(file, "w");
    if(!out) {
        perror("Cannot create config file");
        return -1;
    }
    
    fputs("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n", out);
    fputs("<configuration>\n", out);
    fputs("</configuration>\n", out);
    fclose(out);
    
    return 0;
}

static void add_regex_pattern(CxList *list, char *value,
        unsigned short xmlline) {
    regex_t regex;
    if (regcomp(&regex, value, REG_EXTENDED|REG_NOSUB)) {
        print_warning(xmlline,
                "Invalid regular expression (%s) ... skipped\n", value);
    } else {
        cxListAdd(list, &regex);
    }
}

static int scfg_load_filter(
        xmlNode *node,
        CxList *include,
        CxList *exclude,
        CxList *tags)
{
    node = node->children;
    
    while(node) {
        if(node->type == XML_ELEMENT_NODE) {
            char *value = util_xml_get_text(node);
            if(xstreq(node->name, "include")) {
                if(value) {
                    add_regex_pattern(include, value, node->line);
                }
            } else if(xstreq(node->name, "exclude")) {
                if(value) {
                    add_regex_pattern(exclude, value, node->line);
                }
            } else if(xstreq(node->name, "tags")) {
                if(value) {
                    SyncTagFilter *tagfilter = parse_tagfilter_string(
                            value, DAV_SYNC_TAGFILTER_SCOPE_RESOURCE);
                    if(!tagfilter) {
                        print_error(
                                node->line,
                                "malformed tag filter: %s\n",
                                value);
                        return 1;
                    } else {
                        // get scope
                        xmlChar *scope = xmlGetNoNsProp(node, BAD_CAST "scope");
                        if(scope) {
                            if(xstreq(scope, "resource"))
                            {
                                tagfilter->scope = 
                                        DAV_SYNC_TAGFILTER_SCOPE_RESOURCE;
                            } else if(xstreq(scope, "collection")) {
                                tagfilter->scope = 
                                        DAV_SYNC_TAGFILTER_SCOPE_COLLECTION;
                            } else if(xstreq(scope, "all")) {
                                tagfilter->scope = 
                                        DAV_SYNC_TAGFILTER_SCOPE_RESOURCE
                                      | DAV_SYNC_TAGFILTER_SCOPE_COLLECTION;
                            } else {
                                tagfilter->scope =
                                        DAV_SYNC_TAGFILTER_SCOPE_RESOURCE;
                            }
                        }
                        xmlFree(scope);
                        
                        cxListAdd(tags, tagfilter);
                    }
                }
            } else {
                print_error(node->line,
                        "unknown filter config element: %s\n", node->name);
                return 1;
            }
            if(!value) {
                print_error(node->line,
                        "missing value for filter: %s\n", node->name);
                return 1;
            }
        }
        
        node = node->next;
    }
    
    return 0;
}

Filter* parse_filter(xmlNode *node) {
    CxList *include = cxLinkedListCreate(cxDefaultAllocator, NULL, sizeof(regex_t));
    CxList *exclude = cxLinkedListCreate(cxDefaultAllocator, NULL, sizeof(regex_t));
    CxList *tags = cxLinkedListCreate(cxDefaultAllocator, NULL, CX_STORE_POINTERS);
    
    cxDefineDestructor(include, regfree);
    cxDefineDestructor(exclude, regfree);
    // TODO: set tags destructor
    
    if(scfg_load_filter(node, include, exclude, tags)) {
        return NULL;
    }
    
    Filter *filter = malloc(sizeof(Filter));
    filter->include = include;
    filter->exclude = exclude;
    filter->tags = tags;
    return filter;
}

void init_default_filter(Filter *filter) {
    if(cxListSize(filter->include) == 0) {
        regex_t matchall;
        regcomp(&matchall, ".*", REG_NOSUB);
        cxListAdd(filter->include, &matchall);
    }
    /*
    if(!filter->exclude) {
        regex_t *matchnothing = malloc(sizeof(regex_t));
        regcomp(matchnothing, "///", REG_NOSUB);
        filter->exclude = ucx_list_append(NULL, matchnothing);
    }
    */
}

static TagFormat str2tagformat(const char *str) {
    if(!strcmp(str, "text")) {
        return TAG_FORMAT_TEXT;
    } else if(!strcmp(str, "csv")) {
        return TAG_FORMAT_CSV;
    } else if(!strcmp(str, "xml")) {
        return TAG_FORMAT_XML;
    } else if(!strcmp(str, "macos")) {
        return TAG_FORMAT_MACOS;
    }
    return TAG_FORMAT_UNKNOWN;
}

#define CHECK_VALUE_RET_NULL(element, value) if(!(value)) \
        {print_error(element->line, "missing value element: %s\n", element->name); return NULL;}

static TagConfig* parse_tagconfig(xmlNode *node) {
    TagConfig conf;
    conf.store = TAG_STORE_XATTR;
    conf.local_format = TAG_FORMAT_TEXT;
    conf.server_format = TAG_FORMAT_XML;
    conf.xattr_name = NULL;
    conf.detect_changes = false;
    conf.conflict = TAG_NO_CONFLICT;
    xmlNode *c = node->children;
    
    // TODO: error handling
    while(c) {
        if(c->type == XML_ELEMENT_NODE) {
            char *value = util_xml_get_text(c);
            if(xstreq(c->name, "local-store")) {
                CHECK_VALUE_RET_NULL(c, value);
                if(xstreq(value, "xattr")) {
                    conf.store = TAG_STORE_XATTR;
                } else {
                    return NULL;
                }
                
                xmlChar *format = xmlGetNoNsProp(node, BAD_CAST "format");
                if(format) {
                    conf.local_format = str2tagformat((char*)format);
                    xmlFree(format);
                }
            } else if(xstreq(c->name, "detect-changes")) {
                CHECK_VALUE_RET_NULL(c, value);
                conf.detect_changes = util_getboolean(value);
            } else if(xstreq(c->name, "xattr-name")) {
                if(!value) {
                    return NULL;
                }
                conf.xattr_name = strdup(value);
            } else if(xstreq(c->name, "on-conflict")) {
                CHECK_VALUE_RET_NULL(c, value);
                if(xstreq(value, "no_conflict")) {
                    conf.conflict = TAG_NO_CONFLICT;
                } else if(xstreq(value, "keep_local")) {
                    conf.conflict = TAG_KEEP_LOCAL;
                } else if(xstreq(value, "keep_remote")) {
                    conf.conflict = TAG_KEEP_REMOTE;
                } else if(xstreq(value, "merge")) {
                    conf.conflict = TAG_MERGE;
                } else {
                    fprintf(stderr, "on-conflict: unknown value: %s\n", value);
                    return NULL;
                }
            }
        }
        c = c->next;
    }
    
    if(conf.store == TAG_STORE_XATTR && !conf.xattr_name) {
        switch(conf.local_format) {
            default:
            case TAG_FORMAT_TEXT: 
            case TAG_FORMAT_CSV: 
            case TAG_FORMAT_XML: conf.xattr_name = strdup(DEFAULT_TAG_XATTR); break;
            case TAG_FORMAT_MACOS: conf.xattr_name = strdup(MACOS_TAG_XATTR); break;
        }
    }
    
    TagConfig *tagconfig = malloc(sizeof(TagConfig));
    *tagconfig = conf;
    return tagconfig;
}

static SplitConfig* parse_split(xmlNode *node) {
    Filter *filter = NULL;
    char *minsize = NULL;
    char *blocksize = NULL;
    
    xmlNode *c = node->children;
    while(c) {
        if(xstreq(c->name, "filter")) {
            filter = parse_filter(node);
            if(filter->tags) {
                fprintf(stderr, "splitconfig: tag filter not supported\n");
                free_filter(*filter);
                free(filter);
                return NULL;
            }
        } else if(xstreq(c->name, "minsize")) {
            minsize = util_xml_get_text(c);
        } else if(xstreq(c->name, "blocksize")) {
            blocksize = util_xml_get_text(c);
        }
        c = c->next;
    }
    
    uint64_t sz = 0;
    if(!blocksize) {
        fprintf(stderr, "splitconfig: no blocksize specified\n");
        return NULL;
    }
    size_t bsz_len = strlen(blocksize);
    if(bsz_len < 2) {
        fprintf(stderr, "splitconfig: blocksize too small\n");
        return NULL;
    }
    
    if(!util_szstrtouint(blocksize, &sz)) {
        fprintf(stderr, "splitconfig: blocksize is not a number\n");
        return NULL;
    }
    
    if(!filter && !minsize) {
        fprintf(stderr, "splitconfig: filter or minsize must be specified\n");
        return NULL;
    }
    
    int64_t minsz = -1;
    if(minsize) {
        uint64_t m;
        if(!util_szstrtouint(minsize, &m)) {
            fprintf(stderr, "splitconfig: minsize is not a number\n");
            return NULL;
        }
        minsz = (int64_t)m;
    }
    
    SplitConfig *sc = calloc(1, sizeof(SplitConfig));
    if(filter) {
        init_default_filter(filter);
    }
    sc->minsize = minsz;
    sc->blocksize = (size_t)sz;
    return sc;    
}

static CxList* parse_splitconfig(xmlNode *node, int *error) {
    CxList *splitconfig = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    int err = 0;
    xmlNode *c = node->children;
    while(c) {
        if(c->type == XML_ELEMENT_NODE && xstreq(c->name, "split")) {
            SplitConfig *sc = parse_split(c);
            if(sc) {
                cxListAdd(splitconfig, sc);
            } else {
                err = 1;
                break;
            }
        }
        c = c->next;
    }
    
    if(error) {
        *error = err;
    }
    return splitconfig;
}

static Versioning* parse_versioning_config(xmlNode *node) {
    Versioning v;
    v.always = FALSE;
    v.type = VERSIONING_SIMPLE;
    v.collection = VERSIONING_DEFAULT_PATH;
    
    int err = 0;
    
    xmlChar *attr_type = xmlGetNoNsProp(node, BAD_CAST "type");
    xmlChar *attr_always = xmlGetNoNsProp(node, BAD_CAST "always");
    
    if(attr_type) {
        if(xstreq(attr_type, "simple")) {
            v.type = VERSIONING_SIMPLE;
        } else if(xstreq(attr_type, "deltav")) {
            v.type = VERSIONING_DELTAV;
        } else {
            print_error(node->line,
                        "type attribute: unknown value: %s\n",
                        attr_type);
            err = 1;
        }
        xmlFree(attr_type);
    }
    if(attr_always) {
        v.always = util_getboolean((const char*)attr_always);
        xmlFree(attr_always);
    }
    
    if(err) {
        return NULL;
    }
    
    xmlNode *c = node->children;
    while(c) {
        if(c->type == XML_ELEMENT_NODE) {
            char *value = util_xml_get_text(c);
            if(xstreq(c->name, "history")) {
                CHECK_VALUE_RET_NULL(c, value);
                v.collection = value;
            } 
        }
        c = c->next;
    }
    
    v.collection = strdup(v.collection); 
    
    Versioning *versioning = malloc(sizeof(Versioning));
    *versioning = v;
    return versioning;
}

static int scfg_load_directory(xmlNode *node) {
    char *name = NULL;
    char *path = NULL;
    char *trash = NULL;
    char *collection = NULL;
    char *repository = NULL;
    char *database = NULL;
    char *logfile = NULL;
    TagConfig *tagconfig = NULL;
    Versioning *versioning = NULL;
    CxList *include = cxLinkedListCreateSimple(sizeof(regex_t));
    CxList *exclude = cxLinkedListCreateSimple(sizeof(regex_t));
    CxList *tagfilter = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    CxList *splitconfig = NULL;
    int max_retry = 0;
    int allow_cmd = SYNC_CMD_PULL | SYNC_CMD_PUSH
                  | SYNC_CMD_ARCHIVE | SYNC_CMD_RESTORE;
    bool backuppull = false;
    bool lockpull = false;
    bool lockpush = false;
    bool hashing = false;
    bool store_hash = false;
    bool pull_skip_hashing = false;
    //bool detect_copy = false;
    time_t lock_timeout = 0;
    uint32_t metadata = 0;
    uint32_t symlink = 0;
    PushStrategy pushstrat = PUSH_STRATEGY_METADATA;
    
    unsigned short parentlineno = node->line;
    node = node->children;
    while(node) {
        if(node->type == XML_ELEMENT_NODE) {
            char *value = util_xml_get_text(node);
            /* every key needs a value */
            if(!value && !xstreq(node->name, "versioning")) {
                // TODO: only report if value is required
                print_error(node->line,
                        "missing value for directory element: %s\n",
                        node->name);
                return 1;
            }
            if(xstreq(node->name, "name")) {
                name = value;
            } else if(xstreq(node->name, "path")) {
                path = value;
            } else if(xstreq(node->name, "trash")) {
                trash = value;
            } else if(xstreq(node->name, "collection")) {
                collection = value;
            } else if(xstreq(node->name, "repository")) {
                repository = value;
            } else if(xstreq(node->name, "filter")) {
                if(scfg_load_filter(node, include, exclude, tagfilter)) {
                    return 1;
                }
            } else if(xstreq(node->name, "database")) {
                database = value;
            } else if(xstreq(node->name, "logfile")) {
                logfile = value;
            } else if(xstreq(node->name, "tagconfig")) {
                tagconfig = parse_tagconfig(node);
            } else if(xstreq(node->name, "splitconfig")) {
                int err = 0;
                splitconfig = parse_splitconfig(node, &err);
                if(err) {
                    return 1;
                }
            } else if(xstreq(node->name, "metadata")) {
                uint32_t md = 0;
                
                const char *delims = " ,\t\r\n";
                char *metadatastr = strdup(value);
                char *m = strtok(metadatastr, delims);
                while(m) {
                    if(!strcmp(m, "mtime")) {
                        md |= FINFO_MTIME;
                    } else if(!strcmp(m, "mode")) {
                        md |= FINFO_MODE;
                    } else if(!strcmp(m, "owner")) {
                        md |= FINFO_OWNER;
                    } else if(!strcmp(m, "xattr")) {
                        md |= FINFO_XATTR;
                    } else if(!strcmp(m, "all")) {
                        md |= FINFO_MTIME | FINFO_MODE | FINFO_OWNER | FINFO_XATTR;
                    }
                    m = strtok(NULL, delims);
                }
                free(metadatastr);
                
                metadata = md;
            } else if(xstreq(node->name, "versioning")) {
                versioning = parse_versioning_config(node);
            } else if(xstreq(node->name, "max-retry")) {
                int64_t i;
                if(util_strtoint(value, &i) && i >= 0) {
                    max_retry = (int)i;
                } else {
                    print_warning(node->line, "unsigned integer value "
                            "expected in <max-retry> element\n");
                }
            } else if(xstreq(node->name, "allow-cmd")) {
                int cmds = 0;
                const char *delims = " ,\t\r\n";
                char *cmdstr = strdup(value);
                char *cmd = strtok(cmdstr, delims);
                while(cmd) {
                    if(!strcmp(cmd, "pull")) {
                        cmds |= SYNC_CMD_PULL;
                    } else if(!strcmp(cmd, "push")) {
                        cmds |= SYNC_CMD_PUSH;
                    } else if(!strcmp(cmd, "archive")) {
                        cmds |= SYNC_CMD_ARCHIVE;
                    } else if(!strcmp(cmd, "restore")) {
                        cmds |= SYNC_CMD_RESTORE;
                    }
                    cmd = strtok(NULL, delims);
                }
                free(cmdstr);
                allow_cmd = cmds;
                
            } else if(xstreq(node->name, "backup-on-pull")) {
                backuppull = util_getboolean(value);
            } else if(xstreq(node->name, "lock-pull")) {
                lockpull = util_getboolean(value);
            } else if(xstreq(node->name, "lock-push")) {
                lockpush = util_getboolean(value);
            } else if(xstreq(node->name, "lock-timeout")) {
                int64_t t = 0;
                if(util_strtoint(value, &t)) {
                    lock_timeout = (time_t)t;
                } else {
                    print_warning(node->line, "integer value "
                            "expected in <lock-timeout> element\n");
                }
            } else if(xstreq(node->name, "hashing")) {
                hashing = util_getboolean(value);
                store_hash = hashing;
            } else if(xstreq(node->name, "push-strategy")) {
                if(value) {
                    if(xstreq(value, "metadata")) {
                        pushstrat = PUSH_STRATEGY_METADATA;
                    } else if(xstreq(value, "hash")) {
                        pushstrat = PUSH_STRATEGY_HASH;
                    }
                }
            } else if(xstreq(node->name, "symlink-intern")) {
                if(!value) {
                    print_error(node->line, "missing value");
                } else if(xstreq(value, "sync")) {
                    symlink |= SYNC_SYMLINK_SYNC;
                } else if(xstreq(value, "follow")) {
                    // nothing, default
                } else if(xstreq(value, "ignore")) {
                    symlink |= SYNC_SYMLINK_IGNORE_INTERN;
                } else {
                    print_error(node->line,
                        "unknown value: %s\n", value);
                }
            } else if(xstreq(node->name, "symlink-extern")) {
                if(!value) {
                    print_error(node->line, "missing value");
                } else if(xstreq(value, "follow")) {
                    // nothing, default
                } else if(xstreq(value, "ignore")) {
                    symlink |= SYNC_SYMLINK_IGNORE_EXTERN;
                } else {
                    print_error(node->line,
                        "unknown value: %s\n", value);
                }
            } else {
                print_error(node->line,
                        "unknown directory config element: %s\n", node->name);
                return 1;
            }
        }
        node = node->next;
    }
    
    if(!name) {
        print_error(parentlineno, "missing name element for directory\n");
        return 1;
    }
    if(!path) {
        print_error(parentlineno,
                "missing path element for directory %s\n", name);
        return 1;
    }
    if(!repository) {
        print_error(parentlineno,
                "missing repository element for directory %s\n", name);
        return 1;
    }
    if(!database) {
        print_error(parentlineno,
                "missing database element for directory %s\n", name);
        return 1;
    }
    
    SyncDirectory *dir = calloc(1, sizeof(SyncDirectory));
    dir->name = strdup(name);
    dir->path = scfg_create_path(path);
    dir->collection = collection ? strdup(collection) : NULL;
    dir->repository = strdup(repository);
    dir->database = strdup(database);
    dir->logfile = logfile ? strdup(logfile) : NULL;
    dir->tagconfig = tagconfig;
    dir->versioning = versioning;
    dir->max_retry = max_retry;
    dir->allow_cmd = allow_cmd;
    dir->backuppull = backuppull;
    dir->lockpull = lockpull;
    dir->lockpush = lockpush;
    dir->hashing = hashing;
    dir->store_hash = store_hash;
    dir->pull_skip_hashing = pull_skip_hashing;
    dir->lock_timeout = lock_timeout;
    dir->metadata = metadata;
    dir->splitconfig = splitconfig;
    dir->symlink = symlink;
    dir->push_strategy = pushstrat;
    if((metadata & FINFO_MODE) == FINFO_MODE) {
        dir->db_settings = DB_STORE_MODE;
    }
    if((metadata & FINFO_OWNER) == FINFO_OWNER) {
        dir->db_settings |= DB_STORE_OWNER;
    }
    
    dir->filter.include = include;
    dir->filter.exclude = exclude;
    dir->filter.tags = tagfilter;
    init_default_filter(&dir->filter);
    
    if (trash && cx_strtrim(cx_str(trash)).length > 0) {
        if (trash[0] == '/' || trash[0] == '$') {
            dir->trash = scfg_create_path(trash);
        } else {
            char *t = util_concat_path(dir->path, trash);
            dir->trash = util_concat_path(t, "/");
            free(t);
        }
        
        if(dir->trash[strlen(dir->trash)-1] != '/') {
            char *t = dir->trash;
            dir->trash = util_concat_path(t, "/");
            free(t);
        }
    } else {
        dir->trash = NULL;
    }
    
    cxMapPut(directories, cx_hash_key_str(name), dir);
    
    return 0;
}

int load_sync_config() {
    directories = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 8);
    
    if(check_config_dir()) {
        fprintf(stderr, "Cannot create .dav directory\n");
        return 1;
    }
    
    char *file = util_concat_path(ENV_HOME, ".dav/sync.xml");
    
    struct stat s;
    if(stat(file, &s)) {
        switch(errno) {
            case ENOENT: {
                if(create_default_sync_config(file)) {
                    return 1;
                }
                break;
            }
            default: {
                perror("Cannot load sync.xml");
            }
        }
        free(file);
        return 0;
    }
    
    xmlDoc *doc = xmlReadFile(file, NULL, 0);
    if(!doc) {
        fprintf(stderr, "Cannot load sync.xml\n");
        free(file);
        return -1;
    }
    
    int ret = 0;
    xmlNode *node = xmlDocGetRootElement(doc)->children;
    while(node && !ret) {
        if(node->type == XML_ELEMENT_NODE) {
            if(xstreq(node->name, "directory")) {
                ret = scfg_load_directory(node);
            } else {
                print_error(node->line,
                        "unknown config element: %s\n", node->name);
                ret = 1;
            }
        }
        node = node->next;
    }
    
    xmlFreeDoc(doc);
    free(file);
    return ret;
}

SyncDirectory* scfg_get_dir(const char *name) {
    return cxMapGet(directories, cx_hash_key_str(name));
}

int scfg_check_dir(SyncDirectory *dir) {
    struct stat s;
    if(stat(dir->path, &s)) {
        int err = errno;
        if(err == ENOENT) {
            fprintf(stderr, "directory %s does not exist.\n", dir->path);
        } else {
            fprintf(stderr, "Cannot stat directory %s.\n", dir->path);
            perror(NULL);
        }
        fprintf(stderr, "Abort.\n");
        return -1;
    }
    
    if(dir->trash) {
        // create trash directory
        mode_t mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
        if (util_mkdir(dir->trash, mode)) {
            if (errno != EEXIST) {
                fprintf(stderr, "Cannot create trash directory: %s\nAbort.\n", dir->trash);
                return -1;
            }
        }
    }
    
    return 0;
}

char* scfg_create_path(const char *cfg) {
    if(!cfg) {
        return NULL;
    }
    if(cfg[0] != '$') {
        return strdup(cfg);
    }
    
    cxstring s = cx_str(cfg);
    cxstring path = cx_strchr(cx_str(cfg), '/');
    char *localpath = NULL;
    if(path.length > 0) {
        // path = $var/path/
        
        cxstring var = cx_strsubsl(s, 1, path.ptr - s.ptr - 1);
        if(var.length > 0) {
            char *env = cx_strdup(var).ptr;
            char *envval = getenv(env);
            free(env);
            if(envval) {
                localpath = util_concat_path(envval, path.ptr);
            } else {
                fprintf(
                        stderr,
                        "Environment variable %.*s not set.\nAbort.\n",
                        (int)var.length,
                        var.ptr);
                exit(-1);
            }
        } else {
            localpath = cx_strdup(path).ptr;
        }
    } else {
        // path = $var
        char *envval = getenv(cfg + 1);
        if(envval) {
            localpath = strdup(envval);
        } else {
            fprintf(
                    stderr,
                    "Environment variable %s not set.\nAbort.\n",
                    cfg);
            exit(-1);
        }
    }
    return localpath;
}


int add_directory(SyncDirectory *dir) {
    char *file = util_concat_path(ENV_HOME, ".dav/sync.xml");
    xmlDoc *doc = xmlReadFile(file, NULL, 0);
    if(!doc) {
        free(file);
        fprintf(stderr, "Cannot load config.xml\n");
        return 1;
    }
    
    xmlNode *root = xmlDocGetRootElement(doc);
    
    xmlNode *dirNode = xmlNewNode(NULL, BAD_CAST "directory");
    xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t");
    
    xmlNewTextChild(dirNode, NULL, BAD_CAST "name", BAD_CAST dir->name);
    xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t");
    
    xmlNewTextChild(dirNode, NULL, BAD_CAST "path", BAD_CAST dir->path);
    xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t");
    
    xmlNewTextChild(dirNode, NULL, BAD_CAST "repository", BAD_CAST dir->repository);
    xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t");
    
    xmlNewTextChild(dirNode, NULL, BAD_CAST "collection", BAD_CAST dir->collection);
    xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t");
    
    if(dir->trash) {
        xmlNewTextChild(dirNode, NULL, BAD_CAST "trash", BAD_CAST dir->trash);
        xmlNodeAddContent(dirNode, BAD_CAST "\n\t\t");
    }
    xmlNewTextChild(dirNode, NULL, BAD_CAST "database", BAD_CAST dir->database);
    xmlNodeAddContent(dirNode, BAD_CAST "\n\t");
    
    xmlNodeAddContent(root, BAD_CAST "\n\t");
    xmlAddChild(root, dirNode);
    xmlNodeAddContent(root, BAD_CAST "\n");
    
    int ret = 0;
    if(xmlSaveFormatFileEnc(file, doc, "UTF-8", 1) == -1) {
        ret = 1;
    }
    xmlFreeDoc(doc);
    free(file);
    
    return ret;
}

char* generate_db_name(const char *basename) {
    char *dbname = NULL;
    int count = -1;
    while(!dbname) {
        cxmutstr name = count < 0 ?
                cx_asprintf("%s-db.xml", basename) : 
                cx_asprintf("%s%d-db.xml", basename, count);
        count++;
        
        CxIterator i = cxMapIteratorValues(directories);
        bool unique = true;
        cx_foreach(SyncDirectory *, dir, i) {
            if(!cx_strcmp(cx_strcast(name), cx_str(dir->database))) {
                unique = false;
                break;
            }
        }
        if(unique) {
            dbname = name.ptr;
            break;
        }
    }
    return dbname;
}

void free_filter(Filter filter) {
    cxListDestroy(filter.include);
    cxListDestroy(filter.exclude);
    cxListDestroy(filter.tags);
}

void free_sync_config() {
    if(directories) {
        CxIterator i = cxMapIteratorValues(directories);
        cx_foreach(SyncDirectory *, dir, i) {
            free(dir->name);
            free(dir->path);
            free(dir->repository);
            free(dir->database);

            if(dir->collection) {
                free(dir->collection);
            }
            if(dir->trash) {
                free(dir->trash);
            }

            free_filter(dir->filter);

            free(dir);
        }

        cxMapDestroy(directories);
    }
}

mercurial