/* virt-ls
 * Copyright (C) 2010-2023 Red Hat Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <unistd.h>
#include <getopt.h>
#include <fcntl.h>
#include <errno.h>
#include <error.h>
#include <locale.h>
#include <limits.h>
#include <assert.h>
#include <time.h>
#include <libintl.h>

#if MAJOR_IN_MKDEV
#include <sys/mkdev.h>
#elif MAJOR_IN_SYSMACROS
#include <sys/sysmacros.h>
#else
#include <sys/types.h>
#endif

#include "human.h"
#include "getprogname.h"

#include "guestfs.h"

#include "options.h"
#include "display-options.h"
#include "guestfs-utils.h"
#include "visit.h"

/* Currently open libguestfs handle. */
guestfs_h *g;

int read_only = 1;
int verbose = 0;
int keys_from_stdin = 0;
int echo_keys = 0;
const char *libvirt_uri = NULL;
int inspector = 1;
int in_guestfish = 0;
int in_virt_rescue = 0;

static int csv = 0;
static int human = 0;
static int enable_uids = 0;
static int enable_times = 0;
static int time_t_output = 0;
static int time_relative = 0; /* 1 = seconds, 2 = days */
static int enable_extra_stats = 0;
static const char *checksum = NULL;

static time_t now;

static int do_ls (const char *dir);
static int do_ls_l (const char *dir);
static int do_ls_R (const char *dir);
static int do_ls_lR (const char *dir);

static void output_start_line (void);
static void output_end_line (void);
static void output_int64 (int64_t);
static void output_int64_dev (int64_t);
static void output_int64_perms (int64_t);
static void output_int64_size (int64_t);
static void output_int64_time (int64_t secs, int64_t nsecs);
static void output_int64_uid (int64_t);
static void output_string (const char *);
static void output_string_link (const char *);

static void __attribute__((noreturn))
usage (int status)
{
  if (status != EXIT_SUCCESS)
    fprintf (stderr, _("Try ‘%s --help’ for more information.\n"),
             getprogname ());
  else {
    printf (_("%s: list files in a virtual machine\n"
              "Copyright (C) 2010-2023 Red Hat Inc.\n"
              "Usage:\n"
              "  %s [--options] -d domname dir [dir ...]\n"
              "  %s [--options] -a disk.img [-a disk.img ...] dir [dir ...]\n"
              "Options:\n"
              "  -a|--add image       Add image\n"
              "  --blocksize[=512|4096]\n"
              "                       Set sector size of the disk for -a option\n"
              "  --checksum[=...]     Display file checksums\n"
              "  -c|--connect uri     Specify libvirt URI for -d option\n"
              "  --csv                Comma-Separated Values output\n"
              "  -d|--domain guest    Add disks from libvirt guest\n"
              "  --echo-keys          Don't turn off echo for passphrases\n"
              "  --extra-stats        Display extra stats\n"
              "  --format[=raw|..]    Force disk format for -a option\n"
              "  --help               Display brief help\n"
              "  -h|--human-readable  Human-readable sizes in output\n"
              "  --key selector       Specify a LUKS key\n"
              "  --keys-from-stdin    Read passphrases from stdin\n"
              "  -l|--long            Long listing\n"
              "  -m|--mount dev[:mnt[:opts[:fstype]]]\n"
              "                       Mount dev on mnt (if omitted, /)\n"
              "  -R|--recursive       Recursive listing\n"
              "  --times              Display file times\n"
              "  --time-days          Display file times as days before now\n"
              "  --time-relative      Display file times as seconds before now\n"
              "  --time-t             Display file times as time_t's\n"
              "  --uids               Display UID, GID\n"
              "  -v|--verbose         Verbose messages\n"
              "  -V|--version         Display version and exit\n"
              "  -x                   Trace libguestfs API calls\n"
              "For more information, see the manpage %s(1).\n"),
            getprogname (), getprogname (),
            getprogname (), getprogname ());
  }
  exit (status);
}

