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

#include <acl/libacl.h>
#include <sys/acl.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/xattr.h>

#include "perms.h"
#include "chase.h"
#include "platform/platform.h"

bool arg_dry_run = false;

static mode_t process_mask_perms(mode_t mode, mode_t current) {
  if ((current & 0111) == 0)
    mode &= ~0111;
  if ((current & 0222) == 0)
    mode &= ~0222;
  if ((current & 0444) == 0)
    mode &= ~0444;
  if (!S_ISDIR(current))
    mode &= ~07000;
  return mode;
}

static int acl_entry_equal(acl_entry_t a, acl_entry_t b) {
  acl_tag_t tag_a, tag_b;

  if (acl_get_tag_type(a, &tag_a) < 0)
    return -errno;
  if (acl_get_tag_type(b, &tag_b) < 0)
    return -errno;

  if (tag_a != tag_b)
    return 0;

  switch (tag_a) {
  case ACL_USER_OBJ:
  case ACL_GROUP_OBJ:
  case ACL_MASK:
  case ACL_OTHER:
    return 1;
  case ACL_USER: {
    uid_t *uid_a = acl_get_qualifier(a);
    if (!uid_a)
      return -errno;
    uid_t *uid_b = acl_get_qualifier(b);
    if (!uid_b) {
      acl_free(uid_a);
      return -errno;
    }
    int eq = *uid_a == *uid_b;
    acl_free(uid_a);
    acl_free(uid_b);
    return eq;
  }
  case ACL_GROUP: {
    gid_t *gid_a = acl_get_qualifier(a);
    if (!gid_a)
      return -errno;
    gid_t *gid_b = acl_get_qualifier(b);
    if (!gid_b) {
      acl_free(gid_a);
      return -errno;
    }
    int eq = *gid_a == *gid_b;
    acl_free(gid_a);
    acl_free(gid_b);
    return eq;
  }
  default:
    return -EINVAL;
  }
}

static int find_acl_entry(acl_t acl, acl_entry_t entry, acl_entry_t *ret) {
  acl_entry_t i;
  int r;

  for (r = acl_get_entry(acl, ACL_FIRST_ENTRY, &i); r == 1;
       r = acl_get_entry(acl, ACL_NEXT_ENTRY, &i)) {
    r = acl_entry_equal(i, entry);
    if (r < 0)
      return r;
    if (r > 0) {
      if (ret)
        *ret = i;
      return 0;
    }
  }
  if (r < 0)
    return -errno;

  return -ENOENT;
}

static int calc_acl_mask_if_needed(acl_t *acl_p) {
  acl_entry_t i;
  int r;
  bool need = false;

  for (r = acl_get_entry(*acl_p, ACL_FIRST_ENTRY, &i); r == 1;
       r = acl_get_entry(*acl_p, ACL_NEXT_ENTRY, &i)) {
    acl_tag_t tag;
    if (acl_get_tag_type(i, &tag) < 0)
      return -errno;
    if (tag == ACL_MASK)
      return 0;
    if (tag == ACL_USER || tag == ACL_GROUP)
      need = true;
  }
  if (r < 0)
    return -errno;

  if (need && acl_calc_mask(acl_p) < 0)
    return -errno;

  return 0;
}

