// SPDX-License-Identifier: GPL-3.0-or-later

#include <errno.h>
#include <fnmatch.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/acl.h>
#include <sys/sysmacros.h>

#include "parse.h"
#include "specifier.h"
#include "chase.h"
#include "glob_portable.h"

bool arg_boot = false;
bool arg_user_mode = false;
char **arg_include_prefixes = NULL;
size_t n_include_prefixes = 0;
char **arg_exclude_prefixes = NULL;
size_t n_exclude_prefixes = 0;

void item_free(Item *i) {
  if (!i)
    return;
  free(i->path);
  free(i->argument);
  for (size_t j = 0; j < i->n_xattrs; j++)
    free(i->xattrs[j]);
  free(i->xattrs);
  if (i->acl_access)
    acl_free(i->acl_access);
  if (i->acl_access_exec)
    acl_free(i->acl_access_exec);
  if (i->acl_default)
    acl_free(i->acl_default);
}

void item_array_free(ItemArray *a) {
  if (!a)
    return;
  for (size_t i = 0; i < a->n_items; i++)
    item_free(&a->items[i]);
  free(a->items);
  free(a->children);
  free(a);
}

void context_init(Context *c) {
  c->items = ordered_hashmap_new();
  c->globs = ordered_hashmap_new();
}

void context_done(Context *c) {
  if (c->items) {
    const char *key;
    void *val;
    ORDERED_HASHMAP_FOREACH(key, val, c->items)
      item_array_free(val);
    ordered_hashmap_free(c->items);
    c->items = NULL;
  }
  if (c->globs) {
    const char *key;
    void *val;
    ORDERED_HASHMAP_FOREACH(key, val, c->globs)
      item_array_free(val);
    ordered_hashmap_free(c->globs);
    c->globs = NULL;
  }
}

ItemArray *context_lookup(Context *c, const char *path) {
  ItemArray *a = ordered_hashmap_get(c->items, path);
  if (a)
    return a;
  return ordered_hashmap_get(c->globs, path);
}

static int parse_devnum(const char *s, dev_t *ret) {
  if (!s || !*s)
    return -EINVAL;

  const char *colon = strchr(s, ':');
  if (!colon)
    return -EINVAL;

  _cleanup_free_ char *maj_str = xstrndup(s, (size_t)(colon - s));
  const char *min_str = colon + 1;

  char *end;
  errno = 0;
  unsigned long maj = strtoul(maj_str, &end, 10);
  if (errno || *end)
    return -EINVAL;

  errno = 0;
  unsigned long min = strtoul(min_str, &end, 10);
  if (errno || *end)
    return -EINVAL;

  *ret = makedev(maj, min);
  return 0;
}

static int parse_xattrs_from_arg(Item *i) {
  const char *p = i->argument;

  for (;;) {
    _cleanup_free_ char *xattr = NULL;
    int r = extract_first_word(&p, &xattr, NULL,
                               EXTRACT_UNQUOTE | EXTRACT_CUNESCAPE);
    if (r < 0) {
      log_warning("Failed to parse extended attribute, ignoring: %s", p);
      break;
    }
    if (r == 0)
      break;

    char *eq = strchr(xattr, '=');
    if (!eq || eq == xattr || !eq[1]) {
      log_warning("Malformed extended attribute, ignoring: %s", xattr);
      continue;
    }

    size_t new_count = i->n_xattrs + 2;
    char **new_xattrs = realloc(i->xattrs, new_count * sizeof(char *));
    if (!new_xattrs)
      return -ENOMEM;
    i->xattrs = new_xattrs;

    *eq = '\0';
    i->xattrs[i->n_xattrs] = xstrdup(xattr);
    i->xattrs[i->n_xattrs + 1] = xstrdup(eq + 1);
    i->n_xattrs = new_count;
  }

  return 0;
}

