out of tree build: erlang
[libguestfs.git] / cat / virt-ls.c
index 3e789b8..b29af27 100644 (file)
@@ -1,5 +1,5 @@
 /* virt-ls
- * Copyright (C) 2010 Red Hat Inc.
+ * Copyright (C) 2010-2011 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
  *
  * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * 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 <locale.h>
 #include <assert.h>
+#include <time.h>
+#include <libintl.h>
 
+#include "human.h"
 #include "progname.h"
 
 #include "guestfs.h"
 guestfs_h *g;
 
 int read_only = 1;
+int live = 0;
 int verbose = 0;
 int keys_from_stdin = 0;
 int echo_keys = 0;
 const char *libvirt_uri = NULL;
 int inspector = 1;
 
+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);
+static void output_int64_uid (int64_t);
+static void output_string (const char *);
+static void output_string_link (const char *);
+
+static int is_reg (int64_t mode);
+static int is_dir (int64_t mode);
+static int is_chr (int64_t mode);
+static int is_blk (int64_t mode);
+static int is_fifo (int64_t mode);
+static int is_lnk (int64_t mode);
+static int is_sock (int64_t mode);
+
+static size_t count_strings (char **);
+static void free_strings (char **);
+static char **take_strings (char **, size_t n, char ***);
+
 static inline char *
 bad_cast (char const *s)
 {
@@ -56,20 +101,29 @@ usage (int status)
   else {
     fprintf (stdout,
            _("%s: list files in a virtual machine\n"
-             "Copyright (C) 2010 Red Hat Inc.\n"
+             "Copyright (C) 2010-2011 Red Hat Inc.\n"
              "Usage:\n"
-             "  %s [--options] -d domname file [dir ...]\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"
+             "  --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"
              "  --keys-from-stdin    Read passphrases from stdin\n"
              "  -l|--long            Long listing\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"
@@ -83,6 +137,9 @@ usage (int status)
 int
 main (int argc, char *argv[])
 {
+  /* Current time for --time-days, --time-relative output. */
+  time (&now);
+
   /* Set global program name that is not polluted with libtool artifacts.  */
   set_program_name (argv[0]);
 
@@ -92,17 +149,30 @@ main (int argc, char *argv[])
 
   enum { HELP_OPTION = CHAR_MAX + 1 };
 
-  static const char *options = "a:c:d:lRvVx";
+  static const char *options = "a:c:d:hlRvVx";
   static const struct option long_options[] = {
     { "add", 1, 0, 'a' },
+    { "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' },
     { "keys-from-stdin", 0, 0, 0 },
     { "long", 0, 0, 'l' },
     { "recursive", 0, 0, 'R' },
+    { "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 }
@@ -112,7 +182,10 @@ main (int argc, char *argv[])
   const char *format = NULL;
   int c;
   int option_index;
-  char mode = 0;
+#define MODE_LS_L  1
+#define MODE_LS_R  2
+#define MODE_LS_LR (MODE_LS_L|MODE_LS_R)
+  int mode = 0;
 
   g = guestfs_create ();
   if (g == NULL) {
@@ -137,6 +210,34 @@ main (int argc, char *argv[])
           format = NULL;
         else
           format = optarg;
+      } 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 {
         fprintf (stderr, _("%s: unknown long option: %s (%d)\n"),
                  program_name, long_options[option_index].name, option_index);
@@ -157,14 +258,15 @@ main (int argc, char *argv[])
       break;
 
     case 'h':
-      usage (EXIT_SUCCESS);
+      human = 1;
+      break;
 
     case 'l':
-      mode = 'l';
+      mode |= MODE_LS_L;
       break;
 
     case 'R':
-      mode = 'R';
+      mode |= MODE_LS_R;
       break;
 
     case 'v':
@@ -227,6 +329,25 @@ main (int argc, char *argv[])
    */
   assert (read_only == 1);
   assert (inspector == 1);
+  assert (live == 0);
+
+  /* Many flags only apply to -lR mode. */
+  if (mode != MODE_LS_LR &&
+      (csv || human || enable_uids || enable_times || enable_extra_stats ||
+       checksum)) {
+    fprintf (stderr, _("%s: used a flag which can only be combined with -lR mode\nFor more information, read the virt-ls(1) man page.\n"),
+             program_name);
+    exit (EXIT_FAILURE);
+  }
+
+  /* CSV && human is unsafe because spreadsheets fail to parse these
+   * fields correctly.  (RHBZ#600977).
+   */
+  if (human && csv) {
+    fprintf (stderr, _("%s: you cannot use -h and --csv options together.\n"),
+             program_name);
+    exit (EXIT_FAILURE);
+  }
 
   /* User must specify at least one directory name on the command line. */
   if (optind >= argc || argc - optind < 1)
@@ -254,93 +375,807 @@ main (int argc, char *argv[])
   while (optind < argc) {
     const char *dir = argv[optind];
 
-    if (mode == 0) {
-      char **lines;
-      size_t i;
+    switch (mode) {
+    case 0:                     /* no -l or -R option */
+      if (do_ls (dir) == -1)
+        errors++;
+      break;
 
-      if ((lines = guestfs_ls (g, dir)) == NULL)
+    case MODE_LS_L:             /* virt-ls -l */
+      if (do_ls_l (dir) == -1)
         errors++;
-      else {
-        for (i = 0; lines[i] != NULL; ++i) {
-          printf ("%s\n", lines[i]);
-          free (lines[i]);
-        }
-        free (lines);
-      }
-    }
-    else if (mode == 'l') {
-      char *out;
-      size_t i;
+      break;
 
-      if ((out = guestfs_ll (g, dir)) == NULL)
+    case MODE_LS_R:             /* virt-ls -R */
+      if (do_ls_R (dir) == -1)
         errors++;
-      else {
-        printf ("%s", out);
-        free (out);
+      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)
+{
+  char **lines;
+  size_t i;
+
+  if ((lines = guestfs_ls (g, dir)) == NULL) {
+    return -1;
+  }
+
+  for (i = 0; lines[i] != NULL; ++i) {
+    printf ("%s\n", lines[i]);
+    free (lines[i]);
+  }
+  free (lines);
+
+  return 0;
+}
+
+static int
+do_ls_l (const char *dir)
+{
+  char *out;
+
+  if ((out = guestfs_ll (g, dir)) == NULL)
+    return -1;
+
+  printf ("%s", out);
+  free (out);
+
+  return 0;
+}
+
+static int
+do_ls_R (const char *dir)
+{
+  /* This is TMP_TEMPLATE_ON_STACK expanded from fish.h. */
+  const char *tmpdir = guestfs_tmpdir ();
+  char tmpfile[strlen (tmpdir) + 32];
+  sprintf (tmpfile, "%s/virtlsXXXXXX", tmpdir);
+
+  int fd = mkstemp (tmpfile);
+  if (fd == -1) {
+    perror ("mkstemp");
+    exit (EXIT_FAILURE);
+  }
+
+  char buf[BUFSIZ]; /* also used below */
+  snprintf (buf, sizeof buf, "/dev/fd/%d", fd);
+
+  if (guestfs_find0 (g, dir, buf) == -1)
+    return -1;
+
+  if (close (fd) == -1) {
+    perror (tmpfile);
+    exit (EXIT_FAILURE);
+  }
+
+  /* The output of find0 is a \0-separated file.  Turn each \0 into
+   * a \n character.
+   */
+  fd = open (tmpfile, O_RDONLY);
+  if (fd == -1) {
+    perror (tmpfile);
+    exit (EXIT_FAILURE);
+  }
+
+  ssize_t r;
+  while ((r = read (fd, buf, sizeof buf)) > 0) {
+    size_t i;
+    for (i = 0; i < (size_t) r; ++i)
+      if (buf[i] == '\0')
+        buf[i] = '\n';
+
+    size_t n = r;
+    while (n > 0) {
+      r = write (1, buf, n);
+      if (r == -1) {
+        perror ("write");
+        exit (EXIT_FAILURE);
       }
+      n -= r;
+    }
+  }
+
+  if (r == -1 || close (fd) == -1) {
+    perror (tmpfile);
+    exit (EXIT_FAILURE);
+  }
+
+ unlink (tmpfile);
+
+ return 0;
+}
+
+/* Adapted from
+https://rwmj.wordpress.com/2010/12/15/tip-audit-virtual-machine-for-setuid-files/
+*/
+static char *full_path (const char *dir, const char *name);
+static struct guestfs_stat_list *lstatlist (const char *dir, char **names);
+static struct guestfs_xattr_list *lxattrlist (const char *dir, char **names);
+static int show_file (const char *dir, const char *name, const struct guestfs_stat *stat, const struct guestfs_xattr_list *xattrs);
+
+typedef int (*visitor_function) (const char *dir, const char *name, const struct guestfs_stat *stat, const struct guestfs_xattr_list *xattrs);
+
+static int
+visit (int depth, const char *dir, visitor_function f)
+{
+  /* Call 'f' with the top directory.  Note that ordinary recursive
+   * visits will not otherwise do this, so we have to have a special
+   * case.
+   */
+  if (depth == 0) {
+    struct guestfs_stat *stat;
+    struct guestfs_xattr_list *xattrs;
+    int r;
+
+    stat = guestfs_lstat (g, dir);
+    if (stat == NULL)
+      return -1;
+
+    xattrs = guestfs_lgetxattrs (g, dir);
+    if (xattrs == NULL) {
+      guestfs_free_stat (stat);
+      return -1;
+    }
+
+    r = f (dir, NULL, stat, xattrs);
+    guestfs_free_stat (stat);
+    guestfs_free_xattr_list (xattrs);
+
+    if (r == -1)
+      return -1;
+  }
+
+  int ret = -1;
+  char **names = NULL;
+  char *path = NULL;
+  size_t i, xattrp;
+  struct guestfs_stat_list *stats = NULL;
+  struct guestfs_xattr_list *xattrs = NULL;
+
+  names = guestfs_ls (g, dir);
+  if (names == NULL)
+    goto out;
+
+  stats = lstatlist (dir, names);
+  if (stats == NULL)
+    goto out;
+
+  xattrs = lxattrlist (dir, names);
+  if (xattrs == NULL)
+    goto out;
+
+  /* Call function on everything in this directory. */
+  for (i = 0, xattrp = 0; names[i] != NULL; ++i, ++xattrp) {
+    struct guestfs_xattr_list file_xattrs;
+    size_t nr_xattrs;
+
+    assert (stats->len >= i);
+    assert (xattrs->len >= xattrp);
+
+    /* Find the list of extended attributes for this file. */
+    assert (strlen (xattrs->val[xattrp].attrname) == 0);
+
+    if (xattrs->val[xattrp].attrval_len == 0) {
+      fprintf (stderr, _("%s: error getting extended attrs for %s %s\n"),
+               program_name, dir, names[i]);
+      goto out;
+    }
+    /* lxattrlist function made sure attrval was \0-terminated, so we can do */
+    if (sscanf (xattrs->val[xattrp].attrval, "%zu", &nr_xattrs) != 1) {
+      fprintf (stderr, _("%s: error: cannot parse xattr count for %s %s\n"),
+               program_name, dir, names[i]);
+      goto out;
+    }
+
+    file_xattrs.len = nr_xattrs;
+    file_xattrs.val = &xattrs->val[xattrp];
+    xattrp += nr_xattrs;
+
+    /* Call the function. */
+    if (f (dir, names[i], &stats->val[i], &file_xattrs) == -1)
+      goto out;
+
+    /* Recursively call visit, but only on directories. */
+    if (is_dir (stats->val[i].mode)) {
+      path = full_path (dir, names[i]);
+      if (visit (depth + 1, path, f) == -1)
+        goto out;
+      free (path); path = NULL;
+    }
+  }
+
+  ret = 0;
+
+ out:
+  free (path);
+  if (names)
+    free_strings (names);
+  if (stats)
+    guestfs_free_stat_list (stats);
+  if (xattrs)
+    guestfs_free_xattr_list (xattrs);
+  return ret;
+}
+
+static char *
+full_path (const char *dir, const char *name)
+{
+  int r;
+  char *path;
+
+  if (STREQ (dir, "/"))
+    r = asprintf (&path, "/%s", name ? name : "");
+  else if (name)
+    r = asprintf (&path, "%s/%s", dir, name);
+  else
+    r = asprintf (&path, "%s", dir);
+
+  if (r == -1) {
+    perror ("asprintf");
+    exit (EXIT_FAILURE);
+  }
+
+  return path;
+}
+
+/* This calls guestfs_lstatlist, but it splits the names list up so that we
+ * don't overrun the libguestfs protocol limit.
+ */
+#define LSTATLIST_MAX 1000
+
+static struct guestfs_stat_list *
+lstatlist (const char *dir, char **names)
+{
+  size_t len = count_strings (names);
+  char **first;
+  size_t old_len;
+  struct guestfs_stat_list *ret, *stats;
+
+  ret = malloc (sizeof *ret);
+  if (ret == NULL) {
+    perror ("malloc");
+    exit (EXIT_FAILURE);
+  }
+  ret->len = 0;
+  ret->val = NULL;
+
+  while (len > 0) {
+    first = take_strings (names, LSTATLIST_MAX, &names);
+    len = len <= LSTATLIST_MAX ? 0 : len - LSTATLIST_MAX;
+
+    stats = guestfs_lstatlist (g, dir, first);
+    /* Note we don't need to free up the strings because take_strings
+     * does not do a deep copy.
+     */
+    free (first);
+
+    if (stats == NULL) {
+      free (ret);
+      return NULL;
     }
-    else if (mode == 'R') {
-      /* This is TMP_TEMPLATE_ON_STACK expanded from fish.h. */
-      const char *tmpdir = guestfs_tmpdir ();
-      char tmpfile[strlen (tmpdir) + 32];
-      sprintf (tmpfile, "%s/virtlsXXXXXX", tmpdir);
-
-      int fd = mkstemp (tmpfile);
-      if (fd == -1) {
-        perror ("mkstemp");
+
+    /* Append stats to ret. */
+    old_len = ret->len;
+    ret->len += stats->len;
+    ret->val = realloc (ret->val, ret->len * sizeof (struct guestfs_stat));
+    if (ret->val == NULL) {
+      perror ("realloc");
+      exit (EXIT_FAILURE);
+    }
+    memcpy (&ret->val[old_len], stats->val,
+            stats->len * sizeof (struct guestfs_stat));
+
+    guestfs_free_stat_list (stats);
+  }
+
+  return ret;
+}
+
+/* Same as above, for lxattrlist.  Note the rather peculiar format
+ * used to return the list of extended attributes (see
+ * guestfs_lxattrlist documentation).
+ */
+#define LXATTRLIST_MAX 1000
+
+static struct guestfs_xattr_list *
+lxattrlist (const char *dir, char **names)
+{
+  size_t len = count_strings (names);
+  char **first;
+  size_t i, old_len;
+  struct guestfs_xattr_list *ret, *xattrs;
+
+  ret = malloc (sizeof *ret);
+  if (ret == NULL) {
+    perror ("malloc");
+    exit (EXIT_FAILURE);
+  }
+  ret->len = 0;
+  ret->val = NULL;
+
+  while (len > 0) {
+    first = take_strings (names, LXATTRLIST_MAX, &names);
+    len = len <= LXATTRLIST_MAX ? 0 : len - LXATTRLIST_MAX;
+
+    xattrs = guestfs_lxattrlist (g, dir, first);
+    /* Note we don't need to free up the strings because take_strings
+     * does not do a deep copy.
+     */
+    free (first);
+
+    if (xattrs == NULL) {
+      free (ret);
+      return NULL;
+    }
+
+    /* Append xattrs to ret. */
+    old_len = ret->len;
+    ret->len += xattrs->len;
+    ret->val = realloc (ret->val, ret->len * sizeof (struct guestfs_xattr));
+    if (ret->val == NULL) {
+      perror ("realloc");
+      exit (EXIT_FAILURE);
+    }
+    for (i = 0; i < xattrs->len; ++i, ++old_len) {
+      /* We have to make a deep copy of the attribute name and value.
+       * The attrval contains 8 bit data.  However make sure also that
+       * it is \0-terminated, because that makes the calling code
+       * simpler.
+       */
+      ret->val[old_len].attrname = strdup (xattrs->val[i].attrname);
+      ret->val[old_len].attrval = malloc (xattrs->val[i].attrval_len + 1);
+      if (ret->val[old_len].attrname == NULL ||
+          ret->val[old_len].attrval == NULL) {
+        perror ("malloc");
         exit (EXIT_FAILURE);
       }
+      ret->val[old_len].attrval_len = xattrs->val[i].attrval_len;
+      memcpy (ret->val[old_len].attrval, xattrs->val[i].attrval,
+              xattrs->val[i].attrval_len);
+      ret->val[i].attrval[ret->val[i].attrval_len] = '\0';
+    }
 
-      char buf[BUFSIZ]; /* also used below */
-      snprintf (buf, sizeof buf, "/dev/fd/%d", fd);
+    guestfs_free_xattr_list (xattrs);
+  }
 
-      if (guestfs_find0 (g, dir, buf) == -1)
-        errors++;
-      else {
-        if (close (fd) == -1) {
-          perror (tmpfile);
-          exit (EXIT_FAILURE);
-        }
+  return ret;
+}
 
-        /* The output of find0 is a \0-separated file.  Turn each \0 into
-         * a \n character.
-         */
-        fd = open (tmpfile, O_RDONLY);
-        if (fd == -1) {
-          perror (tmpfile);
-          exit (EXIT_FAILURE);
-        }
+static int
+do_ls_lR (const char *dir)
+{
+  return visit (0, dir, show_file);
+}
 
-        ssize_t r;
-        while ((r = read (fd, buf, sizeof buf)) > 0) {
-          size_t i;
-          for (i = 0; i < (size_t) r; ++i)
-            if (buf[i] == '\0')
-              buf[i] = '\n';
-
-          size_t n = r;
-          while (n > 0) {
-            r = write (1, buf, n);
-            if (r == -1) {
-              perror ("write");
-              exit (EXIT_FAILURE);
-            }
-            n -= r;
-          }
-        }
+/* 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_stat *stat,
+           const struct guestfs_xattr_list *xattrs)
+{
+  char filetype[2];
+  char *path = NULL, *csum = NULL, *link = NULL;
+
+  /* Display the basic fields. */
+  output_start_line ();
+
+  if (is_reg (stat->mode))
+    filetype[0] = '-';
+  else if (is_dir (stat->mode))
+    filetype[0] = 'd';
+  else if (is_chr (stat->mode))
+    filetype[0] = 'c';
+  else if (is_blk (stat->mode))
+    filetype[0] = 'b';
+  else if (is_fifo (stat->mode))
+    filetype[0] = 'p';
+  else if (is_lnk (stat->mode))
+    filetype[0] = 'l';
+  else if (is_sock (stat->mode))
+    filetype[0] = 's';
+  else
+    filetype[0] = 'u';
+  filetype[1] = '\0';
+  output_string (filetype);
+  output_int64_perms (stat->mode & 07777);
+
+  output_int64_size (stat->size);
+
+  /* Display extra fields when enabled. */
+  if (enable_uids) {
+    output_int64_uid (stat->uid);
+    output_int64_uid (stat->gid);
+  }
+
+  if (enable_times) {
+    output_int64_time (stat->atime);
+    output_int64_time (stat->mtime);
+    output_int64_time (stat->ctime);
+  }
+
+  if (enable_extra_stats) {
+    output_int64_dev (stat->dev);
+    output_int64 (stat->ino);
+    output_int64 (stat->nlink);
+    output_int64_dev (stat->rdev);
+    output_int64 (stat->blocks);
+  }
+
+  /* Disabled for now -- user would definitely want these to be interpreted.
+  if (enable_xattrs)
+    output_xattrs (xattrs);
+  */
+
+  if (checksum && is_reg (stat->mode)) {
+    csum = guestfs_checksum (g, checksum, path);
+    if (!csum)
+      exit (EXIT_FAILURE);
+
+    output_string (csum);
+  }
+
+  path = full_path (dir, name);
+  output_string (path);
+
+  if (is_lnk (stat->mode))
+    /* XXX Fix this for NTFS. */
+    link = guestfs_readlink (g, path);
+  if (link)
+    output_string_link (link);
+
+  output_end_line ();
+
+  free (path);
+  free (csum);
+  free (link);
+
+  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)
+{
+  int c = csv ? ',' : ' ';
+
+  field++;
+  if (field == 1) return;
+
+  if (putchar (c) == EOF) {
+    perror ("putchar");
+    exit (EXIT_FAILURE);
+  }
+}
+
+static void
+output_start_line (void)
+{
+  field = 0;
+}
+
+static void
+output_end_line (void)
+{
+  if (printf ("\n") < 0) {
+    perror ("printf");
+    exit (EXIT_FAILURE);
+  }
+}
 
-        if (r == -1 || close (fd) == -1) {
-          perror (tmpfile);
+static void
+output_string (const char *s)
+{
+  next_field ();
+
+  if (!csv) {
+  print_no_quoting:
+    if (printf ("%s", s) < 0) {
+      perror ("printf");
+      exit (EXIT_FAILURE);
+    }
+  }
+  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) {
+      perror ("putchar");
+      exit (EXIT_FAILURE);
+    }
+    for (i = 0; i < len; ++i) {
+      if (s[i] == '"') {
+        if (putchar ('"') == EOF || putchar ('"') == EOF) {
+          perror ("putchar");
+          exit (EXIT_FAILURE);
+        }
+      } else {
+        if (putchar (s[i]) == EOF) {
+          perror ("putchar");
           exit (EXIT_FAILURE);
         }
       }
+    }
+    if (putchar ('"') == EOF) {
+      perror ("putchar");
+      exit (EXIT_FAILURE);
+    }
+  }
+}
 
-      unlink (tmpfile);
+static void
+output_string_link (const char *link)
+{
+  if (csv)
+    output_string (link);
+  else {
+    next_field ();
+
+    if (printf ("-> %s", link) < 0) {
+      perror ("printf");
+      exit (EXIT_FAILURE);
     }
-    optind++;
   }
+}
 
-  guestfs_close (g);
+static void
+output_int64 (int64_t i)
+{
+  next_field ();
+  /* csv doesn't need escaping */
+  if (printf ("%" PRIi64, i) < 0) {
+    perror ("printf");
+    exit (EXIT_FAILURE);
+  }
+}
 
-  exit (errors == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
+static void
+output_int64_size (int64_t size)
+{
+  char buf[LONGEST_HUMAN_READABLE];
+  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) {
+    perror ("printf");
+    exit (EXIT_FAILURE);
+  }
+}
+
+static void
+output_int64_perms (int64_t i)
+{
+  next_field ();
+  /* csv doesn't need escaping */
+  if (printf ("%04" PRIo64, i) < 0) {
+    perror ("printf");
+    exit (EXIT_FAILURE);
+  }
+}
+
+static void
+output_int64_time (int64_t i)
+{
+  int r;
+
+  next_field ();
+
+  /* csv doesn't need escaping */
+  if (time_t_output) {
+    switch (time_relative) {
+    case 0:                     /* --time-t */
+      r = printf ("%10" PRIi64, i);
+      break;
+    case 1:                     /* --time-relative */
+      r = printf ("%8" PRIi64, now - i);
+      break;
+    case 2:                     /* --time-days */
+    default:
+      r = printf ("%3" PRIi64, (now - i) / 86400);
+      break;
+    }
+  }
+  else {
+    time_t t = (time_t) i;
+    char buf[64];
+    struct tm *tm;
+
+    tm = localtime (&t);
+    if (tm == NULL) {
+      perror ("localtime");
+      exit (EXIT_FAILURE);
+    }
+
+    if (strftime (buf, sizeof buf, "%F %T", tm) == 0) {
+      perror ("strftime");
+      exit (EXIT_FAILURE);
+    }
+
+    r = printf ("%s", buf);
+  }
+
+  if (r < 0) {
+    perror ("printf");
+    exit (EXIT_FAILURE);
+  }
+}
+
+static void
+output_int64_uid (int64_t i)
+{
+  next_field ();
+  /* csv doesn't need escaping */
+  if (printf ("%4" PRIi64, i) < 0) {
+    perror ("printf");
+    exit (EXIT_FAILURE);
+  }
+}
+
+static void
+output_int64_dev (int64_t i)
+{
+  dev_t dev = i;
+
+  next_field ();
+
+  /* csv doesn't need escaping */
+  if (printf ("%d:%d", major (dev), minor (dev)) < 0) {
+    perror ("printf");
+    exit (EXIT_FAILURE);
+  }
+}
+
+/* In the libguestfs API, modes returned by lstat and friends are
+ * defined to contain Linux ABI values.  However since the "current
+ * operating system" might not be Linux, we have to hard-code those
+ * numbers here.
+ */
+static int
+is_reg (int64_t mode)
+{
+  return (mode & 0170000) == 0100000;
+}
+
+static int
+is_dir (int64_t mode)
+{
+  return (mode & 0170000) == 0040000;
+}
+
+static int
+is_chr (int64_t mode)
+{
+  return (mode & 0170000) == 0020000;
+}
+
+static int
+is_blk (int64_t mode)
+{
+  return (mode & 0170000) == 0060000;
+}
+
+static int
+is_fifo (int64_t mode)
+{
+  return (mode & 0170000) == 0010000;
+}
+
+/* symbolic link */
+static int
+is_lnk (int64_t mode)
+{
+  return (mode & 0170000) == 0120000;
+}
+
+static int
+is_sock (int64_t mode)
+{
+  return (mode & 0170000) == 0140000;
+}
+
+/* String functions. */
+static size_t
+count_strings (char **names)
+{
+  size_t ret = 0;
+
+  while (names[ret] != NULL)
+    ret++;
+  return ret;
+}
+
+static void
+free_strings (char **names)
+{
+  size_t i;
+
+  for (i = 0; names[i] != NULL; ++i)
+    free (names[i]);
+  free (names);
+}
+
+/* Take the first 'n' names, returning a newly allocated list.  The
+ * strings themselves are not duplicated.  If 'lastp' is not NULL,
+ * then it is updated with the pointer to the list of remaining names.
+ */
+static char **
+take_strings (char **names, size_t n, char ***lastp)
+{
+  size_t i;
+
+  char **ret = malloc ((n+1) * sizeof (char *));
+  if (ret == NULL) {
+    perror ("malloc");
+    exit (EXIT_FAILURE);
+  }
+
+  for (i = 0; names[i] != NULL && i < n; ++i)
+    ret[i] = names[i];
+
+  ret[i] = NULL;
+
+  if (lastp)
+    *lastp = &names[i];
+
+  return ret;
 }