Add wuffs_base__hexadecimal__decodeN
diff --git a/internal/cgen/base/strconv-impl.c b/internal/cgen/base/strconv-impl.c
index 1d4dbef..74e990b 100644
--- a/internal/cgen/base/strconv-impl.c
+++ b/internal/cgen/base/strconv-impl.c
@@ -1104,6 +1104,46 @@
   } while (0);
 }
 
+// ---------------- Hexadecimal
+
+size_t  //
+wuffs_base__hexadecimal__decode2(wuffs_base__slice_u8 dst,
+                                 wuffs_base__slice_u8 src) {
+  size_t src_len2 = src.len / 2;
+  size_t len = dst.len < src_len2 ? dst.len : src_len2;
+  uint8_t* d = dst.ptr;
+  uint8_t* s = src.ptr;
+  size_t n = len;
+
+  while (n--) {
+    *d = (wuffs_base__parse_number__hexadecimal_digits[s[0]] << 4) |
+         (wuffs_base__parse_number__hexadecimal_digits[s[1]] & 0x0F);
+    d += 1;
+    s += 2;
+  }
+
+  return len;
+}
+
+size_t  //
+wuffs_base__hexadecimal__decode4(wuffs_base__slice_u8 dst,
+                                 wuffs_base__slice_u8 src) {
+  size_t src_len4 = src.len / 4;
+  size_t len = dst.len < src_len4 ? dst.len : src_len4;
+  uint8_t* d = dst.ptr;
+  uint8_t* s = src.ptr;
+  size_t n = len;
+
+  while (n--) {
+    *d = (wuffs_base__parse_number__hexadecimal_digits[s[2]] << 4) |
+         (wuffs_base__parse_number__hexadecimal_digits[s[3]] & 0x0F);
+    d += 1;
+    s += 4;
+  }
+
+  return len;
+}
+
 // ---------------- Unicode and UTF-8
 
 size_t  //
diff --git a/internal/cgen/base/strconv-public.h b/internal/cgen/base/strconv-public.h
index 65c60ec..c6f7ad6 100644
--- a/internal/cgen/base/strconv-public.h
+++ b/internal/cgen/base/strconv-public.h
@@ -120,7 +120,35 @@
   return f;
 }
 
-  // ---------------- Unicode and UTF-8
+// ---------------- Hexadecimal
+
+// wuffs_base__hexadecimal__decode2 converts "6A6b" to "jk", where e.g. 'j' is
+// U+006A. There are 2 source bytes for every destination byte.
+//
+// It returns the number of dst bytes written: the minimum of dst.len and
+// (src.len / 2). Excess source bytes are ignored.
+//
+// It assumes that the src bytes are two hexadecimal digits (0-9, A-F, a-f),
+// repeated. It may write nonsense bytes if not, although it will not read or
+// write out of bounds.
+size_t  //
+wuffs_base__hexadecimal__decode2(wuffs_base__slice_u8 dst,
+                                 wuffs_base__slice_u8 src);
+
+// wuffs_base__hexadecimal__decode4 converts "\\x6A\\x6b" to "jk", where e.g.
+// 'j' is U+006A. There are 4 source bytes for every destination byte.
+//
+// It returns the number of dst bytes written: the minimum of dst.len and
+// (src.len / 4). Excess source bytes are ignored.
+//
+// It assumes that the src bytes are two ignored bytes and then two hexadecimal
+// digits (0-9, A-F, a-f), repeated. It may write nonsense bytes if not,
+// although it will not read or write out of bounds.
+size_t  //
+wuffs_base__hexadecimal__decode4(wuffs_base__slice_u8 dst,
+                                 wuffs_base__slice_u8 src);
+
+// ---------------- Unicode and UTF-8
 
 #define WUFFS_BASE__UNICODE_CODE_POINT__MIN_INCL 0x00000000
 #define WUFFS_BASE__UNICODE_CODE_POINT__MAX_INCL 0x0010FFFF
diff --git a/internal/cgen/data.go b/internal/cgen/data.go
index e745d8b..dac8f33 100644
--- a/internal/cgen/data.go
+++ b/internal/cgen/data.go
@@ -114,6 +114,8 @@
 	"man2 >> 52) == 0) {\n      exp2 = bias;\n    }\n\n    // Pack the bits and return.\n    uint64_t exp2_bits = (uint64_t)((exp2 - bias) & 0x07FF);  // (1 << 11) - 1.\n    uint64_t bits = (man2 & 0x000FFFFFFFFFFFFF) |             // (1 << 52) - 1.\n                    (exp2_bits << 52) |                       //\n                    (h.negative ? 0x8000000000000000 : 0);    // (1 << 63).\n\n    wuffs_base__result_f64 ret;\n    ret.status.repr = NULL;\n    ret.value = wuffs_base__ieee_754_bit_representation__to_f64(bits);\n    return ret;\n  } while (0);\n\nzero:\n  do {\n    uint64_t bits = h.negative ? 0x8000000000000000 : 0;\n\n    wuffs_base__result_f64 ret;\n    ret.status.repr = NULL;\n    ret.value = wuffs_base__ieee_754_bit_representation__to_f64(bits);\n    return ret;\n  } while (0);\n\ninfinity:\n  do {\n    uint64_t bits = h.negative ? 0xFFF0000000000000 : 0x7FF0000000000000;\n\n    wuffs_base__result_f64 ret;\n    ret.status.repr = NULL;\n    ret.value = wuffs_base__ieee_754_bit_representation__to_f64(bits);\n    return ret;\n  } whi" +
 	"le (0);\n}\n\n" +
 	"" +
+	"// ---------------- Hexadecimal\n\nsize_t  //\nwuffs_base__hexadecimal__decode2(wuffs_base__slice_u8 dst,\n                                 wuffs_base__slice_u8 src) {\n  size_t src_len2 = src.len / 2;\n  size_t len = dst.len < src_len2 ? dst.len : src_len2;\n  uint8_t* d = dst.ptr;\n  uint8_t* s = src.ptr;\n  size_t n = len;\n\n  while (n--) {\n    *d = (wuffs_base__parse_number__hexadecimal_digits[s[0]] << 4) |\n         (wuffs_base__parse_number__hexadecimal_digits[s[1]] & 0x0F);\n    d += 1;\n    s += 2;\n  }\n\n  return len;\n}\n\nsize_t  //\nwuffs_base__hexadecimal__decode4(wuffs_base__slice_u8 dst,\n                                 wuffs_base__slice_u8 src) {\n  size_t src_len4 = src.len / 4;\n  size_t len = dst.len < src_len4 ? dst.len : src_len4;\n  uint8_t* d = dst.ptr;\n  uint8_t* s = src.ptr;\n  size_t n = len;\n\n  while (n--) {\n    *d = (wuffs_base__parse_number__hexadecimal_digits[s[2]] << 4) |\n         (wuffs_base__parse_number__hexadecimal_digits[s[3]] & 0x0F);\n    d += 1;\n    s += 4;\n  }\n\n  return len;\n}\n\n" +
+	"" +
 	"// ---------------- Unicode and UTF-8\n\nsize_t  //\nwuffs_base__utf_8__encode(wuffs_base__slice_u8 dst, uint32_t code_point) {\n  if (code_point <= 0x7F) {\n    if (dst.len >= 1) {\n      dst.ptr[0] = (uint8_t)(code_point);\n      return 1;\n    }\n\n  } else if (code_point <= 0x07FF) {\n    if (dst.len >= 2) {\n      dst.ptr[0] = (uint8_t)(0xC0 | ((code_point >> 6)));\n      dst.ptr[1] = (uint8_t)(0x80 | ((code_point >> 0) & 0x3F));\n      return 2;\n    }\n\n  } else if (code_point <= 0xFFFF) {\n    if ((dst.len >= 3) && ((code_point < 0xD800) || (0xDFFF < code_point))) {\n      dst.ptr[0] = (uint8_t)(0xE0 | ((code_point >> 12)));\n      dst.ptr[1] = (uint8_t)(0x80 | ((code_point >> 6) & 0x3F));\n      dst.ptr[2] = (uint8_t)(0x80 | ((code_point >> 0) & 0x3F));\n      return 3;\n    }\n\n  } else if (code_point <= 0x10FFFF) {\n    if (dst.len >= 4) {\n      dst.ptr[0] = (uint8_t)(0xF0 | ((code_point >> 18)));\n      dst.ptr[1] = (uint8_t)(0x80 | ((code_point >> 12) & 0x3F));\n      dst.ptr[2] = (uint8_t)(0x80 | ((code_point >> 6) & 0x3" +
 	"F));\n      dst.ptr[3] = (uint8_t)(0x80 | ((code_point >> 0) & 0x3F));\n      return 4;\n    }\n  }\n\n  return 0;\n}\n\n// wuffs_base__utf_8__byte_length_minus_1 is the byte length (minus 1) of a\n// UTF-8 encoded code point, based on the encoding's initial byte.\n//  - 0x00 is 1-byte UTF-8 (ASCII).\n//  - 0x01 is the start of 2-byte UTF-8.\n//  - 0x02 is the start of 3-byte UTF-8.\n//  - 0x03 is the start of 4-byte UTF-8.\n//  - 0x40 is a UTF-8 tail byte.\n//  - 0x80 is invalid UTF-8.\n//\n// RFC 3629 (UTF-8) gives this grammar for valid UTF-8:\n//    UTF8-1      = %x00-7F\n//    UTF8-2      = %xC2-DF UTF8-tail\n//    UTF8-3      = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /\n//                  %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )\n//    UTF8-4      = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /\n//                  %xF4 %x80-8F 2( UTF8-tail )\n//    UTF8-tail   = %x80-BF\nstatic const uint8_t wuffs_base__utf_8__byte_length_minus_1[256] = {\n    // 0     1     2     3     4     5     6     7\n    // 8     9" +
 	"     A     B     C     D     E     F\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x00 ..= 0x07.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x08 ..= 0x0F.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x10 ..= 0x17.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x18 ..= 0x1F.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x20 ..= 0x27.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x28 ..= 0x2F.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x30 ..= 0x37.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x38 ..= 0x3F.\n\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x40 ..= 0x47.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x48 ..= 0x4F.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x50 ..= 0x57.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x58 ..= 0x5F.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x60 ..= 0x67.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0x68 .." +
