blob: 0aadf35b93e0ff748042fdbb930d2c96eb855bba [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <wordexp.h>
#include <stdbool.h>
#include <sys/resource.h>
#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;
}