From e744dcb38cc52cbe64977efcdd4bc60e802d1b17 Mon Sep 17 00:00:00 2001 From: "Richard W.M. Jones" Date: Thu, 23 Jan 2020 19:52:00 +0000 Subject: [PATCH 11/19] New filter: extentlist. Allows a list of extents to be placed on top of an existing plugin. (cherry picked from commit 3e770b6d6620a62546849a2863638041c0b00640) --- TODO | 4 + configure.ac | 2 + .../nbdkit-cacheextents-filter.pod | 1 + filters/extentlist/Makefile.am | 67 ++++ filters/extentlist/extentlist.c | 326 ++++++++++++++++++ .../extentlist/nbdkit-extentlist-filter.pod | 90 +++++ filters/noextents/nbdkit-noextents-filter.pod | 1 + tests/Makefile.am | 4 + tests/test-extentlist.sh | 175 ++++++++++ 9 files changed, 670 insertions(+) create mode 100644 filters/extentlist/Makefile.am create mode 100644 filters/extentlist/extentlist.c create mode 100644 filters/extentlist/nbdkit-extentlist-filter.pod create mode 100755 tests/test-extentlist.sh diff --git a/TODO b/TODO index d2aca440..2a3e89dc 100644 --- a/TODO +++ b/TODO @@ -187,6 +187,10 @@ Suggestions for filters MBs of extra data) https://github.com/facebook/zstd/issues/395#issuecomment-535875379 +* nbdkit-extentlist-filter could read the extents generated by + qemu-img map, allowing extents to be ported from a qemu block + device. + nbdkit-rate-filter: * allow other kinds of traffic shaping such as VBR diff --git a/configure.ac b/configure.ac index fde498b8..41e68de3 100644 --- a/configure.ac +++ b/configure.ac @@ -896,6 +896,7 @@ filters="\ cow \ delay \ error \ + extentlist \ fua \ log \ nocache \ @@ -979,6 +980,7 @@ AC_CONFIG_FILES([Makefile filters/cow/Makefile filters/delay/Makefile filters/error/Makefile + filters/extentlist/Makefile filters/fua/Makefile filters/log/Makefile filters/nocache/Makefile diff --git a/filters/cacheextents/nbdkit-cacheextents-filter.pod b/filters/cacheextents/nbdkit-cacheextents-filter.pod index fdd2285a..bb2514a4 100644 --- a/filters/cacheextents/nbdkit-cacheextents-filter.pod +++ b/filters/cacheextents/nbdkit-cacheextents-filter.pod @@ -52,6 +52,7 @@ C first appeared in nbdkit 1.14. L, L, +L, L, L, L, diff --git a/filters/extentlist/Makefile.am b/filters/extentlist/Makefile.am new file mode 100644 index 00000000..88a9afe1 --- /dev/null +++ b/filters/extentlist/Makefile.am @@ -0,0 +1,67 @@ +# nbdkit +# Copyright (C) 2019-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. + +include $(top_srcdir)/common-rules.mk + +EXTRA_DIST = nbdkit-extentlist-filter.pod + +filter_LTLIBRARIES = nbdkit-extentlist-filter.la + +nbdkit_extentlist_filter_la_SOURCES = \ + extentlist.c \ + $(top_srcdir)/include/nbdkit-filter.h \ + $(NULL) + +nbdkit_extentlist_filter_la_CPPFLAGS = \ + -I$(top_srcdir)/include \ + -I$(top_srcdir)/common/include \ + -I$(top_srcdir)/common/utils \ + $(NULL) +nbdkit_extentlist_filter_la_CFLAGS = $(WARNINGS_CFLAGS) +nbdkit_extentlist_filter_la_LDFLAGS = \ + -module -avoid-version -shared \ + -Wl,--version-script=$(top_srcdir)/filters/filters.syms \ + $(NULL) +nbdkit_extentlist_filter_la_LIBADD = \ + $(top_builddir)/common/utils/libutils.la \ + $(NULL) + +if HAVE_POD + +man_MANS = nbdkit-extentlist-filter.1 +CLEANFILES += $(man_MANS) + +nbdkit-extentlist-filter.1: nbdkit-extentlist-filter.pod + $(PODWRAPPER) --section=1 --man $@ \ + --html $(top_builddir)/html/$@.html \ + $< + +endif HAVE_POD diff --git a/filters/extentlist/extentlist.c b/filters/extentlist/extentlist.c new file mode 100644 index 00000000..5f4990b3 --- /dev/null +++ b/filters/extentlist/extentlist.c @@ -0,0 +1,326 @@ +/* nbdkit + * Copyright (C) 2019-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. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "cleanup.h" +#include "minmax.h" + +#define HOLE (NBDKIT_EXTENT_HOLE|NBDKIT_EXTENT_ZERO) + +static const char *extentlist; + +/* List of extents. Once we've finally parsed them this will be + * ordered, non-overlapping and have no gaps. + */ +struct extent { + uint64_t offset, length; + uint32_t type; +}; +static struct extent *extents; +static size_t nr_extents, allocated; + +/* Insert an extent before i. If i = nr_extents, inserts at the end. */ +static void +insert_extent (size_t i, struct extent new_extent) +{ + if (nr_extents >= allocated) { + allocated = allocated == 0 ? 1 : allocated * 2; + extents = realloc (extents, (sizeof (struct extent) * allocated)); + if (extents == NULL) { + nbdkit_error ("realloc: %m"); + exit (EXIT_FAILURE); + } + } + memmove (&extents[i+1], &extents[i], + sizeof (struct extent) * (nr_extents-i)); + extents[i] = new_extent; + nr_extents++; +} + +static void +extentlist_unload (void) +{ + free (extents); +} + +/* Called for each key=value passed on the command line. */ +static int +extentlist_config (nbdkit_next_config *next, void *nxdata, + const char *key, const char *value) +{ + if (strcmp (key, "extentlist") == 0) { + if (extentlist != NULL) { + nbdkit_error ("extentlist cannot appear twice"); + exit (EXIT_FAILURE); + } + extentlist = value; + return 0; + } + else + return next (nxdata, key, value); +} + +static int +compare_offsets (const void *ev1, const void *ev2) +{ + const struct extent *e1 = ev1; + const struct extent *e2 = ev2; + + if (e1->offset < e2->offset) + return -1; + else if (e1->offset > e2->offset) + return 1; + else + return 0; +} + +static int +compare_ranges (const void *ev1, const void *ev2) +{ + const struct extent *e1 = ev1; + const struct extent *e2 = ev2; + + if (e1->offset < e2->offset) + return -1; + else if (e1->offset >= e2->offset + e2->length) + return 1; + else + return 0; +} + +/* Similar to parse_extents in plugins/sh/methods.c */ +static void +parse_extentlist (void) +{ + FILE *fp; + CLEANUP_FREE char *line = NULL; + size_t linelen = 0; + ssize_t len; + size_t i; + uint64_t end; + + assert (extentlist != NULL); + assert (extents == NULL); + assert (nr_extents == 0); + + fp = fopen (extentlist, "r"); + if (!fp) { + nbdkit_error ("open: %s: %m", extentlist); + exit (EXIT_FAILURE); + } + + while ((len = getline (&line, &linelen, fp)) != -1) { + const char *delim = " \t"; + char *sp, *p; + int64_t offset, length; + uint32_t type; + + if (len > 0 && line[len-1] == '\n') { + line[len-1] = '\0'; + len--; + } + + if ((p = strtok_r (line, delim, &sp)) == NULL) { + parse_error: + nbdkit_error ("%s: cannot parse %s", extentlist, line); + exit (EXIT_FAILURE); + } + offset = nbdkit_parse_size (p); + if (offset == -1) + exit (EXIT_FAILURE); + + if ((p = strtok_r (NULL, delim, &sp)) == NULL) + goto parse_error; + length = nbdkit_parse_size (p); + if (length == -1) + exit (EXIT_FAILURE); + + /* Skip zero length extents. Makes the rest of the code easier. */ + if (length == 0) + continue; + + if ((p = strtok_r (NULL, delim, &sp)) == NULL) + /* empty type field means allocated data (0) */ + type = 0; + else if (sscanf (p, "%" SCNu32, &type) == 1) + ; + else { + type = 0; + if (strstr (p, "hole") != NULL) + type |= NBDKIT_EXTENT_HOLE; + if (strstr (p, "zero") != NULL) + type |= NBDKIT_EXTENT_ZERO; + } + + insert_extent (nr_extents, + (struct extent){.offset = offset, .length=length, + .type=type}); + } + + fclose (fp); + + /* Sort the extents by offset. */ + qsort (extents, nr_extents, sizeof (struct extent), compare_offsets); + + /* There must not be overlaps at this point. */ + end = 0; + for (i = 0; i < nr_extents; ++i) { + if (extents[i].offset < end || + extents[i].offset + extents[i].length < extents[i].offset) { + nbdkit_error ("extents in the extent list are overlapping"); + exit (EXIT_FAILURE); + } + end = extents[i].offset + extents[i].length; + } + + /* If there's a gap at the beginning, insert a hole|zero extent. */ + if (nr_extents == 0 || extents[0].offset > 0) { + end = nr_extents == 0 ? UINT64_MAX : extents[0].offset; + insert_extent (0, (struct extent){.offset = 0, .length = end, + .type = HOLE}); + } + + /* Now insert hole|zero extents after every extent where there + * is a gap between that extent and the next one. + */ + for (i = 0; i < nr_extents-1; ++i) { + end = extents[i].offset + extents[i].length; + if (end < extents[i+1].offset) + insert_extent (i+1, (struct extent){.offset = end, + .length = extents[i+1].offset - end, + .type = HOLE}); + } + + /* If there's a gap at the end, insert a hole|zero extent. */ + end = extents[nr_extents-1].offset + extents[nr_extents-1].length; + if (end < UINT64_MAX) + insert_extent (nr_extents, (struct extent){.offset = end, + .length = UINT64_MAX-end, + .type = HOLE}); + + /* Debug the final list. */ + for (i = 0; i < nr_extents; ++i) { + nbdkit_debug ("extentlist: " + "extent[%zu] = %" PRIu64 "-%" PRIu64 " (length %" PRIu64 ")" + " type %" PRIu32, + i, extents[i].offset, + extents[i].offset + extents[i].length - 1, + extents[i].length, + extents[i].type); + } +} + +static int +extentlist_config_complete (nbdkit_next_config_complete *next, void *nxdata) +{ + if (extentlist == NULL) { + nbdkit_error ("you must supply the extentlist parameter " + "on the command line"); + return -1; + } + + parse_extentlist (); + + return next (nxdata); +} + +static int +extentlist_can_extents (struct nbdkit_next_ops *next_ops, void *nxdata, + void *handle) +{ + return 1; +} + +/* Use ‘-D extentlist.lookup=1’ to debug the function below. */ +int extentlist_debug_lookup = 0; + +/* Read extents. */ +static int +extentlist_extents (struct nbdkit_next_ops *next_ops, void *nxdata, + void *handle, uint32_t count, uint64_t offset, + uint32_t flags, + struct nbdkit_extents *ret_extents, + int *err) +{ + const struct extent eoffset = { .offset = offset }; + struct extent *p; + ssize_t i; + uint64_t end; + + /* Find the starting point in the extents list. */ + p = bsearch (&eoffset, extents, + nr_extents, sizeof (struct extent), compare_ranges); + assert (p != NULL); + i = p - extents; + + /* Add extents to the output. */ + while (count > 0) { + if (extentlist_debug_lookup) + nbdkit_debug ("extentlist lookup: " + "loop i=%zd count=%" PRIu32 " offset=%" PRIu64, + i, count, offset); + + end = extents[i].offset + extents[i].length; + if (nbdkit_add_extent (ret_extents, offset, end - offset, + extents[i].type) == -1) + return -1; + + count -= MIN (count, end-offset); + offset = end; + i++; + } + + return 0; +} + +static struct nbdkit_filter filter = { + .name = "extentlist", + .longname = "nbdkit extentlist filter", + .unload = extentlist_unload, + .config = extentlist_config, + .config_complete = extentlist_config_complete, + .can_extents = extentlist_can_extents, + .extents = extentlist_extents, +}; + +NBDKIT_REGISTER_FILTER(filter) diff --git a/filters/extentlist/nbdkit-extentlist-filter.pod b/filters/extentlist/nbdkit-extentlist-filter.pod new file mode 100644 index 00000000..adfb4ad8 --- /dev/null +++ b/filters/extentlist/nbdkit-extentlist-filter.pod @@ -0,0 +1,90 @@ +=head1 NAME + +nbdkit-extentlist-filter - place extent list over a plugin + +=head1 SYNOPSIS + + nbdkit --filter=extentlist plugin extentlist=FILENAME + +=head1 DESCRIPTION + +C is an nbdkit filter lets you place a +static list of extents on top of an existing plugin. Extents record +whether or not specific parts of the disk are allocated or sparse. + +You can use this with plugins which cannot get extent information +themselves, but you can get this information from another source. One +place where it is useful is with L because the +sftp protocol does not support reading sparseness information, but you +may be able to get this information directly from the source disk on +the remote server. + +=head1 FILE FORMAT + +The list of extents is specified in a text file. There is one extent +specified per line. Each line has the format: + + offset length type + +The C and C fields may use any format understood by +C. The optional C field may be an integer, +missing (same as 0), or a comma-separated list of the words C +and C. (The fields correspond to the inputs of the +C function, see L). + +An example of a valid set of extents covering a C<10M> disk where the +first megabyte only is allocated data: + + 0 1M + 1M 9M hole,zero + +Or you could omit the C extent since any gaps are assumed +to be holes with that type: + + 0 1M + +The extent list need not cover the whole disk, and does not need to be +in ascending order, but it must I contain overlapping extents. + +=head1 PARAMETERS + +=over 4 + +=item BFILENAME + +Specify the file containing the extent list, in the format described +in L above. + +=back + +=head1 FILES + +=over 4 + +=item F<$filterdir/nbdkit-extentlist-filter.so> + +The filter. + +Use C to find the location of C<$filterdir>. + +=back + +=head1 VERSION + +C first appeared in nbdkit 1.18. + +=head1 SEE ALSO + +L, +L, +L, +L, +L. + +=head1 AUTHORS + +Richard W.M. Jones + +=head1 COPYRIGHT + +Copyright (C) 2020 Red Hat Inc. diff --git a/filters/noextents/nbdkit-noextents-filter.pod b/filters/noextents/nbdkit-noextents-filter.pod index 991ecfe8..0260a5cf 100644 --- a/filters/noextents/nbdkit-noextents-filter.pod +++ b/filters/noextents/nbdkit-noextents-filter.pod @@ -47,6 +47,7 @@ C first appeared in nbdkit 1.14. L, L, +L, L, L, L, diff --git a/tests/Makefile.am b/tests/Makefile.am index 09103fbb..b99952f4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -110,6 +110,7 @@ EXTRA_DIST = \ test-error100.sh \ test-error-triggered.sh \ test-export-name.sh \ + test-extentlist.sh \ test-file-extents.sh \ test-floppy.sh \ test-foreground.sh \ @@ -1009,6 +1010,9 @@ TESTS += \ test-error-triggered.sh \ $(NULL) +# extentlist filter test. +TESTS += test-extentlist.sh + # fua filter test. TESTS += test-fua.sh diff --git a/tests/test-extentlist.sh b/tests/test-extentlist.sh new file mode 100755 index 00000000..7d05de4f --- /dev/null +++ b/tests/test-extentlist.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# nbdkit +# Copyright (C) 2016-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. + +# Test the extentlist filter. + +source ./functions.sh +set -e +set -x + +requires jq --version +requires qemu-img --version +requires qemu-img map --help + +out=test-extentlist.out +input=test-extentlist.in +expected=test-extentlist.expected +files="$out $input $expected" +rm -f $files +cleanup_fn rm $files + +test () +{ + nbdkit -v -D extentlist.lookup=1 \ + -U - \ + --filter=extentlist \ + null size=$1 extentlist=$input \ + --run 'qemu-img map -f raw --output=json $nbd' | + jq -c '.[] | {start:.start, length:.length, data:.data, zero:.zero}' \ + > $out + diff -u $out $expected +} + +# Empty extent list. +cat > $input <<'EOF' +EOF + +cat > $expected <<'EOF' +{"start":0,"length":0,"data":false,"zero":false} +EOF +test 0 +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":false,"zero":true} +EOF +test 1M + +# Extent list covering 0-1M with data. +cat > $input <<'EOF' +0 1M +EOF + +cat > $expected <<'EOF' +{"start":0,"length":0,"data":false,"zero":false} +EOF +test 0 +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":true,"zero":false} +EOF +test 1M + +# Extent list covering 1-2M with data. +cat > $input <<'EOF' +1M 1M +EOF + +cat > $expected <<'EOF' +{"start":0,"length":0,"data":false,"zero":false} +EOF +test 0 +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":false,"zero":true} +EOF +test 1M +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":false,"zero":true} +{"start":1048576,"length":1048576,"data":true,"zero":false} +EOF +test 2M +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":false,"zero":true} +{"start":1048576,"length":1048576,"data":true,"zero":false} +{"start":2097152,"length":1048576,"data":false,"zero":true} +EOF +test 3M + +# Extent list covering 1-2M with data, but in a more fragmented +# way than the above. +cat > $input <<'EOF' +1024K 512K +1536K 512K +EOF + +cat > $expected <<'EOF' +{"start":0,"length":0,"data":false,"zero":false} +EOF +test 0 +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":false,"zero":true} +EOF +test 1M +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":false,"zero":true} +{"start":1048576,"length":1048576,"data":true,"zero":false} +EOF +test 2M +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":false,"zero":true} +{"start":1048576,"length":1048576,"data":true,"zero":false} +{"start":2097152,"length":1048576,"data":false,"zero":true} +EOF +test 3M + +# Adjacent data and holes. +cat > $input <<'EOF' +0 1M +2M 1M +4M 1M +EOF + +cat > $expected <<'EOF' +{"start":0,"length":0,"data":false,"zero":false} +EOF +test 0 +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":true,"zero":false} +EOF +test 1M +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":true,"zero":false} +{"start":1048576,"length":1048576,"data":false,"zero":true} +EOF +test 2M +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":true,"zero":false} +{"start":1048576,"length":1048576,"data":false,"zero":true} +{"start":2097152,"length":1048576,"data":true,"zero":false} +EOF +test 3M +cat > $expected <<'EOF' +{"start":0,"length":1048576,"data":true,"zero":false} +{"start":1048576,"length":1048576,"data":false,"zero":true} +{"start":2097152,"length":1048576,"data":true,"zero":false} +{"start":3145728,"length":1048576,"data":false,"zero":true} +{"start":4194304,"length":1048576,"data":true,"zero":false} +{"start":5242880,"length":1048576,"data":false,"zero":true} +EOF +test 6M -- 2.18.2