[Libguestfs] [nbdkit PATCH 5/5] sh, eval: Implement .default_export

Eric Blake
2020-08-25T15:46:59Z

Use the recently added nbdkit_string_intern to make this possible.
Testsuite coverage is added in both tls-fallback as well as a cleanup
to test-eval-exports.sh.

Signed-off-by: Eric Blake <eblake@redhat.com>
---
 plugins/eval/nbdkit-eval-plugin.pod |  2 ++
 plugins/sh/nbdkit-sh-plugin.pod     | 20 +++++++++--
 plugins/sh/methods.h                |  1 +
 plugins/eval/eval.c                 |  2 ++
 plugins/sh/methods.c                | 54 +++++++++++++++++++++++++++--
 plugins/sh/sh.c                     |  1 +
 plugins/sh/example.sh               |  2 +-
 tests/test-eval-exports.sh          | 35 ++++++++++++-------
 tests/test-tls-fallback.sh          | 11 +++---
 9 files changed, 107 insertions(+), 21 deletions(-)

diff --git a/plugins/eval/nbdkit-eval-plugin.pod
b/plugins/eval/nbdkit-eval-plugin.pod
index 7126c6de..eed97f11 100644
--- a/plugins/eval/nbdkit-eval-plugin.pod
+++ b/plugins/eval/nbdkit-eval-plugin.pod
@@ -110,6 +110,8 @@ features):

 =item B<list_exports=>SCRIPT

+=item B<default_export=>SCRIPT
+
 =item B<open=>SCRIPT

 =item B<pread=>SCRIPT
diff --git a/plugins/sh/nbdkit-sh-plugin.pod b/plugins/sh/nbdkit-sh-plugin.pod
index 07d90b57..b827b658 100644
--- a/plugins/sh/nbdkit-sh-plugin.pod
+++ b/plugins/sh/nbdkit-sh-plugin.pod
@@ -315,8 +315,24 @@ export name or description, although this could be possible
under a
 new mode supporting escape sequences.

 This method is I<not> required; if it is absent, the list of exports
-advertised by nbdkit will be the single export with the empty string
-as a name and no description.
+advertised by nbdkit will be the single name result of C<default_export>
+and no description.
+
+=item C<default_export>
+
+ /path/to/script default_export <readonly> <tls>
+
+The C<readonly> and C<tls> parameters will be C<true> or
C<false>.
+
+On success this should print a name on stdout to use in place of the
+default export C<"">, then exit with code C<0>.  For
convenience, the
+output can be any of the list forms recognized by C<list_exports>, in
+which case the first listed export name is used, and where an empty
+list uses C<"">.  Given the current set of recognized export
lists, it
+is not possible for the resulting name to include a newline.
+
+This method is I<not> required; if it is absent, the default export
+name will be the empty string, C<"">.

 =item C<open>

diff --git a/plugins/sh/methods.h b/plugins/sh/methods.h
index 69017fa4..3fd4ef42 100644
--- a/plugins/sh/methods.h
+++ b/plugins/sh/methods.h
@@ -46,6 +46,7 @@ extern int sh_after_fork (void);
 extern int sh_preconnect (int readonly);
 extern int sh_list_exports (int readonly, int default_only,
                             struct nbdkit_exports *exports);
+extern const char *sh_default_export (int readonly, int is_tls);
 extern void *sh_open (int readonly);
 extern void sh_close (void *handle);
 extern int64_t sh_get_size (void *handle);
