cbor

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 25, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

encoding/cbor

A bounded, reflection-free RFC 8949 codec for TinyGo and Go. It preserves signed integer labels, rejects hostile input, and never maps arbitrary structs, which suits WebAuthn authenticator data and COSE keys as well as compact realtime message formats.

Supported subset

  • unsigned and negative integers, including the full encoded negative range on decode
  • byte and UTF-8 text strings
  • arrays and maps
  • booleans, null, tags, and IEEE 754 half/single/double input
  • definite and indefinite-length input
  • deterministic output, with a selectable map key ordering
  • incremental io.Reader decoding with input, nesting, container, string, and retained-raw-message limits
  • optional duplicate map key rejection; that mode validates a bounded root item before exposing its tokens
  • explicit sequence mode; normal mode rejects bytes after one root item

Two decoders, two encoders

Decoder reads incrementally from an io.Reader and copies what it returns. Use it for input that arrives over a connection, or that is larger than memory should hold at once.

Reader reads from a byte slice already in memory. It borrows strings instead of copying them, skips an item without allocating it, and captures a sub-item at any depth — none of which an incremental decoder can do. Reuse one per connection with Reset.

On the writing side, Encoder writes whole items to an io.Writer, and the Append* functions write into a caller-owned []byte. AppendArrayHeader and AppendMapHeader let a nested structure be written in one pass, rather than requiring every child to be a finished RawMessage first.

buf = cbor.AppendArrayHeader(buf[:0], 4)
buf = cbor.AppendUint(buf, tick)
buf = moveX.AppendCBORTo(buf)   // a type carrying its own encoding
buf = cbor.AppendUint(buf, buttons)

Carrying your own encoding

A type says how it encodes by implementing one of:

type Appender  interface { AppendCBORTo(dst []byte) []byte }
type Decodable interface { DecodeCBORFrom(data []byte) error } // pointer receiver

They append into the destination rather than returning bytes, so a nested field costs no allocation of its own. AppendCBORTo returns no error; the obligation that creates is that it appends exactly one valid, complete item. Nothing in the type system can hold a foreign implementation to that, so Profile.ValidateAppended checks what it actually wrote.

Nesting

Skip, ReadRaw and Profile.Validate drive their own recursion and bound it by MaxNestedLevels. ReadArrayHeader and ReadMapHeader do not — they read one head and return, so the walk over the container is your loop and its depth is yours to bound. Decoder differs here: it keeps a frame stack and refuses a container past the limit from ReadToken.

That is deliberate. A frame stack is the state Reader does not keep, and keeping one would cost an allocation per reader in the path that exists to have none. For untrusted input, validate first:

if err := cbor.World().Validate(data); err != nil { return err }
// now the bytes are known to be legal under the profile's bounds

Foreign types compose to any depth on both sides at zero allocation: encoding because each AppendCBORTo appends into the buffer its parent is already building, decoding because ReadRaw hands each one its own bytes wherever the field sits.

How deep is deep

MaxNestedLevels defaults to 10000 — the same number encoding/json uses, and for the same reason. It is a stack safety net, not a budget: both profiles take it, and no schema should ever meet it.

The net is not optional here. Every walk in this package recurses, and measured on darwin/arm64:

survives fails at how
host Go ~1,000,000 levels ~2,000,000 fatal error: stack overflow
TinyGo ~46,500 levels ~47,000 bare SIGSEGV, no message

TinyGo does not detect stack exhaustion. That is why a limit exists even though a caller should never see it. About 180 bytes of stack go per level, so a target with a small stack wants a smaller number.

Narrow it deliberately if you want a structural check on a known schema:

tight := cbor.Wire().WithMaxNestedLevels(12)

MaxNestedLevels counts nested containers, and a tag is one of them — a tag over an array is two levels, not one.

The two ways of reading count differently, and the difference decides what bound you need:

  • Validate walks the whole document, so depths add. An envelope wrapping a message costs the envelope's depth plus the message's.
  • ReadRaw measures a captured item from zero, so decoding an envelope field by field and handing each payload on as raw bytes costs the larger of the two, not their sum.

This matters for anything that carries a subtree of a document: a patch, a delta, a log entry quoting a message. Such a document is always deeper than the document it describes — for the shape [base, [[op, [path...], value], ...]] it is exactly three levels deeper. A profile whose bound only just fits its messages will refuse every patch of one, and that shows up the first time a delta is generated, not before.

Derive an envelope bound instead of guessing one:

envelope := cbor.World().WithMaxNestedLevels(cbor.World().MaxNestedLevels() + 3)

