[Libguestfs] [PATCH nbdkit 4/4] Add floppy plugin.

Richard W.M. Jones
2018-10-28T10:13:28Z

Create a virtual FAT-formatted floppy disk from a directory of files.

For example:

  nbdkit floppy /path/to/directory

The implementation of this is quite different from nbdkit-iso-plugin
since we cannot use an external program.  Instead this plugin
synthesizes the MBR partition and FAT32 structures that are required.

To do: Implement bootable virtual floppy using syslinux.  This is not
straightforward as you have to either run the syslinux command on the
virtual floppy, or else you have to synthesize the same set of
operations that syslinux performs.  Also you have to deal with CHS
geometry.
---
 configure.ac                            |   2 +
 plugins/floppy/Makefile.am              |  68 ++
 plugins/floppy/directory-lfn.c          | 623 ++++++++++++++++++
 plugins/floppy/floppy.c                 | 198 ++++++
 plugins/floppy/nbdkit-floppy-plugin.pod |  87 +++
 plugins/floppy/virtual-floppy.c         | 797 ++++++++++++++++++++++++
 plugins/floppy/virtual-floppy.h         | 233 +++++++
 plugins/iso/nbdkit-iso-plugin.pod       |   1 +
 tests/Makefile.am                       |   6 +
 tests/test-floppy.sh                    |  67 ++
 10 files changed, 2082 insertions(+)

diff --git a/configure.ac b/configure.ac
index b66a7e2..b872fd6 100644
--- a/configure.ac
+++ b/configure.ac
@@ -599,6 +599,7 @@ non_lang_plugins="\
         example4 \
         ext2 \
         file \
+        floppy \
         guestfs \
         gzip \
         iso \
@@ -657,6 +658,7 @@ AC_CONFIG_FILES([Makefile
                  plugins/example4/Makefile
                  plugins/ext2/Makefile
                  plugins/file/Makefile
+                 plugins/floppy/Makefile
                  plugins/guestfs/Makefile
                  plugins/gzip/Makefile
                  plugins/iso/Makefile