static int parse_acls_from_arg(Item *item) {
  char *access_buf = NULL;
  size_t access_len = 0;
  char *exec_buf = NULL;
  size_t exec_len = 0;
  char *default_buf = NULL;
  size_t default_len = 0;

  const char *p = item->argument;
  while (*p) {
    const char *comma = strchr(p, ',');
    size_t elen = comma ? (size_t)(comma - p) : strlen(p);
    _cleanup_free_ char *entry = xstrndup(p, elen);
    p = comma ? comma + 1 : p + elen;

    int colons = 0;
    bool has_upper_x = false;
    for (size_t j = 0; entry[j]; j++) {
      if (entry[j] == ':')
        colons++;
      if (entry[j] == 'X')
        has_upper_x = true;
    }

    if (colons == 3) {
      const char *first_colon = strchr(entry, ':');
      size_t prefix_len = (size_t)(first_colon - entry);
      if ((prefix_len == 7 && strneq(entry, "default", 7)) ||
          (prefix_len == 1 && entry[0] == 'd')) {
        char *rest = xstrdup(first_colon + 1);
        for (char *q = rest; *q; q++)
          if (*q == 'X')
            *q = 'x';

        size_t rlen = strlen(rest);
        size_t need = default_len + (default_len > 0 ? 1 : 0) + rlen + 1;
        default_buf = realloc(default_buf, need);
        if (!default_buf) {
          free(rest);
          goto oom;
        }
        if (default_len > 0)
          default_buf[default_len++] = ',';
        memcpy(default_buf + default_len, rest, rlen + 1);
        default_len += rlen;
        free(rest);
      }
    } else if (colons == 2) {
      _cleanup_free_ char *fixed = xstrdup(entry);
      for (char *q = fixed; *q; q++)
        if (*q == 'X')
          *q = 'x';

      if (has_upper_x) {
        size_t flen = strlen(fixed);
        size_t need = exec_len + (exec_len > 0 ? 1 : 0) + flen + 1;
        exec_buf = realloc(exec_buf, need);
        if (!exec_buf)
          goto oom;
        if (exec_len > 0)
          exec_buf[exec_len++] = ',';
        memcpy(exec_buf + exec_len, fixed, flen + 1);
        exec_len += flen;
      } else {
        size_t elen2 = strlen(entry);
        size_t need = access_len + (access_len > 0 ? 1 : 0) + elen2 + 1;
        access_buf = realloc(access_buf, need);
        if (!access_buf)
          goto oom;
        if (access_len > 0)
          access_buf[access_len++] = ',';
        memcpy(access_buf + access_len, entry, elen2 + 1);
        access_len += elen2;
      }
    }
  }

  if (access_buf) {
    item->acl_access = acl_from_text(access_buf);
    if (!item->acl_access) {
      log_warning("Failed to parse access ACL '%s', ignoring: %s", access_buf,
                  strerror(errno));
    } else if (!item->append_or_force) {
      acl_calc_mask(&item->acl_access);
    }
    free(access_buf);
  }

  if (exec_buf) {
    item->acl_access_exec = acl_from_text(exec_buf);
    if (!item->acl_access_exec)
      log_warning("Failed to parse exec ACL '%s', ignoring: %s", exec_buf,
                  strerror(errno));
    free(exec_buf);
  }

  if (default_buf) {
    item->acl_default = acl_from_text(default_buf);
    if (!item->acl_default) {
      log_warning("Failed to parse default ACL '%s', ignoring: %s", default_buf,
                  strerror(errno));
    } else if (!item->append_or_force) {
      acl_calc_mask(&item->acl_default);
    }
    free(default_buf);
  }

  return 0;

 oom:
  free(access_buf);
  free(exec_buf);
  free(default_buf);
  return -ENOMEM;
}

static int patch_var_run(const char *fname, unsigned lineno, char **path) {
  const char *k = path_startswith(*path, "/var/run/");
  if (!k || !*k)
    return 0;

  char *n = path_join("/run", k);
  if (!n)
    return -ENOMEM;

  log_info("%s:%u: Patching /var/run/ -> /run/: %s -> %s", fname, lineno, *path,
           n);
  free_and_replace(*path, n);
  return 0;
}

static bool should_include_path(const char *path) {
  for (size_t i = 0; i < n_exclude_prefixes; i++)
    if (path_startswith(path, arg_exclude_prefixes[i])) {
      log_debug("Entry \"%s\" matches exclude prefix \"%s\", skipping.", path,
                arg_exclude_prefixes[i]);
      return false;
    }

  for (size_t i = 0; i < n_include_prefixes; i++)
    if (path_startswith(path, arg_include_prefixes[i])) {
      log_debug("Entry \"%s\" matches include prefix \"%s\".", path,
                arg_include_prefixes[i]);
      return true;
    }

  if (n_include_prefixes == 0)
    return true;

  log_debug("Entry \"%s\" does not match any include prefix, skipping.", path);
  return false;
}