int
main (int argc, char *argv[])
{
  /* Current time for --time-days, --time-relative output. */
  time (&now);

  setlocale (LC_ALL, "");
  bindtextdomain (PACKAGE, LOCALEBASEDIR);
  textdomain (PACKAGE);

  enum { HELP_OPTION = CHAR_MAX + 1 };

  static const char options[] = "a:c:d:hlm:RvVx";
  static const struct option long_options[] = {
    { "add", 1, 0, 'a' },
    { "blocksize", 2, 0, 0 },
    { "checksum", 2, 0, 0 },
    { "checksums", 2, 0, 0 },
    { "csv", 0, 0, 0 },
    { "connect", 1, 0, 'c' },
    { "domain", 1, 0, 'd' },
    { "echo-keys", 0, 0, 0 },
    { "extra-stat", 0, 0, 0 },
    { "extra-stats", 0, 0, 0 },
    { "format", 2, 0, 0 },
    { "help", 0, 0, HELP_OPTION },
    { "human-readable", 0, 0, 'h' },
    { "key", 1, 0, 0 },
    { "keys-from-stdin", 0, 0, 0 },
    { "long", 0, 0, 'l' },
    { "long-options", 0, 0, 0 },
    { "mount", 1, 0, 'm' },
    { "recursive", 0, 0, 'R' },
    { "short-options", 0, 0, 0 },
    { "time", 0, 0, 0 },
    { "times", 0, 0, 0 },
    { "time-days", 0, 0, 0 },
    { "time-relative", 0, 0, 0 },
    { "time-t", 0, 0, 0 },
    { "uid", 0, 0, 0 },
    { "uids", 0, 0, 0 },
    { "verbose", 0, 0, 'v' },
    { "version", 0, 0, 'V' },
    { 0, 0, 0, 0 }
  };
  struct drv *drvs = NULL;
  struct drv *drv;
  struct mp *mps = NULL;
  struct mp *mp;
  char *p;
  const char *format = NULL;
  bool format_consumed = true;
  int blocksize = 0;
  bool blocksize_consumed = true;
  int c;
  int option_index;
#define MODE_LS_L  1
#define MODE_LS_R  2
#define MODE_LS_LR (MODE_LS_L|MODE_LS_R)
  int mode = 0;
  struct key_store *ks = NULL;

  g = guestfs_create ();
  if (g == NULL)
    error (EXIT_FAILURE, errno, "guestfs_create");

  for (;;) {
    c = getopt_long (argc, argv, options, long_options, &option_index);
    if (c == -1) break;

    switch (c) {
    case 0:			/* options which are long only */
      if (STREQ (long_options[option_index].name, "long-options"))
        display_long_options (long_options);
      else if (STREQ (long_options[option_index].name, "short-options"))
        display_short_options (options);
      else if (STREQ (long_options[option_index].name, "keys-from-stdin")) {
        keys_from_stdin = 1;
      } else if (STREQ (long_options[option_index].name, "echo-keys")) {
        echo_keys = 1;
      } else if (STREQ (long_options[option_index].name, "format")) {
        OPTION_format;
      } else if (STREQ (long_options[option_index].name, "blocksize")) {
        OPTION_blocksize;
      } else if (STREQ (long_options[option_index].name, "checksum") ||
                 STREQ (long_options[option_index].name, "checksums")) {
        if (!optarg || STREQ (optarg, ""))
          checksum = "md5";
        else
          checksum = optarg;
      } else if (STREQ (long_options[option_index].name, "csv")) {
        csv = 1;
      } else if (STREQ (long_options[option_index].name, "extra-stat") ||
                 STREQ (long_options[option_index].name, "extra-stats")) {
        enable_extra_stats = 1;
      } else if (STREQ (long_options[option_index].name, "time") ||
                 STREQ (long_options[option_index].name, "times")) {
        enable_times = 1;
      } else if (STREQ (long_options[option_index].name, "time-t")) {
        enable_times = 1;
        time_t_output = 1;
      } else if (STREQ (long_options[option_index].name, "time-relative")) {
        enable_times = 1;
        time_t_output = 1;
        time_relative = 1;
      } else if (STREQ (long_options[option_index].name, "time-days")) {
        enable_times = 1;
        time_t_output = 1;
        time_relative = 2;
      } else if (STREQ (long_options[option_index].name, "uid") ||
                 STREQ (long_options[option_index].name, "uids")) {
        enable_uids = 1;
      } else if (STREQ (long_options[option_index].name, "key")) {
        OPTION_key;
      } else
        error (EXIT_FAILURE, 0,
               _("unknown long option: %s (%d)"),
               long_options[option_index].name, option_index);
      break;

    case 'a':
      OPTION_a;
      break;

    case 'c':
      OPTION_c;
      break;

    case 'd':
      OPTION_d;
      break;

    case 'h':
      human = 1;
      break;

    case 'l':
      mode |= MODE_LS_L;
      break;

    case 'm':
      OPTION_m;
      inspector = 0;
      break;

    case 'R':
      mode |= MODE_LS_R;
      break;

    case 'v':
      OPTION_v;
      break;

    case 'V':
      OPTION_V;
      break;

    case 'x':
      OPTION_x;
      break;

    case HELP_OPTION:
      usage (EXIT_SUCCESS);

    default:
      usage (EXIT_FAILURE);
    }
  }

  /* Old-style syntax?  There were no -a or -d options in the old
   * virt-ls which is how we detect this.
   */
  if (drvs == NULL) {
    /* argc - 1 because last parameter is the single directory name. */
    while (optind < argc - 1) {
      if (strchr (argv[optind], '/') ||
          access (argv[optind], F_OK) == 0) { /* simulate -a option */
        drv = calloc (1, sizeof (struct drv));
        if (!drv)
          error (EXIT_FAILURE, errno, "calloc");
        drv->type = drv_a;
        drv->a.filename = strdup (argv[optind]);
        if (!drv->a.filename)
          error (EXIT_FAILURE, errno, "strdup");
        drv->next = drvs;
        drvs = drv;
      } else {                  /* simulate -d option */
        drv = calloc (1, sizeof (struct drv));
        if (!drv)
          error (EXIT_FAILURE, errno, "calloc");
        drv->type = drv_d;
        drv->d.guest = argv[optind];
        drv->next = drvs;
        drvs = drv;
      }

      optind++;
    }
  }

  /* These are really constants, but they have to be variables for the
   * options parsing code.  Assert here that they have known-good
   * values.
   */
  assert (read_only == 1);
  assert (inspector == 1 || mps != NULL);

  CHECK_OPTION_format_consumed;
  CHECK_OPTION_blocksize_consumed;

  /* Many flags only apply to -lR mode. */
  if (mode != MODE_LS_LR &&
      (csv || human || enable_uids || enable_times || enable_extra_stats ||
       checksum))
    error (EXIT_FAILURE, 0,
           _("used a flag which can only be combined with -lR mode\nFor more information, read the virt-ls(1) man page."));

  /* CSV && human is unsafe because spreadsheets fail to parse these
   * fields correctly.  (RHBZ#600977).
   */
  if (human && csv)
    error (EXIT_FAILURE, 0,
           _("you cannot use -h and --csv options together."));

  /* User must specify at least one directory name on the command line. */
  if (optind >= argc || argc - optind < 1)
    usage (EXIT_FAILURE);

  /* User must have specified some drives. */
  if (drvs == NULL) {
    fprintf (stderr, _("%s: error: you must specify at least one -a or -d option.\n"),
             getprogname ());
    usage (EXIT_FAILURE);
  }

  /* Add drives, inspect and mount. */
  add_drives (drvs);

  if (key_store_requires_network (ks) && guestfs_set_network (g, 1) == -1)
    exit (EXIT_FAILURE);

  if (guestfs_launch (g) == -1)
    exit (EXIT_FAILURE);

  if (mps != NULL)
    mount_mps (mps);
  else
    inspect_mount ();

  /* Free up data structures, no longer needed after this point. */
  free_drives (drvs);
  free_mps (mps);
  free_key_store (ks);

  unsigned errors = 0;

  while (optind < argc) {
    const char *dir = argv[optind];

    switch (mode) {
    case 0:                     /* no -l or -R option */
      if (do_ls (dir) == -1)
        errors++;
      break;

    case MODE_LS_L:             /* virt-ls -l */
      if (do_ls_l (dir) == -1)
        errors++;
      break;

    case MODE_LS_R:             /* virt-ls -R */
      if (do_ls_R (dir) == -1)
        errors++;
      break;

    case MODE_LS_LR:            /* virt-ls -lR */
      if (do_ls_lR (dir) == -1)
        errors++;
      break;

    default:
      abort ();                 /* can't happen */
    }

    optind++;
  }

  guestfs_close (g);

  exit (errors == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
}

static int
do_ls (const char *dir)
{
  size_t i;
  CLEANUP_FREE_STRING_LIST char **lines = guestfs_ls (g, dir);

  if (lines == NULL)
    return -1;

  for (i = 0; lines[i] != NULL; ++i)
    printf ("%s\n", lines[i]);

  return 0;
}

static int
do_ls_l (const char *dir)
{
  CLEANUP_FREE char *out = guestfs_ll (g, dir);

  if (out == NULL)
    return -1;

  printf ("%s", out);

  return 0;
}

static int
do_ls_R (const char *dir)
{
  size_t i;
  CLEANUP_FREE_STRING_LIST char **dirs = guestfs_find (g, dir);

  if (dirs == NULL)
    return -1;

  for (i = 0; dirs[i] != NULL; ++i)
    puts (dirs[i]);

  return 0;
}

static int show_file (const char *dir, const char *name, const struct guestfs_statns *stat, const struct guestfs_xattr_list *xattrs, void *unused);

static int
do_ls_lR (const char *dir)
{
  return visit (g, dir, show_file, NULL);
}

/* This is the function which is called to display all files and
 * directories, and it's where the magic happens.  We are called with
 * full stat and extended attributes for each file, so there is no
 * penalty for displaying anything in those structures.  However if we
 * need other things (eg. checksum) we may have to go back to the
 * appliance and then there can be a very large penalty.
 */
static int
show_file (const char *dir, const char *name,
           const struct guestfs_statns *stat,
           const struct guestfs_xattr_list *xattrs,
           void *unused)
{
  const char *filetype;
  CLEANUP_FREE char *path = NULL, *csum = NULL, *link = NULL;

  /* Display the basic fields. */
  output_start_line ();

  if (guestfs_int_is_reg (stat->st_mode))
    filetype = "-";
  else if (guestfs_int_is_dir (stat->st_mode))
    filetype = "d";
  else if (guestfs_int_is_chr (stat->st_mode))
    filetype = "c";
  else if (guestfs_int_is_blk (stat->st_mode))
    filetype = "b";
  else if (guestfs_int_is_fifo (stat->st_mode))
    filetype = "p";
  else if (guestfs_int_is_lnk (stat->st_mode))
    filetype = "l";
  else if (guestfs_int_is_sock (stat->st_mode))
    filetype = "s";
  else
    filetype = "u";
  output_string (filetype);
  output_int64_perms (stat->st_mode & 07777);

  output_int64_size (stat->st_size);

  /* Display extra fields when enabled. */
  if (enable_uids) {
    output_int64_uid (stat->st_uid);
    output_int64_uid (stat->st_gid);
  }

  if (enable_times) {
    output_int64_time (stat->st_atime_sec, stat->st_atime_nsec);
    output_int64_time (stat->st_mtime_sec, stat->st_mtime_nsec);
    output_int64_time (stat->st_ctime_sec, stat->st_ctime_nsec);
  }

  if (enable_extra_stats) {
    output_int64_dev (stat->st_dev);
    output_int64 (stat->st_ino);
    output_int64 (stat->st_nlink);
    output_int64_dev (stat->st_rdev);
    output_int64 (stat->st_blocks);
  }

  /* Disabled for now -- user would definitely want these to be interpreted.
  if (enable_xattrs)
    output_xattrs (xattrs);
  */

  path = guestfs_int_full_path (dir, name);
  if (!path)
    error (EXIT_FAILURE, errno, "guestfs_int_full_path");

  if (checksum) {
    if (guestfs_int_is_reg (stat->st_mode)) {
      csum = guestfs_checksum (g, checksum, path);
      if (!csum)
        exit (EXIT_FAILURE);

      output_string (csum);
    } else if (csv)
      output_string ("");
  }

  output_string (path);

  if (guestfs_int_is_lnk (stat->st_mode))
    /* XXX Fix this for NTFS. */
    link = guestfs_readlink (g, path);
  if (link)
    output_string_link (link);

  output_end_line ();

  return 0;
}

/* Output functions.
 *
 * Note that we have to be careful to check return values from printf
 * in these functions, because we want to catch ENOSPC errors.
 */
static int field;
static void
next_field (void)
{
  const int c = csv ? ',' : ' ';

  field++;
  if (field == 1) return;

  if (putchar (c) == EOF)
    error (EXIT_FAILURE, errno, "putchar");
}

static void
output_start_line (void)
{
  field = 0;
}

static void
output_end_line (void)
{
  if (printf ("\n") < 0)
    error (EXIT_FAILURE, errno, "printf");
}

static void
output_string (const char *s)
{
  next_field ();

  if (!csv) {
  print_no_quoting:
    if (printf ("%s", s) < 0)
      error (EXIT_FAILURE, errno, "printf");
  }
  else {
    /* Quote CSV string without requiring an external module. */
    size_t i, len;
    int needs_quoting = 0;

    len = strlen (s);

    for (i = 0; i < len; ++i) {
      if (s[i] == ' ' || s[i] == '"' ||
          s[i] == '\n' || s[i] == ',') {
        needs_quoting = 1;
        break;
      }
    }

    if (!needs_quoting)
      goto print_no_quoting;

    /* Quoting for CSV fields. */
    if (putchar ('"') == EOF)
      error (EXIT_FAILURE, errno, "putchar");
    for (i = 0; i < len; ++i) {
      if (s[i] == '"') {
        if (putchar ('"') == EOF || putchar ('"') == EOF)
          error (EXIT_FAILURE, errno, "putchar");
      } else {
        if (putchar (s[i]) == EOF)
          error (EXIT_FAILURE, errno, "putchar");
      }
    }
    if (putchar ('"') == EOF)
      error (EXIT_FAILURE, errno, "putchar");
  }
}

static void
output_string_link (const char *link)
{
  if (csv)
    output_string (link);
  else {
    next_field ();

    if (printf ("-> %s", link) < 0)
      error (EXIT_FAILURE, errno, "printf");
  }
}

static void
output_int64 (int64_t i)
{
  next_field ();
  /* csv doesn't need escaping */
  if (printf ("%" PRIi64, i) < 0)
    error (EXIT_FAILURE, errno, "printf");
}

static void
output_int64_size (int64_t size)
{
  char buf[LONGEST_HUMAN_READABLE];
  const int hopts =
    human_round_to_nearest|human_autoscale|human_base_1024|human_SI;
  int r;

  next_field ();

  if (!csv) {
    if (!human)
      r = printf ("%10" PRIi64, size);
    else
      r = printf ("%10s",
                  human_readable ((uintmax_t) size, buf, hopts, 1, 1));
  } else {
    /* CSV is the same as non-CSV but we don't need to right-align. */
    if (!human)
      r = printf ("%" PRIi64, size);
    else
      r = printf ("%s",
                  human_readable ((uintmax_t) size, buf, hopts, 1, 1));
  }

  if (r < 0)
    error (EXIT_FAILURE, errno, "printf");
}

static void
output_int64_perms (int64_t i)
{
  next_field ();
  /* csv doesn't need escaping */
  if (printf ("%04" PRIo64, (uint64_t) i) < 0)
    error (EXIT_FAILURE, errno, "printf");
}

static void
output_int64_time (int64_t secs, int64_t nsecs)
{
  int r;

  next_field ();

  /* csv doesn't need escaping */
  if (time_t_output) {
    switch (time_relative) {
    case 0:                     /* --time-t */
      r = printf ("%10" PRIi64, secs);
      break;
    case 1:                     /* --time-relative */
      r = printf ("%8" PRIi64, now - secs);
      break;
    case 2:                     /* --time-days */
    default:
      r = printf ("%3" PRIi64, (now - secs) / 86400);
      break;
    }
  }
  else {
    time_t t = (time_t) secs;
    char buf[64];
    struct tm *tm;

    tm = localtime (&t);
    if (tm == NULL)
      error (EXIT_FAILURE, errno, "localtime");

    if (strftime (buf, sizeof buf, "%F %T", tm) == 0)
      error (EXIT_FAILURE, errno, "strftime");

    r = printf ("%s", buf);
  }

  if (r < 0)
    error (EXIT_FAILURE, errno, "printf");
}

static void
output_int64_uid (int64_t i)
{
  next_field ();
  /* csv doesn't need escaping */
  if (printf ("%4" PRIi64, i) < 0)
    error (EXIT_FAILURE, errno, "printf");
}

static void
output_int64_dev (int64_t i)
{
  dev_t dev = i;

  next_field ();

  /* csv doesn't need escaping */
  if (printf ("%ju:%ju",
              (uintmax_t) major (dev), (uintmax_t) minor (dev)) < 0)
    error (EXIT_FAILURE, errno, "printf");
}