static int add_base_acls_if_needed(acl_t *acl_p, int fd) {
  acl_entry_t i;
  int r;
  bool have_user_obj = false, have_group_obj = false, have_other = false;

  for (r = acl_get_entry(*acl_p, ACL_FIRST_ENTRY, &i); r == 1;
       r = acl_get_entry(*acl_p, ACL_NEXT_ENTRY, &i)) {
    acl_tag_t tag;
    if (acl_get_tag_type(i, &tag) < 0)
      return -errno;
    if (tag == ACL_USER_OBJ)
      have_user_obj = true;
    else if (tag == ACL_GROUP_OBJ)
      have_group_obj = true;
    else if (tag == ACL_OTHER)
      have_other = true;
    if (have_user_obj && have_group_obj && have_other)
      return 0;
  }
  if (r < 0)
    return -errno;

  struct stat st;
  if (fstat(fd, &st) < 0)
    return -errno;

  acl_t basic = acl_from_mode(st.st_mode);
  if (!basic)
    return -errno;

  for (r = acl_get_entry(basic, ACL_FIRST_ENTRY, &i); r == 1;
       r = acl_get_entry(basic, ACL_NEXT_ENTRY, &i)) {
    acl_tag_t tag;
    if (acl_get_tag_type(i, &tag) < 0) {
      acl_free(basic);
      return -errno;
    }
    if ((tag == ACL_USER_OBJ && have_user_obj) ||
        (tag == ACL_GROUP_OBJ && have_group_obj) ||
        (tag == ACL_OTHER && have_other))
      continue;

    acl_entry_t dst;
    if (acl_create_entry(acl_p, &dst) < 0) {
      acl_free(basic);
      return -errno;
    }
    if (acl_copy_entry(dst, i) < 0) {
      acl_free(basic);
      return -errno;
    }
  }
  if (r < 0) {
    acl_free(basic);
    return -errno;
  }

  acl_free(basic);
  return 0;
}

int fd_set_perms(Item *i, int fd, const char *path, const struct stat *st,
                 CreationMode creation) {
  struct stat stbuf;

  if (!i->mode_set && !i->uid_set && !i->gid_set)
    return 0;

  if (!st) {
    if (fstat(fd, &stbuf) < 0)
      return log_error_errno(-errno, "fstat(%s) failed: %s", path,
                             strerror(errno));
    st = &stbuf;
  }

  uid_t new_uid =
    (i->uid_set && (creation != CREATION_EXISTING || !i->uid_only_create))
    ? i->uid
    : st->st_uid;
  gid_t new_gid =
    (i->gid_set && (creation != CREATION_EXISTING || !i->gid_only_create))
    ? i->gid
    : st->st_gid;

  bool do_chown = (new_uid != st->st_uid) || (new_gid != st->st_gid);

  mode_t new_mode;
  if (i->mode_set && (creation != CREATION_EXISTING || !i->mode_only_create)) {
    new_mode =
      i->mask_perms ? process_mask_perms(i->mode, st->st_mode) : i->mode;
  } else {
    new_mode = st->st_mode & 07777;
  }

  bool do_chmod = ((new_mode ^ st->st_mode) & 07777) != 0;

  if (do_chmod && do_chown && !S_ISLNK(st->st_mode)) {
    mode_t temp = new_mode & st->st_mode;
    if (((temp ^ st->st_mode) & 07777) != 0) {
      log_debug("Temporarily reducing mode of \"%s\" to %04o", path, temp);
      if (!arg_dry_run) {
        if (fchmod_opath(fd, temp) < 0)
          return log_error_errno(-errno, "fchmod(%s) failed: %s", path,
                                 strerror(errno));
      }
    }
  }

  if (do_chown) {
    log_debug("%s owner of \"%s\" to %u:%u",
              arg_dry_run ? "Would change" : "Changing", path,
              (unsigned)new_uid, (unsigned)new_gid);
    if (!arg_dry_run) {
      if (fchownat(fd, "", new_uid, new_gid, AT_EMPTY_PATH) < 0)
        return log_error_errno(-errno, "fchownat(%s) failed: %s", path,
                               strerror(errno));
    }
  }

  if (do_chmod || do_chown) {
    if (S_ISLNK(st->st_mode)) {
      log_debug("Skipping mode change for symlink %s.", path);
    } else {
      log_debug("%s mode of \"%s\" to %04o",
                arg_dry_run ? "Would change" : "Changing", path, new_mode);
      if (!arg_dry_run) {
        if (fchmod_opath(fd, new_mode) < 0)
          return log_error_errno(-errno, "fchmod(%s) failed: %s", path,
                                 strerror(errno));
      }
    }
  }

  return 0;
}

int fd_set_xattrs(Item *i, int fd, const char *path) {
  if (i->n_xattrs == 0)
    return 0;

  for (size_t j = 0; j + 1 < i->n_xattrs; j += 2) {
    const char *name = i->xattrs[j];
    const char *value = i->xattrs[j + 1];

    log_debug("%s extended attribute '%s=%s' on %s",
              arg_dry_run ? "Would set" : "Setting", name, value, path);

    if (!arg_dry_run) {
      if (fsetxattr(fd, name, value, strlen(value), 0) < 0)
        return log_error_errno(-errno,
                               "Failed to set extended attribute %s=%s on "
                               "'%s': %s",
                               name, value, path, strerror(errno));
    }
  }

  return 0;
}

