Skip to content

CBOR: cbor_tag_handler_t::ignore does not ignore tags 0-5 and 21-23 (head bytes 0xC0-0xC5, 0xD5-0xD7) #5315

Description

@nlohmann

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 0xC00xDB is major type 6 (tag). The switch in parse_cbor_internal() enumerates only 0xC60xD4 and 0xD80xDB (binary_reader.hpp:763-781), so head bytes 0xC00xC5 and 0xD50xD7 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 0xC00xDB.

Expected vs. actual results

Expected: ignore skips the tag head and parses the enclosed data item, uniformly for all of 0xC00xDB.

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
0xC00xC5 0–5 n n n
0xC60xD4 6–20 n Y Y
0xD50xD7 21–23 n n n
0xD80xDB 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 0xc00xc5 and 0xd50xd7 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 0xD80xDB range — the two omitted ranges were likely just missed then.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions