Serve an arbitrary map of regions of the underlying plugin.
---
common-rules.mk | 1 +
configure.ac | 1 +
filters/map/Makefile.am | 61 +++
filters/map/map.c | 256 +++++++++
filters/map/maptype.c | 493 ++++++++++++++++++
filters/map/maptype.h | 76 +++
filters/map/nbdkit-map-filter.pod | 173 ++++++
filters/offset/nbdkit-offset-filter.pod | 1 +
filters/partition/nbdkit-partition-filter.pod | 1 +
plugins/pattern/nbdkit-pattern-plugin.pod | 1 +
tests/Makefile.am | 4 +
tests/test-map-empty.sh | 85 +++
12 files changed, 1153 insertions(+)
diff --git a/common-rules.mk b/common-rules.mk
index f600293..ae8d701 100644
--- a/common-rules.mk
+++ b/common-rules.mk
@@ -66,6 +66,7 @@ filters = \
delay \
fua \
log \
+ map \
nozero \
offset \
partition \
diff --git a/configure.ac b/configure.ac
index e8d0a38..2f40984 100644
--- a/configure.ac
+++ b/configure.ac
@@ -575,6 +575,7 @@ AC_CONFIG_FILES([Makefile
filters/delay/Makefile
filters/fua/Makefile
filters/log/Makefile
+ filters/map/Makefile
filters/nozero/Makefile
filters/offset/Makefile
filters/partition/Makefile
diff --git a/filters/map/Makefile.am b/filters/map/Makefile.am
new file mode 100644
index 0000000..96b5be8
--- /dev/null
+++ b/filters/map/Makefile.am
@@ -0,0 +1,61 @@
+# nbdkit
+# Copyright (C) 2018 Red Hat Inc.
+# All rights reserved.
+#
+# 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.
+
+include $(top_srcdir)/common-rules.mk
+
+EXTRA_DIST = nbdkit-map-filter.pod
+
+filter_LTLIBRARIES = nbdkit-map-filter.la
+
+nbdkit_map_filter_la_SOURCES = \
+ map.c \
+ maptype.c \
+ maptype.h \
+ $(top_srcdir)/include/nbdkit-filter.h
+nbdkit_map_filter_la_CPPFLAGS = \
+ -I$(top_srcdir)/include
+nbdkit_map_filter_la_CFLAGS = \
+ $(WARNINGS_CFLAGS)
+nbdkit_map_filter_la_LDFLAGS = \
+ -module -avoid-version -shared
+
+if HAVE_POD
+
+man_MANS = nbdkit-map-filter.1
+CLEANFILES += $(man_MANS)
+
+nbdkit-map-filter.1: nbdkit-map-filter.pod
+ $(PODWRAPPER) --section=1 --man $@ \
+ --html $(top_builddir)/html/$@.html \
+ $<
+
+endif HAVE_POD
diff --git a/filters/map/map.c b/filters/map/map.c
new file mode 100644
index 0000000..d551104
--- /dev/null
+++ b/filters/map/map.c
@@ -0,0 +1,256 @@
+/* nbdkit
+ * Copyright (C) 2018 Red Hat Inc.
+ * All rights reserved.
+ *
+ * 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.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <inttypes.h>
+#include <ctype.h>
+#include <assert.h>
+
+#include <nbdkit-filter.h>
+
+#include "maptype.h"
+
+#define THREAD_MODEL NBDKIT_THREAD_MODEL_PARALLEL
+
+static char *filename; /* Map filename. */
+
+static void
+map_unload (void)
+{
+ free (filename);
+}
+
+/* Expect map=filename on the command line, pass everything else
+ * through.
+ */
+static int
+map_config (nbdkit_next_config *next, void *nxdata,
+ const char *key, const char *value)
+{
+ if (strcmp (key, "map") == 0) {
+ filename = nbdkit_realpath (value);
+ if (filename == NULL)
+ return -1;
+ return 0;
+ }
+ else
+ return next (nxdata, key, value);
+}
+
+/* Check that map parameter was supplied. */
+static int
+map_config_complete (nbdkit_next_config_complete *next, void *nxdata)
+{
+ if (filename == NULL) {
+ nbdkit_error ("map=<filename> must be passed to the map filter");
+ return -1;
+ }
+
+ return next (nxdata);
+}
+
+#define map_config_help \
+ "map=<FILENAME> (required) Map file."
+
+struct handle {
+ /* We have to load the map file separately for each handle
+ * for a couple of reasons, the second one being critical:
+ *
+ * (1) The map file might change.
+ *
+ * (2) The size of the underlying plugin affects the behaviour
+ * of open-ended intervals in the map.
+ */
+ struct map map;
+};
+
+static void *
+map_open (nbdkit_next_open *next, void *nxdata, int readonly)
+{
+ struct handle *h;
+
+ if (next (nxdata, readonly) == -1)
+ return NULL;
+
+ h = malloc (sizeof *h);
+ if (h == NULL) {
+ nbdkit_error ("malloc: %m");
+ return NULL;
+ }
+ map_init (&h->map);
+
+ return h;
+}
+
+/* Force an early call to get the size of the map, then read the
+ * map file.
+ */
+static int
+map_prepare (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle)
+{
+ struct handle *h = handle;
+ int64_t size;
+
+ size = next_ops->get_size (nxdata);
+ if (size == -1)
+ return -1;
+ nbdkit_debug ("map: plugin size: %" PRIi64, size);
+
+ if (map_load_from_file (filename, size, &h->map) == -1)
+ return -1;
+
+ return 0;
+}
+
+static void
+map_close (void *handle)
+{
+ struct handle *h = handle;
+
+ map_free (&h->map);
+ free (h);
+}
+
+/* Get size. */
+static int64_t
+map_get_size (struct nbdkit_next_ops *next_ops, void *nxdata, void *handle)
+{
+ struct handle *h = handle;
+ int64_t r;
+
+ r = map_size (&h->map);
+ nbdkit_debug ("map: filter size: %" PRIi64, r);
+ return r;
+}
+
+/* Read data. */
+struct pread_data {
+ struct nbdkit_next_ops *next_ops;
+ void *nxdata;
+ void *buf;
+ uint32_t flags;
+ int *err;
+};
+
+static int
+do_pread (void *vp, uint32_t count, uint64_t offs)
+{
+ struct pread_data *data = vp;
+
+ if (data->next_ops->pread (data->nxdata, data->buf,
+ count, offs, data->flags, data->err) == -1)
+ return -1;
+ data->buf += count;
+ return 0;
+}
+
+static int
+do_pread_unmapped (void *vp, uint32_t count)
+{
+ struct pread_data *data = vp;
+
+ /* Unmapped data reads as zeroes. */
+ memset (data->buf, 0, count);
+ return 0;
+}
+
+static int
+map_pread (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle, void *buf, uint32_t count, uint64_t offs,
+ uint32_t flags, int *err)
+{
+ struct handle *h = handle;
+ struct pread_data data = {
+ .next_ops = next_ops,
+ .nxdata = nxdata,
+ .buf = buf,
+ .flags = flags,
+ .err = err,
+ };
+
+ return map_iter (&h->map, count, offs, &data, do_pread, do_pread_unmapped);
+}
+
+/* Write data. */
+static int
+map_pwrite (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle,
+ const void *buf, uint32_t count, uint64_t offs, uint32_t flags,
+ int *err)
+{
+ abort ();
+}
+
+/* Trim data. */
+static int
+map_trim (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle, uint32_t count, uint64_t offs, uint32_t flags,
+ int *err)
+{
+ abort ();
+}
+
+/* Zero data. */
+static int
+map_zero (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle, uint32_t count, uint64_t offs, uint32_t flags,
+ int *err)
+{
+ abort ();
+}
+
+static struct nbdkit_filter filter = {
+ .name = "map",
+ .longname = "nbdkit map filter",
+ .version = PACKAGE_VERSION,
+ .unload = map_unload,
+ .config = map_config,
+ .config_complete = map_config_complete,
+ .config_help = map_config_help,
+ .open = map_open,
+ .prepare = map_prepare,
+ .close = map_close,
+ .get_size = map_get_size,
+ .pread = map_pread,
+ .pwrite = map_pwrite,
+ .trim = map_trim,
+ .zero = map_zero,
+};
+
+NBDKIT_REGISTER_FILTER(filter)
diff --git a/filters/map/maptype.c b/filters/map/maptype.c
new file mode 100644
index 0000000..ef99363
--- /dev/null
+++ b/filters/map/maptype.c
@@ -0,0 +1,493 @@
+/* nbdkit
+ * Copyright (C) 2018 Red Hat Inc.
+ * All rights reserved.
+ *
+ * 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.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <string.h>
+#include <assert.h>
+
+#include <nbdkit-filter.h>
+
+#include "maptype.h"
+
+/* Produce additional debugging of this module. Only useful for
+ * finding bugs in the module, so this should normally be disabled.
+ */
+#define MAPTYPE_DEBUG 0
+
+/* Notes on the implementation.
+ *
+ * Throughout the filter we use the following terminology:
+ *
+ * request / requested etc: The client requested range of bytes to
+ * read or update.
+ *
+ * plugin: The target after the client request is mapped. This is
+ * what is passed along to the underlying plugin (or next filter in
+ * the chain).
+ *
+ * mappings: Single entries (lines) in the map file. They are of the
+ * form (plugin, request), ie. the mapping is done backwards.
+ *
+ * interval: start-end or (start, length).
+ *
+ * Only one mapping can apply to each requested byte. This fact is
+ * crucial as it allows us to store the mappings in a simple array
+ * with no overlapping intervals, and use an efficient binary search
+ * to map incoming requests to the plugin.
+ *
+ * When we read the map file we start with an empty array and add the
+ * intervals to it. At all times we must maintain the invariant that
+ * no intervals in the array may overlap, and therefore we have to
+ * split existing intervals as required. Earlier mappings are
+ * discarded where they overlap with later mappings.
+ */
+
+/* Compare entries by rq_start. */
+static int
+mapping_compare (const void *mv1, const void *mv2)
+{
+ const struct mapping *m1 = mv1;
+ const struct mapping *m2 = mv2;
+
+ if (m1->rq_start < m2->rq_start)
+ return -1;
+ else if (m1->rq_start > m2->rq_start)
+ return 1;
+ else
+ return 0;
+}
+
+/* Return true if two mappings overlap in the request range. */
+static int
+mappings_overlap (const struct mapping *m1, const struct mapping *m2)
+{
+ return m1->rq_end >= m2->rq_start && m1->rq_start <=
m2->rq_end;
+}
+
+void
+map_init (struct map *map)
+{
+ map->nr_map = 0;
+ map->map = NULL;
+}
+
+void
+map_free (struct map *map)
+{
+ if (!map) return;
+ free (map->map);
+ map_init (map);
+}
+
+/* Return the highest address in the map+1. */
+int64_t
+map_size (struct map *map)
+{
+ if (map->nr_map == 0)
+ return 0;
+ else
+ return map->map[map->nr_map-1].rq_end + 1;
+}
+
+/* Add a single new mapping at the end of the map. Does NOT maintain
+ * the invariant, use insert_mapping instead.
+ */
+static int
+add_mapping (struct map *map, const struct mapping *mapping)
+{
+ struct mapping *new_map;
+
+ map->nr_map++;
+ new_map = realloc (map->map, map->nr_map * sizeof map->map[0]);
+ if (new_map == NULL) {
+ nbdkit_error ("realloc: %m");
+ return -1;
+ }
+ map->map = new_map;
+ map->map[map->nr_map-1] = *mapping;
+ return 0;
+}
+
+/* Insert a single new mapping into the map. By splitting
+ * and discarding intervals, this maintains the invariant
+ * described above.
+ */
+static int
+insert_mapping (struct map *map, const struct mapping *new_mapping)
+{
+ size_t i;
+
+ /* Adjust existing mappings if they overlap with this mapping. */
+ for (i = 0; i < map->nr_map; ++i) {
+ if (mappings_overlap (&map->map[i], new_mapping)) {
+ /* The four cases are:
+ *
+ * existing +---+
+ * new +-------------------+
+ * => erase existing mapping
+ *
+ * existing +-------------------+
+ * new +---+
+ * => split existing mapping into two
+ *
+ * existing +-----------+
+ * new +-----+
+ * => adjust start of existing mapping
+ *
+ * existing +-----------+
+ * new +-----+
+ * => adjust end of existing mapping
+ */
+ if (map->map[i].rq_start >= new_mapping->rq_start &&
+ map->map[i].rq_end <= new_mapping->rq_end) {
+ /* Erase map[i]. */
+ memmove (&map->map[i], &map->map[i+1],
+ (map->nr_map-i-1) * sizeof map->map[0]);
+ map->nr_map--;
+ i--;
+ }
+ else if (map->map[i].rq_start < new_mapping->rq_start &&
+ map->map[i].rq_end > new_mapping->rq_end) {
+ struct mapping second;
+ uint64_t offset;
+
+ /* Split map[i] by reducing map[i] and creating second mapping. */
+ second.lineno = map->map[i].lineno;
+ second.rq_start = new_mapping->rq_end+1;
+ second.rq_end = map->map[i].rq_end;
+ offset = new_mapping->rq_end+1 - map->map[i].rq_start;
+ second.plugin_start = map->map[i].plugin_start + offset;
+ if (add_mapping (map, &second) == -1)
+ return -1;
+ map->map[i].rq_end = new_mapping->rq_start-1;
+ }
+ else if (map->map[i].rq_start >= new_mapping->rq_start) {
+ uint64_t offset;
+
+ /* Adjust start of map[i]. */
+ offset = new_mapping->rq_end+1 - map->map[i].rq_start;
+ map->map[i].rq_start = new_mapping->rq_end+1;
+ map->map[i].plugin_start += offset;
+ }
+ else if (map->map[i].rq_end <= new_mapping->rq_end)
+ /* Adjust end of map[i]. */
+ map->map[i].rq_end = new_mapping->rq_start-1;
+ else
+ abort (); /* Should never happen. */
+ }
+ }
+
+ /* Add new mapping at the end. Note that the new mapping does not
+ * need to be adjusted.
+ */
+ return add_mapping (map, new_mapping);
+}
+
+/* Load the map file. */
+int
+map_load_from_file (const char *filename, int64_t plugin_size,
+ struct map *map)
+{
+ /* Set of whitespace in the map file. */
+ static const char whitespace[] = " \t\n\r";
+
+ FILE *fp;
+ ssize_t r;
+ size_t len = 0;
+ char *line = NULL;
+ int lineno = 0;
+ size_t i;
+
+ fp = fopen (filename, "r");
+ if (fp == NULL) {
+ nbdkit_error ("open: %s: %m", filename);
+ return -1;
+ }
+ while ((r = getline (&line, &len, fp)) != -1) {
+ char *p, *q, *saveptr;
+ size_t n;
+ int64_t i;
+ int64_t length; /* signed because -1 means end of input */
+ struct mapping mapping;
+
+ lineno++;
+ mapping.lineno = lineno;
+
+ /* Remove anything after # (comment) character. */
+ p = strchr (line, '#');
+ if (p)
+ *p = '\0';
+
+ /* Trim whitespace at beginning of the line. */
+ n = strspn (line, whitespace);
+ if (n > 0)
+ memmove (line, &line[n], strlen (&line[n]));
+
+ /* Trim whitespace at end of the line (including \n and \r). */
+ n = strlen (line);
+ while (n > 0) {
+ if (strspn (&line[n-1], whitespace) == 0)
+ break;
+ line[n-1] = '\0';
+ n--;
+ }
+
+ /* Ignore blank lines. */
+ if (n == 0)
+ continue;
+
+ /* First field.
+ * Expecting: "start,length" or "start-end" or "start-"
or "start".
+ */
+ p = strtok_r (line, whitespace, &saveptr);
+ if (p == NULL) {
+ /* AFAIK this can never happen. */
+ nbdkit_error ("%s:%d: could not read token", filename, lineno);
+ goto err;
+ }
+ if ((q = strchr (p, ',')) != NULL) { /* start,length */
+ *q = '\0'; q++;
+ i = nbdkit_parse_size (p);
+ if (i == -1)
+ goto err;
+ mapping.plugin_start = i;
+ i = nbdkit_parse_size (q);
+ if (i == -1)
+ goto err;
+ length = i;
+ }
+ else if ((q = strchr (p, '-')) != NULL) { /* start-end or start- */
+ *q = '\0'; q++;
+ i = nbdkit_parse_size (p);
+ if (i == -1)
+ goto err;
+ mapping.plugin_start = i;
+ if (*q == '\0')
+ length = -1;
+ else {
+ i = nbdkit_parse_size (q);
+ if (i == -1)
+ goto err;
+ /* Note: 100-99 is allowed (means zero length). However the
+ * length must not be negative.
+ */
+ if (i < mapping.plugin_start-1) {
+ nbdkit_error ("%s:%d: length < 0", filename, lineno);
+ goto err;
+ }
+ length = i - mapping.plugin_start + 1;
+ }
+ }
+ else { /* start */
+ i = nbdkit_parse_size (p);
+ if (i == -1)
+ goto err;
+ mapping.plugin_start = i;
+ length = -1;
+ }
+
+ /* length == -1 means to the end of the plugin. Calculate that. */
+ if (length == -1)
+ length = plugin_size - mapping.plugin_start;
+
+ /* A zero-length mapping isn't an error, but can be ignored immediately. */
+ if (length == 0)
+ continue;
+
+ /* Second field. Expecting a single offset. */
+ p = strtok_r (NULL, whitespace, &saveptr);
+ i = nbdkit_parse_size (p);
+ if (i == -1)
+ goto err;
+ mapping.rq_start = i;
+
+ /* Calculate the end of the output region. */
+ mapping.rq_end = mapping.rq_start + length - 1;
+
+ /* We just ignore everything on the line after the second field.
+ * But don't put anything there, we might use this for something
+ * in future.
+ */
+
+ /* Debug the line as it was read. */
+ nbdkit_debug ("map: %s:%d: "
+ "plugin.start=%" PRIu64 ", plugin.length=%" PRIi64
", "
+ "request.start=%" PRIu64 ", request.end=%" PRIu64,
+ filename, lineno,
+ mapping.plugin_start, length,
+ mapping.rq_start, mapping.rq_end);
+
+ /* Insert into the map. */
+ if (insert_mapping (map, &mapping) == -1)
+ goto err;
+ }
+
+ fclose (fp);
+ free (line);
+
+ /* The map maintains an invariant that no intervals are overlapping.
+ * However it is not yet sorted which we need for efficient lookups
+ * (using bsearch), so do that now.
+ */
+ if (map->nr_map > 0)
+ qsort (map->map, map->nr_map, sizeof map->map[0], mapping_compare);
+
+ /* Check there are no overlapping mappings. Because of the sort
+ * above we only need to check adjacent pairs so this is quite
+ * efficient and we can do it every time.
+ */
+ if (map->nr_map > 0)
+ for (i = 0; i < map->nr_map-1; ++i)
+ assert (!mappings_overlap (&map->map[i], &map->map[i+1]));
+
+ /* If debugging print the final map. */
+ for (i = 0; i < map->nr_map; ++i)
+ nbdkit_debug ("map: map[%zu] = [%" PRIu64 "-%" PRIu64
":"
+ "%" PRIu64 "] (from %s:%d)",
+ i, map->map[i].rq_start, map->map[i].rq_end,
+ map->map[i].plugin_start, filename, map->map[i].lineno);
+
+ return 0;
+
+ err:
+ fclose (fp);
+ free (line);
+ map_free (map);
+ return -1;
+}
+
+/* Look up a single address in the map.
+ *
+ * If mapped, returns the mapping index (in map[]). In this case
+ * *is_mapped == true.
+ *
+ * If unmapped, returns the mapping index of the next mapped area
+ * (which can be >= nr_map if there are no more mappings). In this
+ * case *is_mapped == false.
+ *
+ * Note this only works because of the invariant that mappings are not
+ * allowed to overlap. See description at top of file.
+ */
+size_t
+map_lookup (const struct map *map, uint64_t p, int *is_mapped)
+{
+ size_t lo, hi, mid;
+
+ /* Deal with the special case where the map is completely empty
+ * because it makes the rest of the code easier.
+ */
+ if (map->nr_map == 0) {
+ *is_mapped = 0;
+ return 0;
+ }
+
+ /* Unmapped, before the first interval? */
+ if (p < map->map[0].rq_start) {
+ *is_mapped = 0;
+ return 0;
+ }
+
+ /* Do a binary search to find the mapping. */
+ lo = 0;
+ hi = map->nr_map;
+ while (lo < hi) {
+ mid = (lo + hi) / 2;
+ if (map->map[mid].rq_start <= p && p <= map->map[mid].rq_end)
+ lo = hi = mid; /* terminates loop */
+ else if (p < map->map[mid].rq_start && hi != mid)
+ hi = mid;
+ else if (map->map[mid].rq_end < p && lo != mid)
+ lo = mid;
+ else
+ lo = hi = mid; /* terminates loop */
+ }
+
+ *is_mapped = p <= map->map[lo].rq_end;
+ return lo;
+}
+
+/* Iterate over the map. */
+int
+map_iter (const struct map *map,
+ uint32_t count, uint64_t offs, void *data,
+ int (*mapped_fn) (void *data, uint32_t count, uint64_t offs),
+ int (*unmapped_fn) (void *data, uint32_t count))
+{
+ size_t i;
+ int is_mapped;
+ size_t len;
+
+ while (count > 0) {
+ i = map_lookup (map, offs, &is_mapped);
+
+ if (MAPTYPE_DEBUG)
+ nbdkit_debug ("map: iter: "
+ "offset %" PRIu64 " %s map[%zu] = "
+ "[%" PRIu64 "-%" PRIu64 ":%" PRIu64
"]",
+ offs, is_mapped ? "mapped to" : "unmapped below",
i,
+ map->map[i].rq_start, map->map[i].rq_end,
+ map->map[i].plugin_start);
+
+ if (is_mapped) {
+ len = map->map[i].rq_end - offs + 1;
+ if (mapped_fn (data,
+ len < count ? len : count,
+ map->map[i].plugin_start + offs
+ - map->map[i].rq_start) == -1)
+ return -1;
+ }
+ else {
+ if (i < map->nr_map)
+ len = map->map[i].rq_start - offs;
+ else
+ len = count; /* No more mappings above this one. */
+ if (unmapped_fn (data,
+ len < count ? len : count) == -1)
+ return -1;
+ }
+
+ if (len < count) {
+ offs += len;
+ count -= len;
+ }
+ else
+ count = 0; /* Fulfilled whole request. */
+ }
+
+ return 0;
+}
diff --git a/filters/map/maptype.h b/filters/map/maptype.h
new file mode 100644
index 0000000..b24e572
--- /dev/null
+++ b/filters/map/maptype.h
@@ -0,0 +1,76 @@
+/* nbdkit
+ * Copyright (C) 2018 Red Hat Inc.
+ * All rights reserved.
+ *
+ * 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.
+ */
+
+#ifndef NBDKIT_MAPTYPE_H
+#define NBDKIT_MAPTYPE_H
+
+struct mapping {
+ /* The start and end (inclusive) of the requested interval. */
+ uint64_t rq_start, rq_end;
+
+ /* The interval this maps to in the plugin. end/length is implied. */
+ uint64_t plugin_start;
+
+ /* The source line for the mapping. */
+ int lineno;
+};
+
+struct map {
+ struct mapping *map; /* List of mappings. */
+ size_t nr_map; /* Number of entries in map array. */
+};
+
+/* Initialize the map structure. */
+extern void map_init (struct map *map);
+
+/* This only frees the map->map array. */
+extern void map_free (struct map *map);
+
+/* Return the highest address in the map + 1. */
+extern int64_t map_size (struct map *map);
+
+/* Load the map from a file, constructing the map structure. */
+extern int map_load_from_file (const char *filename, int64_t plugin_size,
+ struct map *map);
+
+/* Lookup a single address in the map. */
+extern size_t map_lookup (const struct map *map, uint64_t p, int *is_mapped);
+
+/* Iterate over the map. */
+typedef int (*mapped_fn_t) (void *data, uint32_t count, uint64_t offs);
+typedef int (*unmapped_fn_t) (void *data, uint32_t count);
+extern int map_iter (const struct map *map,
+ uint32_t count, uint64_t offs, void *data,
+ mapped_fn_t mapped_fn, unmapped_fn_t unmapped_fn);
+
+#endif /* NBDKIT_MAPTYPE_H */
diff --git a/filters/map/nbdkit-map-filter.pod b/filters/map/nbdkit-map-filter.pod
new file mode 100644
index 0000000..05c6b90
--- /dev/null
+++ b/filters/map/nbdkit-map-filter.pod
@@ -0,0 +1,173 @@
+=head1 NAME
+
+nbdkit-map-filter - nbdkit map filter
+
+=head1 SYNOPSIS
+
+ nbdkit --filter=map plugin map=FILENAME [plugin-args...]
+
+=head1 DESCRIPTION
+
+C<nbdkit-map-filter> is a filter that can serve an arbitrary map of
+regions of the underlying plugin.
+
+It is driven by a map file that contains a list of regions from the
+plugin and where they should be served in the output.
+
+For example this map would divide the plugin data into two 16K halves
+and swap them over:
+
+ # map file
+ 0,16K 16K # aaaaa
+ 16K,16K 0 # bbbbb
+
+When visualised, this map file looks like:
+
+ ┌──────────────┬──────────────┬─── ─ ─ ─
+ Plugin serves ... │ aaaaaaaaaaaa │ bbbbbbbbbbbb │ (extra data)
+ │ 16K │ 16K │
+ └──────────────┴──────────────┴─── ─ ─ ─
+ │ │
+ Filter │ ┌─────────┘
+ transforms ... └──────────────┐
+ │ │
+ ┌──────────▼───┬─────▼────────┐
+ Client sees ... │ bbbbbbbbbbbb │ aaaaaaaaaaaa │
+ └──────────────┴──────────────┘
+
+This is how to simulate L<nbdkit-offset-filter(1)> C<offset> and
+C<range> parameters:
+
+ # offset,range
+ 1M,32M 0
+
+ ┌─────┬─────────────────────┬─── ─ ─ ─
+ Plugin serves ... │ │ ccccccccccccccccccc │ (extra data)
+ │ 1M │ 32M │
+ └─────┴─────────────────────┴─── ─ ─ ─
+ Filter │
+ transforms ... ┌─────┘
+ │
+ ┌─────────▼───────────┐
+ Client sees ... │ ccccccccccccccccccc │
+ └─────────────────────┘
+
+You can also do obscure things like duplicating regions of the source:
+
+ # map file
+ 0,16K 0
+ 0,16K 16K
+
+ ┌──────────────┬─── ─ ─ ─
+ Plugin serves ... │ aaaaaaaaaaaa │ (extra data)
+ │ 16K │
+ └──────────────┴─── ─ ─ ─
+ Filter │
+ transforms ... └───┬──────────┐
+ │ │
+ ┌─────────▼────┬─────▼────────┐
+ Client sees ... │ aaaaaaaaaaaa │ aaaaaaaaaaaa │
+ └──────────────┴──────────────┘
+
+=head2 Map file format
+
+The map file describes how regions from the plugin are mapped to the
+output. There is one line per mapping. Blank lines are ignored.
+C<#> indicates a comment.
+
+Each line (mapping) has one of the following forms:
+
+ start,length offset # see "start,length" below
+ start-end offset # see "start-end" below
+ start- offset # see "start to end of plugin" below
+ start offset # see "start to end of plugin" below
+
+=head2 C<start,length>
+
+ start,length offset
+
+means that the source region starting at byte C<start>, for C<length>
+bytes, is mapped to C<offset> to C<offset+length-1> in the output.
+
+For example:
+
+ 16K,8K 0
+
+maps the 8K-sized region starting at 16K in the source to the
+beginning (ie. from offset 0) of the output.
+
+=head2 C<start-end>
+
+ start-end offset
+
+means that the source region starting at byte C<start> through to byte
+C<end> (inclusive) is mapped to C<offset> through to
+C<offset+(end-start)> in the output.
+
+For example:
+
+ 1024-2047 2048
+
+maps the region starting at byte 1024 and ending at byte 2047
+(inclusive) to bytes 2048-3071 in the output.
+
+=head2 C<start> to end of plugin
+
+ start- offset
+ start offset
+
+If the C<end> field is omitted it means "up to the end of the
+underlying plugin".
+
+=head2 Size modifiers
+
+You can use the usual power-of-2 size modifiers like C<K>, C<M> etc.
+
+=head2 Overlapping mappings
+
+If there are multiple mappings in the map file that may apply to a
+particular byte of the filter output then it is the last one in the
+file which applies.
+
+=head2 Virtual size
+
+The virtual size of the filter output finishes at the last byte of the
+final mapped region. Note this is usually different from the size of
+the underlying plugin.
+
+=head2 Unmapped regions
+
+Any unmapped region (followed by a mapped region and therefore not
+beyond the virtual size) reads as zero and returns an error if
+written.
+
+Any mapping or part of a mapping where the source region refers beyond
+the end of the underlying plugin reads as zero and returns an error if
+written.
+
+=head1 PARAMETERS
+
+=over 4
+
+=item B<map=FILENAME>
+
+Specify the map filename (required). See L</Map file format> above.
+
+=back
+
+=head1 SEE ALSO
+
+L<nbdkit(1)>,
+L<nbdkit-file-plugin(1)>,
+L<nbdkit-filter(3)>,
+L<nbdkit-offset-filter(1)>,
+L<nbdkit-partition-filter(1)>,
+L<nbdkit-truncate-filter(1)>.
+
+=head1 AUTHORS
+
+Richard W.M. Jones
+
+=head1 COPYRIGHT
+
+Copyright (C) 2018 Red Hat Inc.
diff --git a/filters/offset/nbdkit-offset-filter.pod
b/filters/offset/nbdkit-offset-filter.pod
index 6d8f9be..96f9a13 100644
--- a/filters/offset/nbdkit-offset-filter.pod
+++ b/filters/offset/nbdkit-offset-filter.pod
@@ -67,6 +67,7 @@ You can then serve the partition only using:
L<nbdkit(1)>,
L<nbdkit-file-plugin(1)>,
L<nbdkit-filter(3)>,
+L<nbdkit-map-filter(1)>,
L<nbdkit-partition-filter(1)>,
L<nbdkit-truncate-filter(1)>.
diff --git a/filters/partition/nbdkit-partition-filter.pod
b/filters/partition/nbdkit-partition-filter.pod
index 71a7a3a..8d9aabb 100644
--- a/filters/partition/nbdkit-partition-filter.pod
+++ b/filters/partition/nbdkit-partition-filter.pod
@@ -45,6 +45,7 @@ image). To serve the first partition only use:
L<nbdkit(1)>,
L<nbdkit-file-plugin(1)>,
L<nbdkit-filter(3)>,
+L<nbdkit-map-filter(1)>,
L<nbdkit-offset-filter(1)>,
L<nbdkit-truncate-filter(1)>,
L<parted(8)>.
diff --git a/plugins/pattern/nbdkit-pattern-plugin.pod
b/plugins/pattern/nbdkit-pattern-plugin.pod
index d2bcd4d..d37d661 100644
--- a/plugins/pattern/nbdkit-pattern-plugin.pod
+++ b/plugins/pattern/nbdkit-pattern-plugin.pod
@@ -58,6 +58,7 @@ This parameter is required.
L<nbdkit(1)>,
L<nbdkit-plugin(3)>,
+L<nbdkit-map-filter(1)>,
L<nbdkit-null-plugin(1)>,
L<nbdkit-offset-filter(1)>,
L<nbdkit-random-plugin(1)>,
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 4c602d7..2306506 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -58,6 +58,7 @@ EXTRA_DIST = \
test-ip.sh \
test-log.sh \
test.lua \
+ test-map-empty.sh \
test-nozero.sh \
test_ocaml_plugin.ml \
test-ocaml.c \
@@ -547,6 +548,9 @@ TESTS += test-fua.sh
# log filter test.
TESTS += test-log.sh
+# map filter test.
+TESTS += test-map-empty.sh
+
# nozero filter test.
TESTS += test-nozero.sh
diff --git a/tests/test-map-empty.sh b/tests/test-map-empty.sh
new file mode 100755
index 0000000..3789234
--- /dev/null
+++ b/tests/test-map-empty.sh
@@ -0,0 +1,85 @@
+#!/bin/bash -
+# nbdkit
+# Copyright (C) 2018 Red Hat Inc.
+# All rights reserved.
+#
+# 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.
+
+# Test the map filter with an empty map file.
+
+set -e
+
+files="map-empty.out map-empty.pid map-empty.sock"
+rm -f $files
+
+# Test that qemu-img works
+if ! qemu-img --version >/dev/null; then
+ echo "$0: missing or broken qemu-img"
+ exit 77
+fi
+
+# Run nbdkit with pattern plugin and an empty map file on top.
+nbdkit -P map-empty.pid -U map-empty.sock \
+ --filter=map pattern size=10M map=/dev/null
+
+# We may have to wait a short time for the pid file to appear.
+for i in `seq 1 10`; do
+ if test -f map-empty.pid; then
+ break
+ fi
+ sleep 1
+done
+if ! test -f map-empty.pid; then
+ echo "$0: PID file was not created"
+ exit 1
+fi
+
+pid="$(cat map-empty.pid)"
+
+# Kill the nbdkit process on exit.
+cleanup ()
+{
+ status=$?
+
+ kill $pid
+ rm -f $files
+
+ exit $status
+}
+trap cleanup INT QUIT TERM EXIT ERR
+
+LANG=C qemu-img info 'nbd+unix://?socket=map-empty.sock' |
+ grep "^virtual size:" > map-empty.out
+if [ "$(cat map-empty.out)" != "virtual size: 0 (0 bytes)" ]; then
+ echo "$0: unexpected output:"
+ cat map-empty.out
+ exit 1
+fi
+
+# The cleanup() function is called implicitly on exit.
--
2.18.0