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

#include <ctype.h>

#include "util.h"
#include "chase.h"
#include "offline-password.h"

int log_level = LOG_INFO;

char *arg_root = NULL;

const char *startswith(const char *s, const char *prefix) {
  size_t pl = strlen(prefix);
  if (strncmp(s, prefix, pl) == 0)
    return s + pl;
  return NULL;
}

bool endswith(const char *s, const char *suffix) {
  size_t sl = strlen(s);
  size_t xl = strlen(suffix);
  if (xl > sl)
    return false;
  return memcmp(s + sl - xl, suffix, xl) == 0;
}

char *strstrip(char *s) {
  while (*s && isspace((unsigned char)*s))
    s++;
  char *e = s + strlen(s);
  while (e > s && isspace((unsigned char)e[-1]))
    e--;
  *e = '\0';
  return s;
}

char *xasprintf(const char *fmt, ...) {
  va_list ap;
  char *r;

  va_start(ap, fmt);
  if (vasprintf(&r, fmt, ap) < 0) {
    va_end(ap);
    abort();
  }
  va_end(ap);
  return r;
}

int cunescape(const char *s, char **ret) {
  size_t len = strlen(s);
  char *buf = malloc(len + 1);
  if (!buf)
    return -ENOMEM;

  char *o = buf;
  for (const char *p = s; *p;) {
    if (*p != '\\') {
      *o++ = *p++;
      continue;
    }

    p++;
    if (!*p) {
      *o++ = '\\';
      break;
    }

    switch (*p) {
    case 'a':
      *o++ = '\a';
      p++;
      break;
    case 'b':
      *o++ = '\b';
      p++;
      break;
    case 'f':
      *o++ = '\f';
      p++;
      break;
    case 'n':
      *o++ = '\n';
      p++;
      break;
    case 'r':
      *o++ = '\r';
      p++;
      break;
    case 't':
      *o++ = '\t';
      p++;
      break;
    case 'v':
      *o++ = '\v';
      p++;
      break;
    case '\\':
      *o++ = '\\';
      p++;
      break;
    case '"':
      *o++ = '"';
      p++;
      break;
    case '\'':
      *o++ = '\'';
      p++;
      break;

    case 'x': {
      p++;
      if (!isxdigit((unsigned char)p[0])) {
        *o++ = '\\';
        *o++ = 'x';
        break;
      }
      char hex[3] = {p[0], 0, 0};
      p++;
      if (isxdigit((unsigned char)*p))
        hex[1] = *p++;
      *o++ = (char)strtoul(hex, NULL, 16);
      break;
    }

    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7': {
      unsigned val = (unsigned)(*p++ - '0');
      if (*p >= '0' && *p <= '7')
        val = val * 8 + (unsigned)(*p++ - '0');
      if (*p >= '0' && *p <= '7')
        val = val * 8 + (unsigned)(*p++ - '0');
      *o++ = (char)(val & 0xff);
      break;
    }

    default:
      *o++ = '\\';
      *o++ = *p++;
      break;
    }
  }

  *o = '\0';
  *ret = buf;
  return 0;
}

int extract_first_word(const char **p, char **ret, const char *separators,
                       int flags) {
  if (!separators)
    separators = " \t";

  while (**p && strchr(separators, **p))
    (*p)++;

  if (**p == '\0') {
    *ret = NULL;
    return 0;
  }

  size_t cap = 64, len = 0;
  char *buf = malloc(cap);
  if (!buf)
    return -ENOMEM;

#define PUSH(c)					\
  do {						\
    if (len + 1 >= cap) {			\
      cap *= 2;					\
      char *nb = realloc(buf, cap);		\
      if (!nb) {				\
        free(buf);				\
        return -ENOMEM;				\
      }						\
      buf = nb;					\
    }						\
    buf[len++] = (c);				\
  } while (0)

  bool quote_single = false, quote_double = false;

  for (; **p; (*p)++) {
    char c = **p;

    if (c == '\\' && !quote_single) {
      (*p)++;
      if (**p == '\0') {
        if (flags & EXTRACT_RELAX)
          break;
        free(buf);
        return -EINVAL;
      }
      if (flags & EXTRACT_CUNESCAPE) {
        switch (**p) {
        case 'n':
          PUSH('\n');
          continue;
        case 't':
          PUSH('\t');
          continue;
        case 'r':
          PUSH('\r');
          continue;
        case '\\':
          PUSH('\\');
          continue;
        case '"':
          PUSH('"');
          continue;
        case '\'':
          PUSH('\'');
          continue;
        case ' ':
          PUSH(' ');
          continue;
        default:
          if (flags & EXTRACT_RETAIN_ESCAPE)
            PUSH('\\');
          PUSH(**p);
          continue;
        }
      }
      if (flags & EXTRACT_RETAIN_ESCAPE)
        PUSH('\\');
      PUSH(**p);
      continue;
    }

    if (c == '\'' && !(flags & EXTRACT_UNQUOTE ? quote_double : false)) {
      if (flags & EXTRACT_UNQUOTE) {
        quote_single = !quote_single;
        continue;
      }
      PUSH(c);
      continue;
    }

    if (c == '"' && !(flags & EXTRACT_UNQUOTE ? quote_single : false)) {
      if (flags & EXTRACT_UNQUOTE) {
        quote_double = !quote_double;
        continue;
      }
      PUSH(c);
      continue;
    }

    if (!quote_single && !quote_double && strchr(separators, c))
      break;

    PUSH(c);
  }
#undef PUSH

  if ((quote_single || quote_double) && !(flags & EXTRACT_RELAX)) {
    free(buf);
    return -EINVAL;
  }

  buf[len] = '\0';
  *ret = buf;
  return 1;
}

char *path_simplify(char *p) {
  if (!p || !*p)
    return p;

  char *r = p, *w = p;
  bool slash = false;

  for (; *r; r++) {
    if (*r == '/') {
      if (slash)
        continue;
      slash = true;
      if (r[1] == '.' && (r[2] == '/' || r[2] == '\0')) {
        *w++ = '/';
        r++;
        continue;
      }
    } else {
      slash = false;
    }
    *w++ = *r;
  }

  if (w > p + 1 && w[-1] == '/')
    w--;
  *w = '\0';
  return p;
}

char *path_join(const char *dir, const char *file) {
  if (!dir || !*dir)
    return xstrdup(file);
  if (!file || !*file)
    return xstrdup(dir);

  size_t dl = strlen(dir);
  bool ds = dir[dl - 1] == '/';
  bool fs = file[0] == '/';

  if (ds && fs)
    return xasprintf("%s%s", dir, file + 1);
  if (!ds && !fs)
    return xasprintf("%s/%s", dir, file);
  return xasprintf("%s%s", dir, file);
}

const char *path_startswith(const char *path, const char *prefix) {
  if (!path || !prefix)
    return NULL;

  for (;;) {
    while (*path == '/')
      path++;
    while (*prefix == '/')
      prefix++;

    if (*prefix == '\0')
      return path;
    if (*path == '\0')
      return NULL;

    const char *a = path, *b = prefix;
    while (*a && *a != '/' && *b && *b != '/' && *a == *b) {
      a++;
      b++;
    }

    if (*b && *b != '/')
      return NULL;
    if (*a && *a != '/')
      return NULL;

    path = a;
    prefix = b;
  }
}

char *path_parent(const char *path) {
  if (!path || !*path)
    return xstrdup(".");

  size_t len = strlen(path);
  while (len > 1 && path[len - 1] == '/')
    len--;

  if (len == 1 && path[0] == '/')
    return NULL;

  while (len > 0 && path[len - 1] != '/')
    len--;
  if (len == 0)
    return xstrdup(".");
  while (len > 1 && path[len - 1] == '/')
    len--;

  return xstrndup(path, len);
}

const char *path_basename(const char *path) {
  if (!path)
    return NULL;
  const char *b = strrchr(path, '/');
  return b ? b + 1 : path;
}

int parse_duration(const char *s, uint64_t *ret) {
  if (!s || !*s)
    return -EINVAL;

  if (streq(s, "infinity") || streq(s, "-")) {
    *ret = USEC_INFINITY;
    return 0;
  }

  static const struct {
    const char *suffix;
    uint64_t mult;
  } table[] = {
    {"usec", 1},
    {"us", 1},
    {"msec", USEC_PER_MSEC},
    {"ms", USEC_PER_MSEC},
    {"seconds", USEC_PER_SEC},
    {"second", USEC_PER_SEC},
    {"sec", USEC_PER_SEC},
    {"s", USEC_PER_SEC},
    {"minutes", USEC_PER_MIN},
    {"minute", USEC_PER_MIN},
    {"min", USEC_PER_MIN},
    {"m", USEC_PER_MIN},
    {"hours", USEC_PER_HOUR},
    {"hour", USEC_PER_HOUR},
    {"hr", USEC_PER_HOUR},
    {"h", USEC_PER_HOUR},
    {"days", USEC_PER_DAY},
    {"day", USEC_PER_DAY},
    {"d", USEC_PER_DAY},
    {"weeks", USEC_PER_WEEK},
    {"week", USEC_PER_WEEK},
    {"w", USEC_PER_WEEK},
  };

  const char *p = s;
  uint64_t total = 0;
  bool found = false;

  while (*p) {
    while (*p && isspace((unsigned char)*p))
      p++;
    if (!*p)
      break;

    if (!isdigit((unsigned char)*p))
      return -EINVAL;

    char *end;
    errno = 0;
    unsigned long long val = strtoull(p, &end, 10);
    if (errno)
      return -errno;
    p = end;

    while (*p && isspace((unsigned char)*p))
      p++;

    uint64_t mult = USEC_PER_SEC;
    for (size_t i = 0; i < ELEMENTSOF(table); i++) {
      const char *after = startswith(p, table[i].suffix);
      if (after) {
        mult = table[i].mult;
        p = after;
        break;
      }
    }

    total += (uint64_t)val * mult;
    found = true;
  }

  if (!found)
    return -EINVAL;

  *ret = total;
  return 0;
}

int parse_uid(const char *s, uid_t *ret) {
  if (!s || !*s)
    return -EINVAL;

  char *end;
  errno = 0;
  unsigned long val = strtoul(s, &end, 10);
  if (errno)
    return -errno;
  if (*end != '\0')
    return -EINVAL;
  if (val > (unsigned long)UINT32_MAX)
    return -ERANGE;

  *ret = (uid_t)val;
  return 0;
}

int parse_gid(const char *s, gid_t *ret) {
  uid_t tmp;
  int r = parse_uid(s, &tmp);
  if (r < 0)
    return r;
  *ret = (gid_t)tmp;
  return 0;
}

int resolve_user(const char *name, uid_t *ret) {
  if (!name || !*name || streq(name, "-"))
    return -EINVAL;

  if (parse_uid(name, ret) == 0)
    return 0;

  if (arg_root)
    return name_to_uid_offline(arg_root, name, ret);

  errno = 0;
  struct passwd *pw = getpwnam(name);
  if (!pw)
    return errno > 0 ? -errno : -ESRCH;

  *ret = pw->pw_uid;
  return 0;
}

int resolve_group(const char *name, gid_t *ret) {
  if (!name || !*name || streq(name, "-"))
    return -EINVAL;

  if (parse_gid(name, ret) == 0)
    return 0;

  if (arg_root)
    return name_to_gid_offline(arg_root, name, ret);

  errno = 0;
  struct group *gr = getgrnam(name);
  if (!gr)
    return errno > 0 ? -errno : -ESRCH;

  *ret = gr->gr_gid;
  return 0;
}

int parse_mode(const char *s, mode_t *ret) {
  if (!s || !*s)
    return -EINVAL;

  char *end;
  errno = 0;
  unsigned long val = strtoul(s, &end, 8);
  if (errno)
    return -errno;
  if (*end != '\0')
    return -EINVAL;
  if (val > 07777)
    return -ERANGE;

  *ret = (mode_t)val;
  return 0;
}