@@ -329,7 +331,10 @@
 	"" +
 	"// ---------------- IEEE 754 Floating Point\n\n// wuffs_base__parse_number_f64 parses the floating point number in s. For\n// example, if s contains the bytes \"1.5\" then it will return the double 1.5.\n//\n// It returns an error if s does not contain a floating point number.\n//\n// It does not necessarily return an error if the conversion is lossy, e.g. if\n// s is \"0.3\", which double-precision floating point cannot represent exactly.\n//\n// Similarly, the returned value may be infinite (and no error returned) even\n// if s was not \"inf\", when the input is nominally finite but sufficiently\n// larger than DBL_MAX, about 1.8e+308.\n//\n// It is similar to the C standard library's strtod function, but:\n//  - Errors are returned in-band (in a result type), not out-of-band (errno).\n//  - It takes a slice (a pointer and length), not a NUL-terminated C string.\n//  - It does not take an optional endptr argument. It does not allow a partial\n//    parse: it returns an error unless all of s is consumed.\n//  - It does not allow whi" +
 	"tespace, leading or otherwise.\n//  - It does not allow unnecessary leading zeroes (\"0\" is valid and its sole\n//    zero is necessary). All of \"00\", \"0644\" and \"00.7\" are invalid.\n//  - It is not affected by i18n / l10n settings such as environment variables.\n//  - Conversely, it always accepts either ',' or '.' as a decimal separator.\n//    In particular, \"3,141,592\" is always invalid but \"3,141\" is always valid\n//    (and approximately π). The caller is responsible for e.g. previously\n//    rejecting or filtering s if it contains a comma, if that is unacceptable\n//    to the caller. For example, JSON numbers always use a dot '.' and never a\n//    comma ',', regardless of the LOCALE environment variable.\n//  - It does allow arbitrary underscores. For example, \"_3.141_592\" would\n//    successfully parse, again approximately π.\n//  - It does allow \"inf\", \"+Infinity\" and \"-NAN\", case insensitive, but it\n//    does not permit \"nan\" to be followed by an integer mantissa.\n//  - It does not allow hexadecimal float" +
