fish: Don't eat words when completing case-insensitive paths (RHBZ#582993).
[libguestfs.git] / fish / destpaths.c
index 6cddafa..5ed93ec 100644 (file)
@@ -1,5 +1,5 @@
 /* guestfish - the filesystem interactive shell
- * Copyright (C) 2009 Red Hat Inc. 
+ * Copyright (C) 2009 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
 
 #include <config.h>
 
-#define _GNU_SOURCE            // for strndup, asprintf
-
 #include <stdio.h>
 #include <stdlib.h>
+#include <stddef.h>
 #include <string.h>
 
 #ifdef HAVE_LIBREADLINE
 
 #include "fish.h"
 
+#ifdef HAVE_LIBREADLINE
+// From gnulib's xalloc.h:
+/* Return 1 if an array of N objects, each of size S, cannot exist due
+   to size arithmetic overflow.  S must be positive and N must be
+   nonnegative.  This is a macro, not an inline function, so that it
+   works correctly even when SIZE_MAX < N.
+
+   By gnulib convention, SIZE_MAX represents overflow in size
+   calculations, so the conservative dividend to use here is
+   SIZE_MAX - 1, since SIZE_MAX might represent an overflowed value.
+   However, malloc (SIZE_MAX) fails on all known hosts where
+   sizeof (ptrdiff_t) <= sizeof (size_t), so do not bother to test for
+   exactly-SIZE_MAX allocations on such hosts; this avoids a test and
+   branch when S is known to be 1.  */
+# define xalloc_oversized(n, s) \
+    ((size_t) (sizeof (ptrdiff_t) <= sizeof (size_t) ? -1 : -2) / (s) < (n))
+#endif
+
 /* Readline completion for paths on the guest filesystem, also for
  * devices and LVM names.
  */
 
-int complete_dest_paths = 0; /* SEE NOTE */
+int complete_dest_paths = 1;
 