static bool takes_ownership(ItemType t) {
  switch (t) {
  case CREATE_FILE:
  case TRUNCATE_FILE:
  case CREATE_DIRECTORY:
  case TRUNCATE_DIRECTORY:
  case CREATE_SUBVOLUME:
  case CREATE_SUBVOLUME_INHERIT_QUOTA:
  case CREATE_SUBVOLUME_NEW_QUOTA:
  case CREATE_FIFO:
  case CREATE_SYMLINK:
  case CREATE_CHAR_DEVICE:
  case CREATE_BLOCK_DEVICE:
  case COPY_FILES:
  case IGNORE_PATH:
  case IGNORE_DIRECTORY_PATH:
  case REMOVE_PATH:
  case RECURSIVE_REMOVE_PATH:
  case WRITE_FILE:
    return true;
  default:
    return false;
  }
}

static int item_compare(const void *ap, const void *bp) {
  const Item *a = ap, *b = bp;

  if (takes_ownership(a->type) && !takes_ownership(b->type))
    return -1;
  if (!takes_ownership(a->type) && takes_ownership(b->type))
    return 1;

  if (a->type < b->type)
    return -1;
  if (a->type > b->type)
    return 1;
  return 0;
}

static bool is_duplicated_item(ItemArray *existing, const Item *i) {
  for (size_t j = 0; j < existing->n_items; j++) {
    const Item *e = &existing->items[j];

    if (takes_ownership(e->type) && takes_ownership(i->type)) {
      bool compatible =
	(e->argument == NULL && i->argument == NULL) ||
	(e->argument && i->argument && streq(e->argument, i->argument));
      compatible = compatible && e->uid_set == i->uid_set && e->uid == i->uid &&
	e->uid_only_create == i->uid_only_create &&
	e->gid_set == i->gid_set && e->gid == i->gid &&
	e->gid_only_create == i->gid_only_create &&
	e->mode_set == i->mode_set && e->mode == i->mode &&
	e->mode_only_create == i->mode_only_create &&
	e->age_set == i->age_set && e->age == i->age &&
	e->mask_perms == i->mask_perms &&
	e->keep_first_level == i->keep_first_level &&
	e->major_minor == i->major_minor;
      if (!compatible)
        return true;
    }
  }
  return false;
}

static int specifier_expansion_from_arg(Item *i) {
  if (!i->argument && i->n_xattrs == 0)
    return 0;

  switch (i->type) {
  case COPY_FILES:
  case CREATE_SYMLINK:
  case CREATE_FILE:
  case TRUNCATE_FILE:
  case WRITE_FILE: {
    _cleanup_free_ char *unescaped = NULL;
    int r = cunescape(i->argument, &unescaped);
    if (r < 0)
      return r;

    _cleanup_free_ char *resolved = NULL;
    r = specifier_expand(unescaped, arg_user_mode, &resolved);
    if (r < 0)
      return r;

    free_and_replace(i->argument, resolved);
    return 0;
  }

  case SET_XATTR:
  case RECURSIVE_SET_XATTR:
    for (size_t j = 1; j < i->n_xattrs; j += 2) {
      _cleanup_free_ char *resolved = NULL;
      int r = specifier_expand(i->xattrs[j], arg_user_mode, &resolved);
      if (r < 0)
        return r;
      free_and_replace(i->xattrs[j], resolved);
    }
    return 0;

  default:
    return 0;
  }
}

static bool empty_or_dash(const char *s) {
  return !s || !*s || streq(s, "-");
}

static int parse_line(Context *c, const char *fname, unsigned lineno,
                      const char *line) {
  const char *p = line;
  _cleanup_free_ char *action = NULL, *path = NULL, *mode_str = NULL,
    *user = NULL, *group = NULL, *age_str = NULL;

  Item i = {0};
  int r;

  r = extract_first_word(&p, &action, NULL,
                         EXTRACT_UNQUOTE | EXTRACT_CUNESCAPE);
  if (r <= 0)
    return r < 0 ? r : 0;

  r = extract_first_word(&p, &path, NULL, EXTRACT_UNQUOTE | EXTRACT_CUNESCAPE);
  if (r <= 0) {
    log_error("%s:%u: Missing path field.", fname, lineno);
    return -EBADMSG;
  }

  extract_first_word(&p, &mode_str, NULL, EXTRACT_UNQUOTE | EXTRACT_CUNESCAPE);
  extract_first_word(&p, &user, NULL, EXTRACT_UNQUOTE | EXTRACT_CUNESCAPE);
  extract_first_word(&p, &group, NULL, EXTRACT_UNQUOTE | EXTRACT_CUNESCAPE);
  extract_first_word(&p, &age_str, NULL, EXTRACT_UNQUOTE | EXTRACT_CUNESCAPE);

  while (p && *p && (*p == ' ' || *p == '\t'))
    p++;
  if (p && *p && !empty_or_dash(p))
    i.argument = xstrdup(p);

  if (!action || !action[0]) {
    log_error("%s:%u: Empty action field.", fname, lineno);
    return -EBADMSG;
  }

  bool boot = false;
  for (int pos = 1; action[pos]; pos++) {
    if (action[pos] == '!' && !boot)
      boot = true;
    else if (action[pos] == '+' && !i.append_or_force)
      i.append_or_force = true;
    else if (action[pos] == '-' && !i.allow_failure)
      i.allow_failure = true;
    else if (action[pos] == '=')
      i.try_replace = true;
    else if (action[pos] == '~' || action[pos] == '^' || action[pos] == '$' ||
             action[pos] == '?') {
    } else {
      log_error("%s:%u: Unknown modifier '%c' in action '%s'.", fname, lineno,
                action[pos], action);
      return -EBADMSG;
    }
  }

  if (boot && !arg_boot) {
    log_debug("%s:%u: Ignoring entry '%s' (--boot not specified).", fname,
              lineno, action);
    r = 0;
    goto cleanup;
  }

  i.type = (ItemType)action[0];

  {
    _cleanup_free_ char *expanded = NULL;
    r = specifier_expand(path, arg_user_mode, &expanded);
    if (r < 0) {
      log_error("%s:%u: Failed to expand specifiers in path '%s'.", fname,
                lineno, path);
      goto cleanup;
    }
    i.path = expanded;
    expanded = NULL;
  }

  r = patch_var_run(fname, lineno, &i.path);
  if (r < 0)
    goto cleanup;

  if (!path_is_absolute(i.path)) {
    log_error("%s:%u: Path '%s' not absolute.", fname, lineno, i.path);
    r = -EBADMSG;
    goto cleanup;
  }

  path_simplify(i.path);

  switch (i.type) {

  case CREATE_DIRECTORY:
  case TRUNCATE_DIRECTORY:
  case CREATE_SUBVOLUME:
  case CREATE_SUBVOLUME_INHERIT_QUOTA:
  case CREATE_SUBVOLUME_NEW_QUOTA:
  case CREATE_FIFO:
  case IGNORE_PATH:
  case IGNORE_DIRECTORY_PATH:
  case REMOVE_PATH:
  case RECURSIVE_REMOVE_PATH:
  case ADJUST_MODE:
  case RELABEL_PATH:
  case RECURSIVE_RELABEL_PATH:
    if (i.argument) {
      log_warning("%s:%u: %c lines don't take argument fields, ignoring.",
                  fname, lineno, (char)i.type);
      free(i.argument);
      i.argument = NULL;
    }
    break;

  case CREATE_FILE:
  case TRUNCATE_FILE:
    break;

  case WRITE_FILE:
    if (!i.argument) {
      log_error("%s:%u: Write file requires argument.", fname, lineno);
      r = -EBADMSG;
      goto cleanup;
    }
    break;

  case CREATE_SYMLINK:
    break;

  case COPY_FILES:
    break;

  case CREATE_CHAR_DEVICE:
  case CREATE_BLOCK_DEVICE:
    if (!i.argument) {
      log_error("%s:%u: Device file requires argument.", fname, lineno);
      r = -EBADMSG;
      goto cleanup;
    }
    r = parse_devnum(i.argument, &i.major_minor);
    if (r < 0) {
      log_error("%s:%u: Can't parse device major/minor '%s'.", fname, lineno,
                i.argument);
      goto cleanup;
    }
    break;

  case SET_XATTR:
  case RECURSIVE_SET_XATTR:
    if (!i.argument) {
      log_error("%s:%u: Set extended attribute requires argument.", fname,
                lineno);
      r = -EBADMSG;
      goto cleanup;
    }
    r = parse_xattrs_from_arg(&i);
    if (r < 0)
      goto cleanup;
    break;

  case SET_ACL:
  case RECURSIVE_SET_ACL:
    if (!i.argument) {
      log_error("%s:%u: Set ACL requires argument.", fname, lineno);
      r = -EBADMSG;
      goto cleanup;
    }
    r = parse_acls_from_arg(&i);
    if (r < 0)
      goto cleanup;
    break;

  case SET_ATTRIBUTE:
  case RECURSIVE_SET_ATTRIBUTE:
    if (!i.argument) {
      log_error("%s:%u: Set file attribute requires argument.", fname, lineno);
      r = -EBADMSG;
      goto cleanup;
    }
    r = parse_attribute_from_arg(&i);
    if (r < 0)
      goto cleanup;
    break;

  default:
    log_error("%s:%u: Unknown command type '%c'.", fname, lineno, (char)i.type);
    r = -EBADMSG;
    goto cleanup;
  }

  if (!should_include_path(i.path)) {
    r = 0;
    goto cleanup;
  }

  r = specifier_expansion_from_arg(&i);
  if (r < 0) {
    log_error("%s:%u: Failed to expand specifiers in argument.", fname, lineno);
    goto cleanup;
  }

  switch (i.type) {
  case CREATE_SYMLINK:
    if (!i.argument)
      i.argument = path_join("/usr/share/factory", i.path);
    break;

  case COPY_FILES:
    if (!i.argument) {
      i.argument = path_join("/usr/share/factory", i.path);
    } else if (!path_is_absolute(i.argument)) {
      log_error("%s:%u: Copy source path '%s' is not absolute.", fname, lineno,
                i.argument);
      r = -EBADMSG;
      goto cleanup;
    }
    if (i.argument) {
      path_simplify(i.argument);
      _cleanup_free_ char *probe = NULL;
      int pr = chase(i.argument, arg_root, CHASE_PREFIX_ROOT, &probe, NULL);
      if (pr == -ENOENT) {
        log_debug("%s:%u: Copy source '%s' does not exist, skipping.", fname,
                  lineno, i.argument);
        r = 0;
        goto cleanup;
      }
    }
    break;

  default:
    break;
  }

  if (i.type == CREATE_SUBVOLUME || i.type == CREATE_SUBVOLUME_INHERIT_QUOTA ||
      i.type == CREATE_SUBVOLUME_NEW_QUOTA)
    i.type = CREATE_DIRECTORY;

  if (!empty_or_dash(user)) {
    const char *u = user;
    const char *colon = startswith(u, ":");
    if (colon) {
      i.uid_only_create = true;
      u = colon;
    }
    r = resolve_user(u, &i.uid);
    if (r < 0) {
      log_error("%s:%u: Failed to resolve user '%s'.", fname, lineno, u);
      goto cleanup;
    }
    i.uid_set = true;
  }

  if (!empty_or_dash(group)) {
    const char *g = group;
    const char *colon = startswith(g, ":");
    if (colon) {
      i.gid_only_create = true;
      g = colon;
    }
    r = resolve_group(g, &i.gid);
    if (r < 0) {
      log_error("%s:%u: Failed to resolve group '%s'.", fname, lineno, g);
      goto cleanup;
    }
    i.gid_set = true;
  }

  if (!empty_or_dash(mode_str)) {
    const char *mm = mode_str;
    for (;; mm++) {
      if (*mm == '~')
        i.mask_perms = true;
      else if (*mm == ':')
        i.mode_only_create = true;
      else
        break;
    }

    mode_t m;
    r = parse_mode(mm, &m);
    if (r < 0) {
      log_error("%s:%u: Invalid mode '%s'.", fname, lineno, mode_str);
      goto cleanup;
    }
    i.mode = m;
    i.mode_set = true;
  } else {
    switch (i.type) {
    case CREATE_DIRECTORY:
    case TRUNCATE_DIRECTORY:
      i.mode = 0755;
      break;
    default:
      i.mode = 0644;
      break;
    }
  }

  if (!empty_or_dash(age_str)) {
    const char *a = age_str;
    if (*a == '~') {
      i.keep_first_level = true;
      a++;
    }

    const char *colon = strchr(a, ':');
    if (colon)
      a = colon + 1;

    r = parse_duration(a, &i.age);
    if (r < 0) {
      log_error("%s:%u: Invalid age '%s'.", fname, lineno, age_str);
      goto cleanup;
    }
    i.age_set = true;
  }

  OrderedHashmap *h = item_type_is_glob(i.type) ? c->globs : c->items;
  ItemArray *existing = ordered_hashmap_get(h, i.path);

  if (existing) {
    if (is_duplicated_item(existing, &i)) {
      log_info("%s:%u: Duplicate line for path \"%s\", ignoring.", fname,
               lineno, i.path);
      r = 0;
      goto cleanup;
    }
  } else {
    existing = calloc(1, sizeof(ItemArray));
    if (!existing) {
      r = -ENOMEM;
      goto cleanup;
    }
    r = ordered_hashmap_put(h, i.path, existing);
    if (r < 0) {
      free(existing);
      goto cleanup;
    }
  }

  Item *new_items =
    realloc(existing->items, (existing->n_items + 1) * sizeof(Item));
  if (!new_items) {
    r = -ENOMEM;
    goto cleanup;
  }
  existing->items = new_items;
  existing->items[existing->n_items] = i;
  existing->n_items++;

  qsort(existing->items, existing->n_items, sizeof(Item), item_compare);

  return 0;

 cleanup:
  item_free(&i);
  return r;
}

int parse_config_file(Context *c, const char *path) {
  _cleanup_free_ char *contents = NULL;
  const char *fname;
  int r;

  if (streq(path, "-")) {
    r = read_fd_full(STDIN_FILENO, &contents, NULL);
    fname = "<stdin>";
  } else {
    r = read_file_full(path, &contents, NULL);
    fname = path;
  }

  if (r < 0) {
    log_error("Failed to open config file %s", fname);
    return r;
  }

  unsigned lineno = 0;
  int ret = 0;

  char *line = contents;
  while (line && *line) {
    char *nl = strchr(line, '\n');
    if (nl)
      *nl = '\0';

    lineno++;

    char *stripped = strstrip(line);
    if (*stripped && *stripped != '#' && *stripped != ';') {
      r = parse_line(c, fname, lineno, stripped);
      if (r < 0 && ret == 0)
        ret = r;
    }

    line = nl ? nl + 1 : NULL;
  }

  return ret;
}

typedef struct ConfigEntry {
  char *basename;
  char *fullpath;
} ConfigEntry;

static int config_entry_compare(const void *ap, const void *bp) {
  const ConfigEntry *a = ap, *b = bp;
  return strcmp(a->basename, b->basename);
}

struct config_glob_ctx {
  ConfigEntry **entries;
  size_t *n_entries;
  size_t *cap;
};

static int config_glob_trampoline(const char *logical_path, void *userdata) {
  struct config_glob_ctx *ctx = userdata;
  const char *bn = path_basename(logical_path);

  for (size_t j = 0; j < *ctx->n_entries; j++) {
    if (streq((*ctx->entries)[j].basename, bn))
      return 0;
  }

  if (*ctx->n_entries >= *ctx->cap) {
    size_t new_cap = *ctx->cap ? *ctx->cap * 2 : 16;
    ConfigEntry *ne = realloc(*ctx->entries, new_cap * sizeof(ConfigEntry));
    if (!ne)
      return -ENOMEM;
    *ctx->entries = ne;
    *ctx->cap = new_cap;
  }

  char *fullpath = arg_root ? path_join(arg_root, logical_path)
                            : xstrdup(logical_path);
  if (!fullpath)
    return -ENOMEM;

  (*ctx->entries)[*ctx->n_entries].basename = xstrdup(bn);
  (*ctx->entries)[*ctx->n_entries].fullpath = fullpath;
  (*ctx->n_entries)++;
  return 0;
}

int parse_config_files(Context *c, const char *const *config_dirs, int argc,
                       char **argv) {
  ConfigEntry *entries = NULL;
  size_t n_entries = 0, cap = 0;

  if (config_dirs && argc == 0) {
    struct config_glob_ctx ctx = {
      .entries = &entries,
      .n_entries = &n_entries,
      .cap = &cap,
    };
    for (const char *const *d = config_dirs; *d; d++) {
      _cleanup_free_ char *pattern = path_join(*d, "*.conf");
      if (!pattern) {
        free(entries);
        return -ENOMEM;
      }

      int gr = glob_in_root(pattern, arg_root,
                            config_glob_trampoline, &ctx);
      if (gr == -ENOMEM)
        goto oom;
      /* other errors: skip this directory silently, matching previous behaviour */
    }
  }

  if (n_entries > 1)
    qsort(entries, n_entries, sizeof(ConfigEntry), config_entry_compare);

  int ret = 0;
  for (size_t i = 0; i < n_entries; i++) {
    log_debug("Reading config file '%s'.", entries[i].fullpath);
    int r = parse_config_file(c, entries[i].fullpath);
    if (r < 0 && ret == 0)
      ret = r;
  }

  for (size_t i = 0; i < n_entries; i++) {
    free(entries[i].basename);
    free(entries[i].fullpath);
  }
  free(entries);

  for (int i = 0; i < argc; i++) {
    const char *arg = argv[i];

    if (strchr(arg, '/')) {
      log_debug("Reading config file '%s' (from command line).", arg);
      int r = parse_config_file(c, arg);
      if (r < 0 && ret == 0)
        ret = r;
      continue;
    }

    _cleanup_free_ char *resolved = NULL;
    if (config_dirs) {
      for (const char *const *d = config_dirs; *d; d++) {
        char *candidate = path_join(*d, arg);
        if (!candidate)
          return -ENOMEM;
        if (arg_root) {
          char *rooted = path_join(arg_root, candidate);
          free(candidate);
          if (!rooted)
            return -ENOMEM;
          candidate = rooted;
        }
        if (access(candidate, F_OK) == 0) {
          resolved = candidate;
          break;
        }
        free(candidate);
      }
    }

    if (!resolved) {
      log_error("Configuration file '%s' not found in any configuration directory.", arg);
      if (ret == 0)
        ret = -ENOENT;
      continue;
    }

    log_debug("Reading config file '%s' (resolved from '%s').", resolved, arg);
    int r = parse_config_file(c, resolved);
    if (r < 0 && ret == 0)
      ret = r;
  }

  return ret;

 oom:
  for (size_t i = 0; i < n_entries; i++) {
    free(entries[i].basename);
    free(entries[i].fullpath);
  }
  free(entries);
  return -ENOMEM;
}

static int link_parent(Context *c, ItemArray *a) {
  if (a->n_items == 0)
    return 0;

  const char *path = a->items[0].path;

  _cleanup_free_ char *prefix = xstrdup(path);
  for (;;) {
    char *slash = strrchr(prefix, '/');
    if (!slash || slash == prefix) {
      if (slash == prefix && prefix[1] != '\0') {
        prefix[1] = '\0';
        ItemArray *j = ordered_hashmap_get(c->items, prefix);
        if (!j)
          j = ordered_hashmap_get(c->globs, prefix);
        if (j) {
          ItemArray **nc =
	    realloc(j->children, (j->n_children + 1) * sizeof(ItemArray *));
          if (!nc)
            return -ENOMEM;
          j->children = nc;
          j->children[j->n_children++] = a;
          a->parent = j;
          return 1;
        }
      }
      return 0;
    }

    *slash = '\0';
    if (!*prefix)
      continue;

    ItemArray *j = ordered_hashmap_get(c->items, prefix);
    if (!j)
      j = ordered_hashmap_get(c->globs, prefix);
    if (j) {
      bool already = false;
      for (size_t k = 0; k < j->n_children; k++) {
        if (j->children[k] == a) {
          already = true;
          break;
        }
      }
      if (!already) {
        ItemArray **nc =
	  realloc(j->children, (j->n_children + 1) * sizeof(ItemArray *));
        if (!nc)
          return -ENOMEM;
        j->children = nc;
        j->children[j->n_children++] = a;
      }
      a->parent = j;
      return 1;
    }
  }
}

void context_link_parents(Context *c) {
  const char *key;
  void *val;

  ORDERED_HASHMAP_FOREACH(key, val, c->items)
    link_parent(c, val);
  ORDERED_HASHMAP_FOREACH(key, val, c->globs)
    link_parent(c, val);
}