diff --git a/plugins/floppy/Makefile.am b/plugins/floppy/Makefile.am
new file mode 100644
index 0000000..b6c2435
--- /dev/null
+++ b/plugins/floppy/Makefile.am
@@ -0,0 +1,68 @@
+# 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-floppy-plugin.pod
+
+plugin_LTLIBRARIES = nbdkit-floppy-plugin.la
+
+nbdkit_floppy_plugin_la_SOURCES = \
+	directory-lfn.c \
+	floppy.c \
+	virtual-floppy.c \
+	virtual-floppy.h \
+	$(top_srcdir)/include/nbdkit-plugin.h
+
+nbdkit_floppy_plugin_la_CPPFLAGS = \
+	-I$(top_srcdir)/include \
+	-I$(top_srcdir)/common/include \
+	-I$(top_srcdir)/common/regions \
+	-I.
+nbdkit_floppy_plugin_la_CFLAGS = \
+	$(WARNINGS_CFLAGS)
+nbdkit_floppy_plugin_la_LDFLAGS = \
+	-module -avoid-version -shared
+nbdkit_floppy_plugin_la_LIBADD = \
+	$(top_builddir)/common/regions/libregions.la
+
+if HAVE_POD
+
+man_MANS = nbdkit-floppy-plugin.1
+CLEANFILES += $(man_MANS)
+
+nbdkit-floppy-plugin.1: nbdkit-floppy-plugin.pod
+	$(PODWRAPPER) --section=1 --man $@ \
+	    --html $(top_builddir)/html/$@.html \
+	    $<
+
+endif HAVE_POD
diff --git a/plugins/floppy/directory-lfn.c b/plugins/floppy/directory-lfn.c
new file mode 100644
index 0000000..b3b816d
--- /dev/null
+++ b/plugins/floppy/directory-lfn.c
@@ -0,0 +1,623 @@
+/* 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.
+ */
+
+/* This file deals only with directories and long file names (LFNs).
+ * Turns out to be the most complicated part of the FAT format.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <string.h>
+#include <errno.h>
+#include <assert.h>
+#include <iconv.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <nbdkit-plugin.h>
+
+#include "byte-swapping.h"
+
+#include "virtual-floppy.h"
+
+/* Used for dealing with VFAT LFNs when creating a directory. */
+struct lfn {
+  const char *name;             /* Original Unix filename. */
+  char short_base[8];           /* Short basename. */
+  char short_ext[3];            /* Short file extension. */
+  char *lfn;                    /* Long filename for MS-DOS as UTF16-LE. */
+  size_t lfn_size;              /* Size *in bytes* of lfn. */
+};
+
+static int add_volume_label (const char *label, size_t di, struct
virtual_floppy *floppy);
+static int add_dot_entries (size_t di, struct virtual_floppy *floppy);
+static int add_directory_entry (const struct lfn *lfn, uint8_t attributes,
uint32_t file_size, struct stat *statbuf, size_t di, struct virtual_floppy
*floppy);
+static void set_times (const struct stat *statbuf, struct dir_entry *entry);
+static int convert_long_file_names (struct lfn *lfns, size_t n);
+static int convert_to_utf16le (const char *name, char **out, size_t
*output_len);
+static void free_lfns (struct lfn *lfns, size_t n);
+static ssize_t extend_dir_table (size_t di, struct virtual_floppy *floppy);
+
+/* Create the on disk directory table for dirs[di]. */
+int
+create_directory (size_t di, const char *label,
+                  struct virtual_floppy *floppy)
+{
+  size_t i;
+  const size_t nr_subdirs = floppy->dirs[di].nr_subdirs;
+  const size_t nr_files = floppy->dirs[di].nr_files;
+  struct lfn *lfns, *lfn;
+  const char *name;
+  uint8_t attributes;
+  uint32_t file_size;
+  struct stat *statbuf;
+
+  if (di == 0) {
+    /* For root directory, add the volume label entry first. */
+    if (add_volume_label (label, di, floppy) == -1)
+      return -1;
+  }
+  else {
+    /* For subdirectories, add "." and ".." entries. */
+    if (add_dot_entries (di, floppy) == -1)
+      return -1;
+  }
+
+  /* Convert all the filenames in the directory into short and long
+   * names.  This has to be done for the whole directory because
+   * conflicting short names must be renamed.
+   */
+  lfns = calloc (nr_subdirs + nr_files, sizeof (struct lfn));
+  if (lfns == NULL) {
+    nbdkit_error ("calloc: %m");
+    return -1;
+  }
+  for (i = 0; i < nr_subdirs; ++i) {
+    const size_t sdi = floppy->dirs[di].subdirs[i];
+    assert (sdi < floppy->nr_dirs);
+
+    name = floppy->dirs[sdi].name;
+    lfns[i].name = name;
+  }
+  for (i = 0; i < nr_files; ++i) {
+    const size_t fi = floppy->dirs[di].files[i];
+    assert (fi < floppy->nr_files);
+
+    name = floppy->files[fi].name;
+    lfns[nr_subdirs+i].name = name;
+  }
+
+  if (convert_long_file_names (lfns, nr_subdirs + nr_files) == -1) {
+    free_lfns (lfns, nr_subdirs + nr_files);
+    return -1;
+  }
+
+  /* Add subdirectories. */
+  attributes = DIR_ENTRY_SUBDIRECTORY; /* Same as set by Linux kernel. */
+  file_size = 0;
+  for (i = 0; i < nr_subdirs; ++i) {
+    const size_t sdi = floppy->dirs[di].subdirs[i];
+    assert (sdi < floppy->nr_dirs);
+
+    lfn = &lfns[i];
+    statbuf = &floppy->dirs[sdi].statbuf;
+
+    if (add_directory_entry (lfn, attributes, file_size,
+                             statbuf, di, floppy) == -1) {
+      free_lfns (lfns, nr_subdirs + nr_files);
+      return -1;
+    }
+  }
+
+  /* Add files. */
+  attributes = DIR_ENTRY_ARCHIVE; /* Same as set by Linux kernel. */
+  for (i = 0; i < nr_files; ++i) {
+    const size_t fi = floppy->dirs[di].files[i];
+    assert (fi < floppy->nr_files);
+
+    lfn = &lfns[nr_subdirs+i];
+    statbuf = &floppy->files[fi].statbuf;
+    file_size = statbuf->st_size;
+
+    if (add_directory_entry (lfn, attributes, file_size,
+                             statbuf, di, floppy) == -1) {
+      free_lfns (lfns, nr_subdirs + nr_files);
+      return -1;
+    }
+  }
+
+  free_lfns (lfns, nr_subdirs + nr_files);
+  return 0;
+}
+
+/* Add the volume label to dirs[0].table. */
+static int
+add_volume_label (const char *label, size_t di, struct virtual_floppy *floppy)
+{
+  ssize_t i;
+  struct dir_entry entry;
+
+  assert (di == 0);
+
+  memset (&entry, 0, sizeof entry);
+  pad_string (label, 11, entry.name);
+  entry.attributes = DIR_ENTRY_VOLUME_LABEL; /* Same as dosfstools. */
+
+  i = extend_dir_table (di, floppy);
+  if (i == -1)
+    return -1;
+  floppy->dirs[di].table[i] = entry;
+  return 0;
+}
+
+/* Add "." and ".." entries for subdirectories. */
+static int
+add_dot_entries (size_t di, struct virtual_floppy *floppy)
+{
+  ssize_t i, pdi;
+  struct dir_entry entry;
+
+  assert (di != 0);
+
+  memset (&entry, 0, sizeof entry);
+  pad_string (".", 11, entry.name);
+  entry.attributes = DIR_ENTRY_SUBDIRECTORY;
+  set_times (&floppy->dirs[di].statbuf, &entry);
+
+  i = extend_dir_table (di, floppy);
+  if (i == -1)
+    return -1;
+  floppy->dirs[di].table[i] = entry;
+
+  memset (&entry, 0, sizeof entry);
+  pad_string ("..", 11, entry.name);
+  entry.attributes = DIR_ENTRY_SUBDIRECTORY;
+  pdi = floppy->dirs[di].pdi;
+  set_times (&floppy->dirs[pdi].statbuf, &entry);
+
+  i = extend_dir_table (di, floppy);
+  if (i == -1)
+    return -1;
+  floppy->dirs[di].table[i] = entry;
+
+  return 0;
+}
+
+/* Either truncate or pad a string (with spaces). */
+void
+pad_string (const char *label, size_t n, uint8_t *out)
+{
+  const size_t len = strlen (label);
+
+  memcpy (out, label, len <= n ? len : n);
+  if (len < n)
+    memset (out+len, ' ', n-len);
+}
+
+/* Add a directory entry to dirs[di].table. */
+static int
+add_directory_entry (const struct lfn *lfn,
+                     uint8_t attributes, uint32_t file_size,
+                     struct stat *statbuf,
+                     size_t di, struct virtual_floppy *floppy)
+{
+  uint8_t seq, checksum;
+  ssize_t i;
+  size_t j;
+  struct lfn_entry lfn_entry;
+  struct dir_entry entry;
+  int last_seq = 1;
+
+  /* Compute a checksum for the shortname.  In real LFN filesystems
+   * this is used to check whether a non-LFN-aware operating system
+   * (ie. MS-DOS) has edited the directory.
+   */
+  checksum = 0;
+  for (j = 0; j < 8; ++j)
+    checksum = ((checksum & 1) << 7) + (checksum >> 1) +
lfn->short_base[j];
+  for (j = 0; j < 3; ++j)
+    checksum = ((checksum & 1) << 7) + (checksum >> 1) +
lfn->short_ext[j];
+
+  /* LFN support.
+   *
+   * Iterate in reverse over the sequence numbers.  If the filename is:
+   *
+   *   "ABCDEFGHIJKLMNO"
+   *
+   * assuming those are UCS-2 codepoints, so lfn_size = 15*2 = 30,
+   * then we generate these LFN sequences:
+   *
+   *   seq   byte_offset   s[13]
+   *   0x42  26            "NO           "
+   *   0x01  0             "ABCDEFGHIJKLM"
+   */
+  for (seq = 1 + lfn->lfn_size/2/13; seq >= 1; --seq) {
+    size_t byte_offset = (seq-1)*2*13, r;
+    uint16_t s[13];
+
+    /* Copy the portion of the LFN into s.
+     * r = Number of bytes from the original string to copy.
+     */
+    r = lfn->lfn_size - byte_offset;
+    if (r > 26)
+      memcpy (s, &lfn->lfn[byte_offset], 26);
+    else {
+      memcpy (s, &lfn->lfn[byte_offset], r);
+      /* Pad remaining filename with 0. */
+      for (j = r/2; j < 13; ++j)
+        s[j] = htole16 (0);
+    }
+
+    memset (&lfn_entry, 0, sizeof lfn_entry);
+    lfn_entry.seq = seq;
+    if (last_seq) {
+      lfn_entry.seq |= 0x40;
+      last_seq = 0;
+    }
+    lfn_entry.attributes = 0xf;
+    lfn_entry.checksum = checksum;
+
+    /* Copy the name portion to the fields in the LFN entry. */
+    memcpy (lfn_entry.name1, &s[0], 5*2);
+    memcpy (lfn_entry.name2, &s[5], 6*2);
+    memcpy (lfn_entry.name3, &s[11], 2*2);
+
+    i = extend_dir_table (di, floppy);
+    if (i == -1)
+      return -1;
+    memcpy (&floppy->dirs[di].table[i], &lfn_entry, sizeof (struct
dir_entry));
+  }
+
+  /* Create the 8.3 (short name / DOS-compatible) entry. */
+  memset (&entry, 0, sizeof entry);
+  memcpy (entry.name, lfn->short_base, 8);
+  memcpy (entry.name+8, lfn->short_ext, 3);
+  entry.attributes = attributes;
+  set_times (statbuf, &entry);
+  entry.size = htole32 (file_size);
+  /* Note that entry.cluster_hi and .cluster_lo are set later on in
+   * update_directory_first_cluster.
+   */
+
+  i = extend_dir_table (di, floppy);
+  if (i == -1)
+    return -1;
+  floppy->dirs[di].table[i] = entry;
+
+  return 0;
+}
+
+/* Set the {c,m,a}date and {c,m}time fields in the entry structure
+ * based on metadata found in the statbuf.
+ */
+static void
+set_times (const struct stat *statbuf, struct dir_entry *entry)
+{
+  struct tm ctime_tm, mtime_tm, atime_tm;
+
+  localtime_r (&statbuf->st_ctime, &ctime_tm);
+  entry->ctime +    ctime_tm.tm_hour << 11 |
+    ctime_tm.tm_min << 5 |
+    (ctime_tm.tm_sec / 2);
+  entry->ctime_10ms = 100 * (ctime_tm.tm_sec % 2);
+  entry->cdate +    (ctime_tm.tm_year - 80) << 9 |
+    (ctime_tm.tm_mon + 1) << 5 |
+    ctime_tm.tm_mday;
+
+  localtime_r (&statbuf->st_mtime, &mtime_tm);
+  entry->mtime +    mtime_tm.tm_hour << 11 |
+    mtime_tm.tm_min << 5 |
+    (mtime_tm.tm_sec / 2);
+  entry->mdate +    (mtime_tm.tm_year - 80) << 9 |
+    (mtime_tm.tm_mon + 1) << 5 |
+    mtime_tm.tm_mday;
+
+  localtime_r (&statbuf->st_atime, &atime_tm);
+  entry->adate +    (atime_tm.tm_year - 80) << 9 |
+    (atime_tm.tm_mon + 1) << 5 |
+    atime_tm.tm_mday;
+}
+
+#define UPPER_ASCII "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+#define LOWER_ASCII "abcdefghijklmnopqrstuvwxyz"
+
+static int
+convert_long_file_names (struct lfn *lfns, size_t n)
+{
+  /* Characters which are valid in short names. */
+  static const char ok[] +    UPPER_ASCII
+    /* Lowercase is not actually valid, but it makes the
+     * implementation below simpler and we toupper the final string.
+     */
+    LOWER_ASCII
+    "0123456789"
+    /* ~ is also valid but don't include it here because we want to
+     * keep it as a special character for renaming duplicates below.
+     */
+    "!#$%&'()-@^_`{}";
+  size_t i, j, len;
+  struct lfn *lfn;
+
+  /* Split the filenames to generate a list of short basenames + extensions. */
+  for (i = 0; i < n; ++i) {
+    const char *p;
+
+    lfn = &lfns[i];
+
+    len = strspn (lfn->name, ok);
+    memcpy (lfns[i].short_base, lfn->name, len <= 8 ? len : 8);
+    if (len < 8)
+      memset (lfn->short_base+len, ' ', 8-len);
+    /* Look for the extension. */
+    p = strrchr (lfn->name, '.');
+    if (p) {
+      p++;
+      len = strspn (p, ok);
+      memcpy (lfn->short_ext, p, len <= 3 ? len : 3);
+      if (len < 3)
+        memset (lfn->short_ext+len, ' ', 3-len);
+    }
+    else
+      memset (lfn->short_ext, ' ', sizeof lfn->short_ext);
+
+    /* Convert short name to upper case (ASCII only). */
+    for (j = 0; j < 8; ++j) {
+      if (strchr (LOWER_ASCII, lfn->short_base[j]))
+        lfn->short_base[j] -= 32;
+    }
+    for (j = 0; j < 3; ++j) {
+      if (strchr (LOWER_ASCII, lfn->short_ext[j]))
+        lfn->short_ext[j] -= 32;
+    }
+
+    /* Convert the original filename to UTF16-LE.  Maximum LFN length
+     * is 0x3f * 13 = 819 UCS-2 characters.
+     */
+    if (convert_to_utf16le (lfn->name, &lfn->lfn,
&lfn->lfn_size) == -1)
+      return -1;
+    if (lfn->lfn_size > 2*819) {
+      nbdkit_error ("%s: filename is too long", lfn->name);
+      return -1;
+    }
+  }
+
+  /* Now we must see if some short filenames are duplicates and
+   * rename them.  XXX Unfortunately O(n^2).
+   */
+  for (i = 1; i < n; ++i) {
+    for (j = 0; j < i; ++j) {
+      if (memcmp (lfns[i].short_base, lfns[j].short_base, 8) == 0 &&
+          memcmp (lfns[i].short_ext, lfns[j].short_ext, 3) == 0) {
+        char s[9];
+        ssize_t k;
+
+        /* Entry i is a duplicate of j (j < i).  So we will rename i. */
+        lfn = &lfns[i];
+
+        len = snprintf (s, sizeof s, "~%zu", i);
+        assert (len >= 2 && len <= 8);
+
+        k = 8-len;
+        while (k > 0 && lfn->short_base[k] == ' ')
+          k--;
+        memcpy (&lfn->short_base[k], s, len);
+      }
+    }
+  }
+
+  return 0;
+}
+
+static const char lfn_encoding[] +  "UTF-16LE"
+#ifdef __GNU_LIBRARY__
+  "//TRANSLIT"
+#endif
+  ;
+
+static int
+convert_to_utf16le (const char *name, char **out, size_t *output_len)
+{
+  iconv_t ic;
+  const size_t input_len = strlen (name);
+  size_t outalloc, inlen, outlen, prev, r;
+  const char *inp;
+  char *outp;
+
+  /* XXX Assumes locale is UTF-8. */
+  ic = iconv_open (lfn_encoding, "UTF-8");
+  if (ic == (iconv_t)-1) {
+    nbdkit_error ("iconv: %m");
+    return -1;
+  }
+  outalloc = input_len;
+
+ again:
+  inlen = input_len;
+  outlen = outalloc;
+  *out = malloc (outlen + 1);
+  if (out == NULL) {
+    nbdkit_error ("malloc: %m");
+    iconv_close (ic);
+    return -1;
+  }
+  inp = name;
+  outp = *out;
+
+  r = iconv (ic, (char **) &inp, &inlen, &outp, &outlen);
+  if (r == (size_t)-1) {
+    if (errno == E2BIG) {
+      prev = outalloc;
+      /* Try again with a larger buffer. */
+      free (*out);
+      *out = NULL;
+      outalloc *= 2;
+      if (outalloc < prev) {
+        nbdkit_error ("iconv: %m");
+        iconv_close (ic);
+        return -1;
+      }
+      /* Erase errno so we don't return it to the caller by accident. */
+      errno = 0;
+      goto again;
+    }
+    else {
+      /* EILSEQ etc. */
+      nbdkit_error ("iconv: %s: %m", name);
+      free (*out);
+      *out = NULL;
+      return -1;
+    }
+  }
+  *outp = '\0';
+  iconv_close (ic);
+  if (output_len != NULL)
+    *output_len = outp - *out;
+
+  return 0;
+}
+
+static void
+free_lfns (struct lfn *lfns, size_t n)
+{
+  size_t i;
+
+  for (i = 0; i < n; ++i)
+    free (lfns[i].lfn);
+  free (lfns);
+}
+
+/* Extend dirs[di].table by 1 directory entry. */
+static ssize_t
+extend_dir_table (size_t di, struct virtual_floppy *floppy)
+{
+  struct dir_entry *p;
+  size_t i;
+
+  i = floppy->dirs[di].table_entries;
+  p = realloc (floppy->dirs[di].table, sizeof (struct dir_entry) * (i+1));
+  if (p == NULL) {
+    nbdkit_error ("realloc: %m");
+    return -1;
+  }
+  floppy->dirs[di].table = p;
+  floppy->dirs[di].table_entries++;
+  memset (&floppy->dirs[di].table[i], 0, sizeof (struct dir_entry));
+  return i;
+}
+
+/* In create_directory / add_directory_entry above we run before we
+ * have finalised the .first_cluster fields (because that cannot be
+ * done until we have sized all the directories).  Here we fix the
+ * directory entries with the final cluster number.  Note we must only
+ * touch plain directory entries (not the volume label or LFN).
+ */
+int
+update_directory_first_cluster (size_t di, struct virtual_floppy *floppy)
+{
+  size_t i, j, pdi;
+  const size_t nr_subdirs = floppy->dirs[di].nr_subdirs;
+  const size_t nr_files = floppy->dirs[di].nr_files;
+  uint32_t first_cluster;
+  struct dir_entry *entry;
+
+  /* NB: This function makes assumptions about the order in which
+   * subdirectories and files are added to the table so that we can
+   * avoid having to maintain another mapping from subdirs/files to
+   * table entries.
+   */
+  i = 0;
+  for (j = 0; j < floppy->dirs[di].table_entries; ++j) {
+    entry = &floppy->dirs[di].table[j];
+
+    /* Skip LFN entries. */
+    if (entry->attributes == 0xf)
+      continue; /* don't increment i */
+
+    /* Skip the volume label in the root directory. */
+    if (entry->attributes == DIR_ENTRY_VOLUME_LABEL)
+      continue; /* don't increment i */
+
+    /* Set the first cluster of the "." entry to point to self. */
+    if (entry->attributes == DIR_ENTRY_SUBDIRECTORY &&
+        memcmp (entry->name, ".          ", 11) == 0) {
+      first_cluster = floppy->dirs[di].first_cluster;
+      entry->cluster_hi = htole16 (first_cluster >> 16);
+      entry->cluster_lo = htole16 (first_cluster & 0xffff);
+      continue; /* don't increment i */
+    }
+
+    /* Set the first cluster of the ".." entry to point to parent. */
+    if (entry->attributes == DIR_ENTRY_SUBDIRECTORY &&
+        memcmp (entry->name, "..         ", 11) == 0) {
+      pdi = floppy->dirs[di].pdi;
+      first_cluster = floppy->dirs[pdi].first_cluster;
+      entry->cluster_hi = htole16 (first_cluster >> 16);
+      entry->cluster_lo = htole16 (first_cluster & 0xffff);
+      continue; /* don't increment i */
+    }
+
+    /* Otherwise it's a short name entry so we must now update the
+     * first cluster.
+     */
+    if (i < nr_subdirs) {
+      const size_t sdi = floppy->dirs[di].subdirs[i];
+      assert (sdi < floppy->nr_dirs);
+      first_cluster = floppy->dirs[sdi].first_cluster;
+    }
+    else if (i < nr_subdirs + nr_files) {
+      const size_t fi = floppy->dirs[di].files[i-nr_subdirs];
+      assert (fi < floppy->nr_files);
+      first_cluster = floppy->files[fi].first_cluster;
+    }
+    else
+      abort ();
+
+    entry->cluster_hi = htole16 (first_cluster >> 16);
+    entry->cluster_lo = htole16 (first_cluster & 0xffff);
+    ++i;
+  }
+
+  return 0;
+}
diff --git a/plugins/floppy/floppy.c b/plugins/floppy/floppy.c
new file mode 100644
index 0000000..0b7f37d
--- /dev/null
+++ b/plugins/floppy/floppy.c
@@ -0,0 +1,198 @@
+/* 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 <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <nbdkit-plugin.h>
+
+#include "regions.h"
+
+#include "virtual-floppy.h"
+
+/* Directory. */
+static char *dir = NULL;
+
+/* Volume label. */
+static const char *label = "NBDKITFLOPY";
+
+/* Virtual floppy. */
+static struct virtual_floppy floppy;
+
+static void
+floppy_load (void)
+{
+  init_virtual_floppy (&floppy);
+}
+
+static void
+floppy_unload (void)
+{
+  free (dir);
+  free_virtual_floppy (&floppy);
+}
+
+static int
+floppy_config (const char *key, const char *value)
+{
+  if (strcmp (key, "dir") == 0) {
+    dir = nbdkit_realpath (value);
+    if (dir == NULL)
+      return -1;
+  }
+  else if (strcmp (key, "label") == 0) {
+    label = value;
+  }
+  else {
+    nbdkit_error ("unknown parameter '%s'", key);
+    return -1;
+  }
+
+  return 0;
+}
+
+static int
+floppy_config_complete (void)
+{
+  if (dir == NULL) {
+    nbdkit_error ("you must supply the dir=<DIRECTORY> parameter
after the plugin name on the command line");
+    return -1;
+  }
+
+  return create_virtual_floppy (dir, label, &floppy);
+}
+
+#define floppy_config_help \
+  "dir=<DIRECTORY>     (required) The directory to serve.\n" \
+  "label=<LABEL>                  The volume label." \
+
+static void *
+floppy_open (int readonly)
+{
+  /* We don't need a per-connection handle, so this just acts as a
+   * pointer to return.
+   */
+  static int h;
+
+  return &h;
+}
+
+#define THREAD_MODEL NBDKIT_THREAD_MODEL_PARALLEL
+
+/* Get the file size. */
+static int64_t
+floppy_get_size (void *handle)
+{
+  return virtual_size (&floppy.regions);
+}
+
+/* Read data from the file. */
+static int
+floppy_pread (void *handle, void *buf, uint32_t count, uint64_t offset)
+{
+  while (count > 0) {
+    const struct region *region = find_region (&floppy.regions, offset);
+    size_t i, len;
+    const char *host_path;
+    int fd;
+    ssize_t r;
+
+    /* Length to end of region. */
+    len = region->end - offset + 1;
+    if (len > count)
+      len = count;
+
+    switch (region->type) {
+    case region_file:
+      i = region->u.i;
+      assert (i < floppy.nr_files);
+      host_path = floppy.files[i].host_path;
+      fd = open (host_path, O_RDONLY|O_CLOEXEC);
+      if (fd == -1) {
+        nbdkit_error ("open: %s: %m", host_path);
+        return -1;
+      }
+      r = pread (fd, buf, len, offset - region->start);
+      if (r == -1) {
+        nbdkit_error ("pread: %s: %m", host_path);
+        close (fd);
+        return -1;
+      }
+      if (r == 0) {
+        nbdkit_error ("pread: %s: unexpected end of file",
host_path);
+        close (fd);
+        return -1;
+      }
+      close (fd);
+      len = r;
+      break;
+
+    case region_data:
+      memcpy (buf, &region->u.data[offset - region->start], len);
+      break;
+
+    case region_zero:
+      memset (buf, 0, len);
+      break;
+    }
+
+    count -= len;
+    buf += len;
+    offset += len;
+  }
+
+  return 0;
+}
+
+static struct nbdkit_plugin plugin = {
+  .name              = "floppy",
+  .longname          = "nbdkit floppy plugin",
+  .version           = PACKAGE_VERSION,
+  .load              = floppy_load,
+  .unload            = floppy_unload,
+  .config            = floppy_config,
+  .config_complete   = floppy_config_complete,
+  .config_help       = floppy_config_help,
+  .magic_config_key  = "dir",
+  .open              = floppy_open,
+  .get_size          = floppy_get_size,
+  .pread             = floppy_pread,
+  .errno_is_preserved = 1,
+};
+
+NBDKIT_REGISTER_PLUGIN(plugin)
diff --git a/plugins/floppy/nbdkit-floppy-plugin.pod
b/plugins/floppy/nbdkit-floppy-plugin.pod
new file mode 100644
index 0000000..21105de
--- /dev/null
+++ b/plugins/floppy/nbdkit-floppy-plugin.pod
@@ -0,0 +1,87 @@
+=head1 NAME
+
+nbdkit-floppy-plugin - create virtual floppy disk from directory
+
+=head1 SYNOPSIS
+
+ nbdkit floppy [dir=]DIRECTORY
+               [label=LABEL]
+
+=head1 DESCRIPTION
+
+C<nbdkit-floppy-plugin> is a plugin for L<nbdkit(1)> which creates
a
+virtual FAT-formatted floppy disk image from a directory on the fly.
+The files in the specified directory (and subdirectories) appear in
+the virtual floppy, which is served read-only over the NBD protocol.
+
+The virtual floppy disk will have a single partition (using an MBR
+partition table).  In that partition will be a virtual FAT32
+filesystem containing the files.  Long filenames are supported.
+
+=head1 EXAMPLE
+
+Create a virtual floppy disk:
+
+ nbdkit floppy /path/to/directory
+
+=head1 PARAMETERS
+
+=over 4
+
+=item B<dir=>DIRECTORY
+
+Specify the directory containing files and subdirectories which will
+be added to the virtual floppy disk.  Files inside this directory will
+appear in the root directory of the virtual floppy.
+
+This parameter is required.
+
+In nbdkit E<ge> 1.8, C<dir=> may be omitted.  To ensure that the
+directory name does not end up being parsed accidentally as
+C<key=value>, prefix relative paths with C<./> (absolute paths do
not
+need modification).
+
+=item B<label=>LABEL
+
+The optional volume label for the filesystem.  This may be up to 11
+ASCII characters.  If omitted, C<NBDKITFLOPY> is used.
+
+=back
+
+=head1 LIMITATIONS
+
+The maximum size of the disk is around 2TB.  The maximum size of a
+single file is 4GB.  Non-regular files (such as block special,
+symbolic links, sockets) are not supported and will be ignored.
+
+The plugin does not support writes.
+
+The plugin does not save a temporary copy of the files, so you must
+leave the directory alone while nbdkit is running, else you may get an
+error for example if the plugin tries to open one of the files which
+you have moved or deleted.  (This is different from how
+L<nbdkit-iso-plugin(1)> works).
+
+The virtual floppy will not be bootable.  This could be added in
+future (using SYSLINUX) but requires considerable work.  As a
+workaround use L<nbdkit-iso-plugin(1)> instead.
+
+FAT32 is always used, even for small disks (where dosfstools, for
+example, would choose FAT12 or FAT16).  This results in extra wasted
+space, but since it is only I<virtual> wasted space it isn't really
+important, and it simplifies the implementation greatly.
+
+=head1 SEE ALSO
+
+L<nbdkit(1)>,
+L<nbdkit-plugin(3)>,
+L<nbdkit-file-plugin(1)>,
+L<nbdkit-iso-plugin(1)>.
+
+=head1 AUTHORS
+
+Richard W.M. Jones
+
+=head1 COPYRIGHT
+
+Copyright (C) 2018 Red Hat Inc.
diff --git a/plugins/floppy/virtual-floppy.c b/plugins/floppy/virtual-floppy.c
new file mode 100644
index 0000000..d1065b4
--- /dev/null
+++ b/plugins/floppy/virtual-floppy.c
@@ -0,0 +1,797 @@
+/* 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 <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <dirent.h>
+#include <assert.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <nbdkit-plugin.h>
+
+#include "get-current-dir-name.h"
+
+#include "byte-swapping.h"
+#include "regions.h"
+#include "rounding.h"
+
+#include "virtual-floppy.h"
+
+/* This is the Windows 98 OEM name, and some sites recommend using it
+ * for greatest compatibility.
+ */
+#define OEM_NAME "MSWIN4.1"
+
+static ssize_t visit (const char *dir, struct virtual_floppy *floppy);
+static int visit_subdirectory (const char *dir, const char *name, const struct
stat *statbuf, size_t di, struct virtual_floppy *floppy);
+static int visit_file (const char *dir, const char *name, const struct stat
*statbuf, size_t di, struct virtual_floppy *floppy);
+static int create_mbr (struct virtual_floppy *floppy);
+static void chs_too_large (uint8_t *out);
+static int create_partition_boot_sector (const char *label, struct
virtual_floppy *floppy);
+static int create_fsinfo (struct virtual_floppy *floppy);
+static int create_fat (struct virtual_floppy *floppy);
+static void write_fat_file (uint32_t first_cluster, uint32_t nr_clusters,
struct virtual_floppy *floppy);
+static int create_regions (struct virtual_floppy *floppy);
+
+void
+init_virtual_floppy (struct virtual_floppy *floppy)
+{
+  memset (floppy, 0, sizeof *floppy);
+  init_regions (&floppy->regions);
+
+  /* Assert that the on disk struct sizes are correct. */
+  assert (sizeof (struct dir_entry) == 32);
+  assert (sizeof (struct lfn_entry) == 32);
+  assert (sizeof (struct bootsector) == 512);
+  assert (sizeof (struct fsinfo) == 512);
+}
+
+int
+create_virtual_floppy (const char *dir, const char *label,
+                       struct virtual_floppy *floppy)
+{
+  size_t i;
+  uint64_t nr_bytes, nr_clusters;
+  uint32_t cluster;
+
+  if (visit (dir, floppy) == -1)
+    return -1;
+
+  nbdkit_debug ("floppy: %zu directories and %zu files",
+                floppy->nr_dirs, floppy->nr_files);
+
+  /* Create the on disk directory tables. */
+  for (i = 0; i < floppy->nr_dirs; ++i) {
+    if (create_directory (i, label, floppy) == -1)
+      return -1;
+  }
+
+  /* We now have a complete list of directories and files, and
+   * directories have been converted to on disk directory tables.  So
+   * we can assign them to clusters and also precisely calculate the
+   * size of the data region and hence the size of the FAT.
+   *
+   * The first cluster number is always 2 (0 and 1 are reserved), and
+   * (in this implementation) always contains the root directory.
+   */
+  floppy->data_size = 0;
+  cluster = 2;
+  for (i = 0; i < floppy->nr_dirs; ++i) {
+    floppy->dirs[i].first_cluster = cluster;
+    nr_bytes +      ROUND_UP (floppy->dirs[i].table_entries * sizeof (struct
dir_entry),
+                CLUSTER_SIZE);
+    floppy->data_size += nr_bytes;
+    nr_clusters = nr_bytes / CLUSTER_SIZE;
+    if (cluster + nr_clusters > UINT32_MAX) {
+      nbdkit_error ("disk image is too large for the MBR disk
format");
+      return -1;
+    }
+    floppy->dirs[i].nr_clusters = nr_clusters;
+    cluster += nr_clusters;
+  }
+  for (i = 0; i < floppy->nr_files; ++i) {
+    floppy->files[i].first_cluster = cluster;
+    nr_bytes = ROUND_UP (floppy->files[i].statbuf.st_size, CLUSTER_SIZE);
+    floppy->data_size += nr_bytes;
+    nr_clusters = nr_bytes / CLUSTER_SIZE;
+    if (cluster + nr_clusters > UINT32_MAX) {
+      nbdkit_error ("disk image is too large for the MBR disk
format");
+      return -1;
+    }
+    floppy->files[i].nr_clusters = nr_clusters;
+    cluster += nr_clusters;
+  }
+
+  floppy->data_clusters = floppy->data_size / CLUSTER_SIZE;
+
+  /* Despite its name, FAT32 only allows 28 bit cluster numbers, so
+   * give an error if we go beyond this.
+   */
+  if (floppy->data_clusters >= 0x10000000) {
+    nbdkit_error ("disk image is too large for the FAT32 disk
format");
+    return -1;
+  }
+
+  nbdkit_debug ("floppy: %" PRIu64 " data clusters, "
+                "largest cluster number %" PRIu32 ", "
+                "%" PRIu64 " bytes",
+                floppy->data_clusters,
+                cluster-1,
+                floppy->data_size);
+
+  floppy->fat_entries = floppy->data_clusters + 2;
+  floppy->fat_clusters = DIV_ROUND_UP (floppy->fat_entries * 4,
CLUSTER_SIZE);
+
+  nbdkit_debug ("floppy: %" PRIu64 " FAT entries",
floppy->fat_entries);
+
+  /* We can now decide where we will place the FATs and data region on disk. */
+  floppy->fat2_start_sector +    2080 + floppy->fat_clusters *
SECTORS_PER_CLUSTER;
+  floppy->data_start_sector +    floppy->fat2_start_sector +
floppy->fat_clusters * SECTORS_PER_CLUSTER;
+  floppy->data_last_sector +    floppy->data_start_sector +
floppy->data_clusters * SECTORS_PER_CLUSTER - 1;
+
+  /* We now have to go back and update the cluster numbers in the
+   * directory entries (which we didn't have available during
+   * create_directory above).
+   */
+  for (i = 0; i < floppy->nr_dirs; ++i) {
+    if (update_directory_first_cluster (i, floppy) == -1)
+      return -1;
+  }
+
+  /* Create MBR. */
+  if (create_mbr (floppy) == -1)
+    return -1;
+
+  /* Create partition first sector. */
+  if (create_partition_boot_sector (label, floppy) == -1)
+    return -1;
+
+  /* Create filesystem information sector. */
+  if (create_fsinfo (floppy) == -1)
+    return -1;
+
+  /* Allocate and populate FAT. */
+  if (create_fat (floppy) == -1)
+    return -1;
+
+  /* Now we know how large everything is we can create the virtual
+   * disk regions.
+   */
+  if (create_regions (floppy) == -1)
+    return -1;
+
+  return 0;
+}
+
+void
+free_virtual_floppy (struct virtual_floppy *floppy)
+{
+  size_t i;
+
+  free_regions (&floppy->regions);
+
+  free (floppy->fat);
+
+  for (i = 0; i < floppy->nr_files; ++i) {
+    free (floppy->files[i].name);
+    free (floppy->files[i].host_path);
+  }
+  free (floppy->files);
+
+  for (i = 0; i < floppy->nr_dirs; ++i) {
+    free (floppy->dirs[i].name);
+    free (floppy->dirs[i].subdirs);
+    free (floppy->dirs[i].files);
+    free (floppy->dirs[i].table);
+  }
+  free (floppy->dirs);
+}
+
+/* Visit files and directories.
+ *
+ * This constructs the floppy->dirs and floppy->files lists.
+ *
+ * Returns the directory index, or -1 on error.
+ */
+static ssize_t
+visit (const char *dir, struct virtual_floppy *floppy)
+{
+  void *np;
+  size_t di;
+  char *origdir;
+  DIR *DIR;
+  struct dirent *d;
+  int err;
+  struct stat statbuf;
+
+  /* Allocate a new index in the directory array.  Note that the root
+   * directory will always be at dirs[0].
+   */
+  di = floppy->nr_dirs;
+  np = realloc (floppy->dirs, sizeof (struct dir) * (di+1));
+  if (np == NULL) {
+    nbdkit_error ("realloc: %m");
+    goto error0;
+  }
+  floppy->dirs = np;
+  floppy->nr_dirs++;
+  memset (&floppy->dirs[di], 0, sizeof (struct dir));
+
+  /* Because this is called from config_complete, before nbdkit
+   * daemonizes or starts any threads, it's safe to use chdir here and
+   * greatly simplifies the code.  However we must chdir back to the
+   * original directory at the end.
+   */
+  origdir = get_current_dir_name ();
+  if (origdir == NULL) {
+    nbdkit_error ("get_current_dir_name: %m");
+    goto error0;
+  }
+  if (chdir (dir) == -1) {
+    nbdkit_error ("chdir: %s: %m", dir);
+    goto error1;
+  }
+
+  DIR = opendir (".");
+  if (DIR == NULL) {
+    nbdkit_error ("opendir: %s: %m", dir);
+    goto error1;
+  }
+
+  errno = 0;
+  while ((d = readdir (DIR)) != NULL) {
+    if (strcmp (d->d_name, ".") == 0 ||
+        strcmp (d->d_name, "..") == 0)
+      continue;
+
+    if (lstat (d->d_name, &statbuf) == -1) {
+      nbdkit_error ("stat: %s/%s: %m", dir, d->d_name);
+      goto error2;
+    }
+
+    /* Directory. */
+    if (S_ISDIR (statbuf.st_mode)) {
+      if (visit_subdirectory (dir, d->d_name, &statbuf, di, floppy) ==
-1)
+        goto error2;
+    }
+    /* Regular file. */
+    else if (S_ISREG (statbuf.st_mode)) {
+      if (visit_file (dir, d->d_name, &statbuf, di, floppy) == -1)
+        goto error2;
+    }
+    /* else ALL other file types are ignored - see documentation. */
+  }
+
+  /* Did readdir fail? */
+  if (errno != 0) {
+    nbdkit_error ("readdir: %s: %m", dir);
+    goto error2;
+  }
+
+  if (closedir (DIR) == -1) {
+    nbdkit_error ("closedir: %s: %m", dir);
+    goto error1;
+  }
+
+  if (chdir (origdir) == -1) {
+    nbdkit_error ("chdir: %s: %m", origdir);
+    goto error0;
+  }
+  free (origdir);
+  return di;
+
+ error2:
+  closedir (DIR);
+ error1:
+  err = errno;
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-result"
+  chdir (origdir);
+#pragma GCC diagnostic pop
+  errno = err;
+  free (origdir);
+ error0:
+  return -1;
+}
+
+/* This is called to visit a subdirectory in a directory.  It
+ * recursively calls the visit() function, and then adds the
+ * subdirectory to the list of subdirectories in the parent.
+ */
+static int
+visit_subdirectory (const char *dir, const char *name,
+                    const struct stat *statbuf, size_t di,
+                    struct virtual_floppy *floppy)
+{
+  void *np;
+  char *subdir;
+  ssize_t sdi;                  /* subdirectory index */
+  size_t i;
+
+  if (asprintf (&subdir, "%s/%s", dir, name) == -1) {
+    nbdkit_error ("asprintf: %m");
+    return -1;
+  }
+  /* Recursively visit this directory.  As a side effect this adds the
+   * new subdirectory to the global list of directories, and returns
+   * the index in that list (sdi).
+   */
+  sdi = visit (subdir, floppy);
+  if (sdi == -1) {
+    free (subdir);
+    return -1;
+  }
+  free (subdir);
+
+  /* We must set sdi->name because visit() cannot set it. */
+  floppy->dirs[sdi].name = strdup (name);
+  if (floppy->dirs[sdi].name == NULL) {
+    nbdkit_error ("strdup: %m");
+    return -1;
+  }
+  floppy->dirs[sdi].statbuf = *statbuf;
+  floppy->dirs[sdi].pdi = di;
+
+  /* Add to the list of subdirs in the parent directory (di). */
+  i = floppy->dirs[di].nr_subdirs;
+  np = realloc (floppy->dirs[di].subdirs, sizeof (size_t) * (i+1));
+  if (np == NULL) {
+    nbdkit_error ("realloc: %m");
+    return -1;
+  }
+  floppy->dirs[di].subdirs = np;
+  floppy->dirs[di].nr_subdirs++;
+  floppy->dirs[di].subdirs[i] = sdi;
+
+  return 0;
+}
+
+/* This is called to visit a file in a directory.  It performs some
+ * checks and then adds the file to the global list of files, and also
+ * adds the file to the list of files in the parent directory.
+ */
+static int
+visit_file (const char *dir, const char *name,
+            const struct stat *statbuf, size_t di,
+            struct virtual_floppy *floppy)
+{
+  void *np;
+  char *host_path;
+  size_t fi, i;
+
+  if (asprintf (&host_path, "%s/%s", dir, name) == -1) {
+    nbdkit_error ("asprintf: %m");
+    return -1;
+  }
+
+  if (statbuf->st_size >= UINT32_MAX) {
+    nbdkit_error ("%s: file is larger than maximum supported by
VFAT",
+                  host_path);
+    free (host_path);
+    return -1;
+  }
+
+  /* Add to global list of files. */
+  fi = floppy->nr_files;
+  np = realloc (floppy->files, sizeof (struct file) * (fi+1));
+  if (np == NULL) {
+    nbdkit_error ("realloc: %m");
+    free (host_path);
+    return -1;
+  }
+  floppy->files = np;
+  floppy->nr_files++;
+  floppy->files[fi].name = strdup (name);
+  if (floppy->files[fi].name == NULL) {
+    nbdkit_error ("strdup: %m");
+    free (host_path);
+    return -1;
+  }
+  floppy->files[fi].host_path = host_path;
+  floppy->files[fi].statbuf = *statbuf;
+
+  /* Add to the list of files in the parent directory (di). */
+  i = floppy->dirs[di].nr_files;
+  np = realloc (floppy->dirs[di].files, sizeof (size_t) * (i+1));
+  if (np == NULL) {
+    nbdkit_error ("realloc: %m");
+    return -1;
+  }
+  floppy->dirs[di].files = np;
+  floppy->dirs[di].nr_files++;
+  floppy->dirs[di].files[i] = fi;
+
+  return 0;
+}
+
+/* Create the Master Boot Record sector of the disk. */
+static int
+create_mbr (struct virtual_floppy *floppy)
+{
+  uint32_t num_sectors;
+  uint64_t last_sector;
+
+  /* The last sector number in the partition. */
+  last_sector +    floppy->data_start_sector
+    + floppy->data_clusters * SECTORS_PER_CLUSTER
+    - 1;
+
+  nbdkit_debug ("floppy: last sector %" PRIu64, last_sector);
+
+  if (last_sector >= UINT32_MAX) {
+    nbdkit_error ("disk image is too large for the MBR disk format");
+    return -1;
+  }
+  num_sectors = last_sector - 2048 + 1;
+
+  memcpy (floppy->mbr.oem_name, OEM_NAME, sizeof floppy->mbr.oem_name);
+
+  /* We could choose a random disk signature, but it seems safer to
+   * leave the field zero.
+   */
+  floppy->mbr.disk_signature = htole32 (0);
+  floppy->mbr.boot_signature[0] = 0x55;
+  floppy->mbr.boot_signature[1] = 0xAA;
+
+  /* Only one partition. */
+  floppy->mbr.partition[0].bootable = 0;
+  chs_too_large (floppy->mbr.partition[0].chs);
+  floppy->mbr.partition[0].part_type = 0x0c;
+  chs_too_large (floppy->mbr.partition[0].chs2);
+  floppy->mbr.partition[0].start_sector = htole32 (2048);
+  floppy->mbr.partition[0].num_sectors = htole32 (num_sectors);
+
+  return 0;
+}
+
+static void
+chs_too_large (uint8_t *out)
+{
+  const int c = 1023, h = 254, s = 63;
+
+  out[0] = h;
+  out[1] = (c & 0x300) >> 2 | s;
+  out[2] = c & 0xff;
+}
+
+static int
+create_partition_boot_sector (const char *label, struct virtual_floppy *floppy)
+{
+  memcpy (floppy->bootsect.oem_name, OEM_NAME,
+          sizeof floppy->bootsect.oem_name);
+
+  floppy->bootsect.bytes_per_sector = htole16 (SECTOR_SIZE);
+  floppy->bootsect.sectors_per_cluster = SECTORS_PER_CLUSTER;
+  floppy->bootsect.reserved_sectors = htole16 (32);
+  floppy->bootsect.nr_fats = 2;
+  floppy->bootsect.nr_root_dir_entries = htole16 (0);
+  floppy->bootsect.old_nr_sectors = htole16 (0);
+  floppy->bootsect.media_descriptor = 0xf8;
+  floppy->bootsect.old_sectors_per_fat = htole16 (0);
+  floppy->bootsect.sectors_per_track = htole16 (0);
+  floppy->bootsect.nr_heads = htole16 (0);
+  floppy->bootsect.nr_hidden_sectors = htole32 (0);
+  floppy->bootsect.nr_sectors = htole32 (floppy->data_last_sector + 1);
+
+  floppy->bootsect.sectors_per_fat +    htole32 (floppy->fat_clusters *
SECTORS_PER_CLUSTER);
+  floppy->bootsect.mirroring = htole16 (0);
+  floppy->bootsect.fat_version = htole16 (0);
+  floppy->bootsect.root_directory_cluster = htole32 (2);
+  floppy->bootsect.fsinfo_sector = htole16 (1);
+  floppy->bootsect.backup_bootsect = htole16 (6);
+  floppy->bootsect.physical_drive_number = 0;
+  floppy->bootsect.extended_boot_signature = 0x29;
+  /* The volume ID should be generated based on the filesystem
+   * creation date and time, but the old qemu VVFAT driver just used a
+   * fixed number here.
+   */
+  floppy->bootsect.volume_id = htole32 (0x01020304);
+  pad_string (label, 11, floppy->bootsect.volume_label);
+  memcpy (floppy->bootsect.fstype, "FAT32   ", 8);
+
+  floppy->bootsect.boot_signature[0] = 0x55;
+  floppy->bootsect.boot_signature[1] = 0xAA;
+
+  return 0;
+}
+
+static int
+create_fsinfo (struct virtual_floppy *floppy)
+{
+  floppy->fsinfo.signature[0] = 0x52; /* "RRaA" */
+  floppy->fsinfo.signature[1] = 0x52;
+  floppy->fsinfo.signature[2] = 0x61;
+  floppy->fsinfo.signature[3] = 0x41;
+  floppy->fsinfo.signature2[0] = 0x72; /* "rrAa" */
+  floppy->fsinfo.signature2[1] = 0x72;
+  floppy->fsinfo.signature2[2] = 0x41;
+  floppy->fsinfo.signature2[3] = 0x61;
+  floppy->fsinfo.free_data_clusters = htole32 (0);
+  floppy->fsinfo.last_free_cluster = htole32 (2 + floppy->data_clusters);
+  floppy->fsinfo.signature3[0] = 0x00;
+  floppy->fsinfo.signature3[1] = 0x00;
+  floppy->fsinfo.signature3[2] = 0x55;
+  floppy->fsinfo.signature3[3] = 0xAA;
+  return 0;
+}
+
+/* Allocate and populate the File Allocation Table. */
+static int
+create_fat (struct virtual_floppy *floppy)
+{
+  size_t i;
+
+  /* Note there is only one copy held in memory.  The two FAT
+   * regions in the virtual disk point to the same copy.
+   */
+  floppy->fat = calloc (floppy->fat_entries, 4);
+  if (floppy->fat == NULL) {
+    nbdkit_error ("calloc: %m");
+    return -1;
+  }
+
+  /* Populate the FAT.  First two entries are reserved and
+   * contain standard data.
+   */
+  floppy->fat[0] = htole32 (0x0ffffff8);
+  floppy->fat[1] = htole32 (0x0fffffff);
+
+  for (i = 0; i < floppy->nr_dirs; ++i) {
+    write_fat_file (floppy->dirs[i].first_cluster,
+                    floppy->dirs[i].nr_clusters, floppy);
+  }
+  for (i = 0; i < floppy->nr_files; ++i) {
+    write_fat_file (floppy->files[i].first_cluster,
+                    floppy->files[i].nr_clusters, floppy);
+  }
+
+  return 0;
+}
+
+static void
+write_fat_file (uint32_t first_cluster, uint32_t nr_clusters,
+                struct virtual_floppy *floppy)
+{
+  uint32_t cl;
+
+  /* It's possible for directories or files to have zero size.  These
+   * don't occupy any space in the disk or FAT so we just skip them
+   * here.
+   */
+  if (nr_clusters == 0)
+    return;
+
+  for (cl = 0; cl < nr_clusters - 1; ++cl) {
+    assert (first_cluster + cl < floppy->fat_entries);
+    /* Directories and files are stored contiguously so the entry in
+     * the FAT always points to the next cluster (except for the
+     * last one, handled below).
+     */
+    floppy->fat[first_cluster + cl] = htole32 (first_cluster + cl + 1);
+  }
+
+  /* Last cluster / end of file marker. */
+  floppy->fat[first_cluster + cl] = htole32 (0x0fffffff);
+}
+
+/* Lay out the final virtual disk. */
+static int
+create_regions (struct virtual_floppy *floppy)
+{
+  struct region region;
+  size_t i;
+
+  /* MBR. */
+  region.start = 0;
+  region.end = SECTOR_SIZE-1;
+  region.len = region.end - region.start + 1;
+  region.type = region_data;
+  region.u.data = (void *) &floppy->mbr;
+  region.description = "MBR";
+  if (append_region (&floppy->regions, region) == -1)
+    return -1;
+
+  /* Free space before first partition. */
+  region.start = SECTOR_SIZE;
+  region.end = 2048*SECTOR_SIZE-1;
+  region.len = region.end - region.start + 1;
+  region.type = region_zero;
+  region.description = "first partition alignment";
+  if (append_region (&floppy->regions, region) == -1)
+    return -1;
+
+  /* Partition boot sector. */
+  region.start = 2048*SECTOR_SIZE;
+  region.end = 2049*SECTOR_SIZE-1;
+  region.len = region.end - region.start + 1;
+  region.type = region_data;
+  region.u.data = (void *) &floppy->bootsect;
+  region.description = "partition boot sector";
+  if (append_region (&floppy->regions, region) == -1)
+    return -1;
+
+  /* Filesystem information sector. */
+  region.start = 2049*SECTOR_SIZE;
+  region.end = 2050*SECTOR_SIZE-1;
+  region.len = region.end - region.start + 1;
+  region.type = region_data;
+  region.u.data = (void *) &floppy->fsinfo;
+  region.description = "filesystem information sector";
+  if (append_region (&floppy->regions, region) == -1)
+    return -1;
+
+  /* Free space (reserved sectors 2-5). */
+  region.start = 2050*SECTOR_SIZE;
+  region.end = 2054*SECTOR_SIZE-1;
+  region.len = region.end - region.start + 1;
+  region.type = region_zero;
+  region.description = "reserved sectors 2-5";
+  if (append_region (&floppy->regions, region) == -1)
+    return -1;
+
+  /* Backup boot sector. */
+  region.start = 2054*SECTOR_SIZE;
+  region.end = 2055*SECTOR_SIZE-1;
+  region.len = region.end - region.start + 1;
+  region.type = region_data;
+  region.u.data = (void *) &floppy->bootsect;
+  region.description = "backup boot sector";
+  if (append_region (&floppy->regions, region) == -1)
+    return -1;
+
+  /* Free space (reserved sectors 7-31). */
+  region.start = 2055*SECTOR_SIZE;
+  region.end = 2080*SECTOR_SIZE-1;
+  region.len = region.end - region.start + 1;
+  region.type = region_zero;
+  region.description = "reserved sectors 7-31";
+  if (append_region (&floppy->regions, region) == -1)
+    return -1;
+
+  /* First copy of FAT. */
+  region.start = 2080*SECTOR_SIZE;
+  region.len = floppy->fat_entries*4;
+  region.end = region.start + region.len - 1;
+  region.type = region_data;
+  region.u.data = (void *) floppy->fat;
+  region.description = "FAT #1";
+  if (append_region (&floppy->regions, region) == -1)
+    return -1;
+
+  /* Free space after FAT (optional). */
+  region.start = region.end + 1;
+  region.end = floppy->fat2_start_sector*SECTOR_SIZE - 1;
+  region.len = region.end - region.start + 1;
+  region.type = region_zero;
+  region.description = "FAT #2 alignment";
+  if (region.len > 0 && append_region (&floppy->regions,
region) == -1)
+    return -1;
+
+  /* Second copy of FAT. */
+  region.start = floppy->fat2_start_sector*SECTOR_SIZE;
+  region.len = floppy->fat_entries*4;
+  region.end = region.start + region.len - 1;
+  region.type = region_data;
+  region.u.data = (void *) floppy->fat;
+  region.description = "FAT #2";
+  if (append_region (&floppy->regions, region) == -1)
+    return -1;
+
+  /* Free space after FAT (optional). */
+  region.start = region.end + 1;
+  region.end = floppy->data_start_sector*SECTOR_SIZE - 1;
+  region.len = region.end - region.start + 1;
+  region.type = region_zero;
+  region.description = "data region alignment";
+  if (region.len > 0 && append_region (&floppy->regions,
region) == -1)
+    return -1;
+
+  /* Now we're into the data region.  We add all directory tables
+   * first.
+   */
+  for (i = 0; i < floppy->nr_dirs; ++i) {
+    /* It's possible for a directory to be completely empty, in which
+     * case it doesn't occupy a region or cluster.
+     */
+    if (floppy->dirs[i].table_entries == 0)
+      continue;
+
+    region.start = region.end + 1;
+    region.len = floppy->dirs[i].table_entries * sizeof (struct dir_entry);
+    region.end = region.start + region.len - 1;
+    region.type = region_data;
+    region.u.data = (void *) floppy->dirs[i].table;
+    region.description = i == 0 ? "root directory" :
"directory";
+    if (append_region (&floppy->regions, region) == -1)
+      return -1;
+
+    /* Optional free space to align to end of cluster. */
+    region.start = region.end + 1;
+    region.end = ROUND_UP (region.start, CLUSTER_SIZE) - 1;
+    region.len = region.end - region.start + 1;
+    region.type = region_zero;
+    region.description +      i == 0 ? "root directory padding" :
"directory padding";
+    if (region.len > 0 && append_region (&floppy->regions,
region) == -1)
+      return -1;
+  }
+
+  /* Add all files. */
+  for (i = 0; i < floppy->nr_files; ++i) {
+    /* It's possible for a file to have zero size, in which case it
+     * doesn't occupy a region or cluster.
+     */
+    if (floppy->files[i].statbuf.st_size == 0)
+      continue;
+
+    region.start = region.end + 1;
+    region.len = floppy->files[i].statbuf.st_size;
+    region.end = region.start + region.len - 1;
+    region.type = region_file;
+    region.u.i = i;
+    region.description = "file";
+    if (append_region (&floppy->regions, region) == -1)
+      return -1;
+
+    /* Optional free space to align to end of cluster. */
+    region.start = region.end + 1;
+    region.end = ROUND_UP (region.start, CLUSTER_SIZE) - 1;
+    region.len = region.end - region.start + 1;
+    region.type = region_zero;
+    region.description = "file padding";
+    if (region.len > 0 && append_region (&floppy->regions,
region) == -1)
+      return -1;
+  }
+
+  nbdkit_debug ("floppy: %zu regions, "
+                "total disk size %" PRIi64,
+                nr_regions (&floppy->regions),
+                virtual_size (&floppy->regions));
+
+  return 0;
+}
diff --git a/plugins/floppy/virtual-floppy.h b/plugins/floppy/virtual-floppy.h
new file mode 100644
index 0000000..66b5e22
--- /dev/null
+++ b/plugins/floppy/virtual-floppy.h
@@ -0,0 +1,233 @@
+/* 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_VIRTUAL_FLOPPY_H
+#define NBDKIT_VIRTUAL_FLOPPY_H
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "regions.h"
+
+struct partition_entry {
+  uint8_t bootable;             /* 0x00 or 0x80 if bootable */
+  uint8_t chs[3];               /* always set to chs_too_large */
+  uint8_t part_type;            /* partition type byte - 0x0C = FAT32 with
LBA*/
+  uint8_t chs2[3];              /* always set to chs_too_large */
+  uint32_t start_sector;        /* 2048 */
+  uint32_t num_sectors;
+} __attribute__((packed));
+
+struct bootsector {
+  uint8_t jmp_insn[3];
+  uint8_t oem_name[8];                 /* 0x0003 */
+
+  /* BIOS Parameter Block, only required for first sector of FAT. */
+  uint16_t bytes_per_sector;           /* 0x000B */
+  uint8_t sectors_per_cluster;         /* 0x000D */
+  uint16_t reserved_sectors;           /* 0x000E */
+  uint8_t nr_fats;                     /* 0x0010 */
+  uint16_t nr_root_dir_entries;        /* 0x0011 - always 0 for FAT32 */
+  uint16_t old_nr_sectors;             /* 0x0013 - always 0 */
+  uint8_t media_descriptor;            /* 0x0015 - always 0xF8 */
+  uint16_t old_sectors_per_fat;        /* 0x0016 */
+  uint16_t sectors_per_track;          /* 0x0018 - always 0 for LBA */
+  uint16_t nr_heads;                   /* 0x001A - always 0 for LBA */
+  uint32_t nr_hidden_sectors;          /* 0x001C */
+  uint32_t nr_sectors;                 /* 0x0020 */
+
+  /* FAT32 Extended BIOS Parameter Block. */
+  uint32_t sectors_per_fat;            /* 0x0024 */
+  uint16_t mirroring;                  /* 0x0028 */
+  uint16_t fat_version;                /* 0x002A */
+  uint32_t root_directory_cluster;     /* 0x002C */
+  uint16_t fsinfo_sector;              /* 0x0030 */
+  uint16_t backup_bootsect;            /* 0x0032 */
+  uint8_t reserved[12];                /* 0x0034 */
+  uint8_t physical_drive_number;       /* 0x0040 */
+  uint8_t unused;                      /* 0x0041 */
+  uint8_t extended_boot_signature;     /* 0x0042 */
+  uint32_t volume_id;                  /* 0x0043 */
+  uint8_t volume_label[11];            /* 0x0047 */
+  uint8_t fstype[8];                   /* 0x0052 - "FAT32   " */
+
+  uint8_t unused2[350];
+
+  /* Partition table.  Not present in first sector of filesystem. */
+  uint32_t disk_signature;             /* 0x01B8 */
+  uint16_t zero;                       /* 0x01BC - 0x00 0x00 */
+  struct partition_entry partition[4]; /* 0x01BE - partition table */
+
+  uint8_t boot_signature[2];           /* 0x01FE - 0x55 0xAA */
+} __attribute__((packed));
+
+struct fsinfo {
+  uint8_t signature[4];         /* 0x52 0x52 0x61 0x41 "RRaA" */
+  uint8_t reserved[480];
+  uint8_t signature2[4];        /* 0x72 0x72 0x41 0x61 "rrAa" */
+  uint32_t free_data_clusters;
+  uint32_t last_free_cluster;
+  uint8_t reserved2[12];
+  uint8_t signature3[4];        /* 0x00 0x00 0x55 0xAA */
+} __attribute__((packed));
+
+struct file {
+  char *name;                   /* Filename. */
+  char *host_path;              /* Path of file on the host. */
+  struct stat statbuf;          /* stat(2) information, including size. */
+  uint32_t first_cluster;       /* First cluster containing this file. */
+  uint32_t nr_clusters;         /* Number of clusters. */
+};
+
+/* On disk directory entry (non-LFN). */
+struct dir_entry {
+  uint8_t name[8 + 3];
+  uint8_t attributes;           /* 0x0B */
+#define DIR_ENTRY_READONLY     0x01
+#define DIR_ENTRY_HIDDEN       0x02
+#define DIR_ENTRY_SYSTEM       0x04
+#define DIR_ENTRY_VOLUME_LABEL 0x08
+#define DIR_ENTRY_SUBDIRECTORY 0x10
+#define DIR_ENTRY_ARCHIVE      0x20
+  uint8_t unused;               /* 0x0C */
+  uint8_t ctime_10ms;           /* 0x0D - ctime seconds in 10ms units */
+  uint16_t ctime;               /* 0x0E */
+  uint16_t cdate;               /* 0x10 */
+  uint16_t adate;               /* 0x12 */
+  uint16_t cluster_hi;          /* 0x14 - first cluster (high word) */
+  uint16_t mtime;               /* 0x16 */
+  uint16_t mdate;               /* 0x18 */
+  uint16_t cluster_lo;          /* 0x1A - first cluster (low word) */
+  uint32_t size;                /* 0x1C - file size */
+} __attribute__((packed));
+
+/* On disk directory entry (LFN). */
+struct lfn_entry {
+  uint8_t seq;                  /* sequence number */
+  uint16_t name1[5];            /* first five UTF-16LE characters */
+  uint8_t attributes;           /* 0x0B - always 0x0F */
+  uint8_t type;                 /* 0x0C - always 0x00 */
+  uint8_t checksum;             /* 0x0D - DOS file name checksum */
+  uint16_t name2[6];            /* next six UTF-16LE characters */
+  uint16_t cluster_lo;          /* 0x1A - always 0x0000 */
+  uint16_t name3[2];            /* last two UTF-16LE characters */
+} __attribute__((packed));
+
+struct dir {
+  size_t pdi;                   /* Link to parent directory (for root, 0). */
+  char *name;                   /* Directory name (for root, NULL). */
+  struct stat statbuf;          /* stat(2) information (for root, zeroes). */
+  uint32_t first_cluster;       /* First cluster containing this dir. */
+  uint32_t nr_clusters;         /* Number of clusters. */
+
+  /* List of subdirectories.  This is actually a list of indexes
+   * into the floppy->dirs array.
+   */
+  size_t *subdirs;
+  size_t nr_subdirs;
+
+  /* List of files in this directory.  This is actually a list of
+   * indexes into the floppy->files array.
+   */
+  size_t *files;
+  size_t nr_files;
+
+  /* On disk directory table. */
+  struct dir_entry *table;
+  size_t table_entries;
+};
+
+struct virtual_floppy {
+  /* Virtual disk layout. */
+  struct regions regions;
+
+  /* Disk MBR. */
+  struct bootsector mbr;
+
+  /* Partition boot/first sector (also used for backup copy). */
+  struct bootsector bootsect;
+
+  /* Filesystem information sector. */
+  struct fsinfo fsinfo;
+
+  /* File Allocation Table (also used for second copy). */
+  uint32_t *fat;
+
+  /* All regular files found. */
+  struct file *files;
+  size_t nr_files;
+
+  /* Directories.  dirs[0] == root directory. */
+  struct dir *dirs;
+  size_t nr_dirs;
+
+  uint64_t fat_entries;         /* Size of FAT (number of 32 bit entries). */
+  uint64_t fat_clusters;        /* Size of FAT (clusters on disk). */
+  uint64_t data_size;           /* Size of data region (bytes). */
+  uint64_t data_clusters;       /* Size of data region (clusters). */
+
+  /* The disk layout:
+   * sector 0:          MBR
+   * sector 2048:       partition first sector
+   * sector 2049:       filesystem information sector
+   * sector 2050-2053:  unused (reserved sectors 2-5)
+   * sector 2054:       backup first sector
+   * sector 2055-2079:  unused (reserved sectors 7-31)
+   * sector 2080:       FAT
+   * fat2_start_sector  FAT (second copy)
+   * data_start_sector  data region
+   * data_last_sector   last sector of data region
+   */
+  uint32_t fat2_start_sector;
+  uint32_t data_start_sector;
+  uint32_t data_last_sector;
+};
+
+#define SECTOR_SIZE 512
+
+/* Don't change SECTORS_PER_CLUSTER without also considering the disk
+ * layout.  It shouldn't be necessary to change this since this
+ * supports the maximum possible disk size, and only wastes virtual
+ * space.
+ */
+#define SECTORS_PER_CLUSTER 32
+#define CLUSTER_SIZE (SECTOR_SIZE * SECTORS_PER_CLUSTER)
+
+extern void init_virtual_floppy (struct virtual_floppy *floppy);
+extern int create_virtual_floppy (const char *dir, const char *label, struct
virtual_floppy *floppy);
+extern void free_virtual_floppy (struct virtual_floppy *floppy);
+extern int create_directory (size_t di, const char *label, struct
virtual_floppy *floppy);
+extern int update_directory_first_cluster (size_t di, struct virtual_floppy
*floppy);
+extern void pad_string (const char *label, size_t n, uint8_t *out);
+
+#endif /* NBDKIT_VIRTUAL_FLOPPY_H */
diff --git a/plugins/iso/nbdkit-iso-plugin.pod
b/plugins/iso/nbdkit-iso-plugin.pod
index e910c29..98f55e3 100644
--- a/plugins/iso/nbdkit-iso-plugin.pod
+++ b/plugins/iso/nbdkit-iso-plugin.pod
@@ -94,6 +94,7 @@ quite large.
 L<nbdkit(1)>,
 L<nbdkit-plugin(3)>,
 L<nbdkit-file-plugin(1)>,
+L<nbdkit-floppy-plugin(1)>,
 L<genisoimage(1)>,
 L<mkisofs(1)>.

diff --git a/tests/Makefile.am b/tests/Makefile.am
index d0d08c6..05a2b6f 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -55,6 +55,7 @@ EXTRA_DIST = \
 	test-dump-config.sh \
 	test-dump-plugin.sh \
 	test-dump-plugin-example4.sh \
+	test-floppy.sh \
 	test-foreground.sh \
 	test-fua.sh \
 	test-help.sh \
@@ -336,6 +337,11 @@ test_file_block_SOURCES = test-file-block.c test.h
 test_file_block_CFLAGS = $(WARNINGS_CFLAGS) $(LIBGUESTFS_CFLAGS)
 test_file_block_LDADD = libtest.la $(LIBGUESTFS_LIBS)

+# floppy plugin test.
+if HAVE_GUESTFISH
+TESTS += test-floppy.sh
+endif HAVE_GUESTFISH
+
 # gzip plugin test.
 if HAVE_ZLIB
 if HAVE_GUESTFISH
diff --git a/tests/test-floppy.sh b/tests/test-floppy.sh
new file mode 100755
index 0000000..7b2eb40
--- /dev/null
+++ b/tests/test-floppy.sh
@@ -0,0 +1,67 @@
+#!/usr/bin/env 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 floppy / FAT32 plugin.
+
+source ./functions.sh
+set -e
+
+files="floppy.pid floppy.sock floppy.test1 floppy.test2"
+rm -f $files
+cleanup_fn rm -f $files
+
+# When testing this we need to use a directory which won't change
+# during the test (so not the current directory).
+start_nbdkit -P floppy.pid -U floppy.sock floppy $srcdir/../plugins
+
+# Check the floppy content.
+guestfish --ro --format=raw -a "nbd://?socket=$PWD/floppy.sock" -m
/dev/sda1 <<'EOF'
+  ll /
+  ll /floppy/
+  ll /iso/
+
+# This reads out all the directory entries which is a useful test.
+  tar-out / - | cat >/dev/null
+
+# Check some files exist.
+  is-file /floppy/Makefile.am
+  is-file /floppy/nbdkit-floppy-plugin.pod
+
+# Download some files and compare to local copies.
+  download /floppy/Makefile.am floppy.test1
+  download /iso/Makefile.am floppy.test2
+EOF
+
+# Compare downloaded files to local versions.
+cmp floppy.test1 $srcdir/../plugins/floppy/Makefile.am
+cmp floppy.test2 $srcdir/../plugins/iso/Makefile.am
--
2.19.0.rc0