Neither profile bounds nesting to anything a schema would meet, so this only matters if you narrow one. MaxNestedLevels(), MaxContainerItems() and MaxInputBytes() report what a profile carries, so the arithmetic can be written down rather than assumed.

Profiles

A Profile is a named subset of CBOR that both ends of a protocol agree on. CTAP2 canonical CBOR, COSE and RFC 8949 §4.2 deterministic encoding are all restrictions of this kind.

type Profile struct {
	Name              string
	RequireSortedKeys bool
	KeyOrder          KeyOrder
	RejectMaps        bool
	RejectTags        bool
	RejectFloats      bool
	RejectIndefinite  bool
	RejectTextKeys    bool
}

The zero value restricts nothing — every well-formed CBOR item is legal under it. Two presets are supplied because they are standards rather than anyone's application: Canonical() for CTAP2 and COSE (length-first keys, no indefinite lengths, floats permitted) and Deterministic() for RFC 8949 §4.2.1 (the same with bytewise keys).

Anything else is a struct literal. A compact fixed-schema wire format, for instance:

wire := cbor.Profile{
	Name:             "wire",
	RejectMaps:       true,
	RejectTags:       true,
	RejectFloats:     true,
	RejectIndefinite: true,
	RejectTextKeys:   true,
}

if err := wire.Validate(data, opts); err != nil { /* not a wire message */ }

Validate answers the question without decoding the item into anything.

Profiles carry no limits

A profile says what the format may contain. DecoderOptions says what this process is willing to read. They are separate because they answer to different owners:

profile limits
belongs to the protocol the deployment
must both peers agree? yes — a disagreement is a defect no — a server and a browser client differ by design
changing it changes the format changes a setting

So they are passed together rather than bundled: Validate(data, opts), NewReader(data, opts), ReaderOver(data, opts). Mixing them makes a deployment decision look like a protocol change, and hides a protocol change inside a deployment one.

Floats

Floats are ordinary CBOR here and are supported throughout: AppendFloat, ReadFloat, shortest-form encoding, one canonical NaN. Profile.RejectFloats is opt-in, for a format that carries scaled integers and wants a float in it to be a caught protocol violation rather than a value. It is refused at encode and at decode.

Fixed-point is not a concept this package has. A scaled type carries its own encoding through AppendCBORTo / DecodeCBORFrom, and the codec never learns what the scale was.

Cost

A fixed-shape wire message, encoded and decoded through a reused buffer and a reused Reader:

ns/op allocs/op
encode, Append* 9.2 0
encode, WriteArray over RawMessage children 100.4 5
decode, Reader 42.4 0
decode, Decoder over an io.Reader 168.0 7
Wire().Validate 23.9 0
World().Validate 90.0 0
Skip an unknown field 24.2 0

Measured on darwin/arm64 with go test -bench . -benchtime 3s. The point is the allocation column: the steady state of a tick loop has to be free of it.

Errors

Every refusal wraps one of the package sentinels, so errors.Is works as before, and adds where it happened:

cbor: malformed input: reserved additional information 28 (at byte 5, at [2][1])

errors.As reaches the *cbor.Error for the offset and the route. Nothing tracks a route while decoding succeeds — it is built afterwards by walking the input again — so a message that decodes cleanly pays nothing for this.

Map key ordering

RFC 8949 defines two deterministic orderings and they disagree. EncoderOptions.KeyOrder selects one:

  • LengthFirstKeyOrder (the zero value) sorts shorter encoded keys first, then bytewise. This is §4.2.3, and it is what CTAP2 canonical CBOR and COSE require.
  • BytewiseKeyOrder sorts bytewise over the whole encoded key. This is §4.2.1 Core Deterministic Encoding.

The keys -1 and 100 encode as 20 and 1864, and the two rules order them differently. WriteRaw enforces whichever ordering the encoder was configured with, so a raw item canonical under one is rejected under the other.

Intentionally unsupported

  • reflection-based struct or arbitrary Go value mapping
  • CBOR diagnostic notation
  • unassigned simple values and CBOR undefined
  • indefinite-length encoding (it cannot be deterministic)
  • COSE signing, encryption, or key management

For untrusted maps, enable DecoderOptions.RejectDuplicateMapKeys. This mode may retain one root item up to MaxRawMessageBytes so duplicate keys can be checked before application code observes any field.

Documentation

Overview

Package cbor implements a bounded, reflection-free subset of RFC 8949.

Decoder exposes typed tokens instead of mapping CBOR to arbitrary Go values. Encoder writes deterministic output and provides helpers for deterministically ordered arrays and maps. Nothing in either path uses reflection, io.ReadAll, or struct tags.

Map key ordering

RFC 8949 defines two deterministic map key orderings, and they produce different bytes for the same map. EncoderOptions.KeyOrder selects between them. The zero value is LengthFirstKeyOrder, the section 4.2.3 length-first ordering that CTAP2 canonical CBOR and COSE require; BytewiseKeyOrder is the section 4.2.1 Core Deterministic Encoding ordering. Callers that care which one reaches the wire should set the field rather than inherit it.

Bounds

Every limit is explicit and every limit is enforced before anything is reserved: a declared length never becomes an allocation on its own, because the length prefix of a string arrives before its bytes and is attacker controlled. DecoderOptions.RejectDuplicateMapKeys additionally validates a bounded root item before exposing any of its tokens, at the cost of retaining up to MaxRawMessageBytes.

Profiles

A Profile is a named subset of CBOR that both ends of a protocol agree on -- CTAP2 canonical CBOR and RFC 8949 deterministic encoding are two, and Canonical and Deterministic supply them. It carries no resource limits: those are a property of the process doing the reading, not of the format, and they live in DecoderOptions. A caller with its own subset writes a struct literal.

Intended uses

Formats where preserving integer labels, holding a byte-exact encoding, and rejecting hostile input matter more than mapping arbitrary Go values. WebAuthn authenticator data and COSE_Key are one such case; a compact fixed-schema message format is another.

The package intentionally does not support struct tags, diagnostic notation, COSE signing, encryption, key management, or indefinite-length output.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrMalformed       = errors.New("cbor: malformed input")
	ErrTruncated       = errors.New("cbor: truncated input")
	ErrLimitExceeded   = errors.New("cbor: limit exceeded")
	ErrDuplicateMapKey = errors.New("cbor: duplicate map key")
	ErrExtraneousData  = errors.New("cbor: extraneous data after root item")
	ErrUnexpectedToken = errors.New("cbor: unexpected token")
	ErrIntegerOverflow = errors.New("cbor: integer does not fit int64")
	// ErrProfileViolation reports CBOR that is well formed but not legal under
	// the profile it was read or written under.
	ErrProfileViolation = errors.New("cbor: profile violation")
	// ErrFloatRefused reports a float where the configuration carries scaled
	// integers instead. It is an ErrProfileViolation with its own identity,
	// because a format that excludes floats usually does so to keep results
	// reproducible, and that is worth naming separately from every other way a
	// profile can be violated.
	ErrFloatRefused = fmt.Errorf("%w: float", ErrProfileViolation)
)

Functions

func AppendArrayHeader

func AppendArrayHeader(dst []byte, n int) []byte

AppendArrayHeader appends a definite-length array head for n items, which the caller then appends in order.

This is the half of the encoder that was missing. WriteArray takes children that are already finished byte slices, so a parent cannot be written until every descendant has been built and copied; the cost of that scales with the depth of the tree. Appending a header instead lets a nested structure be written in one pass into one buffer.

func AppendBool

func AppendBool(dst []byte, v bool) []byte

AppendBool appends a boolean.

func AppendBytes

func AppendBytes(dst, v []byte) []byte

AppendBytes appends a byte string.

func AppendFloat

func AppendFloat(dst []byte, v float64) []byte

AppendFloat appends a float in the shortest form that round-trips, which is what makes float output deterministic. A wire Profile refuses floats outright; this exists for the world profile and for COSE.

func AppendInt

func AppendInt(dst []byte, v int64) []byte

AppendInt appends an integer of either sign in its shortest form.

func AppendMapHeader

func AppendMapHeader(dst []byte, n int) []byte

AppendMapHeader appends a definite-length map head for n key/value pairs.

Nothing sorts the pairs the caller then appends. A streaming writer cannot: sorting needs every key at once, which is the shape this exists to avoid. Generated code knows its field set before it runs and can emit the pairs in order; hand-written callers that need the sort should keep using Encoder.WriteMap, and either way Validate under a Profile is what proves the result.

func AppendNegative

func AppendNegative(dst []byte, arg uint64) []byte

AppendNegative appends the negative integer -1-arg. It reaches the whole encoded negative range, including the half below the int64 floor that AppendInt cannot express; the decoder has always been able to read that far.

func AppendNull

func AppendNull(dst []byte) []byte

AppendNull appends the null value.

func AppendRaw

func AppendRaw(dst []byte, raw RawMessage) []byte