static int set_acl_on_fd(int fd, const char *path, acl_type_t type, acl_t acl,
                         bool modify) {
  if (!acl)
    return 0;

  acl_t dup = NULL;

  if (modify) {
    acl_t existing = NULL;
    int gr = acl_get_for_fd(fd, -1, NULL, path, type, &existing);
    if (gr < 0) {
      int saved = -gr;
      if (saved == ENODATA || saved == ENOSYS || saved == EOPNOTSUPP) {
        dup = acl_dup(acl);
        if (!dup)
          return -ENOMEM;
        goto apply;
      }
      return log_error_errno(gr, "Failed to get existing ACL on '%s': %s",
                             path, strerror(saved));
    }

    dup = acl_dup(existing);
    acl_free(existing);
    if (!dup)
      return -ENOMEM;

    acl_entry_t entry;
    for (int r = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); r == 1;
         r = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
      acl_entry_t target;
      int fr = find_acl_entry(dup, entry, &target);
      if (fr == -ENOENT) {
        if (acl_create_entry(&dup, &target) < 0) {
          acl_free(dup);
          return -errno;
        }
      } else if (fr < 0) {
        acl_free(dup);
        return fr;
      }
      if (acl_copy_entry(target, entry) < 0) {
        acl_free(dup);
        return -errno;
      }
    }

    calc_acl_mask_if_needed(&dup);
  } else {
    dup = acl_dup(acl);
    if (!dup)
      return -ENOMEM;
  }

 apply:;
  int r = add_base_acls_if_needed(&dup, fd);
  if (r < 0) {
    acl_free(dup);
    return r;
  }

  log_debug("%s %s ACL on %s", arg_dry_run ? "Would set" : "Setting",
            type == ACL_TYPE_ACCESS ? "access" : "default", path);

  if (!arg_dry_run) {
    int sr = acl_set_for_fd(fd, -1, NULL, path, type, dup);
    if (sr < 0) {
      int saved = -sr;
      acl_free(dup);
      if (saved == ENOSYS || saved == EOPNOTSUPP || saved == ENOTSUP) {
        log_debug("ACLs not supported on %s, ignoring.", path);
        return 0;
      }
      return log_error_errno(sr, "Failed to set ACL on '%s': %s", path,
                             strerror(saved));
    }
  }

  acl_free(dup);
  return 0;
}