int safe_open(const char *path, int flags, mode_t mode,
              unsigned expected_type) {
  int fd = open(path, flags | O_NOFOLLOW | O_CLOEXEC, mode);
  if (fd < 0)
    return -errno;

  if (expected_type != 0) {
    struct stat st;
    if (fstat(fd, &st) < 0) {
      int e = errno;
      close(fd);
      return -e;
    }
    if ((st.st_mode & S_IFMT) != expected_type) {
      close(fd);
      return -ELOOP;
    }
  }

  return fd;
}

int open_dir(const char *path) {
  int fd = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
  if (fd < 0)
    return -errno;
  return fd;
}

int open_dir_at(int dir_fd, const char *path) {
  int fd =
    openat(dir_fd, path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
  if (fd < 0)
    return -errno;
  return fd;
}

int open_parent(const char *path, const char **ret_basename) {
  const char *bn = path_basename(path);
  if (!bn || !*bn || streq(bn, "/"))
    return -EINVAL;

  _cleanup_free_ char *parent = path_parent(path);
  if (!parent)
    return -ENOMEM;

  int fd = open_dir(parent);
  if (fd < 0)
    return fd;

  *ret_basename = bn;
  return fd;
}

DIR *xfdopendir(int fd) {
  int fd2 = fcntl(fd, F_DUPFD_CLOEXEC, 3);
  if (fd2 < 0)
    return NULL;

  DIR *d = fdopendir(fd2);
  if (!d) {
    close(fd2);
    return NULL;
  }
  return d;
}

int mkdirat_safe(int dir_fd, const char *name, mode_t mode) {
  if (mkdirat(dir_fd, name, mode) < 0) {
    if (errno == EEXIST) {
      int fd =
	openat(dir_fd, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
      if (fd < 0) {
        if (errno == ENOTDIR || errno == ELOOP)
          return -EEXIST;
        return -errno;
      }
      close(fd);
      return 0;
    }
    return -errno;
  }
  return 0;
}

int read_fd_full(int fd, char **ret, size_t *ret_size) {
  size_t alloc = 4096;
  char *buf = malloc(alloc);
  if (!buf)
    return -ENOMEM;

  size_t total = 0;
  for (;;) {
    if (total + 1 >= alloc) {
      if (alloc > SIZE_MAX / 2) {
        free(buf);
        return -ENOMEM;
      }
      alloc *= 2;
      char *nb = realloc(buf, alloc);
      if (!nb) {
        free(buf);
        return -ENOMEM;
      }
      buf = nb;
    }
    ssize_t n = read(fd, buf + total, alloc - total - 1);
    if (n < 0) {
      if (errno == EINTR)
        continue;
      free(buf);
      return -errno;
    }
    if (n == 0)
      break;
    total += (size_t)n;
  }

  buf[total] = '\0';
  *ret = buf;
  if (ret_size)
    *ret_size = total;
  return 0;
}

int read_file_full(const char *path, char **ret, size_t *ret_size) {
  _cleanup_close_ int fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
  if (fd < 0)
    return -errno;

  struct stat st;
  if (fstat(fd, &st) < 0)
    return -errno;
  if (!S_ISREG(st.st_mode))
    return -EINVAL;

  return read_fd_full(fd, ret, ret_size);
}

int rm_rf_children(int dir_fd) {
  DIR *d = xfdopendir(dir_fd);
  if (!d)
    return -errno;

  int ret = 0;
  struct dirent *de;

  errno = 0;
  while ((de = readdir(d))) {
    if (streq(de->d_name, ".") || streq(de->d_name, ".."))
      continue;

    bool is_dir = false;
#ifdef _DIRENT_HAVE_D_TYPE
    if (de->d_type == DT_DIR)
      is_dir = true;
    else if (de->d_type != DT_UNKNOWN)
      is_dir = false;
    else
#endif
      {
	struct stat st;
	if (fstatat(dir_fd, de->d_name, &st, AT_SYMLINK_NOFOLLOW) < 0) {
	  if (ret == 0)
	    ret = -errno;
	  continue;
	}
	is_dir = S_ISDIR(st.st_mode);
      }

    if (is_dir) {
      int sub_fd = openat(dir_fd, de->d_name,
                          O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
      if (sub_fd < 0) {
        if (ret == 0)
          ret = -errno;
        continue;
      }

      int r = rm_rf_children(sub_fd);
      close(sub_fd);
      if (r < 0 && ret == 0)
        ret = r;

      if (unlinkat(dir_fd, de->d_name, AT_REMOVEDIR) < 0 && ret == 0)
        ret = -errno;
    } else {
      if (unlinkat(dir_fd, de->d_name, 0) < 0 && ret == 0)
        ret = -errno;
    }
    errno = 0;
  }
  if (errno && ret == 0)
    ret = -errno;

  closedir(d);
  return ret;
}

int rm_rf(const char *path) {
  _cleanup_free_ char *resolved = NULL;
  if (arg_root) {
    int cr = chase(path, arg_root,
                   CHASE_PREFIX_ROOT | CHASE_NOFOLLOW | CHASE_NONEXISTENT,
                   &resolved, NULL);
    if (cr == -ENOENT)
      return 0;
    if (cr < 0)
      return cr;
    path = resolved;
  }

  if (unlink(path) == 0)
    return 0;
  if (errno != EISDIR && errno != EPERM)
    return -errno;

  _cleanup_close_ int fd =
    open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
  if (fd < 0)
    return -errno;

  int r = rm_rf_children(fd);
  if (rmdir(path) < 0 && r == 0)
    r = -errno;
  return r;
}

const char *get_boot_id(void) {
  static char buf[37];
  static bool cached = false;

  if (cached)
    return buf;

  _cleanup_free_ char *contents = NULL;
  if (read_file_full("/proc/sys/kernel/random/boot_id", &contents, NULL) < 0)
    return NULL;

  char *s = strstrip(contents);
  size_t l = strlen(s);
  if (l >= sizeof(buf))
    l = sizeof(buf) - 1;
  memcpy(buf, s, l);
  buf[l] = '\0';
  cached = true;
  return buf;
}

const char *get_machine_id(void) {
  static char buf[33];
  static bool cached = false;

  if (cached)
    return buf;

  _cleanup_free_ char *contents = NULL;
  if (arg_root) {
    _cleanup_close_ int fd = chase_and_open("/etc/machine-id", arg_root,
                                            CHASE_PREFIX_ROOT,
                                            O_RDONLY | O_CLOEXEC, NULL);
    if (fd < 0)
      return NULL;
    if (read_fd_full(fd, &contents, NULL) < 0)
      return NULL;
  } else {
    if (read_file_full("/etc/machine-id", &contents, NULL) < 0)
      return NULL;
  }

  char *s = strstrip(contents);
  size_t l = strlen(s);
  if (l >= sizeof(buf))
    l = sizeof(buf) - 1;
  memcpy(buf, s, l);
  buf[l] = '\0';
  cached = true;
  return buf;
}

ssize_t readlink_dynamic(const char *restrict path, char **buf) {
	size_t bufsize = 256;
	*buf = NULL;
	for (;;) {
		*buf = realloc(*buf, bufsize);
		if (!*buf)
			return -ENOMEM;
		ssize_t len = readlink(path, *buf, bufsize);
		if (len < 0)
			return -errno;
		if ((size_t)len < bufsize) {
			(*buf)[len] = '\0';
			return len;
		}
		bufsize *= 2;
	}
}

ssize_t readlinkat_dynamic(int dirfd, const char *restrict path, char **buf) {
	size_t bufsize = 256;
	*buf = NULL;
	for (;;) {
		*buf = realloc(*buf, bufsize);
		if (!*buf)
			return -ENOMEM;
		ssize_t len = readlinkat(dirfd, path, *buf, bufsize);
		if (len < 0)
			return -errno;
		if ((size_t)len < bufsize) {
			(*buf)[len] = '\0';
			return len;
		}
		bufsize *= 2;
	}
}