AppendRaw appends an item that is already encoded, without validating it.

func AppendTag

func AppendTag(dst []byte, tag uint64) []byte

AppendTag appends a tag head. The tagged content is the next item appended.

func AppendText

func AppendText(dst []byte, v string) []byte

AppendText appends a text string. It does not validate UTF-8.

func AppendUint

func AppendUint(dst []byte, v uint64) []byte

AppendUint appends an unsigned integer in its shortest form.

func Validate

func Validate(data []byte, opts DecoderOptions) error

Validate checks that data contains exactly one bounded CBOR item.

Types

type Appender

type Appender interface {
	AppendCBORTo(dst []byte) []byte
}

Appender is implemented by a type that carries its own CBOR encoding.

AppendCBORTo appends exactly one complete CBOR item to dst and returns the extended buffer, the way the strconv.Append and time.AppendFormat families do. It takes a destination rather than returning a fresh slice because a value returning bytes allocates once per value and undoes the caller's buffer pooling at every nested field, which a caller encoding thousands of messages a second cannot afford.

It returns no error. The append path below this point carries none, and the obligation that creates is on the implementation: for every value of the type, it must append one valid, complete CBOR item. Nothing here can check that, so an encoder that cares -- one writing under a Profile -- should validate the bytes a foreign implementation appended before they reach the wire. See Profile.ValidateAppended.

type Decodable

type Decodable interface {
	DecodeCBORFrom(data []byte) error
}

Decodable is implemented by a type that carries its own CBOR decoding.

DecodeCBORFrom decodes the single CBOR item in data into the receiver, which must be a pointer. data holds exactly one item and nothing after it; Reader.ReadRaw is what produces it, at whatever depth the field sits.

type Decoder

type Decoder struct {
	// contains filtered or unexported fields
}

Decoder incrementally reads CBOR from an io.Reader without io.ReadAll. ReadToken exposes container boundaries; typed Read methods are conveniences that reject a token of the wrong kind.

Example
data := []byte{0xa2, 0x01, 0x02, 0x20, 0x42, 0xca, 0xfe}
d, _ := NewDecoder(bytes.NewReader(data), DecoderOptions{RejectDuplicateMapKeys: true})
pairs, _, _ := d.ReadMap()
fmt.Println(pairs)
Output:
2

func NewDecoder

func NewDecoder(r io.Reader, opts DecoderOptions) (*Decoder, error)

func (*Decoder) ReadArray

func (d *Decoder) ReadArray() (length int, indefinite bool, err error)

func (*Decoder) ReadBool

func (d *Decoder) ReadBool() (bool, error)

func (*Decoder) ReadBytes

func (d *Decoder) ReadBytes() ([]byte, error)

func (*Decoder) ReadFloat

func (d *Decoder) ReadFloat() (float64, error)

func (*Decoder) ReadInt

func (d *Decoder) ReadInt() (int64, error)

func (*Decoder) ReadMap

func (d *Decoder) ReadMap() (pairs int, indefinite bool, err error)

func (*Decoder) ReadNull

func (d *Decoder) ReadNull() error

func (*Decoder) ReadRaw

func (d *Decoder) ReadRaw() (RawMessage, error)

ReadRaw reads and validates exactly one item while retaining at most MaxRawMessageBytes. In non-sequence mode it also rejects trailing data.

func (*Decoder) ReadTag

func (d *Decoder) ReadTag() (uint64, error)

func (*Decoder) ReadText

func (d *Decoder) ReadText() (string, error)

func (*Decoder) ReadToken

func (d *Decoder) ReadToken() (Token, error)

ReadToken reads the next typed token. Definite container end tokens are synthesized when the declared item count is exhausted. For a non-sequence decoder, the call after the root token returns io.EOF only after confirming that no trailing byte exists.

func (*Decoder) ReadUint

func (d *Decoder) ReadUint() (uint64, error)

type DecoderOptions

type DecoderOptions struct {
	MaxInputBytes          int64
	MaxNestedLevels        int
	MaxContainerItems      int
	MaxStringBytes         int
	MaxRawMessageBytes     int
	RejectDuplicateMapKeys bool
	Sequence               bool
	// RejectFloats refuses every float on input. A format that carries scaled
	// integers wants this: a float in it is a protocol violation rather than a
	// value, and catching it on arrival makes it an error rather than a
	// disagreement about what the message meant. Profile.RejectFloats sets it.
	RejectFloats bool
}

DecoderOptions controls resource limits and stream behavior. A zero limit selects a conservative default. Negative limits are rejected by NewDecoder.

