CBOR usually arrives from somewhere you do not control: a network peer, a sensor, a browser relaying an authenticator. zucbor treats every input as hostile, and this vignette shows what that means in practice.
library(zucbor)
hex <- function(h) as.raw(strtoi(substring(h, seq(1, nchar(h), 2), seq(2, nchar(h), 2)), 16L))cbor_decode() works in two phases. The first walks the
whole input and checks it: well-formedness, UTF-8, the content of the
tags it knows, duplicate map keys and the limits. Only when all of that
passes does the second phase build R values. So a nine-byte header that
claims an array of 2^64 − 1 elements is refused before R allocates
anything:
tryCatch(cbor_decode(hex("9bffffffffffffffff")),
zucbor_error = function(e) conditionMessage(e))
#> [1] "CBOR parse error at byte 0: unexpected end of data"cbor_validate() runs exactly that first phase and
nothing else. It returns TRUE or FALSE; with
error = TRUE it raises the condition that explains a
FALSE:
cbor_validate(hex("830102"))
#> [1] FALSE
tryCatch(cbor_validate(hex("830102"), error = TRUE), zucbor_error = function(e) e$offset)
#> [1] 3The two agree about the bytes. They disagree only where the bytes are
valid CBOR that R cannot hold, such as a text string containing U+0000,
which cbor_decode() refuses with
zucbor_unrepresentable.
Three limits bound what an input can cost.
| Limit | Default | What it bounds |
|---|---|---|
max_size |
64 MiB | the input, in bytes |
max_depth |
256 | nesting of arrays, maps and tags (at most 1023) |
max_items |
1,000,000 | data items, counting every tag and string chunk |
max_items matters even though max_size
bounds the input: one byte of CBOR can become dozens of bytes of R
objects, so 64 MiB of empty arrays would otherwise ask R for
gigabytes.
deep <- c(rep(as.raw(0x81), 300), as.raw(0x00)) # [[[...[0]...]]]
tryCatch(cbor_decode(deep), zucbor_limit_error = function(e) c(e$limit, e$limit_value))
#> [1] "max_depth" "256"
tryCatch(cbor_decode(hex("83010203"), max_items = 3),
zucbor_limit_error = function(e) class(e)[1])
#> [1] "zucbor_item_limit"cbor_read() reads a file, URL or connection, and never
reads more than max_size + 1 bytes, so an endless stream
fails instead of filling memory. A log or a socket that is a long CBOR
sequence is read item by item with cbor_read_seq(each =):
then max_size bounds each item rather than the stream, and
each item is still checked whole before it is built.
RFC 8949 leaves duplicate keys to the application, and they are a classic way to make two parsers of the same signed structure see different values. zucbor refuses them by default, comparing keys by value: the integer 1 written in one byte and in two is the same key.
cbor_validate(hex("a2010018010000")) # {1: 0, 1: 0}, the second 1 longer
#> [1] FALSE
cbor_decode(hex("a2616101616102"), duplicate_keys = TRUE) # {"a": 1, "a": 2}
#> <cbor_map: 2 entries>
#> [[a]]
#> [1] 1
#> [[a]]
#> [1] 2When duplicates are allowed, the map comes back as a
cbor_map, which keeps both entries in order; a named list
could not say which was which.
Some protocols require a single encoding for each value, so that a
re-encoded structure matches what was signed.
deterministic = TRUE refuses anything that is not RFC 8949
core deterministic encoding: the shortest integers, lengths and floats,
definite lengths, map keys in bytewise order and bignums in preferred
form.
cbor_validate(hex("1801"), deterministic = TRUE) # 1 in two bytes
#> [1] FALSE
cbor_validate(hex("a203040102"), deterministic = TRUE) # keys out of order
#> [1] FALSEEverything cbor_encode() writes passes that check, and
decoding then encoding deterministic input gives back the same
bytes.
Every error is a condition inheriting zucbor_error, so
one handler can catch them all, and its subclass says what went
wrong:
classify <- function(x) tryCatch({cbor_decode(x); "ok"}, zucbor_error = function(e) class(e)[1])
vapply(list(hex("8201"), hex("61ff"), hex("a201000100"), hex("6100"), hex("0102")), classify, "")
#> [1] "zucbor_parse_error" "zucbor_invalid_error" "zucbor_duplicate_key"
#> [4] "zucbor_unrepresentable" "zucbor_parse_error"Conditions raised while checking input also carry
offset, the byte offset of the fault, and
status, the name of the underlying status. See
?"zucbor-conditions" for the whole hierarchy.