diff --git a/plugins/eval/eval.c b/plugins/eval/eval.c
index 2bd5e79f..a123734e 100644
--- a/plugins/eval/eval.c
+++ b/plugins/eval/eval.c
@@ -68,6 +68,7 @@ static const char *known_methods[] = {
   "close",
   "config",
   "config_complete",
+  "default_export",
   "dump_plugin",
   "extents",
   "flush",
@@ -395,6 +396,7 @@ static struct nbdkit_plugin plugin = {

   .preconnect        = sh_preconnect,
   .list_exports      = sh_list_exports,
+  .default_export    = sh_default_export,
   .open              = sh_open,
   .close             = sh_close,

diff --git a/plugins/sh/methods.c b/plugins/sh/methods.c
index 07fb3e48..a2631b39 100644
--- a/plugins/sh/methods.c
+++ b/plugins/sh/methods.c
@@ -241,7 +241,9 @@ parse_exports (const char *script,
 {
   const char *n, *d, *p, *q;

-  /* The first line determines how to parse the rest of s */
+  /* The first line determines how to parse the rest of s.  Keep
+   * sh_default_export in sync with this.
+   */
   if ((p = skip_prefix (s, "INTERLEAVED\n")) != NULL) {
     n = p;
     while ((d = strchr (n, '\n')) != NULL) {
@@ -310,13 +312,18 @@ sh_list_exports (int readonly, int default_only,
                          default_only ? "true" : "false",
NULL };
   CLEANUP_FREE char *s = NULL;
   size_t slen;
+  const char *def;

   switch (call_read (&s, &slen, args)) {
   case OK:
     return parse_exports (script, s, slen, exports);

   case MISSING:
-    return nbdkit_add_export (exports, "", NULL);
+    /* Match what is done for a C plugin. */
+    def = sh_default_export (readonly, nbdkit_is_tls ());
+    if (!def)
+      return 0;
+    return nbdkit_add_export (exports, def, NULL);

   case ERROR:
     return -1;
@@ -331,6 +338,49 @@ sh_list_exports (int readonly, int default_only,
   }
 }

+const char *
+sh_default_export (int readonly, int is_tls)
+{
+  const char *method = "default_export";
+  const char *script = get_script (method);
+  const char *args[] = { script, method, readonly ? "true" :
"false",
+                         is_tls ? "true" : "false", NULL };
+  CLEANUP_FREE char *s = NULL;
+  size_t slen;
+  const char *p, *n;
+  CLEANUP_FREE char *name = NULL;
+
+  switch (call_read (&s, &slen, args)) {
+  case OK:
+    /* The first line determines how to parse the rest of s.  For now,
+     * all export modes treat the next line as the first export.
+     */
+    if ((p = skip_prefix (s, "INTERLEAVED\n")) != NULL ||
+        (p = skip_prefix (s, "NAMES+DESCRIPTIONS\n")) != NULL ||
+        (p = skip_prefix (s, "NAMES\n")) != NULL)
+      ;
+    else
+      p = s;
+    n = strchr (p, '\n') ?: s + slen;
+    name = strndup (p, n - p);
+    return nbdkit_string_intern (name);
+
+  case MISSING:
+    return nbdkit_string_intern ("");
+
+  case ERROR:
+    return NULL;
+
+  case RET_FALSE:
+    nbdkit_error ("%s: %s method returned unexpected code (3/false)",
+                  script, method);
+    errno = EIO;
+    return NULL;
+
+  default: abort ();
+  }
+}
+
 void *
 sh_open (int readonly)
 {
diff --git a/plugins/sh/sh.c b/plugins/sh/sh.c
index 374888a4..4fcc2a5a 100644
--- a/plugins/sh/sh.c
+++ b/plugins/sh/sh.c
@@ -301,6 +301,7 @@ static struct nbdkit_plugin plugin = {

   .preconnect        = sh_preconnect,
   .list_exports      = sh_list_exports,
+  .default_export    = sh_default_export,
   .open              = sh_open,
   .close             = sh_close,

diff --git a/plugins/sh/example.sh b/plugins/sh/example.sh
index 4f547db0..e0e1b773 100755
--- a/plugins/sh/example.sh
+++ b/plugins/sh/example.sh
@@ -85,7 +85,7 @@ case "$1" in
         echo parallel
         ;;

-    list_exports)
+    list_exports | default_export)
         # The following lists the names of all files in the current
         # directory that do not contain whitespace, backslash, or single
         # quotes.  No description accompanies the export names.
diff --git a/tests/test-eval-exports.sh b/tests/test-eval-exports.sh
index 7c946378..14b4c2ce 100755
--- a/tests/test-eval-exports.sh
+++ b/tests/test-eval-exports.sh
@@ -37,8 +37,8 @@ source ./functions.sh
 set -e
 set -x

-# requires nbdinfo --version # nbdinfo 1.3.9 was broken, so check this instead:
-requires nbdkit -U - memory 1 --run 'nbdinfo --size --json
"$uri"'
+requires nbdinfo --version
+requires nbdsh -c 'print (h.set_full_info)'
 requires jq --version

 sock=$(mktemp -u)
@@ -57,20 +57,19 @@ diff -u <(jq -c \
           '[.exports[] | [."export-name", .description,
."export-size"]]' \
           eval-exports.out) <(printf %s\\n
'[["",null,0]]')

-# Start a long-running server with .list_exports set to varying contents
+# Start a long-running server with .list_exports and .default_export
+# set to varying contents
 start_nbdkit -P eval-exports.pid -U $sock eval get_size='echo
"$2"|wc -c' \
-    open='echo "$3"' list_exports="cat
'$PWD/eval-exports.list'"
+    open='echo "$3"' list_exports="cat
'$PWD/eval-exports.list'" \
+    default_export="cat '$PWD/eval-exports.list'"

 # do_nbdkit EXPNAME EXPOUT
 do_nbdkit ()
 {
     # Check how the default export name is handled
-    # nbdinfo currently makes multiple connections, so we can't use the
-    # long-running server for validating default export name.
-    # XXX FIXME: requires .default_export in eval
-    : || nbdkit -U - -v eval list_exports="cat
'$PWD/eval-exports.list'" \
-      open='[ "$3" = "'"$1"'" ] || {
echo EINVAL wrong export >&2; exit 1; }' \
-      get_size='echo 0' --run 'nbdsh -u "$uri" -c
"exit()"'
+    nbdinfo --no-content nbd+unix://\?socket=$sock >eval-exports.out
+    diff -u <(sed -n 's/export="\(.*\)":/\1/p'
eval-exports.out) \
+         <(printf %s\\n "$1")
     # Check what exports are listed
     nbdinfo --list --json nbd+unix://\?socket=$sock >eval-exports.out
     cat eval-exports.out
@@ -79,9 +78,21 @@ do_nbdkit ()
               eval-exports.out) <(printf %s\\n "$2")
 }

-# With no file, .list_exports fails, but connecting works
+# With no file, .list_exports and .default_export both fail, preventing
+# connection to the default export, but not other exports
 nbdinfo --list --json nbd+unix://\?socket=$sock && fail=1
-nbdsh -u nbd+unix://\?socket=$sock -c 'quit()'
+nbdsh -u nbd+unix://\?socket=$sock -c 'quit()' && fail=1
+nbdsh -u nbd+unix:///name\?socket=$sock -c 'quit()'
+
+# Setting .default_export but not .list_exports advertises the canonical name
+nbdkit -U - eval default_export='echo hello' get_size='echo 0'
\
+       --run 'nbdinfo --list "$uri"' >eval-exports.out
+diff -u <(grep '^export=' eval-exports.out) <(echo
'export="hello":')
+
+# Failing .default_export without .list_exports results in an empty list
+nbdkit -U - eval default_export='echo ENOENT >&2; exit 1'
get_size='echo 0' \
+       --run 'nbdinfo --list "$uri"' >eval-exports.out
+diff -u <(grep '^export=' eval-exports.out) /dev/null

 # Various spellings of empty lists, producing 0 exports
 for fmt in '' 'NAMES\n' 'INTERLEAVED\n'
'NAMES+DESCRIPTIONS\n'; do
diff --git a/tests/test-tls-fallback.sh b/tests/test-tls-fallback.sh
index cf54a606..94449f31 100755
--- a/tests/test-tls-fallback.sh
+++ b/tests/test-tls-fallback.sh
@@ -60,12 +60,15 @@ cleanup_fn rm -f $files
 # Run dual-mode server
 start_nbdkit -P $pid -U $sock --tls=on --tls-psk=keys.psk \
     --filter=tls-fallback sh - tlsreadme=$'dummy\n' <<\EOF
+check () {
+ if test "$1" != true; then
+   echo 'EINVAL unexpected tls mode' 2>&1; exit 1
+ fi
+}
 case $1 in
   list_exports) echo NAMES; echo hello; echo world ;;
-  open) if test "$4" != true; then
-      echo 'EINVAL unexpected tls mode' 2>&1; exit 1
-    fi
-    echo $3 ;;
+  default_export) check "$3"; echo hello ;;
+  open) check "$4"; echo $3 ;;
   get_size) echo "$2" | wc -c ;;
   pread) echo "$2" | dd skip=$4 count=$3 iflag=skip_bytes,count_bytes
;;
   can_write | can_trim) exit 0 ;;
--
2.28.0