type Encoder

type Encoder struct {
	// contains filtered or unexported fields
}

Encoder writes deterministic RFC 8949. Each method writes one complete item. WriteArray and WriteMap validate their child RawMessages before writing; WriteMap sorts keys under EncoderOptions.KeyOrder, which defaults to the length-first ordering CTAP2 and COSE require rather than to RFC 8949 Core Deterministic Encoding. Indefinite-length output is not supported, because it cannot be deterministic.

Example
var out bytes.Buffer
e, _ := NewEncoder(&out, EncoderOptions{})
key, _ := MarshalText("alg")
_ = e.WriteMap([]MapEntry{{Key: key, Value: MarshalInt(-7)}})
fmt.Println(len(out.Bytes()))
Output:
6

func NewEncoder

func NewEncoder(w io.Writer, opts EncoderOptions) (*Encoder, error)
Example
package main

import (
	"bytes"
	"fmt"

	"github.com/shibukawa/tinygodriver/encoding/cbor"
)

func main() {
	var encoded bytes.Buffer
	encoder, err := cbor.NewEncoder(&encoded, cbor.EncoderOptions{})
	if err != nil {
		panic(err)
	}
	if err := encoder.WriteText("ok"); err != nil {
		panic(err)
	}
	fmt.Printf("%x\n", encoded.Bytes())
}
Output:
626f6b

func (*Encoder) Reset

func (e *Encoder) Reset(w io.Writer) error

Reset points the Encoder at a new writer, keeping its options, so a session can hold one encoder instead of allocating one per message.

func (*Encoder) WriteArray

func (e *Encoder) WriteArray(items []RawMessage) error

func (*Encoder) WriteBool

func (e *Encoder) WriteBool(v bool) error

func (*Encoder) WriteBytes

func (e *Encoder) WriteBytes(v []byte) error

func (*Encoder) WriteFloat

func (e *Encoder) WriteFloat(v float64) error

func (*Encoder) WriteInt

func (e *Encoder) WriteInt(v int64) error

func (*Encoder) WriteMap

func (e *Encoder) WriteMap(entries []MapEntry) error

func (*Encoder) WriteNegative

func (e *Encoder) WriteNegative(arg uint64) error

WriteNegative writes the negative integer -1-arg. See AppendNegative.

func (*Encoder) WriteNull

func (e *Encoder) WriteNull() error

func (*Encoder) WriteRaw

func (e *Encoder) WriteRaw(raw RawMessage) error

WriteRaw writes one already deterministic item after validating it.

func (*Encoder) WriteTag

func (e *Encoder) WriteTag(tag uint64, content RawMessage) error

func (*Encoder) WriteText

func (e *Encoder) WriteText(v string) error

func (*Encoder) WriteUint

func (e *Encoder) WriteUint(v uint64) error

type EncoderOptions

type EncoderOptions struct {
	MaxNestedLevels   int
	MaxContainerItems int
	MaxStringBytes    int
	// KeyOrder selects the map key ordering WriteMap emits and WriteRaw
	// enforces. The zero value keeps the CTAP2 and COSE ordering.
	KeyOrder KeyOrder
}

EncoderOptions controls deterministic output limits and the map key ordering. Zero selects a conservative default. Indefinite-length output is deliberately unsupported.

type Error

type Error struct {
	// Offset is the byte position in the input at which the failing item began.
	Offset int64
	// Path names the container route to the failing item where one is known,
	// as it would be written in diagnostic notation, and is empty otherwise.
	Path string
	Err  error
}

Error locates a decode failure. It wraps one of the package sentinels, so errors.Is keeps working unchanged, and adds the offset at which the failing item began.

A byte-format error is otherwise only as useful as the caller's ability to find it. Rejecting one attestation needs the sentinel; comparing two builds that disagree about a message needs the position.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type KeyOrder

type KeyOrder uint8

KeyOrder selects which deterministic map key ordering an Encoder emits and enforces. RFC 8949 defines two, and they produce different bytes for the same map, so the choice is part of the wire contract rather than an internal detail. The zero value is LengthFirstKeyOrder.

const (
	// LengthFirstKeyOrder sorts shorter encoded keys first and breaks ties
	// bytewise. This is the length-first ordering of RFC 8949 section 4.2.3,
	// which is what CTAP2 canonical CBOR and COSE require.
	LengthFirstKeyOrder KeyOrder = iota
	// BytewiseKeyOrder sorts bytewise lexicographically over the whole encoded
	// key with no length pass. This is RFC 8949 section 4.2.1 Core
	// Deterministic Encoding.
	BytewiseKeyOrder
)

func (KeyOrder) String

func (o KeyOrder) String() string

type MapEntry

type MapEntry struct {
	Key   RawMessage
	Value RawMessage
}

MapEntry is one encoded key/value pair for Encoder.WriteMap. Key and Value must each contain exactly one CBOR item. Keys are sorted according to Core Deterministic Encoding before they are written.

type Profile

type Profile struct {
	// Name appears in this profile's refusals. It is otherwise unused.
	Name string

	// RequireSortedKeys demands that map keys appear in KeyOrder, strictly
	// ascending. RFC 8949 deterministic encoding requires this; plain CBOR does
	// not, so the zero value does not either.
	RequireSortedKeys bool
	// KeyOrder selects which deterministic ordering RequireSortedKeys demands.
	// See KeyOrder: the two RFC 8949 orderings produce different bytes.
	KeyOrder KeyOrder

	// RejectMaps refuses major type 5 outright. A format that encodes structs
	// as fixed-order arrays has no use for it, and refusing it is what keeps
	// field names off the wire.
	RejectMaps bool
	// RejectTags refuses major type 6.
	RejectTags bool
	// RejectFloats refuses every float, at encode and at decode. A format
	// carrying scaled integers wants this: a float in it is a protocol
	// violation rather than a value.
	RejectFloats bool
	// RejectIndefinite refuses indefinite-length items. Deterministic encoding
	// requires this, since an indefinite item has more than one spelling.
	RejectIndefinite bool
	// RejectTextKeys refuses a text string as a map key, leaving integer
	// labels. COSE and CTAP2 use integer labels for the same reason.
	RejectTextKeys bool
}

A Profile is a named restriction on which CBOR is legal: a subset of the format that both ends of a protocol agree on. CTAP2 canonical CBOR, COSE and RFC 8949 section 4.2 deterministic encoding are all restrictions of this kind.

A Profile carries no resource limits, and the distinction is the point.

What a profile says is a property of the protocol: both peers must agree on it, a disagreement is a defect, and changing it changes the format. How large an item a particular process is willing to read is a property of that process: a dedicated server and a browser client can hold different answers, and there is nothing to agree on. Those live in DecoderOptions, chosen per deployment, and are passed alongside a profile rather than baked into one.

Mixing them, which this type used to do, makes a deployment decision look like a protocol change and hides a protocol change inside a deployment one.

The zero value restricts nothing: every well-formed CBOR item is legal under it. Build one by naming what to refuse.

wire := cbor.Profile{
	Name:             "wire",
	RejectMaps:       true,
	RejectTags:       true,
	RejectFloats:     true,
	RejectIndefinite: true,
	RejectTextKeys:   true,
}

A Profile only enforces. It never infers which profile a message should be read under; that belongs to the schema and to the caller.

func Canonical added in v1.2.7

func Canonical() Profile

Canonical returns the profile CTAP2 canonical CBOR and COSE require: map keys in length-first order, no indefinite lengths, everything else permitted.

This is the shape a WebAuthn attestation is checked against. Floats are not refused, because nothing in COSE forbids them.

func Deterministic added in v1.2.7

func Deterministic() Profile

Deterministic returns RFC 8949 section 4.2.1 Core Deterministic Encoding: the same restrictions as Canonical with bytewise key ordering instead of length-first.

func (Profile) NewReader

func (p Profile) NewReader(data []byte, opts DecoderOptions) (*Reader, error)

NewReader returns a Reader over data with the caller's limits and this profile's decoder-visible restrictions.

func (Profile) ReaderOver

func (p Profile) ReaderOver(data []byte, opts DecoderOptions) Reader

ReaderOver returns a Reader by value on the same terms as NewReader. See ReaderOver.

func (Profile) Validate

func (p Profile) Validate(data []byte, opts DecoderOptions) error

Validate reports whether data is exactly one item legal under p and within the limits in opts. It answers the question without decoding the item into anything, which is what makes it usable as a boundary check on bytes from somewhere else.

Depth arithmetic

Validate walks the whole document, so nesting adds up: an envelope that wraps a message costs the envelope's depth plus the message's. A patch or delta carrying a subtree of a document is therefore deeper than the document.

Reading does not add up the same way. Reader.ReadRaw measures a captured item from zero, so decoding an envelope field by field and handing each payload on as raw bytes costs the larger of the two depths rather than their sum.