-	"ing point numbers.\nwuffs_base__result_f64  //\nwuffs_base__parse_number_f64(wuffs_base__slice_u8 s);\n\n// wuffs_base__ieee_754_bit_representation__etc converts between a double\n// precision numerical value and its IEEE 754 64-bit representation (1 sign\n// bit, 11 exponent bits, 52 explicit significand bits).\n//\n// For example, it converts between:\n//  - +1.0 and 0x3FF0_0000_0000_0000.\n//  - +5.5 and 0x4016_0000_0000_0000.\n//  - -inf and 0xFFF0_0000_0000_0000.\n//\n// See https://en.wikipedia.org/wiki/Double-precision_floating-point_format\n\nstatic inline uint64_t  //\nwuffs_base__ieee_754_bit_representation__from_f64(double f) {\n  uint64_t u = 0;\n  if (sizeof(uint64_t) == sizeof(double)) {\n    memcpy(&u, &f, sizeof(uint64_t));\n  }\n  return u;\n}\n\nstatic inline double  //\nwuffs_base__ieee_754_bit_representation__to_f64(uint64_t u) {\n  double f = 0;\n  if (sizeof(uint64_t) == sizeof(double)) {\n    memcpy(&f, &u, sizeof(uint64_t));\n  }\n  return f;\n}\n\n  " +
+	"ing point numbers.\nwuffs_base__result_f64  //\nwuffs_base__parse_number_f64(wuffs_base__slice_u8 s);\n\n// wuffs_base__ieee_754_bit_representation__etc converts between a double\n// precision numerical value and its IEEE 754 64-bit representation (1 sign\n// bit, 11 exponent bits, 52 explicit significand bits).\n//\n// For example, it converts between:\n//  - +1.0 and 0x3FF0_0000_0000_0000.\n//  - +5.5 and 0x4016_0000_0000_0000.\n//  - -inf and 0xFFF0_0000_0000_0000.\n//\n// See https://en.wikipedia.org/wiki/Double-precision_floating-point_format\n\nstatic inline uint64_t  //\nwuffs_base__ieee_754_bit_representation__from_f64(double f) {\n  uint64_t u = 0;\n  if (sizeof(uint64_t) == sizeof(double)) {\n    memcpy(&u, &f, sizeof(uint64_t));\n  }\n  return u;\n}\n\nstatic inline double  //\nwuffs_base__ieee_754_bit_representation__to_f64(uint64_t u) {\n  double f = 0;\n  if (sizeof(uint64_t) == sizeof(double)) {\n    memcpy(&f, &u, sizeof(uint64_t));\n  }\n  return f;\n}\n\n" +
+	"" +
+	"// ---------------- Hexadecimal\n\n// wuffs_base__hexadecimal__decode2 converts \"6A6b\" to \"jk\", where e.g. 'j' is\n// U+006A. There are 2 source bytes for every destination byte.\n//\n// It returns the number of dst bytes written: the minimum of dst.len and\n// (src.len / 2). Excess source bytes are ignored.\n//\n// It assumes that the src bytes are two hexadecimal digits (0-9, A-F, a-f),\n// repeated. It may write nonsense bytes if not, although it will not read or\n// write out of bounds.\nsize_t  //\nwuffs_base__hexadecimal__decode2(wuffs_base__slice_u8 dst,\n                                 wuffs_base__slice_u8 src);\n\n// wuffs_base__hexadecimal__decode4 converts \"\\\\x6A\\\\x6b\" to \"jk\", where e.g.\n// 'j' is U+006A. There are 4 source bytes for every destination byte.\n//\n// It returns the number of dst bytes written: the minimum of dst.len and\n// (src.len / 4). Excess source bytes are ignored.\n//\n// It assumes that the src bytes are two ignored bytes and then two hexadecimal\n// digits (0-9, A-F, a-f), repeated. It may wri" +
+	"te nonsense bytes if not,\n// although it will not read or write out of bounds.\nsize_t  //\nwuffs_base__hexadecimal__decode4(wuffs_base__slice_u8 dst,\n                                 wuffs_base__slice_u8 src);\n\n" +
 	"" +
 	"// ---------------- Unicode and UTF-8\n\n#define WUFFS_BASE__UNICODE_CODE_POINT__MIN_INCL 0x00000000\n#define WUFFS_BASE__UNICODE_CODE_POINT__MAX_INCL 0x0010FFFF\n\n#define WUFFS_BASE__UNICODE_REPLACEMENT_CHARACTER 0x0000FFFD\n\n#define WUFFS_BASE__UNICODE_SURROGATE__MIN_INCL 0x0000D800\n#define WUFFS_BASE__UNICODE_SURROGATE__MAX_INCL 0x0000DFFF\n\n#define WUFFS_BASE__ASCII__MIN_INCL 0x00\n#define WUFFS_BASE__ASCII__MAX_INCL 0x7F\n\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH__MIN_INCL 1\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH__MAX_INCL 4\n\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH_1__CODE_POINT__MIN_INCL 0x00000000\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH_1__CODE_POINT__MAX_INCL 0x0000007F\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH_2__CODE_POINT__MIN_INCL 0x00000080\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH_2__CODE_POINT__MAX_INCL 0x000007FF\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH_3__CODE_POINT__MIN_INCL 0x00000800\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH_3__CODE_POINT__MAX_INCL 0x0000FFFF\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH_4__CODE_POINT_" +
 	"_MIN_INCL 0x00010000\n#define WUFFS_BASE__UTF_8__BYTE_LENGTH_4__CODE_POINT__MAX_INCL 0x0010FFFF\n\n" +
