Exposing .list_exports through to shell scripts makes testing export
listing a lot more feasible. The design chosen here is amenable to
'ls -1' or 'find' output provided there are no newlines in the files
being listed, while also being flexible enough to support a future
format addition if we find ourselves needing a way to express escape
sequences or parsing machine-readable code such as JSON.
Signed-off-by: Eric Blake <eblake(a)redhat.com>
---
plugins/eval/nbdkit-eval-plugin.pod | 2 +
plugins/sh/nbdkit-sh-plugin.pod | 52 ++++++++++++++
tests/Makefile.am | 2 +
plugins/sh/methods.h | 4 +-
plugins/eval/eval.c | 2 +
plugins/sh/methods.c | 106 +++++++++++++++++++++++++++
plugins/sh/sh.c | 1 +
plugins/sh/example.sh | 8 +++
tests/test-eval-exports.sh | 108 ++++++++++++++++++++++++++++
9 files changed, 284 insertions(+), 1 deletion(-)
create mode 100755 tests/test-eval-exports.sh
diff --git a/plugins/eval/nbdkit-eval-plugin.pod b/plugins/eval/nbdkit-eval-plugin.pod
index 7e25a01f..7126c6de 100644
--- a/plugins/eval/nbdkit-eval-plugin.pod
+++ b/plugins/eval/nbdkit-eval-plugin.pod
@@ -108,6 +108,8 @@ features):
=item B<is_rotational=>SCRIPT
+=item B<list_exports=>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 771c6bc0..678116f2 100644
--- a/plugins/sh/nbdkit-sh-plugin.pod
+++ b/plugins/sh/nbdkit-sh-plugin.pod
@@ -266,6 +266,58 @@ with status C<1>; unrecognized output is ignored.
/path/to/script preconnect <readonly> <exportname>
+=item C<list_exports>
+
+ /path/to/script list_exports <readonly> <default_only>
+
+The C<readonly> parameter will be C<true> or C<false>. The
+C<default_only> parameter will be C<true> if the caller is only
+interested in the canonical name of the default export, or C<false> to
+get a full list of export names; the script may safely ignore this
+parameter and always provide a full list if desired.
+
+The first line of output informs nbdkit how to parse the rest of the
+output, the remaining lines then supply the inputs of the C
+C<nbdkit_add_export> function (see L<nbdkit-plugin(3)>), as follows:
+
+=over 4
+
+=item NAMES
+
+The remaining output provides one export name per line, and no export
+will be given a description. For convenience, this form is also
+assumed if the first output line does not match one of the recognized
+parse modes.
+
+=item INTERLEAVED
+
+The remaining output provides pairs of lines, the first line being an
+export name, and the second the corresponding description.
+
+=item NAMES+DESCRIPTIONS
+
+The number of remaining lines is counted, with the first half being
+used as export names, and the second half providing descriptions to
+pair with names from the first half.
+
+An example of using this form to list files in the current directory,
+followed by their L<ls(1)> long description, would be:
+
+ echo NAMES+DESCRIPTIONS
+ ls
+ ls -l
+
+=back
+
+Note that other output modes might be introduced in the future; in
+particular, none of the existing modes allow a literal newline in an
+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.
+
=item C<open>
/path/to/script open <readonly> <exportname>
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 79be5639..186749e0 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -616,10 +616,12 @@ test_data_LDADD = libtest.la $(LIBGUESTFS_LIBS)
TESTS += \
test-eval.sh \
test-eval-file.sh \
+ test-eval-exports.sh \
$(NULL)
EXTRA_DIST += \
test-eval.sh \
test-eval-file.sh \
+ test-eval-exports.sh \
$(NULL)
# file plugin test.
diff --git a/plugins/sh/methods.h b/plugins/sh/methods.h
index 08a5ed17..69017fa4 100644
--- a/plugins/sh/methods.h
+++ b/plugins/sh/methods.h
@@ -1,5 +1,5 @@
/* nbdkit
- * Copyright (C) 2018 Red Hat Inc.
+ * Copyright (C) 2018-2020 Red Hat Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -44,6 +44,8 @@ extern int sh_thread_model (void);
extern int sh_get_ready (void);
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 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 54c5029e..2bd5e79f 100644
--- a/plugins/eval/eval.c
+++ b/plugins/eval/eval.c
@@ -74,6 +74,7 @@ static const char *known_methods[] = {
"get_ready",
"get_size",
"is_rotational",
+ "list_exports",
"missing",
"open",
"pread",
@@ -393,6 +394,7 @@ static struct nbdkit_plugin plugin = {
.after_fork = sh_after_fork,
.preconnect = sh_preconnect,
+ .list_exports = sh_list_exports,
.open = sh_open,
.close = sh_close,
diff --git a/plugins/sh/methods.c b/plugins/sh/methods.c
index 8257103e..9f247524 100644
--- a/plugins/sh/methods.c
+++ b/plugins/sh/methods.c
@@ -225,6 +225,112 @@ struct sh_handle {
int can_zero;
};
+/* If @s begins with @prefix, return the next offset, else NULL */
+static const char *
+skip_prefix (const char *s, const char *prefix)
+{
+ size_t len = strlen (prefix);
+ if (strncmp (s, prefix, len) == 0)
+ return s + len;
+ return NULL;
+}
+
+static int
+parse_exports (const char *script,
+ const char *s, size_t slen, struct nbdkit_exports *exports)
+{
+ const char *n, *d, *p, *q;
+
+ /* The first line determines how to parse the rest of s */
+ if ((p = skip_prefix (s, "INTERLEAVED\n")) != NULL) {
+ n = p;
+ while ((d = strchr (n, '\n')) != NULL) {
+ p = strchr (d + 1, '\n') ?: d + 1;
+ CLEANUP_FREE char *name = strndup (n, d - n);
+ CLEANUP_FREE char *desc = strndup (d + 1, p - d - 1);
+ if (!name || !desc) {
+ nbdkit_error ("%s: strndup: %m", script);
+ return -1;
+ }
+ if (nbdkit_add_export (exports, name, desc) == -1)
+ return -1;
+ n = p + 1;
+ }
+ }
+ else if ((p = skip_prefix (s, "NAMES+DESCRIPTIONS\n")) != NULL) {
+ n = d = p;
+ /* Searching from both ends, using memrchr, would be less work, but
+ * memrchr is not widely portable. Multiple passes isn't too bad.
+ */
+ while (p && (p = strchr (p, '\n')) != NULL) {
+ p = strchr (p + 1, '\n');
+ if (p)
+ p++;
+ d = strchr (d, '\n') + 1;
+ }
+ s = d;
+ while (n < s) {
+ p = strchr (n, '\n');
+ q = strchr (d, '\n') ?: d;
+ CLEANUP_FREE char *name = strndup (n, p - n);
+ CLEANUP_FREE char *desc = strndup (d, q - d);
+ if (!name || !desc) {
+ nbdkit_error ("%s: strndup: %m", script);
+ return -1;
+ }
+ if (nbdkit_add_export (exports, name, desc) == -1)
+ return -1;
+ n = p + 1;
+ d = q + 1;
+ }
+ }
+ else {
+ n = skip_prefix (s, "NAMES\n") ?: s;
+ while ((p = strchr (n, '\n')) != NULL) {
+ CLEANUP_FREE char *name = strndup (n, p - n);
+ if (!name) {
+ nbdkit_error ("%s: strndup: %m", script);
+ return -1;
+ }
+ if (nbdkit_add_export (exports, name, NULL) == -1)
+ return -1;
+ n = p + 1;
+ }
+ }
+ return 0;
+}
+
+int
+sh_list_exports (int readonly, int default_only,
+ struct nbdkit_exports *exports)
+{
+ const char *method = "list_exports";
+ const char *script = get_script (method);
+ const char *args[] = { script, method, readonly ? "true" :
"false",
+ default_only ? "true" : "false", NULL };
+ CLEANUP_FREE char *s = NULL;
+ size_t slen;
+
+ switch (call_read (&s, &slen, args)) {
+ case OK:
+ return parse_exports (script, s, slen, exports);
+
+ case MISSING:
+ return nbdkit_add_export (exports, "", NULL);
+
+ case ERROR:
+ return -1;
+
+ case RET_FALSE:
+ nbdkit_error ("%s: %s method returned unexpected code (3/false)",
+ script, method);
+ errno = EIO;
+ return -1;
+
+ default: abort ();
+ }
+}
+
void *
sh_open (int readonly)
{
diff --git a/plugins/sh/sh.c b/plugins/sh/sh.c
index 9e484823..374888a4 100644
--- a/plugins/sh/sh.c
+++ b/plugins/sh/sh.c
@@ -300,6 +300,7 @@ static struct nbdkit_plugin plugin = {
.after_fork = sh_after_fork,
.preconnect = sh_preconnect,
+ .list_exports = sh_list_exports,
.open = sh_open,
.close = sh_close,
diff --git a/plugins/sh/example.sh b/plugins/sh/example.sh
index 99e4e890..4f547db0 100755
--- a/plugins/sh/example.sh
+++ b/plugins/sh/example.sh
@@ -85,6 +85,14 @@ case "$1" in
echo parallel
;;
+ list_exports)
+ # 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.
+ # The first file listed is used when a client requests export ''.
+ find . -type f \! -name "*['\\\\[:space:]]*"
+ ;;
+
open)
# Open a new client connection.
diff --git a/tests/test-eval-exports.sh b/tests/test-eval-exports.sh
new file mode 100755
index 00000000..543774b6
--- /dev/null
+++ b/tests/test-eval-exports.sh
@@ -0,0 +1,108 @@
+#!/usr/bin/env bash
+# nbdkit
+# Copyright (C) 2018-2020 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# * Neither the name of Red Hat nor the names of its contributors may be
+# used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+# This is an example from the nbdkit-eval-plugin(1) manual page.
+# Check here that it doesn't regress.
+
+source ./functions.sh
+set -e
+set -x
+
+requires nbdsh -c 'print (h.get_list_export_description)'
+requires nbdinfo --help
+requires jq --version
+
+files="eval-exports.list eval-exports.out"
+rm -f $files
+cleanup_fn rm -f $files
+
+# do_nbdkit [skip_list] EXPOUT
+do_nbdkit ()
+{
+ # Hack: since we never pass args that would go through .config, we can
+ # define a dummy .config to avoid defining .list_export
+ hack=
+ if test $1 = skip_list; then
+ hack=config=
+ shift
+ else
+ cat eval-exports.list
+ fi
+ nbdkit -U - -v eval ${hack}list_exports='cat eval-exports.list' \
+ get_size='echo 0' --run 'nbdinfo --list --json "$uri"'
>eval-exports.out
+ cat eval-exports.out
+ diff -u <(jq -c '[.exports[] | [."export-name", .description]]'
\
+ eval-exports.out) <(printf %s\\n "$1")
+}
+
+# Control case: no .list_exports, which defaults to advertising ""
+rm -f eval-exports.list
+do_nbdkit skip_list '[["",null]]'
+
+# Various spellings of empty lists, producing 0 exports
+for fmt in '' 'NAMES\n' 'INTERLEAVED\n'
'NAMES+DESCRIPTIONS\n'; do
+ printf "$fmt" >eval-exports.list
+ do_nbdkit '[]'
+done
+
+# Various spellings of explicit list for the default export, no description
+for fmt in '\n' 'NAMES\n\n' 'INTERLEAVED\n\n'
'INTERLEAVED\n\n\n' \
+ 'NAMES+DESCRIPTIONS\n\n' 'NAMES+DESCRIPTIONS\n\n\n'; do
+ printf "$fmt" >eval-exports.list
+ do_nbdkit '[["",null]]'
+done
+
+# A non-default name
+for fmt in 'name\n' 'NAMES\nname\n'; do
+ printf "$fmt" >eval-exports.list
+ do_nbdkit '[["name",null]]'
+done
+
+# One export with a description
+for fmt in 'INTERLEAVED\nname\ndesc\n'
'NAMES+DESCRIPTIONS\nname\ndesc\n'; do
+ printf "$fmt" >eval-exports.list
+ do_nbdkit '[["name","desc"]]'
+done
+
+# Multiple exports, with correct number of lines
+for fmt in 'INTERLEAVED\nname 1\ndesc 1\nname 2\ndesc 2\n' \
+ 'NAMES+DESCRIPTIONS\nname 1\nname 2\ndesc 1\ndesc 2\n'; do
+ printf "$fmt" >eval-exports.list
+ do_nbdkit '[["name 1","desc 1"],["name
2","desc 2"]]'
+done
+
+# Multiple exports, with final description line missing
+for fmt in 'INTERLEAVED\nname 1\ndesc 1\nname 2\n' \
+ 'NAMES+DESCRIPTIONS\nname 1\nname 2\ndesc 1\n'; do
+ printf "$fmt" >eval-exports.list
+ do_nbdkit '[["name 1","desc 1"],["name
2",null]]'
+done
--
2.28.0