Description
cbor_tag_handler_t::ignore is documented as "ignore tags", but from_cbor(…, cbor_tag_handler_t::ignore) still throws parse_error.112 for CBOR tags 0–5 and 21–23.
Every byte in 0xC0–0xDB is major type 6 (tag). The switch in parse_cbor_internal() enumerates only 0xC6–0xD4 and 0xD8–0xDB (binary_reader.hpp:763-781), so head bytes 0xC0–0xC5 and 0xD5–0xD7 fall through to default: and are reported as "invalid byte" — in every handler mode, including ignore and store.
The gap is at both ends of the range and covers the tags most likely to appear in real data: 0 (RFC 3339 date/time string), 1 (epoch time), 2/3 (bignum), 4 (decimal fraction), 5 (bigfloat), and 21/22/23 (expected base64url/base64/base16 conversion).
There is also an interop consequence. An encoder using preferred serialization (RFC 8949 §4.1) encodes a tag number below 24 in the 1-byte head, e.g. tag 23 as 0xD7. This library writes binary subtypes as 0xD8 0x17 instead (see write_cbor, binary_writer.hpp:310-332), so its own round-trip works — but a byte string tagged 23 by a conforming encoder cannot be read back at all, in any mode.
Reproduction steps
Parse a tagged value with cbor_tag_handler_t::ignore for each tag head byte in 0xC0–0xDB.
Expected vs. actual results
Expected: ignore skips the tag head and parses the enclosed data item, uniformly for all of 0xC0–0xDB.
Actual, sweeping the whole range with the tagged item being a byte string (Y = parses, n = parse_error.112):
| head byte |
tag |
error |
ignore |
store |
0xC0–0xC5 |
0–5 |
n |
n |
n |
0xC6–0xD4 |
6–20 |
n |
Y |
Y |
0xD5–0xD7 |
21–23 |
n |
n |
n |
0xD8–0xDB |
1/2/4/8-byte argument |
n |
Y |
Y |
A realistic document fails:
map with tag-0 datetime, ignore : THROW: [json.exception.parse_error.112]
parse error at byte 4: syntax error while parsing CBOR value: invalid byte: 0xC0
Minimal code example
#include <nlohmann/json.hpp>
#include <iostream>
using json = nlohmann::json;
int main()
{
// {"t": 0("2026-07-27T00:00:00Z")} -- a text string tagged with tag 0
const std::vector<std::uint8_t> v{
0xA1, 0x61, 0x74, 0xC0, 0x74, '2','0','2','6','-','0','7','-','2','7',
'T','0','0',':','0','0',':','0','0','Z'};
// works: tag 6 in the same position is skipped
const std::vector<std::uint8_t> w{0xC6, 0x18, 0x2A};
std::cout << json::from_cbor(w, true, true, json::cbor_tag_handler_t::ignore) << "\n";
// throws: tag 0 is not skipped
std::cout << json::from_cbor(v, true, true, json::cbor_tag_handler_t::ignore) << "\n";
}
Error messages
[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing CBOR value: invalid byte: 0xC0
Compiler and operating system
g++ 13.3.0, Ubuntu 24.04, -std=c++11
Library version
develop @ 8ec98e2
Would the fix be breaking?
Not in the API/ABI sense — no signatures, types, or enumerators change, and the amalgamated header stays source- and binary-compatible.
Behaviorally it is a narrow, opt-in-scoped change:
- Default behavior is untouched.
cbor_tag_handler_t::error is the default and must keep throwing on every tag, including these. Callers who never pass a tag handler see no difference at all.
ignore/store callers would see input that currently throws start to parse. That is a real behavior change, but it moves in the direction those callers explicitly asked for. Code that relies on ignore throwing for tag 0 while silently accepting tag 6 seems very unlikely to be intentional.
- This is not a request to interpret the tags. No bignum/date decoding is implied — the tag head is skipped and the enclosed data item is parsed on its own, exactly as tags 6–20 are handled today. A tagged text string stays a string;
0xC2 over a byte string stays a byte string, not an integer.
- Test and doc churn.
unit-cbor.cpp's "all CBOR first bytes" test lists 0xc0–0xc5 and 0xd5–0xd7 in its unsupported set, and the "Incomplete mapping" warning in docs/mkdocs/docs/features/binary_formats/cbor.md lists them as parse errors. Both would need updating, which also means the current behavior is documented rather than accidental — so this may warrant a release note even though nothing breaks.
A conservative alternative, if changing ignore is considered too much: leave the reader as is and only document the asymmetry more explicitly. That keeps the interop gap, though.
Related: #1968 added tag skipping in 3.9.0 and closed on the 0xD9 0xD9 0xF7 self-describe case, which lands inside the supported 0xD8–0xDB range — the two omitted ranges were likely just missed then.
Description
cbor_tag_handler_t::ignoreis documented as "ignore tags", butfrom_cbor(…, cbor_tag_handler_t::ignore)still throwsparse_error.112for CBOR tags 0–5 and 21–23.Every byte in
0xC0–0xDBis major type 6 (tag). Theswitchinparse_cbor_internal()enumerates only0xC6–0xD4and0xD8–0xDB(binary_reader.hpp:763-781), so head bytes0xC0–0xC5and0xD5–0xD7fall through todefault:and are reported as "invalid byte" — in every handler mode, includingignoreandstore.The gap is at both ends of the range and covers the tags most likely to appear in real data: 0 (RFC 3339 date/time string), 1 (epoch time), 2/3 (bignum), 4 (decimal fraction), 5 (bigfloat), and 21/22/23 (expected base64url/base64/base16 conversion).
There is also an interop consequence. An encoder using preferred serialization (RFC 8949 §4.1) encodes a tag number below 24 in the 1-byte head, e.g. tag 23 as
0xD7. This library writes binary subtypes as0xD8 0x17instead (seewrite_cbor,binary_writer.hpp:310-332), so its own round-trip works — but a byte string tagged 23 by a conforming encoder cannot be read back at all, in any mode.Reproduction steps
Parse a tagged value with
cbor_tag_handler_t::ignorefor each tag head byte in0xC0–0xDB.Expected vs. actual results
Expected:
ignoreskips the tag head and parses the enclosed data item, uniformly for all of0xC0–0xDB.Actual, sweeping the whole range with the tagged item being a byte string (
Y= parses,n=parse_error.112):errorignorestore0xC0–0xC50xC6–0xD40xD5–0xD70xD8–0xDBA realistic document fails:
Minimal code example
Error messages
Compiler and operating system
g++ 13.3.0, Ubuntu 24.04,
-std=c++11Library version
develop@ 8ec98e2Would the fix be breaking?
Not in the API/ABI sense — no signatures, types, or enumerators change, and the amalgamated header stays source- and binary-compatible.
Behaviorally it is a narrow, opt-in-scoped change:
cbor_tag_handler_t::erroris the default and must keep throwing on every tag, including these. Callers who never pass a tag handler see no difference at all.ignore/storecallers would see input that currently throws start to parse. That is a real behavior change, but it moves in the direction those callers explicitly asked for. Code that relies onignorethrowing for tag 0 while silently accepting tag 6 seems very unlikely to be intentional.0xC2over a byte string stays a byte string, not an integer.unit-cbor.cpp's "all CBOR first bytes" test lists0xc0–0xc5and0xd5–0xd7in itsunsupportedset, and the "Incomplete mapping" warning indocs/mkdocs/docs/features/binary_formats/cbor.mdlists them as parse errors. Both would need updating, which also means the current behavior is documented rather than accidental — so this may warrant a release note even though nothing breaks.A conservative alternative, if changing
ignoreis considered too much: leave the reader as is and only document the asymmetry more explicitly. That keeps the interop gap, though.Related: #1968 added tag skipping in 3.9.0 and closed on the
0xD9 0xD9 0xF7self-describe case, which lands inside the supported0xD8–0xDBrange — the two omitted ranges were likely just missed then.