The default nesting bound is a stack safety net set far past any schema, so neither arithmetic matters unless a caller narrows it deliberately.

func (Profile) ValidateAppended

func (p Profile) ValidateAppended(dst []byte, before int, opts DecoderOptions) error

ValidateAppended checks the item a foreign AppendCBORTo just wrote, given the buffer and the length it had before the call.

A type that carries its own encoding cannot be made to honour a profile by the type system: nothing stops it appending a float, an indefinite-length item, or an unsorted map into a message that must not contain one. Nothing downstream would notice either, because the bytes are well-formed CBOR -- they are simply not the CBOR this profile promised. Running this after the call is what turns that from a silent divergence into an error.

type RawMessage

type RawMessage []byte

RawMessage holds exactly one validated CBOR item when returned by Decoder. Callers constructing RawMessage directly should pass it through Validate or Encoder.WriteRaw before trusting it.

func MarshalBool

func MarshalBool(v bool) RawMessage

func MarshalBytes

func MarshalBytes(v []byte) RawMessage

func MarshalFloat

func MarshalFloat(v float64) RawMessage

func MarshalInt

func MarshalInt(v int64) RawMessage

func MarshalNegative

func MarshalNegative(arg uint64) RawMessage

MarshalNegative returns the negative integer -1-arg as a RawMessage, reaching the range MarshalInt cannot. See AppendNegative.

func MarshalNull

func MarshalNull() RawMessage

func MarshalText

func MarshalText(v string) (RawMessage, error)

func MarshalUint

func MarshalUint(v uint64) RawMessage

Raw constructors make deterministic primitive values convenient to use in WriteArray and WriteMap.

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader decodes CBOR from a byte slice already in memory. It is the counterpart of Decoder for callers that have the whole item: it borrows strings from the input instead of copying them, skips an item without allocating it, and captures a sub-item at any depth, none of which an incremental io.Reader decoder can do.

Every slice a Reader returns aliases the input. They stay valid for as long as the caller keeps the input alive and does not modify it; Reset severs nothing, so a slice from a previous input outlives the Reader but not the bytes behind it. Callers that need independent storage should copy.

A Reader is not safe for concurrent use. Reuse one per connection rather than allocating one per message; that is what Reset is for.

Nesting

Skip, ReadRaw and Profile.Validate drive their own recursion and bound it by DecoderOptions.MaxNestedLevels. ReadArrayHeader and ReadMapHeader do not: they read one head and return, and the walk over the container is the caller's own loop, so its depth is the caller's to bound. This differs from Decoder, which keeps a frame stack and refuses a container past the limit from ReadToken.

The difference is deliberate. A frame stack is the state a Reader does not keep, and keeping one would cost an allocation per reader in the path that exists to have none. For untrusted input, call Profile.Validate first: it answers whether the bytes are legal under a bound without decoding them into anything, and generated code walking a fixed schema then recurses no deeper than the schema does.

func NewReader

func NewReader(data []byte, opts DecoderOptions) (*Reader, error)

NewReader returns a Reader over data. A zero limit in opts selects the same conservative default NewDecoder uses.

func ReaderOver

func ReaderOver(data []byte, opts DecoderOptions) Reader

ReaderOver returns a Reader by value, for a caller that wants it on the stack.

NewReader returns a pointer, which escapes, and a DecodeCBORFrom implementation runs once per field per message: at that rate the constructor is the allocation. A negative limit is treated as unset here rather than refused, because there is no error to return.

func (*Reader) Done

func (r *Reader) Done() bool

Done reports whether the whole input has been consumed.

func (*Reader) Offset

func (r *Reader) Offset() int

Offset reports how many bytes have been consumed, which is where the next item begins.

func (*Reader) Peek

func (r *Reader) Peek() (TokenKind, error)

Peek reports the kind of the next item without consuming it. It is the way to branch on an optional field: a caller that would otherwise have to capture and rewind can dispatch instead.

func (*Reader) ReadArrayHeader

func (r *Reader) ReadArrayHeader() (length int, indefinite bool, err error)

ReadArrayHeader reads an array head and returns its length. A definite-length array reports indefinite false and its item count; an indefinite one reports true and a length of -1, and ends at the item Peek reports as EndArray.

It bounds the item count but not the nesting depth, which the caller's own walk owns. See the Nesting section of the Reader documentation.

func (*Reader) ReadBool

func (r *Reader) ReadBool() (bool, error)

ReadBool reads a boolean.

func (*Reader) ReadBreak

func (r *Reader) ReadBreak() error

ReadBreak consumes the break that ends an indefinite-length container.

