// SPDX-License-Identifier: GPL-2.0-only /* Copyright (C) 2019 Daniel Borkmann */ #include #include #include #include #include #include #include #include "l2md.h" enum { STATE_NONE = 0, STATE_GENERAL, STATE_REPO, STATE_MAX, }; static void config_dump(struct config *cfg) { struct config_repo *repo; struct config_url *url; uint32_t i, j; if (!verbose_enabled) return; verbose("general.base = %s\n", cfg->general.base); verbose("general.mode = %s\n", cfg->ops->name); verbose("general.%s = %s\n", cfg->ops->name, cfg->general.out); verbose("general.period = %u\n", cfg->general.period); repo_for_each(cfg, repo, i) { verbose("repos.%s.%s = %s\n", repo->name, cfg->ops->name, repo->out); verbose("repos.%s.initial_import = %u\n", repo->name, repo->initial_import); url_for_each(repo, url, j) { verbose("repos.%s.url = %s\n", repo->name, url->path); verbose("repos.%s.oid = %s\n", repo->name, url->oid_known ? url->oid : "[unknown]"); } } verbose("oneshot = %d\n", cfg->oneshot); } static void config_probe_oids(struct config *cfg) { struct config_repo *repo; struct config_url *url; char path[PATH_MAX]; uint32_t i, j; int ret; repo_for_each(cfg, repo, i) { url_for_each(repo, url, j) { repo_local_oid(cfg, repo, url, path, sizeof(path)); ret = xread_file(path, url->oid, sizeof(url->oid) - 1, false); if (!ret) url->oid_known = true; } } } static void config_set_basedir(struct config *cfg, const char *dir) { wordexp_t p; wordexp(dir, &p, 0); dir = p.we_wordv[0]; __strlcpy(cfg->general.base, dir, sizeof(cfg->general.base)); wordfree(&p); } static void config_set_ops(struct config *cfg, const struct mail_ops* ops) { cfg->ops = ops; } static void config_check_ops(struct config *cfg, const struct mail_ops* ops) { if (cfg->ops != ops) panic("mode %s in [general] must match [repo *] mode %s\n", cfg->ops->name, ops->name); } static void config_set_mode(struct config *cfg, const char *mode) { if (!strncmp(mode, "maildir", sizeof("maildir"))) config_set_ops(cfg, &ops_maildir); else if (!strncmp(mode, "pipe", sizeof("pipe"))) config_set_ops(cfg, &ops_pipe); else panic("Unknown mode: %s!\n", mode); } static void config_set_out(struct config *cfg, const char *ctx, bool root) { struct config_repo *repo = repo_last(cfg); char *out = root ? cfg->general.out : repo->out; wordexp_t p; wordexp(ctx, &p, 0); ctx = p.we_wordv[0]; __strlcpy(out, ctx, sizeof(repo->out)); wordfree(&p); } static void config_set_initial_import(struct config *cfg, uint32_t limit) { struct config_repo *repo = repo_last(cfg); repo->initial_import = limit; if (repo->initial_import > 0) repo->limit = true; } static void config_set_repo_status(struct config *cfg, bool status) { struct config_repo *repo = repo_last(cfg); repo->sync_enabled = status; } static void config_new_url(struct config *cfg, const char *git_url) { struct config_repo *repo = repo_last(cfg); struct config_url *url; repo->urls_num++; repo->urls = xrealloc(repo->urls, sizeof(*repo->urls) * repo->urls_num); url = url_last(repo); memset(url, 0, sizeof(*url)); __strlcpy(url->path, git_url, sizeof(url->path)); } static void config_new_repo(struct config *cfg, const char *name) { struct config_repo *repo; cfg->repos_num++; cfg->repos = xrealloc(cfg->repos, sizeof(*cfg->repos) * cfg->repos_num); repo = repo_last(cfg); memset(repo, 0, sizeof(*repo)); config_set_out(cfg, cfg->general.out, false); config_set_repo_status(cfg, true); __strlcpy(repo->name, name, sizeof(repo->name)); } static void config_set_defaults(struct config *cfg, const char *homedir) { char default_data_dir[PATH_MAX]; char *data_dir; cfg->general.period = 60; /* Default base is ~/.l2md/ if it exists, otherwise * ~/${XDG_DATA_HOME}/l2md/. Fallback for empty XDG_DATA_DIR is * ~/.local/share/. */ slprintf(default_data_dir, sizeof(default_data_dir), "%s/.l2md", homedir); if (access(default_data_dir, F_OK) >= 0) data_dir = default_data_dir; else if (!(data_dir = getenv("XDG_DATA_HOME"))) { slprintf(default_data_dir, sizeof(default_data_dir), "%s/.local/share/l2md", homedir); data_dir = default_data_dir; } verbose("Using base dir %s\n", default_data_dir); __strlcpy(cfg->general.base, data_dir, sizeof(cfg->general.base)); config_set_ops(cfg, &ops_maildir); cfg->ops->set_defaults(cfg); } static void config_ulimits(void) { struct rlimit limit; int ret; ret = getrlimit(RLIMIT_NOFILE, &limit); if (ret < 0) panic("Cannot retrieve rlimit!\n"); /* The git repos we're dealing with can have a lot of * pack files potentially, hence increase open file limit * to help libgit2's revwalk. * * If there is a better API supported by native libgit2 * for doing repack, we should use that instead: * * https://github.com/libgit2/libgit2/issues/3247 */ limit.rlim_cur = limit.rlim_max; ret = setrlimit(RLIMIT_NOFILE, &limit); if (ret < 0) panic("Cannot set rlimit!\n"); } void config_uninit(struct config *cfg) { struct config_repo *repo; uint32_t i; repo_for_each(cfg, repo, i) xfree(repo->urls); xfree(cfg->repos); xfree(cfg); } static FILE *config_open(const char *homedir) { struct dirinfo { const char *env; const char *suffix; const char *fallback; bool prefix_home_to_fallback; }; struct dirinfo dirinfo[] = { { "XDG_CONFIG_HOME", "l2md/config", ".config", true, }, { "HOME", ".l2mdconfig", }, { }, }; struct dirinfo *cur; char path[PATH_MAX]; FILE *fp; for (cur = dirinfo; cur->suffix; cur++) { char *env = getenv(cur->env); if (env) slprintf(path, sizeof(path), "%s/%s", env, cur->suffix); else if (cur->fallback) { if (cur->prefix_home_to_fallback) slprintf(path, sizeof(path), "%s/%s/%s", homedir, cur->fallback, cur->suffix); else slprintf(path, sizeof(path), "%s/%s", cur->fallback, cur->suffix); } verbose("Attempting to open config file %s\n", path); fp = fopen(path, "r"); if (fp) return fp; if (errno != ENOENT) panic("Cannot open config %s: %s\n", path, strerror(errno)); } return panic("Unable to find config. Please check l2md README.\n"), NULL; } struct config *config_init(int argc, char **argv) { const char *homedir = getenv("HOME"); char buff[1024], tmp[1024] = {}; bool seen[STATE_MAX] = {}; int state = STATE_NONE; struct config *cfg; FILE *fp; if (argc > 1) { if (argc == 2 && !strncmp(argv[1], "--verbose", sizeof("--verbose"))) verbose_enabled = true; else panic("Usage: %s [--verbose]\n", argv[0]); } if (!homedir) panic("Cannot retrieve $HOME from env!\n"); fp = config_open(homedir); config_ulimits(); cfg = xzmalloc(sizeof(*cfg)); config_set_defaults(cfg, homedir); while (fgets(buff, sizeof(buff), fp)) { uint32_t val; if (buff[0] == '#' || buff[0] == '\n') continue; seen[state] = true; switch (state) { case STATE_NONE: state_next: if (!strcmp(buff, "[general]\n")) { state = STATE_GENERAL; } else if (sscanf(buff, "[repo %1023[a-z0-9-]]\n", tmp) == 1) { state = STATE_REPO; config_new_repo(cfg, tmp); } else { panic("Cannot parse: '%s'\n", buff); } break; case STATE_GENERAL: if (seen[STATE_REPO]) { panic("[general] config must be before [repo *] config\n"); } else if (sscanf(buff, "\tperiod = %u", &val) == 1) { cfg->general.period = val; } else if (sscanf(buff, "\tmode = %1023s", tmp) == 1) { config_set_mode(cfg, tmp); } else if (sscanf(buff, "\tmaildir = %1023s", tmp) == 1) { config_set_ops(cfg, &ops_maildir); config_set_out(cfg, tmp, true); } else if (sscanf(buff, "\tpipe = %1023s", tmp) == 1) { config_set_ops(cfg, &ops_pipe); config_set_out(cfg, tmp, true); } else if (sscanf(buff, "\tbase = %1023s", tmp) == 1) { config_set_basedir(cfg, tmp); } else if (sscanf(buff, "\toneshot = %u", &val) == 1) { cfg->oneshot = val; } else { goto state_next; } break; case STATE_REPO: if (sscanf(buff, "\turl = %1023s", tmp) == 1) { config_new_url(cfg, tmp); } else if (sscanf(buff, "\tmaildir = %1023s", tmp) == 1) { config_check_ops(cfg, &ops_maildir); config_set_out(cfg, tmp, false); } else if (sscanf(buff, "\tpipe = %1023s", tmp) == 1) { config_check_ops(cfg, &ops_pipe); config_set_out(cfg, tmp, false); } else if (sscanf(buff, "\tinitial_import = %u", &val) == 1) { config_set_initial_import(cfg, val); } else if (sscanf(buff, "\tsync_enabled = %u", &val) == 1) { config_set_repo_status(cfg, val); } else { goto state_next; } break; default: panic("Invalid parser state: %d\n", state); }; } fclose(fp); config_probe_oids(cfg); config_dump(cfg); return cfg; }