diff --git a/release/c/wuffs-unsupported-snapshot.c b/release/c/wuffs-unsupported-snapshot.c
index e59aa6d..0d1b1aa 100644
--- a/release/c/wuffs-unsupported-snapshot.c
+++ b/release/c/wuffs-unsupported-snapshot.c
@@ -3591,7 +3591,35 @@
   return f;
 }
 
-  // ---------------- Unicode and UTF-8
+// ---------------- Hexadecimal
+
+// wuffs_base__hexadecimal__decode2 converts "6A6b" to "jk", where e.g. 'j' is
+// U+006A. There are 2 source bytes for every destination byte.
+//
+// It returns the number of dst bytes written: the minimum of dst.len and
+// (src.len / 2). Excess source bytes are ignored.
+//
+// It assumes that the src bytes are two hexadecimal digits (0-9, A-F, a-f),
+// repeated. It may write nonsense bytes if not, although it will not read or
+// write out of bounds.
+size_t  //
+wuffs_base__hexadecimal__decode2(wuffs_base__slice_u8 dst,
+                                 wuffs_base__slice_u8 src);
+
+// wuffs_base__hexadecimal__decode4 converts "\\x6A\\x6b" to "jk", where e.g.
+// 'j' is U+006A. There are 4 source bytes for every destination byte.
+//
+// It returns the number of dst bytes written: the minimum of dst.len and
+// (src.len / 4). Excess source bytes are ignored.
+//
+// It assumes that the src bytes are two ignored bytes and then two hexadecimal
+// digits (0-9, A-F, a-f), repeated. It may write nonsense bytes if not,
+// although it will not read or write out of bounds.
+size_t  //
+wuffs_base__hexadecimal__decode4(wuffs_base__slice_u8 dst,
+                                 wuffs_base__slice_u8 src);
+
+// ---------------- Unicode and UTF-8
 
 #define WUFFS_BASE__UNICODE_CODE_POINT__MIN_INCL 0x00000000
 #define WUFFS_BASE__UNICODE_CODE_POINT__MAX_INCL 0x0010FFFF
@@ -9642,6 +9670,46 @@
   } while (0);
 }
 