-/* NOTE: This is currently disabled by default (with no way to
- * enable it).  That's because it's not particularly natural.
- *
- * Also there is a quite serious performance problem.  When listing
- * even moderately long directories, this takes many seconds.  The
- * reason is because it calls guestfs_is_dir on each directory
- * entry, thus lots of round trips to the server.  We could have
- * a "readdir and stat each entry" call to ease this.
- */
+struct word {
+  char *name;
+  int is_dir;
+};
+
+#ifdef HAVE_LIBREADLINE
+static void
+free_words (struct word *words, size_t nr_words)
+{
+  size_t i;
+
+  /* NB. 'words' array is NOT NULL-terminated. */
+  for (i = 0; i < nr_words; ++i)
+    free (words[i].name);
+  free (words);
+}
+
+static int
+compare_words (const void *vp1, const void *vp2)
+{
+  const struct word *w1 = (const struct word *) vp1;
+  const struct word *w2 = (const struct word *) vp2;
+  return strcmp (w1->name, w2->name);
+}
+#endif
 
 char *
 complete_dest_paths_generator (const char *text, int state)
 {
 #ifdef HAVE_LIBREADLINE
 
-  static int len, index;
-  static char **words = NULL;
-  static int nr_words = 0;
-  char *word;
+  static size_t len, index;
+  static struct word *words = NULL;
+  static size_t nr_words = 0;
   guestfs_error_handler_cb old_error_cb;
   void *old_error_cb_data;
 
@@ -73,43 +105,56 @@ complete_dest_paths_generator (const char *text, int state)
 
   if (!state) {
     char **strs;
-    int i, n;
 
     len = strlen (text);
     index = 0;
 
-    if (words) {
-      /* NB. 'words' array is NOT NULL-terminated. */
-      for (i = 0; i < nr_words; ++i)
-       free (words[i]);
-      free (words);
-    }
+    if (words) free_words (words, nr_words);
 
     words = NULL;
     nr_words = 0;
 
     SAVE_ERROR_CB
 
+/* Silently do nothing if an allocation fails */
 #define APPEND_STRS_AND_FREE                                           \
+  do {                                                                 \
     if (strs) {                                                                \
-      n = count_strings (strs);                                                \
-      words = realloc (words, sizeof (char *) * (nr_words + n));       \
-      for (i = 0; i < n; ++i)                                          \
-       words[nr_words++] = strs[i];                                    \
+      size_t i;                                                                \
+      size_t n = count_strings (strs);                                 \
+                                                                        \
+      if ( n > 0 && ! xalloc_oversized (nr_words + n, sizeof (struct word))) { \
+        struct word *w;                                                        \
+        w = realloc (words, sizeof (struct word) * (nr_words + n));    \
+                                                                        \
+        if (w == NULL) {                                               \
+          free_words (words, nr_words);                                        \
+          words = NULL;                                                        \
+          nr_words = 0;                                                        \
+        } else {                                                       \
+          words = w;                                                   \
+          for (i = 0; i < n; ++i) {                                    \
+            words[nr_words].name = strs[i];                            \
+            words[nr_words].is_dir = 0;                                        \
+            nr_words++;                                                        \
+          }                                                            \
+        }                                                              \
+      }                                                                        \
       free (strs);                                                     \
-    }
+    }                                                                  \
+  } while (0)
 
     /* Is it a device? */
-    if (len < 5 || strncmp (text, "/dev/", 5) == 0) {
+    if (len < 5 || STREQLEN (text, "/dev/", 5)) {
       /* Get a list of everything that can possibly begin with /dev/ */
       strs = guestfs_list_devices (g);
-      APPEND_STRS_AND_FREE
+      APPEND_STRS_AND_FREE;
 
       strs = guestfs_list_partitions (g);
-      APPEND_STRS_AND_FREE
+      APPEND_STRS_AND_FREE;
 
       strs = guestfs_lvs (g);
-      APPEND_STRS_AND_FREE
+      APPEND_STRS_AND_FREE;
     }
 
     if (len < 1 || text[0] == '/') {
@@ -117,27 +162,52 @@ complete_dest_paths_generator (const char *text, int state)
        * in that directory, otherwise list everything in /
        */
       char *p, *dir;
+      struct guestfs_dirent_list *dirents;
 
       p = strrchr (text, '/');
       dir = p && p > text ? strndup (text, p - text) : strdup ("/");
-
-      strs = guestfs_ls (g, dir);
-
-      /* Prepend directory to names. */
-      if (strs) {
-       for (i = 0; strs[i]; ++i) {
-         p = NULL;
-         if (strcmp (dir, "/") == 0)
-           asprintf (&p, "/%s", strs[i]);
-         else
-           asprintf (&p, "%s/%s", dir, strs[i]);
-         free (strs[i]);
-         strs[i] = p;
-       }
+      if (dir) {
+        dirents = guestfs_readdir (g, dir);
+
+        /* Prepend directory to names before adding them to the list
+         * of words.
+         */
+        if (dirents) {
+          size_t i;
+
+          for (i = 0; i < dirents->len; ++i) {
+            int err;
+
+            if (STRNEQ (dirents->val[i].name, ".") &&
+                STRNEQ (dirents->val[i].name, "..")) {
+              if (STREQ (dir, "/"))
+                err = asprintf (&p, "/%s", dirents->val[i].name);
+              else
+                err = asprintf (&p, "%s/%s", dir, dirents->val[i].name);
+              if (err >= 0) {
+                if (!xalloc_oversized (nr_words+1, sizeof (struct word))) {
+                  struct word *w;
+
+                  w = realloc (words, sizeof (struct word) * (nr_words+1));
+                  if (w == NULL) {
+                    free_words (words, nr_words);
+                    words = NULL;
+                    nr_words = 0;
+                  }
+                  else {
+                    words = w;
+                    words[nr_words].name = p;
+                    words[nr_words].is_dir = dirents->val[i].ftyp == 'd';
+                    nr_words++;
+                  }
+                }
+              }
+            }
+          }
+
+          guestfs_free_dirent_list (dirents);
+        }
       }
-
-      free (dir);
-      APPEND_STRS_AND_FREE
     }
 
     /* else ...  In theory we could complete other things here such as VG
@@ -150,20 +220,34 @@ complete_dest_paths_generator (const char *text, int state)
   /* This inhibits ordinary (local filename) completion. */
   rl_attempted_completion_over = 1;
 
+  /* Sort the words so the list is stable over multiple calls. */
+  qsort (words, nr_words, sizeof (struct word), compare_words);
+
   /* Complete the string. */
   while (index < nr_words) {
-    word = words[index];
+    struct word *word;
+
+    word = &words[index];
     index++;
-    if (strncasecmp (word, text, len) == 0) {
-      /* Is it a directory? */
-      if (strncmp (word, "/dev/", 5) != 0) {
-       SAVE_ERROR_CB
-       if (guestfs_is_dir (g, word) > 0)
-         rl_completion_append_character = '/';
-       RESTORE_ERROR_CB
-      }
 
-      return strdup (word);
+    /* Whether we should match case insensitively here or not is
+     * determined by the value of the completion-ignore-case readline
+     * variable.  Default to case insensitive.  (See: RHBZ#582993).
+     */
+    char *cic_var = rl_variable_value ("completion-ignore-case");
+    int cic = 1;
+    if (cic_var && STREQ (cic_var, "off"))
+      cic = 0;
+
+    int matches =
+      cic ? STRCASEEQLEN (word->name, text, len)
+          : STREQLEN (word->name, text, len);
+
+    if (matches) {
+      if (word->is_dir)
+        rl_completion_append_character = '/';
+
+      return strdup (word->name);
     }
   }