Add example/cbor-to-json
diff --git a/doc/changelog.md b/doc/changelog.md
index 6cb1da8..27150c5 100644
--- a/doc/changelog.md
+++ b/doc/changelog.md
@@ -10,6 +10,7 @@
 - Added `base` library support for UTF-8.
 - Added `base` library support for `atoi`-like string conversion.
 - Added `endwhile` syntax.
+- Added `example/cbor-to-json`.
 - Added `example/convert-to-nia`.
 - Added `example/imageviewer`.
 - Added `example/json-to-cbor`.
diff --git a/example/cbor-to-json/cbor-to-json.cc b/example/cbor-to-json/cbor-to-json.cc
new file mode 100644
index 0000000..0cfb1ac
--- /dev/null
+++ b/example/cbor-to-json/cbor-to-json.cc
@@ -0,0 +1,663 @@
+// Copyright 2020 The Wuffs Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// ----------------
+
+/*
+cbor-to-json reads CBOR (a binary format) from stdin and writes the equivalent
+formatted JSON (a text format) to stdout.
+
+See the "const char* g_usage" string below for details.
+
+----
+
+To run:
+
+$CXX cbor-to-json.cc && ./a.out < ../../test/data/json-things.cbor; rm -f a.out
+
+for a C++ compiler $CXX, such as clang++ or g++.
+*/
+
+#if defined(__cplusplus) && (__cplusplus < 201103L)
+#error "This C++ program requires -std=c++11 or later"
+#endif
+
+#include <stdio.h>
+
+#include <string>
+#include <vector>
+
+// Wuffs ships as a "single file C library" or "header file library" as per
+// https://github.com/nothings/stb/blob/master/docs/stb_howto.txt
+//
+// To use that single file as a "foo.c"-like implementation, instead of a
+// "foo.h"-like header, #define WUFFS_IMPLEMENTATION before #include'ing or
+// compiling it.
+#define WUFFS_IMPLEMENTATION
+
+// Defining the WUFFS_CONFIG__MODULE* macros are optional, but it lets users of
+// release/c/etc.c whitelist which parts of Wuffs to build. That file contains
+// the entire Wuffs standard library, implementing a variety of codecs and file
+// formats. Without this macro definition, an optimizing compiler or linker may
+// very well discard Wuffs code for unused codecs, but listing the Wuffs
+// modules we use makes that process explicit. Preprocessing means that such
+// code simply isn't compiled.
+#define WUFFS_CONFIG__MODULES
+#define WUFFS_CONFIG__MODULE__AUX__BASE
+#define WUFFS_CONFIG__MODULE__AUX__CBOR
+#define WUFFS_CONFIG__MODULE__BASE
+#define WUFFS_CONFIG__MODULE__CBOR
+
+// If building this program in an environment that doesn't easily accommodate
+// relative includes, you can use the script/inline-c-relative-includes.go
+// program to generate a stand-alone C++ file.
+#include "../../release/c/wuffs-unsupported-snapshot.c"
+
+#define TRY(error_msg)         \
+  do {                         \
+    std::string z = error_msg; \
+    if (!z.empty()) {          \
+      return z;                \
+    }                          \
+  } while (false)
+
+static const char* g_usage =
+    "Usage: cbor-to-json -flags input.cbor\n"
+    "\n"
+    "Flags:\n"
+    "    -c      -compact-output\n"
+    "    -s=NUM  -spaces=NUM\n"
+    "    -t      -tabs\n"
+    "            -output-cbor-metadata-as-comments\n"
+    "            -output-extra-comma\n"
+    "            -output-inf-nan-numbers\n"
+    "\n"
+    "The input.cbor filename is optional. If absent, it reads from stdin.\n"
+    "\n"
+    "----\n"
+    "\n"
+    "cbor-to-json reads CBOR (a binary format) from stdin and writes the\n"
+    "equivalent formatted JSON (a text format) to stdout.\n"
+    "\n"
+    "The output JSON's arrays' and objects' elements are indented, each on\n"
+    "its own line. Configure this with the -c / -compact-output, -s=NUM /\n"
+    "-spaces=NUM (for NUM ranging from 0 to 8) and -t / -tabs flags.\n"
+    "\n"
+    "The conversion may be lossy. For example, CBOR metadata such as tags or\n"
+    "distinguishing undefined from null are either dropped or, with\n"
+    "-output-cbor-metadata-as-comments, converted to \"/*comments*/\". Such\n"
+    "comments are non-compliant with the JSON specification but many parsers\n"
+    "accept them.\n"
+    "\n"
+    "The -output-extra-comma flag writes output like \"[1,2,]\", with a comma\n"
+    "after the final element of a JSON list or dictionary. Such commas are\n"
+    "non-compliant with the JSON specification but many parsers accept them\n"
+    "and they can produce simpler line-based diffs. This flag is ignored when\n"
+    "-compact-output is set.\n"
+    "\n"
+    "The -output-inf-nan-numbers flag writes Inf and NaN instead of a\n"
+    "substitute null value. Such values are non-compliant with the JSON\n"
+    "specification but many parsers accept them.\n"
+    "\n"
+    "CBOR is more permissive about map keys but JSON only allows strings.\n"
+    "When converting from -i=cbor to -o=json, this program rejects keys other\n"
+    "than integers and strings (CBOR major types 0, 1, 2 and 3). Integer\n"
+    "keys like 123 quoted to be string keys like \"123\".\n"
+    "\n"
+    "The CBOR specification permits implementations to set their own maximum\n"
+    "input depth. This CBOR implementation sets it to 1024.";
+
+// ----
+
+// parse_flags enforces that g_flags.spaces <= 8 (the length of
+// INDENT_SPACES_STRING).
+#define INDENT_SPACES_STRING "        "
+#define INDENT_TAB_STRING "\t"
+
+uint8_t g_dst_array[32768];
+wuffs_base__io_buffer g_dst;
+
+uint32_t g_depth;
+
+enum class context {
+  none,
+  in_list_after_bracket,
+  in_list_after_value,
+  in_dict_after_brace,
+  in_dict_after_key,
+  in_dict_after_value,
+} g_ctx;
+
+bool g_wrote_to_dst;
+
+std::vector<uint64_t> g_cbor_tags;
+
+std::vector<uint32_t> g_quirks;
+
+struct {
+  int remaining_argc;
+  char** remaining_argv;
+
+  bool compact_output;
+  bool output_cbor_metadata_as_comments;
+  bool output_extra_comma;
+  bool output_inf_nan_numbers;
+  size_t spaces;
+  bool tabs;
+} g_flags = {0};
+
+std::string  //
+parse_flags(int argc, char** argv) {
+  g_flags.spaces = 4;
+
+  int c = (argc > 0) ? 1 : 0;  // Skip argv[0], the program name.
+  for (; c < argc; c++) {
+    char* arg = argv[c];
+    if (*arg++ != '-') {
+      break;
+    }
+
+    // A double-dash "--foo" is equivalent to a single-dash "-foo". As special
+    // cases, a bare "-" is not a flag (some programs may interpret it as
+    // stdin) and a bare "--" means to stop parsing flags.
+    if (*arg == '\x00') {
+      break;
+    } else if (*arg == '-') {
+      arg++;
+      if (*arg == '\x00') {
+        c++;
+        break;
+      }
+    }
+
+    if (!strcmp(arg, "c") || !strcmp(arg, "compact-output")) {
+      g_flags.compact_output = true;
+      continue;
+    }
+    if (!strcmp(arg, "output-cbor-metadata-as-comments")) {
+      g_flags.output_cbor_metadata_as_comments = true;
+      continue;
+    }
+    if (!strcmp(arg, "output-extra-comma")) {
+      g_flags.output_extra_comma = true;
+      continue;
+    }
+    if (!strcmp(arg, "output-inf-nan-numbers")) {
+      g_flags.output_inf_nan_numbers = true;
+      continue;
+    }
+    if (!strncmp(arg, "s=", 2) || !strncmp(arg, "spaces=", 7)) {
+      while (*arg++ != '=') {
+      }
+      if (('0' <= arg[0]) && (arg[0] <= '8') && (arg[1] == '\x00')) {
+        g_flags.spaces = arg[0] - '0';
+        continue;
+      }
+      return g_usage;
+    }
+    if (!strcmp(arg, "t") || !strcmp(arg, "tabs")) {
+      g_flags.tabs = true;
+      continue;
+    }
+
+    return g_usage;
+  }
+
+  g_flags.remaining_argc = argc - c;
+  g_flags.remaining_argv = argv + c;
+  return "";
+}
+
+// ----
+
+std::string  //
+flush_dst() {
+  while (true) {
+    size_t n = g_dst.reader_length();
+    if (n == 0) {
+      break;
+    }
+    ssize_t i = fwrite(g_dst.reader_pointer(), 1, n, stdout);
+    if (i >= 0) {
+      g_dst.meta.ri += i;
+    }
+    if (i < n) {
+      return "main: error writing to stdout";
+    }
+  }
+  g_dst.compact();
+  return "";
+}
+
+std::string  //
+write_dst(const void* s, size_t n) {
+  const uint8_t* p = static_cast<const uint8_t*>(s);
+  while (n > 0) {
+    size_t i = g_dst.writer_length();
+    if (i == 0) {
+      TRY(flush_dst());
+      i = g_dst.writer_length();
+      if (i == 0) {
+        return "main: g_dst buffer is full";
+      }
+    }
+
+    if (i > n) {
+      i = n;
+    }
+    memcpy(g_dst.data.ptr + g_dst.meta.wi, p, i);
+    g_dst.meta.wi += i;
+    p += i;
+    n -= i;
+    g_wrote_to_dst = true;
+  }
+  return "";
+}
+
+// ----
+
+class Callbacks : public wuffs_aux::DecodeCborCallbacks {
+ public:
+  Callbacks() = default;
+
+  std::string WritePreambleAndUpdateContext() {
+    // Write preceding punctuation, whitespace and indentation. Update g_ctx.
+    do {
+      switch (g_ctx) {
+        case context::none:
+          // No-op.
+          break;
+        case context::in_list_after_bracket:
+          TRY(write_dst("\n", g_flags.compact_output ? 0 : 1));
+          g_ctx = context::in_list_after_value;
+          break;
+        case context::in_list_after_value:
+          TRY(write_dst(",\n", g_flags.compact_output ? 1 : 2));
+          break;
+        case context::in_dict_after_brace:
+          TRY(write_dst("\n", g_flags.compact_output ? 0 : 1));
+          g_ctx = context::in_dict_after_key;
+          break;
+        case context::in_dict_after_key:
+          TRY(write_dst(": ", g_flags.compact_output ? 1 : 2));
+          g_ctx = context::in_dict_after_value;
+          goto skip_indentation;
+        case context::in_dict_after_value:
+          TRY(write_dst(",\n", g_flags.compact_output ? 1 : 2));
+          g_ctx = context::in_dict_after_key;
+          break;
+      }
+
+      if (!g_flags.compact_output) {
+        for (size_t i = 0; i < g_depth; i++) {
+          TRY(write_dst(g_flags.tabs ? INDENT_TAB_STRING : INDENT_SPACES_STRING,
+                        g_flags.tabs ? 1 : g_flags.spaces));
+        }
+      }
+    } while (false);
+  skip_indentation:
+
+    // Write any CBOR tags.
+    if (g_flags.output_cbor_metadata_as_comments) {
+      for (const auto& cbor_tag : g_cbor_tags) {
+        uint8_t buf[WUFFS_BASE__U64__BYTE_LENGTH__MAX_INCL];
+        size_t n = wuffs_base__render_number_u64(
+            wuffs_base__make_slice_u8(&buf[0],
+                                      WUFFS_BASE__U64__BYTE_LENGTH__MAX_INCL),
+            cbor_tag, WUFFS_BASE__RENDER_NUMBER_XXX__DEFAULT_OPTIONS);
+        TRY(write_dst("/*cbor:tag", 10));
+        TRY(write_dst(&buf[0], n));
+        TRY(write_dst("*/", 2));
+      }
+      g_cbor_tags.clear();
+    }
+
+    return "";
+  }
+
+  virtual std::string AppendNull() {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_ctx == context::in_dict_after_key) {
+      return "main: invalid JSON map key";
+    }
+    return write_dst("null", 4);
+  }
+
+  virtual std::string AppendUndefined() {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_ctx == context::in_dict_after_key) {
+      return "main: invalid JSON map key";
+    }
+    // JSON's closest approximation to "undefined" is "null".
+    if (g_flags.output_cbor_metadata_as_comments) {
+      return write_dst("/*cbor:undefined*/null", 22);
+    }
+    return write_dst("null", 4);
+  }
+
+  virtual std::string AppendBool(bool val) {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_ctx == context::in_dict_after_key) {
+      return "main: invalid JSON map key";
+    }
+    if (val) {
+      return write_dst("true", 4);
+    }
+    return write_dst("false", 5);
+  }
+
+  virtual std::string AppendF64(double val) {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_ctx == context::in_dict_after_key) {
+      return "main: invalid JSON map key";
+    }
+
+    uint8_t buf[64];
+    constexpr uint32_t precision = 0;
+    size_t n = wuffs_base__render_number_f64(
+        wuffs_base__make_slice_u8(&buf[0], sizeof buf), val, precision,
+        WUFFS_BASE__RENDER_NUMBER_FXX__JUST_ENOUGH_PRECISION);
+    if (!g_flags.output_inf_nan_numbers) {
+      // JSON numbers don't include Infinities or NaNs. For such numbers, their
+      // IEEE 754 bit representation's 11 exponent bits are all on.
+      uint64_t u =
+          wuffs_base__ieee_754_bit_representation__from_f64_to_u64(val);
+      if (((u >> 52) & 0x7FF) == 0x7FF) {
+        if (g_flags.output_cbor_metadata_as_comments) {
+          TRY(write_dst("/*cbor:", 7));
+          TRY(write_dst(&buf[0], n));
+          TRY(write_dst("*/", 2));
+        }
+        return write_dst("null", 4);
+      }
+    }
+    return write_dst(&buf[0], n);
+  }
+
+  virtual std::string AppendI64(int64_t val) {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_ctx == context::in_dict_after_key) {
+      TRY(write_dst("\"", 1));
+    }
+
+    uint8_t buf[WUFFS_BASE__I64__BYTE_LENGTH__MAX_INCL];
+    size_t n = wuffs_base__render_number_i64(
+        wuffs_base__make_slice_u8(&buf[0], sizeof buf), val,
+        WUFFS_BASE__RENDER_NUMBER_XXX__DEFAULT_OPTIONS);
+    TRY(write_dst(&buf[0], n));
+
+    if (g_ctx == context::in_dict_after_key) {
+      TRY(write_dst("\"", 1));
+    }
+    return "";
+  }
+
+  virtual std::string AppendU64(uint64_t val) {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_ctx == context::in_dict_after_key) {
+      TRY(write_dst("\"", 1));
+    }
+
+    uint8_t buf[WUFFS_BASE__U64__BYTE_LENGTH__MAX_INCL];
+    size_t n = wuffs_base__render_number_u64(
+        wuffs_base__make_slice_u8(&buf[0], sizeof buf), val,
+        WUFFS_BASE__RENDER_NUMBER_XXX__DEFAULT_OPTIONS);
+    TRY(write_dst(&buf[0], n));
+
+    if (g_ctx == context::in_dict_after_key) {
+      TRY(write_dst("\"", 1));
+    }
+    return "";
+  }
+
+  virtual std::string AppendByteString(std::string&& val) {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_flags.output_cbor_metadata_as_comments) {
+      TRY(write_dst("/*cbor:base64url*/\"", 19));
+    } else {
+      TRY(write_dst("\"", 1));
+    }
+
+    const uint8_t* ptr =
+        static_cast<const uint8_t*>(static_cast<const void*>(val.data()));
+    size_t len = val.length();
+    while (len > 0) {
+      constexpr bool closed = true;
+      wuffs_base__transform__output o = wuffs_base__base_64__encode(
+          g_dst.writer_slice(),
+          wuffs_base__make_slice_u8(const_cast<uint8_t*>(ptr), len), closed,
+          WUFFS_BASE__BASE_64__URL_ALPHABET);
+      g_dst.meta.wi += o.num_dst;
+      ptr += o.num_src;
+      len -= o.num_src;
+      if (o.status.repr == nullptr) {
+        if (len != 0) {
+          return "main: internal error: inconsistent base-64 length";
+        }
+        break;
+      } else if (o.status.repr != wuffs_base__suspension__short_write) {
+        return o.status.message();
+      }
+      TRY(flush_dst());
+    }
+
+    return write_dst("\"", 1);
+  }
+
+  virtual std::string AppendTextString(std::string&& val) {
+    TRY(WritePreambleAndUpdateContext());
+    TRY(write_dst("\"", 1));
+    const uint8_t* ptr =
+        static_cast<const uint8_t*>(static_cast<const void*>(val.data()));
+    size_t len = val.length();
+  loop:
+    if (len > 0) {
+      for (size_t i = 0; i < len; i++) {
+        uint8_t c = ptr[i];
+        if ((c == '"') || (c == '\\') || (c < 0x20)) {
+          TRY(write_dst(ptr, i));
+          TRY(AppendAsciiByte(c));
+          ptr += i + 1;
+          len -= i + 1;
+          goto loop;
+        }
+      }
+      TRY(write_dst(ptr, len));
+    }
+    return write_dst("\"", 1);
+  }
+
+  std::string AppendAsciiByte(uint8_t c) {
+    switch (c) {
+      case '\b':
+        return write_dst("\\b", 2);
+      case '\f':
+        return write_dst("\\f", 2);
+      case '\n':
+        return write_dst("\\n", 2);
+      case '\r':
+        return write_dst("\\r", 2);
+      case '\t':
+        return write_dst("\\t", 2);
+      case '\"':
+        return write_dst("\\\"", 2);
+      case '\\':
+        return write_dst("\\\\", 2);
+    }
+    static const char* hex_digits = "0123456789ABCDEF";
+    uint8_t esc6[6];
+    esc6[0] = '\\';
+    esc6[1] = 'u';
+    esc6[2] = '0';
+    esc6[3] = '0';
+    esc6[4] = hex_digits[c >> 4];
+    esc6[5] = hex_digits[c & 0x1F];
+    return write_dst(&esc6[0], 6);
+  }
+
+  virtual std::string AppendMinus1MinusX(uint64_t val) {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_ctx == context::in_dict_after_key) {
+      TRY(write_dst("\"", 1));
+    }
+
+    val++;
+    if (val == 0) {
+      // See the cbor.TOKEN_VALUE_MINOR__MINUS_1_MINUS_X comment re overflow.
+      TRY(write_dst("-18446744073709551616", 21));
+    } else {
+      uint8_t buf[1 + WUFFS_BASE__U64__BYTE_LENGTH__MAX_INCL];
+      uint8_t* b = &buf[0];
+      *b++ = '-';
+      size_t n = wuffs_base__render_number_u64(
+          wuffs_base__make_slice_u8(b, WUFFS_BASE__U64__BYTE_LENGTH__MAX_INCL),
+          val, WUFFS_BASE__RENDER_NUMBER_XXX__DEFAULT_OPTIONS);
+      TRY(write_dst(&buf[0], 1 + n));
+    }
+
+    if (g_ctx == context::in_dict_after_key) {
+      TRY(write_dst("\"", 1));
+    }
+    return "";
+  }
+
+  virtual std::string AppendCborSimpleValue(uint8_t val) {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_ctx == context::in_dict_after_key) {
+      return "main: invalid JSON map key";
+    }
+
+    if (!g_flags.output_cbor_metadata_as_comments) {
+      return write_dst("null", 4);
+    }
+    uint8_t buf[WUFFS_BASE__U64__BYTE_LENGTH__MAX_INCL];
+    size_t n = wuffs_base__render_number_u64(
+        wuffs_base__make_slice_u8(&buf[0],
+                                  WUFFS_BASE__U64__BYTE_LENGTH__MAX_INCL),
+        val, WUFFS_BASE__RENDER_NUMBER_XXX__DEFAULT_OPTIONS);
+    TRY(write_dst("/*cbor:simple", 13));
+    TRY(write_dst(&buf[0], n));
+    return write_dst("*/null", 6);
+  }
+
+  virtual std::string AppendCborTag(uint64_t val) {
+    // No call to WritePreambleAndUpdateContext. A CBOR tag isn't a value. It
+    // decorates the upcoming value.
+    if (g_flags.output_cbor_metadata_as_comments) {
+      g_cbor_tags.push_back(val);
+    }
+    return "";
+  }
+
+  virtual std::string Push(uint32_t flags) {
+    TRY(WritePreambleAndUpdateContext());
+    if (g_ctx == context::in_dict_after_key) {
+      return "main: invalid JSON map key";
+    }
+
+    g_depth++;
+    g_ctx = (flags & WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_LIST)
+                ? context::in_list_after_bracket
+                : context::in_dict_after_brace;
+    return write_dst(
+        (flags & WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_LIST) ? "[" : "{", 1);
+  }
+
+  virtual std::string Pop(uint32_t flags) {
+    // No call to WritePreambleAndUpdateContext. We write the extra comma,
+    // outdent, etc. ourselves.
+    g_depth--;
+    if (g_flags.compact_output) {
+      // No-op.
+    } else if ((g_ctx != context::in_list_after_bracket) &&
+               (g_ctx != context::in_dict_after_brace)) {
+      if (g_flags.output_extra_comma) {
+        TRY(write_dst(",\n", 2));
+      } else {
+        TRY(write_dst("\n", 1));
+      }
+      for (size_t i = 0; i < g_depth; i++) {
+        TRY(write_dst(g_flags.tabs ? INDENT_TAB_STRING : INDENT_SPACES_STRING,
+                      g_flags.tabs ? 1 : g_flags.spaces));
+      }
+    }
+    g_ctx = (flags & WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_LIST)
+                ? context::in_list_after_value
+                : context::in_dict_after_value;
+    return write_dst(
+        (flags & WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_LIST) ? "]" : "}", 1);
+  }
+};
+
+// ----
+
+std::string  //
+main1(int argc, char** argv) {
+  g_dst = wuffs_base__ptr_u8__writer(
+      &g_dst_array[0], (sizeof(g_dst_array) / sizeof(g_dst_array[0])));
+  g_depth = 0;
+  g_ctx = context::none;
+  g_wrote_to_dst = false;
+
+  TRY(parse_flags(argc, argv));
+
+  FILE* in = stdin;
+  if (g_flags.remaining_argc > 1) {
+    return g_usage;
+  } else if (g_flags.remaining_argc == 1) {
+    in = fopen(g_flags.remaining_argv[0], "r");
+    if (!in) {
+      return std::string("main: cannot read input file");
+    }
+  }
+
+  return wuffs_aux::DecodeCbor(
+             Callbacks(), wuffs_aux::sync_io::FileInput(in),
+             wuffs_base__make_slice_u32(g_quirks.data(), g_quirks.size()))
+      .error_message;
+}
+
+// ----
+
+int  //
+compute_exit_code(std::string status_msg) {
+  if (status_msg.empty()) {
+    return 0;
+  }
+  fputs(status_msg.c_str(), stderr);
+  fputc('\n', stderr);
+  // Return an exit code of 1 for regular (forseen) errors, e.g. badly
+  // formatted or unsupported input.
+  //
+  // Return an exit code of 2 for internal (exceptional) errors, e.g. defensive
+  // run-time checks found that an internal invariant did not hold.
+  //
+  // Automated testing, including badly formatted inputs, can therefore
+  // discriminate between expected failure (exit code 1) and unexpected failure
+  // (other non-zero exit codes). Specifically, exit code 2 for internal
+  // invariant violation, exit code 139 (which is 128 + SIGSEGV on x86_64
+  // linux) for a segmentation fault (e.g. null pointer dereference).
+  return (status_msg.find("internal error:") != std::string::npos) ? 2 : 1;
+}
+
+int  //
+main(int argc, char** argv) {
+  std::string z = main1(argc, argv);
+  if (g_wrote_to_dst) {
+    std::string z1 = write_dst("\n", 1);
+    std::string z2 = flush_dst();
+    z = !z.empty() ? z : (!z1.empty() ? z1 : z2);
+  }
+  return compute_exit_code(z);
+}
diff --git a/internal/cgen/auxiliary/cbor.cc b/internal/cgen/auxiliary/cbor.cc
new file mode 100644
index 0000000..976bf9b
--- /dev/null
+++ b/internal/cgen/auxiliary/cbor.cc
@@ -0,0 +1,343 @@
+// After editing this file, run "go generate" in the ../data directory.
+
+// Copyright 2020 The Wuffs Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// ---------------- Auxiliary - CBOR
+
+#if !defined(WUFFS_CONFIG__MODULES) || defined(WUFFS_CONFIG__MODULE__AUX__CBOR)
+
+#include <utility>
+
+namespace wuffs_aux {
+
+DecodeCborResult::DecodeCborResult(std::string&& error_message0,
+                                   uint64_t cursor_position0)
+    : error_message(std::move(error_message0)),
+      cursor_position(cursor_position0) {}
+
+void DecodeCborCallbacks::Done(DecodeCborResult& result,
+                               sync_io::Input& input,
+                               IOBuffer& buffer) {}
+
+DecodeCborResult  //
+DecodeCbor(DecodeCborCallbacks&& callbacks,
+           sync_io::Input&& input,
+           wuffs_base__slice_u32 quirks) {
+  // Prepare the wuffs_base__io_buffer and the resultant error_message.
+  wuffs_base__io_buffer* io_buf = input.BringsItsOwnIOBuffer();
+  wuffs_base__io_buffer fallback_io_buf = wuffs_base__empty_io_buffer();
+  std::unique_ptr<uint8_t[]> fallback_io_array(nullptr);
+  if (!io_buf) {
+    fallback_io_array = std::unique_ptr<uint8_t[]>(new uint8_t[4096]);
+    fallback_io_buf = wuffs_base__ptr_u8__writer(fallback_io_array.get(), 4096);
+    io_buf = &fallback_io_buf;
+  }
+  size_t cursor_index = 0;
+  std::string ret_error_message;
+  std::string io_error_message;
+
+  do {
+    // Prepare the low-level CBOR decoder.
+    wuffs_cbor__decoder::unique_ptr dec = wuffs_cbor__decoder::alloc();
+    if (!dec) {
+      ret_error_message = "wuffs_aux::CborDecoder: out of memory";
+      goto done;
+    }
+    for (size_t i = 0; i < quirks.len; i++) {
+      dec->set_quirk_enabled(quirks.ptr[i], true);
+    }
+
+    // Prepare the wuffs_base__tok_buffer.
+    wuffs_base__token tok_array[256];
+    wuffs_base__token_buffer tok_buf =
+        wuffs_base__slice_token__writer(wuffs_base__make_slice_token(
+            &tok_array[0], (sizeof(tok_array) / sizeof(tok_array[0]))));
+    wuffs_base__status tok_status = wuffs_base__make_status(nullptr);
+
+    // Prepare other state.
+    uint32_t depth = 0;
+    std::string str;
+    int64_t extension_category = 0;
+    uint64_t extension_detail = 0;
+
+    // Valid token's VBCs range in 0 ..= 15. Values over that are for tokens
+    // from outside of the base package, such as the CBOR package.
+    constexpr int64_t EXT_CAT__CBOR_TAG = 16;
+
+    // Loop, doing these two things:
+    //  1. Get the next token.
+    //  2. Process that token.
+    while (true) {
+      // 1. Get the next token.
+
+      while (tok_buf.meta.ri >= tok_buf.meta.wi) {
+        if (tok_status.repr == nullptr) {
+          // No-op.
+        } else if (tok_status.repr == wuffs_base__suspension__short_write) {
+          tok_buf.compact();
+        } else if (tok_status.repr == wuffs_base__suspension__short_read) {
+          // Read from input to io_buf.
+          if (!io_error_message.empty()) {
+            ret_error_message = std::move(io_error_message);
+            goto done;
+          } else if (cursor_index != io_buf->meta.ri) {
+            ret_error_message =
+                "wuffs_aux::CborDecoder: internal error: bad cursor_index";
+            goto done;
+          } else if (io_buf->meta.closed) {
+            ret_error_message =
+                "wuffs_aux::CborDecoder: internal error: io_buf is closed";
+            goto done;
+          }
+          io_buf->compact();
+          if (io_buf->meta.wi >= io_buf->data.len) {
+            ret_error_message =
+                "wuffs_aux::CborDecoder: internal error: io_buf is full";
+            goto done;
+          }
+          cursor_index = io_buf->meta.ri;
+          io_error_message = input.CopyIn(io_buf);
+        } else {
+          ret_error_message = tok_status.message();
+          goto done;
+        }
+
+        if (WUFFS_CBOR__DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE != 0) {
+          ret_error_message =
+              "wuffs_aux::CborDecoder: internal error: bad WORKBUF_LEN";
+          goto done;
+        }
+        wuffs_base__slice_u8 work_buf = wuffs_base__empty_slice_u8();
+        tok_status = dec->decode_tokens(&tok_buf, io_buf, work_buf);
+      }
+
+      wuffs_base__token token = tok_buf.data.ptr[tok_buf.meta.ri++];
+      uint64_t token_len = token.length();
+      if ((io_buf->meta.ri < cursor_index) ||
+          ((io_buf->meta.ri - cursor_index) < token_len)) {
+        ret_error_message =
+            "wuffs_aux::CborDecoder: internal error: bad token indexes";
+        goto done;
+      }
+      uint8_t* token_ptr = io_buf->data.ptr + cursor_index;
+      cursor_index += token_len;
+
+      // 2. Process that token.
+
+      uint64_t vbd = token.value_base_detail();
+
+      if (extension_category != 0) {
+        int64_t ext = token.value_extension();
+        if ((ext >= 0) && !token.continued()) {
+          extension_detail = (extension_detail
+                              << WUFFS_BASE__TOKEN__VALUE_EXTENSION__NUM_BITS) |
+                             static_cast<uint64_t>(ext);
+          switch (extension_category) {
+            case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_SIGNED:
+              extension_category = 0;
+              ret_error_message =
+                  callbacks.AppendI64(static_cast<int64_t>(extension_detail));
+              goto parsed_a_value;
+            case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_UNSIGNED:
+              extension_category = 0;
+              ret_error_message = callbacks.AppendU64(extension_detail);
+              goto parsed_a_value;
+            case EXT_CAT__CBOR_TAG:
+              extension_category = 0;
+              ret_error_message = callbacks.AppendCborTag(extension_detail);
+              if (!ret_error_message.empty()) {
+                goto done;
+              }
+              continue;
+          }
+        }
+        ret_error_message =
+            "wuffs_aux::CborDecoder: internal error: bad extended token";
+        goto done;
+      }
+
+      switch (token.value_base_category()) {
+        case WUFFS_BASE__TOKEN__VBC__FILLER:
+          continue;
+
+        case WUFFS_BASE__TOKEN__VBC__STRUCTURE: {
+          if (vbd & WUFFS_BASE__TOKEN__VBD__STRUCTURE__PUSH) {
+            ret_error_message = callbacks.Push(static_cast<uint32_t>(vbd));
+            if (!ret_error_message.empty()) {
+              goto done;
+            }
+            depth++;
+            continue;
+          }
+          ret_error_message = callbacks.Pop(static_cast<uint32_t>(vbd));
+          depth--;
+          goto parsed_a_value;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__STRING: {
+          if (vbd & WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_0_DST_1_SRC_DROP) {
+            // No-op.
+          } else if (vbd &
+                     WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_1_DST_1_SRC_COPY) {
+            const char* ptr =  // Convert from (uint8_t*).
+                static_cast<const char*>(static_cast<void*>(token_ptr));
+            str.append(ptr, token_len);
+          } else {
+            goto fail;
+          }
+          if (token.continued()) {
+            continue;
+          }
+          ret_error_message =
+              (vbd & WUFFS_BASE__TOKEN__VBD__STRING__CHAIN_MUST_BE_UTF_8)
+                  ? callbacks.AppendTextString(std::move(str))
+                  : callbacks.AppendByteString(std::move(str));
+          str.clear();
+          goto parsed_a_value;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__UNICODE_CODE_POINT: {
+          uint8_t u[WUFFS_BASE__UTF_8__BYTE_LENGTH__MAX_INCL];
+          size_t n = wuffs_base__utf_8__encode(
+              wuffs_base__make_slice_u8(
+                  &u[0], WUFFS_BASE__UTF_8__BYTE_LENGTH__MAX_INCL),
+              static_cast<uint32_t>(vbd));
+          const char* ptr =  // Convert from (uint8_t*).
+              static_cast<const char*>(static_cast<void*>(&u[0]));
+          str.append(ptr, n);
+          if (token.continued()) {
+            continue;
+          }
+          goto fail;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__LITERAL: {
+          if (vbd & WUFFS_BASE__TOKEN__VBD__LITERAL__NULL) {
+            ret_error_message = callbacks.AppendNull();
+          } else if (vbd & WUFFS_BASE__TOKEN__VBD__LITERAL__UNDEFINED) {
+            ret_error_message = callbacks.AppendUndefined();
+          } else {
+            ret_error_message = callbacks.AppendBool(
+                vbd & WUFFS_BASE__TOKEN__VBD__LITERAL__TRUE);
+          }
+          goto parsed_a_value;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__NUMBER: {
+          const uint64_t cfp_fbbe_fifb =
+              WUFFS_BASE__TOKEN__VBD__NUMBER__CONTENT_FLOATING_POINT |
+              WUFFS_BASE__TOKEN__VBD__NUMBER__FORMAT_BINARY_BIG_ENDIAN |
+              WUFFS_BASE__TOKEN__VBD__NUMBER__FORMAT_IGNORE_FIRST_BYTE;
+          if ((vbd & cfp_fbbe_fifb) == cfp_fbbe_fifb) {
+            double f;
+            switch (token_len) {
+              case 3:
+                f = wuffs_base__ieee_754_bit_representation__from_u16_to_f64(
+                    wuffs_base__load_u16be__no_bounds_check(token_ptr + 1));
+                break;
+              case 5:
+                f = wuffs_base__ieee_754_bit_representation__from_u32_to_f64(
+                    wuffs_base__load_u32be__no_bounds_check(token_ptr + 1));
+                break;
+              case 9:
+                f = wuffs_base__ieee_754_bit_representation__from_u64_to_f64(
+                    wuffs_base__load_u64be__no_bounds_check(token_ptr + 1));
+                break;
+              default:
+                goto fail;
+            }
+            ret_error_message = callbacks.AppendF64(f);
+            goto parsed_a_value;
+          }
+          goto fail;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_SIGNED: {
+          if (token.continued()) {
+            extension_category = WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_SIGNED;
+            extension_detail =
+                static_cast<uint64_t>(token.value_base_detail__sign_extended());
+            continue;
+          }
+          ret_error_message =
+              callbacks.AppendI64(token.value_base_detail__sign_extended());
+          goto parsed_a_value;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_UNSIGNED: {
+          if (token.continued()) {
+            extension_category =
+                WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_UNSIGNED;
+            extension_detail = vbd;
+            continue;
+          }
+          ret_error_message = callbacks.AppendU64(vbd);
+          goto parsed_a_value;
+        }
+      }
+
+      if (token.value_major() == WUFFS_CBOR__TOKEN_VALUE_MAJOR) {
+        uint64_t value_minor = token.value_minor();
+        if (value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__MINUS_1_MINUS_X) {
+          if (token_len == 9) {
+            ret_error_message = callbacks.AppendMinus1MinusX(
+                wuffs_base__load_u64be__no_bounds_check(token_ptr + 1));
+            goto parsed_a_value;
+          }
+        } else if (value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__SIMPLE_VALUE) {
+          ret_error_message =
+              callbacks.AppendCborSimpleValue(static_cast<uint8_t>(
+                  value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_MASK));
+          goto parsed_a_value;
+        } else if (value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__TAG) {
+          if (token.continued()) {
+            extension_category = EXT_CAT__CBOR_TAG;
+            extension_detail =
+                value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_MASK;
+            continue;
+          }
+          ret_error_message = callbacks.AppendCborTag(
+              value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_MASK);
+          if (!ret_error_message.empty()) {
+            goto done;
+          }
+          continue;
+        }
+      }
+
+    fail:
+      ret_error_message =
+          "wuffs_aux::CborDecoder: internal error: unexpected token";
+      goto done;
+
+    parsed_a_value:
+      if (!ret_error_message.empty() || (depth == 0)) {
+        goto done;
+      }
+    }
+  } while (false);
+
+done:
+  DecodeCborResult result(
+      std::move(ret_error_message),
+      wuffs_base__u64__sat_add(io_buf->meta.pos, cursor_index));
+  callbacks.Done(result, input, *io_buf);
+  return result;
+}
+
+}  // namespace wuffs_aux
+
+#endif  // !defined(WUFFS_CONFIG__MODULES) ||
+        // defined(WUFFS_CONFIG__MODULE__AUX__CBOR)
diff --git a/internal/cgen/auxiliary/cbor.hh b/internal/cgen/auxiliary/cbor.hh
new file mode 100644
index 0000000..a16b49a
--- /dev/null
+++ b/internal/cgen/auxiliary/cbor.hh
@@ -0,0 +1,80 @@
+// After editing this file, run "go generate" in the ../data directory.
+
+// Copyright 2020 The Wuffs Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// ---------------- Auxiliary - CBOR
+
+namespace wuffs_aux {
+
+struct DecodeCborResult {
+  DecodeCborResult(std::string&& error_message0, uint64_t cursor_position0);
+
+  std::string error_message;
+  uint64_t cursor_position;
+};
+
+class DecodeCborCallbacks {
+ public:
+  // AppendXxx are called for leaf nodes: literals, numbers, strings, etc.
+
+  virtual std::string AppendNull() = 0;
+  virtual std::string AppendUndefined() = 0;
+  virtual std::string AppendBool(bool val) = 0;
+  virtual std::string AppendF64(double val) = 0;
+  virtual std::string AppendI64(int64_t val) = 0;
+  virtual std::string AppendU64(uint64_t val) = 0;
+  virtual std::string AppendByteString(std::string&& val) = 0;
+  virtual std::string AppendTextString(std::string&& val) = 0;
+  virtual std::string AppendMinus1MinusX(uint64_t val) = 0;
+  virtual std::string AppendCborSimpleValue(uint8_t val) = 0;
+  virtual std::string AppendCborTag(uint64_t val) = 0;
+
+  // Push and Pop are called for container nodes: CBOR arrays (lists) and CBOR
+  // maps (dictionaries).
+  //
+  // The flags bits combine exactly one of:
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_NONE
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_LIST
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_DICT
+  // and exactly one of:
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_NONE
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_LIST
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_DICT
+
+  virtual std::string Push(uint32_t flags) = 0;
+  virtual std::string Pop(uint32_t flags) = 0;
+
+  // Done is always the last Callback method called by DecodeCbor, whether or
+  // not parsing the input as CBOR encountered an error. Even when successful,
+  // trailing data may remain in input and buffer.
+  //
+  // Do not keep a reference to buffer or buffer.data.ptr after Done returns,
+  // as DecodeCbor may then de-allocate the backing array.
+  virtual void Done(DecodeCborResult& result,
+                    sync_io::Input& input,
+                    IOBuffer& buffer);
+};
+
+// DecodeCbor calls callbacks based on the CBOR-formatted data in input.
+//
+// On success, the returned error_message is empty and cursor_position counts
+// the number of bytes consumed. On failure, error_message is non-empty and
+// cursor_position is the location of the error. That error may be a content
+// error (invalid CBOR) or an input error (e.g. network failure).
+DecodeCborResult DecodeCbor(DecodeCborCallbacks&& callbacks,
+                            sync_io::Input&& input,
+                            wuffs_base__slice_u32 quirks);
+
+}  // namespace wuffs_aux
diff --git a/internal/cgen/data/data.go b/internal/cgen/data/data.go
index c3e8d27..2df619d 100644
--- a/internal/cgen/data/data.go
+++ b/internal/cgen/data/data.go
@@ -625,6 +625,27 @@
 	"// --------\n\n}  // namespace sync_io\n\n}  // namespace wuffs_aux\n" +
 	""
 
+const AuxCborCc = "" +
+	"// ---------------- Auxiliary - CBOR\n\n#if !defined(WUFFS_CONFIG__MODULES) || defined(WUFFS_CONFIG__MODULE__AUX__CBOR)\n\n#include <utility>\n\nnamespace wuffs_aux {\n\nDecodeCborResult::DecodeCborResult(std::string&& error_message0,\n                                   uint64_t cursor_position0)\n    : error_message(std::move(error_message0)),\n      cursor_position(cursor_position0) {}\n\nvoid DecodeCborCallbacks::Done(DecodeCborResult& result,\n                               sync_io::Input& input,\n                               IOBuffer& buffer) {}\n\nDecodeCborResult  //\nDecodeCbor(DecodeCborCallbacks&& callbacks,\n           sync_io::Input&& input,\n           wuffs_base__slice_u32 quirks) {\n  // Prepare the wuffs_base__io_buffer and the resultant error_message.\n  wuffs_base__io_buffer* io_buf = input.BringsItsOwnIOBuffer();\n  wuffs_base__io_buffer fallback_io_buf = wuffs_base__empty_io_buffer();\n  std::unique_ptr<uint8_t[]> fallback_io_array(nullptr);\n  if (!io_buf) {\n    fallback_io_array = std::unique_ptr<uint8_t[]>(ne" +
+	"w uint8_t[4096]);\n    fallback_io_buf = wuffs_base__ptr_u8__writer(fallback_io_array.get(), 4096);\n    io_buf = &fallback_io_buf;\n  }\n  size_t cursor_index = 0;\n  std::string ret_error_message;\n  std::string io_error_message;\n\n  do {\n    // Prepare the low-level CBOR decoder.\n    wuffs_cbor__decoder::unique_ptr dec = wuffs_cbor__decoder::alloc();\n    if (!dec) {\n      ret_error_message = \"wuffs_aux::CborDecoder: out of memory\";\n      goto done;\n    }\n    for (size_t i = 0; i < quirks.len; i++) {\n      dec->set_quirk_enabled(quirks.ptr[i], true);\n    }\n\n    // Prepare the wuffs_base__tok_buffer.\n    wuffs_base__token tok_array[256];\n    wuffs_base__token_buffer tok_buf =\n        wuffs_base__slice_token__writer(wuffs_base__make_slice_token(\n            &tok_array[0], (sizeof(tok_array) / sizeof(tok_array[0]))));\n    wuffs_base__status tok_status = wuffs_base__make_status(nullptr);\n\n    // Prepare other state.\n    uint32_t depth = 0;\n    std::string str;\n    int64_t extension_category = 0;\n    uint64_t extension" +
+	"_detail = 0;\n\n    // Valid token's VBCs range in 0 ..= 15. Values over that are for tokens\n    // from outside of the base package, such as the CBOR package.\n    constexpr int64_t EXT_CAT__CBOR_TAG = 16;\n\n    // Loop, doing these two things:\n    //  1. Get the next token.\n    //  2. Process that token.\n    while (true) {\n      // 1. Get the next token.\n\n      while (tok_buf.meta.ri >= tok_buf.meta.wi) {\n        if (tok_status.repr == nullptr) {\n          // No-op.\n        } else if (tok_status.repr == wuffs_base__suspension__short_write) {\n          tok_buf.compact();\n        } else if (tok_status.repr == wuffs_base__suspension__short_read) {\n          // Read from input to io_buf.\n          if (!io_error_message.empty()) {\n            ret_error_message = std::move(io_error_message);\n            goto done;\n          } else if (cursor_index != io_buf->meta.ri) {\n            ret_error_message =\n                \"wuffs_aux::CborDecoder: internal error: bad cursor_index\";\n            goto done;\n          } else if" +
+	" (io_buf->meta.closed) {\n            ret_error_message =\n                \"wuffs_aux::CborDecoder: internal error: io_buf is closed\";\n            goto done;\n          }\n          io_buf->compact();\n          if (io_buf->meta.wi >= io_buf->data.len) {\n            ret_error_message =\n                \"wuffs_aux::CborDecoder: internal error: io_buf is full\";\n            goto done;\n          }\n          cursor_index = io_buf->meta.ri;\n          io_error_message = input.CopyIn(io_buf);\n        } else {\n          ret_error_message = tok_status.message();\n          goto done;\n        }\n\n        if (WUFFS_CBOR__DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE != 0) {\n          ret_error_message =\n              \"wuffs_aux::CborDecoder: internal error: bad WORKBUF_LEN\";\n          goto done;\n        }\n        wuffs_base__slice_u8 work_buf = wuffs_base__empty_slice_u8();\n        tok_status = dec->decode_tokens(&tok_buf, io_buf, work_buf);\n      }\n\n      wuffs_base__token token = tok_buf.data.ptr[tok_buf.meta.ri++];\n      uint64_t t" +
+	"oken_len = token.length();\n      if ((io_buf->meta.ri < cursor_index) ||\n          ((io_buf->meta.ri - cursor_index) < token_len)) {\n        ret_error_message =\n            \"wuffs_aux::CborDecoder: internal error: bad token indexes\";\n        goto done;\n      }\n      uint8_t* token_ptr = io_buf->data.ptr + cursor_index;\n      cursor_index += token_len;\n\n      // 2. Process that token.\n\n      uint64_t vbd = token.value_base_detail();\n\n      if (extension_category != 0) {\n        int64_t ext = token.value_extension();\n        if ((ext >= 0) && !token.continued()) {\n          extension_detail = (extension_detail\n                              << WUFFS_BASE__TOKEN__VALUE_EXTENSION__NUM_BITS) |\n                             static_cast<uint64_t>(ext);\n          switch (extension_category) {\n            case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_SIGNED:\n              extension_category = 0;\n              ret_error_message =\n                  callbacks.AppendI64(static_cast<int64_t>(extension_detail));\n              g" +
+	"oto parsed_a_value;\n            case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_UNSIGNED:\n              extension_category = 0;\n              ret_error_message = callbacks.AppendU64(extension_detail);\n              goto parsed_a_value;\n            case EXT_CAT__CBOR_TAG:\n              extension_category = 0;\n              ret_error_message = callbacks.AppendCborTag(extension_detail);\n              if (!ret_error_message.empty()) {\n                goto done;\n              }\n              continue;\n          }\n        }\n        ret_error_message =\n            \"wuffs_aux::CborDecoder: internal error: bad extended token\";\n        goto done;\n      }\n\n      switch (token.value_base_category()) {\n        case WUFFS_BASE__TOKEN__VBC__FILLER:\n          continue;\n\n        case WUFFS_BASE__TOKEN__VBC__STRUCTURE: {\n          if (vbd & WUFFS_BASE__TOKEN__VBD__STRUCTURE__PUSH) {\n            ret_error_message = callbacks.Push(static_cast<uint32_t>(vbd));\n            if (!ret_error_message.empty()) {\n              goto done;\n   " +
+	"         }\n            depth++;\n            continue;\n          }\n          ret_error_message = callbacks.Pop(static_cast<uint32_t>(vbd));\n          depth--;\n          goto parsed_a_value;\n        }\n\n        case WUFFS_BASE__TOKEN__VBC__STRING: {\n          if (vbd & WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_0_DST_1_SRC_DROP) {\n            // No-op.\n          } else if (vbd &\n                     WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_1_DST_1_SRC_COPY) {\n            const char* ptr =  // Convert from (uint8_t*).\n                static_cast<const char*>(static_cast<void*>(token_ptr));\n            str.append(ptr, token_len);\n          } else {\n            goto fail;\n          }\n          if (token.continued()) {\n            continue;\n          }\n          ret_error_message =\n              (vbd & WUFFS_BASE__TOKEN__VBD__STRING__CHAIN_MUST_BE_UTF_8)\n                  ? callbacks.AppendTextString(std::move(str))\n                  : callbacks.AppendByteString(std::move(str));\n          str.clear();\n          goto par" +
+	"sed_a_value;\n        }\n\n        case WUFFS_BASE__TOKEN__VBC__UNICODE_CODE_POINT: {\n          uint8_t u[WUFFS_BASE__UTF_8__BYTE_LENGTH__MAX_INCL];\n          size_t n = wuffs_base__utf_8__encode(\n              wuffs_base__make_slice_u8(\n                  &u[0], WUFFS_BASE__UTF_8__BYTE_LENGTH__MAX_INCL),\n              static_cast<uint32_t>(vbd));\n          const char* ptr =  // Convert from (uint8_t*).\n              static_cast<const char*>(static_cast<void*>(&u[0]));\n          str.append(ptr, n);\n          if (token.continued()) {\n            continue;\n          }\n          goto fail;\n        }\n\n        case WUFFS_BASE__TOKEN__VBC__LITERAL: {\n          if (vbd & WUFFS_BASE__TOKEN__VBD__LITERAL__NULL) {\n            ret_error_message = callbacks.AppendNull();\n          } else if (vbd & WUFFS_BASE__TOKEN__VBD__LITERAL__UNDEFINED) {\n            ret_error_message = callbacks.AppendUndefined();\n          } else {\n            ret_error_message = callbacks.AppendBool(\n                vbd & WUFFS_BASE__TOKEN__VBD__LITER" +
+	"AL__TRUE);\n          }\n          goto parsed_a_value;\n        }\n\n        case WUFFS_BASE__TOKEN__VBC__NUMBER: {\n          const uint64_t cfp_fbbe_fifb =\n              WUFFS_BASE__TOKEN__VBD__NUMBER__CONTENT_FLOATING_POINT |\n              WUFFS_BASE__TOKEN__VBD__NUMBER__FORMAT_BINARY_BIG_ENDIAN |\n              WUFFS_BASE__TOKEN__VBD__NUMBER__FORMAT_IGNORE_FIRST_BYTE;\n          if ((vbd & cfp_fbbe_fifb) == cfp_fbbe_fifb) {\n            double f;\n            switch (token_len) {\n              case 3:\n                f = wuffs_base__ieee_754_bit_representation__from_u16_to_f64(\n                    wuffs_base__load_u16be__no_bounds_check(token_ptr + 1));\n                break;\n              case 5:\n                f = wuffs_base__ieee_754_bit_representation__from_u32_to_f64(\n                    wuffs_base__load_u32be__no_bounds_check(token_ptr + 1));\n                break;\n              case 9:\n                f = wuffs_base__ieee_754_bit_representation__from_u64_to_f64(\n                    wuffs_base__load_u64be__" +
+	"no_bounds_check(token_ptr + 1));\n                break;\n              default:\n                goto fail;\n            }\n            ret_error_message = callbacks.AppendF64(f);\n            goto parsed_a_value;\n          }\n          goto fail;\n        }\n\n        case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_SIGNED: {\n          if (token.continued()) {\n            extension_category = WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_SIGNED;\n            extension_detail =\n                static_cast<uint64_t>(token.value_base_detail__sign_extended());\n            continue;\n          }\n          ret_error_message =\n              callbacks.AppendI64(token.value_base_detail__sign_extended());\n          goto parsed_a_value;\n        }\n\n        case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_UNSIGNED: {\n          if (token.continued()) {\n            extension_category =\n                WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_UNSIGNED;\n            extension_detail = vbd;\n            continue;\n          }\n          ret_error_message = ca" +
+	"llbacks.AppendU64(vbd);\n          goto parsed_a_value;\n        }\n      }\n\n      if (token.value_major() == WUFFS_CBOR__TOKEN_VALUE_MAJOR) {\n        uint64_t value_minor = token.value_minor();\n        if (value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__MINUS_1_MINUS_X) {\n          if (token_len == 9) {\n            ret_error_message = callbacks.AppendMinus1MinusX(\n                wuffs_base__load_u64be__no_bounds_check(token_ptr + 1));\n            goto parsed_a_value;\n          }\n        } else if (value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__SIMPLE_VALUE) {\n          ret_error_message =\n              callbacks.AppendCborSimpleValue(static_cast<uint8_t>(\n                  value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_MASK));\n          goto parsed_a_value;\n        } else if (value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__TAG) {\n          if (token.continued()) {\n            extension_category = EXT_CAT__CBOR_TAG;\n            extension_detail =\n                value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_" +
+	"MASK;\n            continue;\n          }\n          ret_error_message = callbacks.AppendCborTag(\n              value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_MASK);\n          if (!ret_error_message.empty()) {\n            goto done;\n          }\n          continue;\n        }\n      }\n\n    fail:\n      ret_error_message =\n          \"wuffs_aux::CborDecoder: internal error: unexpected token\";\n      goto done;\n\n    parsed_a_value:\n      if (!ret_error_message.empty() || (depth == 0)) {\n        goto done;\n      }\n    }\n  } while (false);\n\ndone:\n  DecodeCborResult result(\n      std::move(ret_error_message),\n      wuffs_base__u64__sat_add(io_buf->meta.pos, cursor_index));\n  callbacks.Done(result, input, *io_buf);\n  return result;\n}\n\n}  // namespace wuffs_aux\n\n#endif  // !defined(WUFFS_CONFIG__MODULES) ||\n        // defined(WUFFS_CONFIG__MODULE__AUX__CBOR)\n" +
+	""
+
+const AuxCborHh = "" +
+	"// ---------------- Auxiliary - CBOR\n\nnamespace wuffs_aux {\n\nstruct DecodeCborResult {\n  DecodeCborResult(std::string&& error_message0, uint64_t cursor_position0);\n\n  std::string error_message;\n  uint64_t cursor_position;\n};\n\nclass DecodeCborCallbacks {\n public:\n  // AppendXxx are called for leaf nodes: literals, numbers, strings, etc.\n\n  virtual std::string AppendNull() = 0;\n  virtual std::string AppendUndefined() = 0;\n  virtual std::string AppendBool(bool val) = 0;\n  virtual std::string AppendF64(double val) = 0;\n  virtual std::string AppendI64(int64_t val) = 0;\n  virtual std::string AppendU64(uint64_t val) = 0;\n  virtual std::string AppendByteString(std::string&& val) = 0;\n  virtual std::string AppendTextString(std::string&& val) = 0;\n  virtual std::string AppendMinus1MinusX(uint64_t val) = 0;\n  virtual std::string AppendCborSimpleValue(uint8_t val) = 0;\n  virtual std::string AppendCborTag(uint64_t val) = 0;\n\n  // Push and Pop are called for container nodes: CBOR arrays (lists) and CBOR\n  // maps (dictiona" +
+	"ries).\n  //\n  // The flags bits combine exactly one of:\n  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_NONE\n  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_LIST\n  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_DICT\n  // and exactly one of:\n  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_NONE\n  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_LIST\n  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_DICT\n\n  virtual std::string Push(uint32_t flags) = 0;\n  virtual std::string Pop(uint32_t flags) = 0;\n\n  // Done is always the last Callback method called by DecodeCbor, whether or\n  // not parsing the input as CBOR encountered an error. Even when successful,\n  // trailing data may remain in input and buffer.\n  //\n  // Do not keep a reference to buffer or buffer.data.ptr after Done returns,\n  // as DecodeCbor may then de-allocate the backing array.\n  virtual void Done(DecodeCborResult& result,\n                    sync_io::Input& input,\n                    IOBuffer& buffer);\n};\n\n// DecodeCbor calls callbacks based on the CBOR-form" +
+	"atted data in input.\n//\n// On success, the returned error_message is empty and cursor_position counts\n// the number of bytes consumed. On failure, error_message is non-empty and\n// cursor_position is the location of the error. That error may be a content\n// error (invalid CBOR) or an input error (e.g. network failure).\nDecodeCborResult DecodeCbor(DecodeCborCallbacks&& callbacks,\n                            sync_io::Input&& input,\n                            wuffs_base__slice_u32 quirks);\n\n}  // namespace wuffs_aux\n" +
+	""
+
 const AuxJsonCc = "" +
 	"// ---------------- Auxiliary - JSON\n\n#if !defined(WUFFS_CONFIG__MODULES) || defined(WUFFS_CONFIG__MODULE__AUX__JSON)\n\n#include <utility>\n\nnamespace wuffs_aux {\n\nDecodeJsonResult::DecodeJsonResult(std::string&& error_message0,\n                                   uint64_t cursor_position0)\n    : error_message(std::move(error_message0)),\n      cursor_position(cursor_position0) {}\n\nDecodeJsonResult  //\nDecodeJson(DecodeJsonCallbacks&& callbacks,\n           sync_io::Input&& input,\n           wuffs_base__slice_u32 quirks) {\n  // Prepare the wuffs_base__io_buffer and the resultant error_message.\n  wuffs_base__io_buffer* io_buf = input.BringsItsOwnIOBuffer();\n  wuffs_base__io_buffer fallback_io_buf = wuffs_base__empty_io_buffer();\n  std::unique_ptr<uint8_t[]> fallback_io_array(nullptr);\n  if (!io_buf) {\n    fallback_io_array = std::unique_ptr<uint8_t[]>(new uint8_t[4096]);\n    fallback_io_buf = wuffs_base__ptr_u8__writer(fallback_io_array.get(), 4096);\n    io_buf = &fallback_io_buf;\n  }\n  size_t cursor_index = 0;\n  s" +
 	"td::string ret_error_message;\n  std::string io_error_message;\n\n  do {\n    // Prepare the low-level JSON decoder.\n    wuffs_json__decoder::unique_ptr dec = wuffs_json__decoder::alloc();\n    if (!dec) {\n      ret_error_message = \"wuffs_aux::JsonDecoder: out of memory\";\n      goto done;\n    }\n    for (size_t i = 0; i < quirks.len; i++) {\n      dec->set_quirk_enabled(quirks.ptr[i], true);\n    }\n\n    // Prepare the wuffs_base__tok_buffer.\n    wuffs_base__token tok_array[256];\n    wuffs_base__token_buffer tok_buf =\n        wuffs_base__slice_token__writer(wuffs_base__make_slice_token(\n            &tok_array[0], (sizeof(tok_array) / sizeof(tok_array[0]))));\n    wuffs_base__status tok_status = wuffs_base__make_status(nullptr);\n\n    // Prepare other state.\n    uint32_t depth = 0;\n    std::string str;\n\n    // Loop, doing these two things:\n    //  1. Get the next token.\n    //  2. Process that token.\n    while (true) {\n      // 1. Get the next token.\n\n      while (tok_buf.meta.ri >= tok_buf.meta.wi) {\n        if (tok_sta" +
@@ -644,10 +665,12 @@
 	""
 
 var AuxNonBaseCcFiles = []string{
+	AuxCborCc,
 	AuxJsonCc,
 }
 
 var AuxNonBaseHhFiles = []string{
+	AuxCborHh,
 	AuxJsonHh,
 }
 
diff --git a/internal/cgen/data/gen.go b/internal/cgen/data/gen.go
index a9d732a..65a9c85 100644
--- a/internal/cgen/data/gen.go
+++ b/internal/cgen/data/gen.go
@@ -99,6 +99,8 @@
 
 		{"../auxiliary/base.cc", "AuxBaseCc"},
 		{"../auxiliary/base.hh", "AuxBaseHh"},
+		{"../auxiliary/cbor.cc", "AuxCborCc"},
+		{"../auxiliary/cbor.hh", "AuxCborHh"},
 		{"../auxiliary/json.cc", "AuxJsonCc"},
 		{"../auxiliary/json.hh", "AuxJsonHh"},
 	}
diff --git a/release/c/wuffs-unsupported-snapshot.c b/release/c/wuffs-unsupported-snapshot.c
index 9e9a316..b0a2fb1 100644
--- a/release/c/wuffs-unsupported-snapshot.c
+++ b/release/c/wuffs-unsupported-snapshot.c
@@ -5648,6 +5648,8 @@
 
 // ---------------- Public Consts
 
+#define WUFFS_CBOR__DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE 0
+
 #define WUFFS_CBOR__TOKEN_VALUE_MAJOR 787997
 
 #define WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_MASK 262143
@@ -8409,6 +8411,71 @@
 
 }  // namespace wuffs_aux
 
+// ---------------- Auxiliary - CBOR
+
+namespace wuffs_aux {
+
+struct DecodeCborResult {
+  DecodeCborResult(std::string&& error_message0, uint64_t cursor_position0);
+
+  std::string error_message;
+  uint64_t cursor_position;
+};
+
+class DecodeCborCallbacks {
+ public:
+  // AppendXxx are called for leaf nodes: literals, numbers, strings, etc.
+
+  virtual std::string AppendNull() = 0;
+  virtual std::string AppendUndefined() = 0;
+  virtual std::string AppendBool(bool val) = 0;
+  virtual std::string AppendF64(double val) = 0;
+  virtual std::string AppendI64(int64_t val) = 0;
+  virtual std::string AppendU64(uint64_t val) = 0;
+  virtual std::string AppendByteString(std::string&& val) = 0;
+  virtual std::string AppendTextString(std::string&& val) = 0;
+  virtual std::string AppendMinus1MinusX(uint64_t val) = 0;
+  virtual std::string AppendCborSimpleValue(uint8_t val) = 0;
+  virtual std::string AppendCborTag(uint64_t val) = 0;
+
+  // Push and Pop are called for container nodes: CBOR arrays (lists) and CBOR
+  // maps (dictionaries).
+  //
+  // The flags bits combine exactly one of:
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_NONE
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_LIST
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_DICT
+  // and exactly one of:
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_NONE
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_LIST
+  //  - WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_DICT
+
+  virtual std::string Push(uint32_t flags) = 0;
+  virtual std::string Pop(uint32_t flags) = 0;
+
+  // Done is always the last Callback method called by DecodeCbor, whether or
+  // not parsing the input as CBOR encountered an error. Even when successful,
+  // trailing data may remain in input and buffer.
+  //
+  // Do not keep a reference to buffer or buffer.data.ptr after Done returns,
+  // as DecodeCbor may then de-allocate the backing array.
+  virtual void Done(DecodeCborResult& result,
+                    sync_io::Input& input,
+                    IOBuffer& buffer);
+};
+
+// DecodeCbor calls callbacks based on the CBOR-formatted data in input.
+//
+// On success, the returned error_message is empty and cursor_position counts
+// the number of bytes consumed. On failure, error_message is non-empty and
+// cursor_position is the location of the error. That error may be a content
+// error (invalid CBOR) or an input error (e.g. network failure).
+DecodeCborResult DecodeCbor(DecodeCborCallbacks&& callbacks,
+                            sync_io::Input&& input,
+                            wuffs_base__slice_u32 quirks);
+
+}  // namespace wuffs_aux
+
 // ---------------- Auxiliary - JSON
 
 namespace wuffs_aux {
@@ -28859,6 +28926,334 @@
 #endif  // !defined(WUFFS_CONFIG__MODULES) ||
         // defined(WUFFS_CONFIG__MODULE__AUX__BASE)
 
+// ---------------- Auxiliary - CBOR
+
+#if !defined(WUFFS_CONFIG__MODULES) || defined(WUFFS_CONFIG__MODULE__AUX__CBOR)
+
+#include <utility>
+
+namespace wuffs_aux {
+
+DecodeCborResult::DecodeCborResult(std::string&& error_message0,
+                                   uint64_t cursor_position0)
+    : error_message(std::move(error_message0)),
+      cursor_position(cursor_position0) {}
+
+void DecodeCborCallbacks::Done(DecodeCborResult& result,
+                               sync_io::Input& input,
+                               IOBuffer& buffer) {}
+
+DecodeCborResult  //
+DecodeCbor(DecodeCborCallbacks&& callbacks,
+           sync_io::Input&& input,
+           wuffs_base__slice_u32 quirks) {
+  // Prepare the wuffs_base__io_buffer and the resultant error_message.
+  wuffs_base__io_buffer* io_buf = input.BringsItsOwnIOBuffer();
+  wuffs_base__io_buffer fallback_io_buf = wuffs_base__empty_io_buffer();
+  std::unique_ptr<uint8_t[]> fallback_io_array(nullptr);
+  if (!io_buf) {
+    fallback_io_array = std::unique_ptr<uint8_t[]>(new uint8_t[4096]);
+    fallback_io_buf = wuffs_base__ptr_u8__writer(fallback_io_array.get(), 4096);
+    io_buf = &fallback_io_buf;
+  }
+  size_t cursor_index = 0;
+  std::string ret_error_message;
+  std::string io_error_message;
+
+  do {
+    // Prepare the low-level CBOR decoder.
+    wuffs_cbor__decoder::unique_ptr dec = wuffs_cbor__decoder::alloc();
+    if (!dec) {
+      ret_error_message = "wuffs_aux::CborDecoder: out of memory";
+      goto done;
+    }
+    for (size_t i = 0; i < quirks.len; i++) {
+      dec->set_quirk_enabled(quirks.ptr[i], true);
+    }
+
+    // Prepare the wuffs_base__tok_buffer.
+    wuffs_base__token tok_array[256];
+    wuffs_base__token_buffer tok_buf =
+        wuffs_base__slice_token__writer(wuffs_base__make_slice_token(
+            &tok_array[0], (sizeof(tok_array) / sizeof(tok_array[0]))));
+    wuffs_base__status tok_status = wuffs_base__make_status(nullptr);
+
+    // Prepare other state.
+    uint32_t depth = 0;
+    std::string str;
+    int64_t extension_category = 0;
+    uint64_t extension_detail = 0;
+
+    // Valid token's VBCs range in 0 ..= 15. Values over that are for tokens
+    // from outside of the base package, such as the CBOR package.
+    constexpr int64_t EXT_CAT__CBOR_TAG = 16;
+
+    // Loop, doing these two things:
+    //  1. Get the next token.
+    //  2. Process that token.
+    while (true) {
+      // 1. Get the next token.
+
+      while (tok_buf.meta.ri >= tok_buf.meta.wi) {
+        if (tok_status.repr == nullptr) {
+          // No-op.
+        } else if (tok_status.repr == wuffs_base__suspension__short_write) {
+          tok_buf.compact();
+        } else if (tok_status.repr == wuffs_base__suspension__short_read) {
+          // Read from input to io_buf.
+          if (!io_error_message.empty()) {
+            ret_error_message = std::move(io_error_message);
+            goto done;
+          } else if (cursor_index != io_buf->meta.ri) {
+            ret_error_message =
+                "wuffs_aux::CborDecoder: internal error: bad cursor_index";
+            goto done;
+          } else if (io_buf->meta.closed) {
+            ret_error_message =
+                "wuffs_aux::CborDecoder: internal error: io_buf is closed";
+            goto done;
+          }
+          io_buf->compact();
+          if (io_buf->meta.wi >= io_buf->data.len) {
+            ret_error_message =
+                "wuffs_aux::CborDecoder: internal error: io_buf is full";
+            goto done;
+          }
+          cursor_index = io_buf->meta.ri;
+          io_error_message = input.CopyIn(io_buf);
+        } else {
+          ret_error_message = tok_status.message();
+          goto done;
+        }
+
+        if (WUFFS_CBOR__DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE != 0) {
+          ret_error_message =
+              "wuffs_aux::CborDecoder: internal error: bad WORKBUF_LEN";
+          goto done;
+        }
+        wuffs_base__slice_u8 work_buf = wuffs_base__empty_slice_u8();
+        tok_status = dec->decode_tokens(&tok_buf, io_buf, work_buf);
+      }
+
+      wuffs_base__token token = tok_buf.data.ptr[tok_buf.meta.ri++];
+      uint64_t token_len = token.length();
+      if ((io_buf->meta.ri < cursor_index) ||
+          ((io_buf->meta.ri - cursor_index) < token_len)) {
+        ret_error_message =
+            "wuffs_aux::CborDecoder: internal error: bad token indexes";
+        goto done;
+      }
+      uint8_t* token_ptr = io_buf->data.ptr + cursor_index;
+      cursor_index += token_len;
+
+      // 2. Process that token.
+
+      uint64_t vbd = token.value_base_detail();
+
+      if (extension_category != 0) {
+        int64_t ext = token.value_extension();
+        if ((ext >= 0) && !token.continued()) {
+          extension_detail = (extension_detail
+                              << WUFFS_BASE__TOKEN__VALUE_EXTENSION__NUM_BITS) |
+                             static_cast<uint64_t>(ext);
+          switch (extension_category) {
+            case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_SIGNED:
+              extension_category = 0;
+              ret_error_message =
+                  callbacks.AppendI64(static_cast<int64_t>(extension_detail));
+              goto parsed_a_value;
+            case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_UNSIGNED:
+              extension_category = 0;
+              ret_error_message = callbacks.AppendU64(extension_detail);
+              goto parsed_a_value;
+            case EXT_CAT__CBOR_TAG:
+              extension_category = 0;
+              ret_error_message = callbacks.AppendCborTag(extension_detail);
+              if (!ret_error_message.empty()) {
+                goto done;
+              }
+              continue;
+          }
+        }
+        ret_error_message =
+            "wuffs_aux::CborDecoder: internal error: bad extended token";
+        goto done;
+      }
+
+      switch (token.value_base_category()) {
+        case WUFFS_BASE__TOKEN__VBC__FILLER:
+          continue;
+
+        case WUFFS_BASE__TOKEN__VBC__STRUCTURE: {
+          if (vbd & WUFFS_BASE__TOKEN__VBD__STRUCTURE__PUSH) {
+            ret_error_message = callbacks.Push(static_cast<uint32_t>(vbd));
+            if (!ret_error_message.empty()) {
+              goto done;
+            }
+            depth++;
+            continue;
+          }
+          ret_error_message = callbacks.Pop(static_cast<uint32_t>(vbd));
+          depth--;
+          goto parsed_a_value;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__STRING: {
+          if (vbd & WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_0_DST_1_SRC_DROP) {
+            // No-op.
+          } else if (vbd &
+                     WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_1_DST_1_SRC_COPY) {
+            const char* ptr =  // Convert from (uint8_t*).
+                static_cast<const char*>(static_cast<void*>(token_ptr));
+            str.append(ptr, token_len);
+          } else {
+            goto fail;
+          }
+          if (token.continued()) {
+            continue;
+          }
+          ret_error_message =
+              (vbd & WUFFS_BASE__TOKEN__VBD__STRING__CHAIN_MUST_BE_UTF_8)
+                  ? callbacks.AppendTextString(std::move(str))
+                  : callbacks.AppendByteString(std::move(str));
+          str.clear();
+          goto parsed_a_value;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__UNICODE_CODE_POINT: {
+          uint8_t u[WUFFS_BASE__UTF_8__BYTE_LENGTH__MAX_INCL];
+          size_t n = wuffs_base__utf_8__encode(
+              wuffs_base__make_slice_u8(
+                  &u[0], WUFFS_BASE__UTF_8__BYTE_LENGTH__MAX_INCL),
+              static_cast<uint32_t>(vbd));
+          const char* ptr =  // Convert from (uint8_t*).
+              static_cast<const char*>(static_cast<void*>(&u[0]));
+          str.append(ptr, n);
+          if (token.continued()) {
+            continue;
+          }
+          goto fail;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__LITERAL: {
+          if (vbd & WUFFS_BASE__TOKEN__VBD__LITERAL__NULL) {
+            ret_error_message = callbacks.AppendNull();
+          } else if (vbd & WUFFS_BASE__TOKEN__VBD__LITERAL__UNDEFINED) {
+            ret_error_message = callbacks.AppendUndefined();
+          } else {
+            ret_error_message = callbacks.AppendBool(
+                vbd & WUFFS_BASE__TOKEN__VBD__LITERAL__TRUE);
+          }
+          goto parsed_a_value;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__NUMBER: {
+          const uint64_t cfp_fbbe_fifb =
+              WUFFS_BASE__TOKEN__VBD__NUMBER__CONTENT_FLOATING_POINT |
+              WUFFS_BASE__TOKEN__VBD__NUMBER__FORMAT_BINARY_BIG_ENDIAN |
+              WUFFS_BASE__TOKEN__VBD__NUMBER__FORMAT_IGNORE_FIRST_BYTE;
+          if ((vbd & cfp_fbbe_fifb) == cfp_fbbe_fifb) {
+            double f;
+            switch (token_len) {
+              case 3:
+                f = wuffs_base__ieee_754_bit_representation__from_u16_to_f64(
+                    wuffs_base__load_u16be__no_bounds_check(token_ptr + 1));
+                break;
+              case 5:
+                f = wuffs_base__ieee_754_bit_representation__from_u32_to_f64(
+                    wuffs_base__load_u32be__no_bounds_check(token_ptr + 1));
+                break;
+              case 9:
+                f = wuffs_base__ieee_754_bit_representation__from_u64_to_f64(
+                    wuffs_base__load_u64be__no_bounds_check(token_ptr + 1));
+                break;
+              default:
+                goto fail;
+            }
+            ret_error_message = callbacks.AppendF64(f);
+            goto parsed_a_value;
+          }
+          goto fail;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_SIGNED: {
+          if (token.continued()) {
+            extension_category = WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_SIGNED;
+            extension_detail =
+                static_cast<uint64_t>(token.value_base_detail__sign_extended());
+            continue;
+          }
+          ret_error_message =
+              callbacks.AppendI64(token.value_base_detail__sign_extended());
+          goto parsed_a_value;
+        }
+
+        case WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_UNSIGNED: {
+          if (token.continued()) {
+            extension_category =
+                WUFFS_BASE__TOKEN__VBC__INLINE_INTEGER_UNSIGNED;
+            extension_detail = vbd;
+            continue;
+          }
+          ret_error_message = callbacks.AppendU64(vbd);
+          goto parsed_a_value;
+        }
+      }
+
+      if (token.value_major() == WUFFS_CBOR__TOKEN_VALUE_MAJOR) {
+        uint64_t value_minor = token.value_minor();
+        if (value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__MINUS_1_MINUS_X) {
+          if (token_len == 9) {
+            ret_error_message = callbacks.AppendMinus1MinusX(
+                wuffs_base__load_u64be__no_bounds_check(token_ptr + 1));
+            goto parsed_a_value;
+          }
+        } else if (value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__SIMPLE_VALUE) {
+          ret_error_message =
+              callbacks.AppendCborSimpleValue(static_cast<uint8_t>(
+                  value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_MASK));
+          goto parsed_a_value;
+        } else if (value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__TAG) {
+          if (token.continued()) {
+            extension_category = EXT_CAT__CBOR_TAG;
+            extension_detail =
+                value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_MASK;
+            continue;
+          }
+          ret_error_message = callbacks.AppendCborTag(
+              value_minor & WUFFS_CBOR__TOKEN_VALUE_MINOR__DETAIL_MASK);
+          if (!ret_error_message.empty()) {
+            goto done;
+          }
+          continue;
+        }
+      }
+
+    fail:
+      ret_error_message =
+          "wuffs_aux::CborDecoder: internal error: unexpected token";
+      goto done;
+
+    parsed_a_value:
+      if (!ret_error_message.empty() || (depth == 0)) {
+        goto done;
+      }
+    }
+  } while (false);
+
+done:
+  DecodeCborResult result(
+      std::move(ret_error_message),
+      wuffs_base__u64__sat_add(io_buf->meta.pos, cursor_index));
+  callbacks.Done(result, input, *io_buf);
+  return result;
+}
+
+}  // namespace wuffs_aux
+
+#endif  // !defined(WUFFS_CONFIG__MODULES) ||
+        // defined(WUFFS_CONFIG__MODULE__AUX__CBOR)
+
 // ---------------- Auxiliary - JSON
 
 #if !defined(WUFFS_CONFIG__MODULES) || defined(WUFFS_CONFIG__MODULE__AUX__JSON)
diff --git a/std/cbor/decode_cbor.wuffs b/std/cbor/decode_cbor.wuffs
index 4db9c56..7153384 100644
--- a/std/cbor/decode_cbor.wuffs
+++ b/std/cbor/decode_cbor.wuffs
@@ -18,6 +18,8 @@
 pri status "#internal error: inconsistent I/O"
 pri status "#internal error: inconsistent token length"
 
+pub const DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE : base.u64 = 0
+
 // TOKEN_VALUE_MAJOR is the base-38 encoding of "cbor".
 pub const TOKEN_VALUE_MAJOR : base.u32 = 0x0C_061D
 
diff --git a/test/data/README.md b/test/data/README.md
index ba67800..7608679 100644
--- a/test/data/README.md
+++ b/test/data/README.md
@@ -34,7 +34,8 @@
 <nigeltao@golang.org>.
 
 `cbor-rfc-7049-examples.cbor` is the concatenated examples from RFC 7049. The
-`cbor-rfc-7049-examples.*.json` files were then generated by `example/jsonptr`.
+`cbor-rfc-7049-examples.*.json` files were then generated by
+`example/cbor-to-json`.
 
 `crude-flag.*` is an original animation by Nigel Tao
 <nigeltao@golang.org>. See the `lib/nie` documentation.