+// ---------------- Hexadecimal
+
+size_t  //
+wuffs_base__hexadecimal__decode2(wuffs_base__slice_u8 dst,
+                                 wuffs_base__slice_u8 src) {
+  size_t src_len2 = src.len / 2;
+  size_t len = dst.len < src_len2 ? dst.len : src_len2;
+  uint8_t* d = dst.ptr;
+  uint8_t* s = src.ptr;
+  size_t n = len;
+
+  while (n--) {
+    *d = (wuffs_base__parse_number__hexadecimal_digits[s[0]] << 4) |
+         (wuffs_base__parse_number__hexadecimal_digits[s[1]] & 0x0F);
+    d += 1;
+    s += 2;
+  }
+
+  return len;
+}
+
+size_t  //
+wuffs_base__hexadecimal__decode4(wuffs_base__slice_u8 dst,
+                                 wuffs_base__slice_u8 src) {
+  size_t src_len4 = src.len / 4;
+  size_t len = dst.len < src_len4 ? dst.len : src_len4;
+  uint8_t* d = dst.ptr;
+  uint8_t* s = src.ptr;
+  size_t n = len;
+
+  while (n--) {
+    *d = (wuffs_base__parse_number__hexadecimal_digits[s[2]] << 4) |
+         (wuffs_base__parse_number__hexadecimal_digits[s[3]] & 0x0F);
+    d += 1;
+    s += 4;
+  }
+
+  return len;
+}
+
 // ---------------- Unicode and UTF-8
 
 size_t  //
diff --git a/test/c/std/json.c b/test/c/std/json.c
index 9de587d..07d943c 100644
--- a/test/c/std/json.c
+++ b/test/c/std/json.c
@@ -349,6 +349,55 @@
 }
 
 const char*  //
+test_strconv_hexadecimal() {
+  CHECK_FOCUS(__func__);
+
+  {
+    const char* str = "6A6b7";  // The "7" should be ignored.
+    wuffs_base__slice_u8 dst = global_have_slice;
+    wuffs_base__slice_u8 src =
+        wuffs_base__make_slice_u8((void*)str, strlen(str));
+    size_t have = wuffs_base__hexadecimal__decode2(dst, src);
+    if (have != 2) {
+      RETURN_FAIL("decode2: have %zu, want 2", have);
+    }
+    if (global_have_array[0] != 0x6A) {
+      RETURN_FAIL("decode2: dst[0]: have 0x%02X, want 0x6A",
+                  (int)(global_have_array[0]));
+    }
+    if (global_have_array[1] != 0x6B) {
+      RETURN_FAIL("decode2: dst[1]: have 0x%02X, want 0x6B",
+                  (int)(global_have_array[1]));
+    }
+  }
+
+  {
+    const char* str = "\\xa9\\x00\\xFe";
+    wuffs_base__slice_u8 dst = global_have_slice;
+    wuffs_base__slice_u8 src =
+        wuffs_base__make_slice_u8((void*)str, strlen(str));
+    size_t have = wuffs_base__hexadecimal__decode4(dst, src);
+    if (have != 3) {
+      RETURN_FAIL("decode4: have %zu, want 3", have);
+    }
+    if (global_have_array[0] != 0xA9) {
+      RETURN_FAIL("decode4: dst[0]: have 0x%02X, want 0xA9",
+                  (int)(global_have_array[0]));
+    }
+    if (global_have_array[1] != 0x00) {
+      RETURN_FAIL("decode4: dst[1]: have 0x%02X, want 0x00",
+                  (int)(global_have_array[1]));
+    }
+    if (global_have_array[2] != 0xFE) {
+      RETURN_FAIL("decode4: dst[2]: have 0x%02X, want 0xFE",
+                  (int)(global_have_array[2]));
+    }
+  }
+
+  return NULL;
+}
+
+const char*  //
 test_strconv_parse_number_f64() {
   CHECK_FOCUS(__func__);
 
@@ -1830,6 +1879,7 @@
     // These strconv tests are really testing the Wuffs base library. They
     // aren't specific to the std/json code, but putting them here is as good
     // as any other place.
+    test_strconv_hexadecimal,
     test_strconv_hpd_rounded_integer,
     test_strconv_hpd_shift,
     test_strconv_parse_number_f64,