int fd_set_acls(Item *i, int fd, const char *path, const struct stat *st,
                CreationMode creation) {
  (void)creation;
  struct stat stbuf;

  if (!i->acl_access && !i->acl_access_exec && !i->acl_default)
    return 0;

  if (!st) {
    if (fstat(fd, &stbuf) < 0)
      return log_error_errno(-errno, "fstat(%s) failed: %s", path,
                             strerror(errno));
    st = &stbuf;
  }

  if (!S_ISREG(st->st_mode) && !S_ISDIR(st->st_mode)) {
    log_debug("Skipping ACL for '%s' (not a regular file or directory).", path);
    return 0;
  }

  int r = 0;
  bool modify = i->append_or_force;

  if (i->acl_access_exec) {
    bool has_exec = S_ISDIR(st->st_mode) || (st->st_mode & 0111);

    if (has_exec) {
      acl_t combined;
      if (i->acl_access) {
        combined = acl_dup(i->acl_access);
        if (!combined)
          return -ENOMEM;

        acl_entry_t entry;
        for (int rv =
	       acl_get_entry(i->acl_access_exec, ACL_FIRST_ENTRY, &entry);
             rv == 1;
             rv = acl_get_entry(i->acl_access_exec, ACL_NEXT_ENTRY, &entry)) {
          acl_entry_t new_entry;
          if (acl_create_entry(&combined, &new_entry) < 0) {
            acl_free(combined);
            return -errno;
          }
          if (acl_copy_entry(new_entry, entry) < 0) {
            acl_free(combined);
            return -errno;
          }
        }
        if (!modify)
          calc_acl_mask_if_needed(&combined);
      } else {
        combined = acl_dup(i->acl_access_exec);
        if (!combined)
          return -ENOMEM;
        if (!modify)
          calc_acl_mask_if_needed(&combined);
      }

      r = set_acl_on_fd(fd, path, ACL_TYPE_ACCESS, combined, modify);
      acl_free(combined);
    } else {
      acl_t stripped = acl_dup(i->acl_access_exec);
      if (!stripped)
        return -ENOMEM;

      acl_entry_t entry;
      for (int rv = acl_get_entry(stripped, ACL_FIRST_ENTRY, &entry); rv == 1;
           rv = acl_get_entry(stripped, ACL_NEXT_ENTRY, &entry)) {
        acl_permset_t permset;
        if (acl_get_permset(entry, &permset) == 0)
          acl_delete_perm(permset, ACL_EXECUTE);
      }

      acl_t combined;
      if (i->acl_access) {
        combined = acl_dup(i->acl_access);
        if (!combined) {
          acl_free(stripped);
          return -ENOMEM;
        }
        acl_entry_t e2;
        for (int rv = acl_get_entry(stripped, ACL_FIRST_ENTRY, &e2); rv == 1;
             rv = acl_get_entry(stripped, ACL_NEXT_ENTRY, &e2)) {
          acl_entry_t new_entry;
          if (acl_create_entry(&combined, &new_entry) < 0) {
            acl_free(combined);
            acl_free(stripped);
            return -errno;
          }
          acl_copy_entry(new_entry, e2);
        }
        if (!modify)
          calc_acl_mask_if_needed(&combined);
        acl_free(stripped);
      } else {
        combined = stripped;
        if (!modify)
          calc_acl_mask_if_needed(&combined);
      }

      r = set_acl_on_fd(fd, path, ACL_TYPE_ACCESS, combined, modify);
      acl_free(combined);
    }
  } else if (i->acl_access) {
    r = set_acl_on_fd(fd, path, ACL_TYPE_ACCESS, i->acl_access, modify);
  }

  if (r < 0)
    return r;

  if (i->acl_default && S_ISDIR(st->st_mode)) {
    r = set_acl_on_fd(fd, path, ACL_TYPE_DEFAULT, i->acl_default, modify);
    if (r < 0)
      return r;
  }

  return 0;
}

int path_set_perms(Item *i, const char *path, CreationMode creation) {
  _cleanup_close_ int fd = chase_and_open(path, arg_root,
                                          CHASE_PREFIX_ROOT | CHASE_NOFOLLOW,
                                          O_RDONLY | O_CLOEXEC | O_PATH, NULL);
  if (fd < 0) {
    if (fd == -ENOENT)
      return 0;
    return fd;
  }
  return fd_set_perms(i, fd, path, NULL, creation);
}

int path_set_xattrs(Item *i, const char *path) {
  if (i->n_xattrs == 0)
    return 0;

  _cleanup_close_ int fd = chase_and_open(path, arg_root,
                                          CHASE_PREFIX_ROOT | CHASE_NOFOLLOW,
                                          O_RDONLY | O_CLOEXEC, NULL);
  if (fd < 0) {
    if (fd == -ENOENT)
      return 0;
    if (fd == -ELOOP) {
      log_debug("Cannot set xattrs on symlink '%s', skipping.", path);
      return 0;
    }
    return fd;
  }
  return fd_set_xattrs(i, fd, path);
}

int path_set_acls(Item *i, const char *path, CreationMode creation) {
  if (!i->acl_access && !i->acl_access_exec && !i->acl_default)
    return 0;

  _cleanup_close_ int fd = chase_and_open(path, arg_root,
                                          CHASE_PREFIX_ROOT | CHASE_NOFOLLOW,
                                          O_RDONLY | O_CLOEXEC | O_PATH, NULL);
  if (fd < 0) {
    if (fd == -ENOENT)
      return 0;
    return fd;
  }
  return fd_set_acls(i, fd, path, NULL, creation);
}