func (*Reader) ReadBytes

func (r *Reader) ReadBytes() ([]byte, error)

ReadBytes returns a byte string borrowed from the input.

func (*Reader) ReadFloat

func (r *Reader) ReadFloat() (float64, error)

ReadFloat reads a half, single, or double precision float.

func (*Reader) ReadInt

func (r *Reader) ReadInt() (int64, error)

ReadInt reads an integer of either sign. A negative integer outside the int64 range is ErrIntegerOverflow; ReadRaw preserves it where the value itself is needed.

func (*Reader) ReadInt8

func (r *Reader) ReadInt8() (int8, error)

Sized integer reads. The encoder writes the shortest form, so a field declared int32 arrives as anything from one to five bytes and its width is carried by the schema rather than the bytes. These enforce the declared width, so a value outside it is a protocol error rather than a silent wrap.

func (*Reader) ReadInt16

func (r *Reader) ReadInt16() (int16, error)

func (*Reader) ReadInt32

func (r *Reader) ReadInt32() (int32, error)

func (*Reader) ReadInt64

func (r *Reader) ReadInt64() (int64, error)

ReadInt64 is ReadInt under the name the sized set uses.

func (*Reader) ReadMapHeader

func (r *Reader) ReadMapHeader() (pairs int, indefinite bool, err error)

ReadMapHeader reads a map head and returns its pair count, on the same terms as ReadArrayHeader, nesting included.

func (*Reader) ReadNull

func (r *Reader) ReadNull() error

ReadNull reads the null value.

func (*Reader) ReadRaw

func (r *Reader) ReadRaw() (RawMessage, error)

ReadRaw returns the bytes of exactly one item, at whatever depth the Reader currently sits, and advances past it. The result aliases the input.

This is the primitive a type that carries its own decoding needs: a field of a foreign type is always nested inside something, and handing that type its own bytes is the only way to decode it without knowing its shape.

The captured item is measured from zero against MaxNestedLevels, so the depth already spent reaching it does not count against it. Decoding an envelope field by field therefore costs the larger of the envelope's depth and its payload's, where validating the whole document at once costs their sum. See Profile.Validate.

func (*Reader) ReadTag

func (r *Reader) ReadTag() (uint64, error)

ReadTag reads a tag head and leaves its content as the next item.

func (*Reader) ReadText

func (r *Reader) ReadText() (string, error)

ReadText returns a text string as a Go string, which copies it.

func (*Reader) ReadTextBytes

func (r *Reader) ReadTextBytes() ([]byte, error)

ReadTextBytes returns a text string borrowed from the input, without the allocation ReadText's string conversion costs.

func (*Reader) ReadUint

func (r *Reader) ReadUint() (uint64, error)

ReadUint reads an unsigned integer.

func (*Reader) ReadUint8

func (r *Reader) ReadUint8() (uint8, error)

func (*Reader) ReadUint16

func (r *Reader) ReadUint16() (uint16, error)

func (*Reader) ReadUint32

func (r *Reader) ReadUint32() (uint32, error)

func (*Reader) ReadUint64

func (r *Reader) ReadUint64() (uint64, error)

ReadUint64 is ReadUint under the name the sized set uses.

func (*Reader) Remaining

func (r *Reader) Remaining() int

Remaining reports how many bytes are left unconsumed.

func (*Reader) Reset

func (r *Reader) Reset(data []byte)

Reset points the Reader at a new input, keeping its options. Slices returned before the call still alias the old input.

func (*Reader) Skip

func (r *Reader) Skip() error

Skip advances past exactly one complete item, at any depth, without allocating and without materializing what it passed. It is how a decoder tolerates a field it does not know: the alternative, capturing the item only to discard it, is what makes an evolving schema expensive.

type Token

type Token struct {
	Kind       TokenKind
	Argument   uint64
	Bytes      []byte
	Text       string
	Bool       bool
	Float      float64
	Length     int
	Indefinite bool
}

Token is a typed CBOR token. Argument stores the unsigned argument for integers and tags. A NegativeInteger represents -1-Argument, retaining the full RFC 8949 range even when it cannot fit in int64.

func (Token) Int64

func (t Token) Int64() (int64, error)

type TokenKind

type TokenKind uint8
const (
	InvalidToken TokenKind = iota
	UnsignedInteger
	NegativeInteger
	ByteString
	TextString
	StartArray
	EndArray
	StartMap
	EndMap
	Boolean
	Null
	Tag
	Float
)

func (TokenKind) String

func (k TokenKind) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL