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

#include <fnmatch.h>
#include <sys/file.h>
#ifdef __linux__
#include <sys/mount.h>
#endif

#include "clean.h"
#include "perms.h"
#include "glob_portable.h"
#include "chase.h"

#define MAX_DEPTH 256


static char **live_sockets = NULL;
static size_t n_live_sockets = 0;
static bool sockets_loaded = false;

static void load_unix_sockets(void) {
  if (sockets_loaded)
    return;
  sockets_loaded = true;

  _cleanup_fclose_ FILE *f = fopen("/proc/net/unix", "re");
  if (!f) {
    log_debug("Cannot open /proc/net/unix: %s", strerror(errno));
    return;
  }

  char line[4096];
  if (!fgets(line, sizeof(line), f))
    return;

  while (fgets(line, sizeof(line), f)) {
    char *p = strchr(line, ':');
    if (!p)
      continue;
    p++;

    for (int i = 0; i < 6; i++) {
      while (*p == ' ')
        p++;
      while (*p && *p != ' ' && *p != '\n')
        p++;
    }
    while (*p == ' ')
      p++;

    size_t len = strlen(p);
    while (len > 0 && (p[len - 1] == '\n' || p[len - 1] == '\r'))
      len--;
    if (len == 0 || p[0] != '/')
      continue;

    live_sockets = realloc(live_sockets, sizeof(char *) * (n_live_sockets + 1));
    if (!live_sockets)
      abort();
    live_sockets[n_live_sockets++] = xstrndup(p, len);
  }
}

bool unix_socket_alive(const char *path) {
  load_unix_sockets();

  for (size_t i = 0; i < n_live_sockets; i++) {
    if (streq(live_sockets[i], path))
      return true;
  }
  return false;
}

static bool find_glob_match(Context *c, const char *path) {
  const char *key;
  void *v;
  ORDERED_HASHMAP_FOREACH(key, v, c->globs) {
    ItemArray *a = v;
    for (size_t j = 0; j < a->n_items; j++) {
      if (fnmatch(a->items[j].path, path, FNM_PATHNAME | FNM_PERIOD) == 0)
        return true;
    }
  }
  return false;
}

static bool needs_cleanup(uint64_t mtime_usec, uint64_t cutoff_usec) {
  if (cutoff_usec == 0)
    return true;
  if (mtime_usec == 0)
    return true;
  return mtime_usec < cutoff_usec;
}

static uint64_t timespec_to_usec(struct timespec ts) {
  return (uint64_t)ts.tv_sec * USEC_PER_SEC + (uint64_t)ts.tv_nsec / 1000;
}

int dir_cleanup(Context *c, const char *path, DIR *d, uint64_t self_mtime_usec,
                uint64_t cutoff_usec, dev_t rootdev, bool keep_first_level,
                int maxdepth) {
  int r = 0;
  bool deleted = false;
  struct timespec original_mtime = {0, 0};

  struct stat dir_st;
  if (fstat(dirfd(d), &dir_st) == 0)
    original_mtime = dir_st.st_mtim;

  struct dirent *de;
  while ((de = readdir(d))) {
    if (streq(de->d_name, ".") || streq(de->d_name, ".."))
      continue;

    struct stat st;
    if (fstatat(dirfd(d), de->d_name, &st, AT_SYMLINK_NOFOLLOW) < 0) {
      if (errno == ENOENT)
        continue;
      log_warning("fstatat(%s/%s) failed: %s", path, de->d_name,
                  strerror(errno));
      r = r ?: -errno;
      continue;
    }

#ifdef __linux__
    if (st.st_dev != rootdev) {
      log_debug("Ignoring \"%s/%s\": different mount point.", path, de->d_name);
      continue;
    }
#endif

    _cleanup_free_ char *sub_path = path_join(path, de->d_name);
    if (!sub_path) {
      r = r ?: -ENOMEM;
      continue;
    }

    if (ordered_hashmap_get(c->items, sub_path)) {
      log_debug("Ignoring \"%s\": has its own seedfiles entry.", sub_path);
      continue;
    }
    if (find_glob_match(c, sub_path)) {
      log_debug("Ignoring \"%s\": matched by a glob entry.", sub_path);
      continue;
    }

    uint64_t mtime_usec = timespec_to_usec(st.st_mtim);

    if (S_ISDIR(st.st_mode)) {
      if (st.st_uid == 0 && streq(de->d_name, "lost+found")) {
        log_debug("Ignoring \"%s\": root-owned lost+found.", sub_path);
        continue;
      }

      if (maxdepth > 0) {
        int sub_fd = openat(dirfd(d), de->d_name,
                            O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC | O_RDONLY);
        if (sub_fd < 0) {
          if (errno == ENOENT)
            continue;
          log_warning("Cannot open \"%s\": %s", sub_path, strerror(errno));
          r = r ?: -errno;
          continue;
        }

        if (!arg_dry_run && flock(sub_fd, LOCK_EX | LOCK_NB) < 0) {
          log_debug("Cannot lock \"%s\", skipping.", sub_path);
          close(sub_fd);
          continue;
        }

        DIR *sub_dir = xfdopendir(sub_fd);
        if (!sub_dir) {
          close(sub_fd);
          continue;
        }

        int q = dir_cleanup(c, sub_path, sub_dir, mtime_usec, cutoff_usec,
                            rootdev, false, maxdepth - 1);
        closedir(sub_dir);
        r = r ?: q;
      }

      if (keep_first_level) {
        log_debug("Keeping \"%s\" (keep_first_level).", sub_path);
        continue;
      }

      if (!needs_cleanup(mtime_usec, cutoff_usec))
        continue;

      if (arg_dry_run) {
        log_info("Would remove directory \"%s\".", sub_path);
        continue;
      }

      log_debug("Removing directory \"%s\".", sub_path);
      if (unlinkat(dirfd(d), de->d_name, AT_REMOVEDIR) < 0) {
        if (errno != ENOENT && errno != ENOTEMPTY)
          log_warning("Failed to remove \"%s\": %s", sub_path, strerror(errno));
      } else {
        deleted = true;
      }
    } else {
      if (st.st_mode & S_ISVTX) {
        log_debug("Keeping \"%s\": has sticky bit.", sub_path);
        continue;
      }

      if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) {
        log_debug("Skipping device node \"%s\".", sub_path);
        continue;
      }

      if (S_ISSOCK(st.st_mode) && unix_socket_alive(sub_path)) {
        log_debug("Skipping live unix socket \"%s\".", sub_path);
        continue;
      }

      if (keep_first_level) {
        log_debug("Keeping \"%s\" (keep_first_level).", sub_path);
        continue;
      }

      if (!needs_cleanup(mtime_usec, cutoff_usec))
        continue;

      if (!arg_dry_run) {
        int file_fd = openat(dirfd(d), de->d_name,
                             O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
        if (file_fd >= 0) {
          if (flock(file_fd, LOCK_EX | LOCK_NB) < 0) {
            log_debug("Cannot lock \"%s\", skipping.", sub_path);
            close(file_fd);
            continue;
          }
          log_debug("Removing \"%s\".", sub_path);
          if (unlinkat(dirfd(d), de->d_name, 0) < 0) {
            if (errno != ENOENT)
              log_warning("Failed to remove \"%s\": %s", sub_path,
                          strerror(errno));
          } else {
            deleted = true;
          }
          close(file_fd);
          continue;
        }
        if (errno != ENOENT && errno != ELOOP)
          log_debug("Cannot open \"%s\" for locking: %s", sub_path,
                    strerror(errno));
      }

      if (arg_dry_run) {
        log_info("Would remove \"%s\".", sub_path);
        continue;
      }

      log_debug("Removing \"%s\".", sub_path);
      if (unlinkat(dirfd(d), de->d_name, 0) < 0) {
        if (errno != ENOENT)
          log_warning("Failed to remove \"%s\": %s", sub_path, strerror(errno));
      } else {
        deleted = true;
      }
    }
  }

  if (deleted && self_mtime_usec != USEC_INFINITY) {
    struct timespec ts[2];
    ts[0] = original_mtime;
    ts[1] = original_mtime;
    futimens(dirfd(d), ts);
  }

  return r;
}

struct glob_item_ctx {
  Context *c;
  Item *item;
  int (*func)(Context *, Item *, const char *);
};

static int glob_item_trampoline(const char *logical_path, void *userdata) {
  struct glob_item_ctx *ctx = userdata;
  return ctx->func(ctx->c, ctx->item, logical_path);
}

static int glob_item(Context *c, Item *i,
                     int (*func)(Context *, Item *, const char *)) {
  struct glob_item_ctx ctx = { .c = c, .item = i, .func = func };
  return glob_in_root(i->path, arg_root, glob_item_trampoline, &ctx);
}

static int clean_item_instance(Context *c, Item *i, const char *instance) {
  if (!i->age_set)
    return 0;

  uint64_t now = now_usec();
  if (now < i->age)
    return 0;
  uint64_t cutoff = now - i->age;

  _cleanup_close_ int fd = chase_and_open(instance, arg_root,
                                          CHASE_PREFIX_ROOT | CHASE_NOFOLLOW,
                                          O_DIRECTORY | O_CLOEXEC | O_RDONLY,
                                          NULL);
  if (fd < 0) {
    if (fd == -ENOENT || fd == -ENOTDIR)
      return 0;
    return log_error_errno(fd, "Failed to open '%s': %s", instance,
                           strerror(-fd));
  }

  struct stat st;
  if (fstat(fd, &st) < 0)
    return log_error_errno(-errno, "fstat(%s) failed: %s", instance,
                           strerror(errno));

  DIR *d = xfdopendir(fd);
  if (!d)
    return -errno;
  fd = -1;

  uint64_t mtime = timespec_to_usec(st.st_mtim);
  int r = dir_cleanup(c, instance, d, mtime, cutoff, st.st_dev,
                      i->keep_first_level, MAX_DEPTH);
  closedir(d);
  return r;
}

static int remove_item_instance(Context *c, Item *i, const char *instance) {
  (void)c;

  if (arg_dry_run) {
    log_info("Would remove '%s'.", instance);
    return 0;
  }

  switch (i->type) {
  case REMOVE_PATH: {
    _cleanup_free_ char *resolved = NULL;
    int cr = chase(instance, arg_root,
                   CHASE_PREFIX_ROOT | CHASE_NOFOLLOW | CHASE_NONEXISTENT,
                   &resolved, NULL);
    if (cr == -ENOENT)
      return 0;
    if (cr < 0)
      return log_error_errno(cr, "Failed to resolve '%s': %s", instance,
                             strerror(-cr));
    if (remove(resolved) < 0 && errno != ENOENT)
      return log_error_errno(-errno, "Failed to remove '%s': %s", instance,
                             strerror(errno));
    return 0;
  }

  case RECURSIVE_REMOVE_PATH: {
    _cleanup_free_ char *resolved = NULL;
    int cr = chase(instance, arg_root,
                   CHASE_PREFIX_ROOT | CHASE_NOFOLLOW | CHASE_NONEXISTENT,
                   &resolved, NULL);
    if (cr == -ENOENT)
      return 0;
    if (cr < 0)
      return log_error_errno(cr, "Failed to resolve '%s': %s", instance,
                             strerror(-cr));

    _cleanup_close_ int fd =
      open(resolved, O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC | O_RDONLY);
    if (fd < 0) {
      if (errno == ENOENT)
        return 0;
      if (errno == ENOTDIR) {
        if (remove(resolved) < 0 && errno != ENOENT)
          return log_error_errno(-errno, "Failed to remove '%s': %s", instance,
                                 strerror(errno));
        return 0;
      }
      return log_error_errno(-errno, "Failed to open '%s': %s", instance,
                             strerror(errno));
    }

    struct stat st;
    if (fstat(fd, &st) < 0)
      return log_error_errno(-errno, "fstat(%s) failed: %s", instance,
                             strerror(errno));

    DIR *d = xfdopendir(fd);
    if (!d)
      return -errno;
    fd = -1;

    int r = dir_cleanup(c, instance, d, USEC_INFINITY, 0, st.st_dev, false,
                        MAX_DEPTH);
    closedir(d);

    if (rmdir(resolved) < 0 && errno != ENOENT && errno != ENOTEMPTY)
      log_warning("Failed to rmdir '%s': %s", instance, strerror(errno));
    return r;
  }

  default:
    return 0;
  }
}

int clean_item(Context *c, Item *i) {
  switch (i->type) {
  case CREATE_DIRECTORY:
  case TRUNCATE_DIRECTORY:
  case CREATE_SUBVOLUME:
  case CREATE_SUBVOLUME_INHERIT_QUOTA:
  case CREATE_SUBVOLUME_NEW_QUOTA:
  case COPY_FILES:
    return clean_item_instance(c, i, i->path);

  case IGNORE_PATH:
  case IGNORE_DIRECTORY_PATH:
    return glob_item(c, i, clean_item_instance);

  default:
    return 0;
  }
}

int remove_item(Context *c, Item *i) {
  switch (i->type) {
  case TRUNCATE_DIRECTORY:
    if (arg_dry_run) {
      log_info("Would empty directory '%s'.", i->path);
      return 0;
    }
    {
      _cleanup_close_ int fd = chase_and_open(i->path, arg_root,
                                              CHASE_PREFIX_ROOT | CHASE_NOFOLLOW,
                                              O_DIRECTORY | O_CLOEXEC | O_RDONLY,
                                              NULL);
      if (fd < 0) {
        if (fd == -ENOENT || fd == -ENOTDIR)
          return 0;
        return log_error_errno(fd, "Failed to open '%s': %s", i->path,
                               strerror(-fd));
      }

      struct stat st;
      if (fstat(fd, &st) < 0)
        return log_error_errno(-errno, "fstat(%s) failed: %s", i->path,
                               strerror(errno));

      DIR *d = xfdopendir(fd);
      if (!d)
        return -errno;
      fd = -1;

      int r = dir_cleanup(c, i->path, d, USEC_INFINITY, 0, st.st_dev, false,
                          MAX_DEPTH);
      closedir(d);
      return r;
    }

  case REMOVE_PATH:
  case RECURSIVE_REMOVE_PATH:
    return glob_item(c, i, remove_item_instance);

  default:
    return 0;
  }
}
