christiangeorgelucas/cbor-tools
v0.1.0GoThe cbor-tools package provides a comprehensive set of tools for encoding and decoding CBOR (Concise Binary Object Representation) data, allowing seamless conversion between JSON and CBOR formats. It supports various features such as well-formedness validation, detailed structural inspection, and rendering of CBOR items in human-readable formats, making it ideal for developers working with binary data serialization and deserialization.
Use cases
- •Convert JSON data to CBOR for efficient storage
- •Inspect the structure of CBOR items for debugging
- •Validate the well-formedness of CBOR data
- •Decode CBOR sequences into JSON for processing
- •Render CBOR data in a human-readable format
Nodes (8)
The DecodeCbor node decodes CBOR (Concise Binary Object Representation) byte data into JSON format, adhering to specific conversion conventions for various data types. It handles malformed input gracefully by returning structured error responses instead of crashing.
View source
package nodes
import (
"context"
"christiangeorgelucas/cbor-tools/axiom"
gen "christiangeorgelucas/cbor-tools/gen"
)
// DecodeCbor decodes CBOR bytes into JSON text, using this package's
// CBOR<->JSON convention.
func DecodeCbor(ctx context.Context, ax axiom.Context, input *gen.CborInput) (*gen.DecodeCborResponse, error) {
j, err := cborToJSON(input.GetData())
if err != nil {
return &gen.DecodeCborResponse{Error: err.Error()}, nil
}
return &gen.DecodeCborResponse{Json: j}, nil
}
The DecodeSequence node decodes a CBOR Sequence as defined in RFC 8742, converting zero or more concatenated CBOR data items into a list of JSON texts while preserving their stream order. It handles malformed or truncated data gracefully by returning structured error messages instead of crashing.
View source
package nodes
import (
"context"
"fmt"
"christiangeorgelucas/cbor-tools/axiom"
gen "christiangeorgelucas/cbor-tools/gen"
)
// DecodeSequence decodes a CBOR Sequence (RFC 8742) — zero or more complete
// CBOR data items concatenated back-to-back — into a list of JSON texts, in
// stream order.
func DecodeSequence(ctx context.Context, ax axiom.Context, input *gen.CborInput) (*gen.DecodeSequenceResponse, error) {
data := input.GetData()
items := make([]string, 0)
for len(data) > 0 {
rest, jv, err := decodeOneCBORAsJSONReady(data)
if err != nil {
return &gen.DecodeSequenceResponse{Error: fmt.Sprintf("item %d: %v", len(items), err)}, nil
}
b, err := jsonMarshal(jv)
if err != nil {
return &gen.DecodeSequenceResponse{Error: fmt.Sprintf("item %d: %v", len(items), err)}, nil
}
items = append(items, b)
data = rest
}
return &gen.DecodeSequenceResponse{ItemsJson: items, Count: int32(len(items))}, nil
}
The EncodeCbor node encodes a JSON text value into CBOR bytes, adhering to specific conventions for handling data types not natively supported by JSON. It offers an option for canonical encoding to ensure deterministic output, while also providing structured error handling for malformed JSON inputs.
View source
package nodes
import (
"context"
"christiangeorgelucas/cbor-tools/axiom"
gen "christiangeorgelucas/cbor-tools/gen"
)
// EncodeCbor encodes a JSON text value into CBOR bytes, using this
// package's CBOR<->JSON convention. Set canonical for RFC 8949 §4.2 Core
// Deterministic Encoding.
func EncodeCbor(ctx context.Context, ax axiom.Context, input *gen.EncodeCborRequest) (*gen.CborResult, error) {
data, err := jsonToCBOR(input.GetJson(), input.GetCanonical())
if err != nil {
return &gen.CborResult{Error: err.Error()}, nil
}
return &gen.CborResult{Data: data}, nil
}
The EncodeSequence node encodes a list of JSON text values into a single CBOR Sequence byte stream, where each value is CBOR-encoded independently and concatenated. It handles malformed JSON or unsupported values by returning structured errors instead of crashing.
View source
package nodes
import (
"bytes"
"context"
"fmt"
"christiangeorgelucas/cbor-tools/axiom"
gen "christiangeorgelucas/cbor-tools/gen"
)
// EncodeSequence encodes a list of JSON text values into a single CBOR
// Sequence (RFC 8742) byte stream — the inverse of DecodeSequence.
func EncodeSequence(ctx context.Context, ax axiom.Context, input *gen.EncodeSequenceRequest) (*gen.CborResult, error) {
var buf bytes.Buffer
for i, item := range input.GetItemsJson() {
data, err := jsonToCBOR(item, input.GetCanonical())
if err != nil {
return &gen.CborResult{Error: fmt.Sprintf("item %d: %v", i, err)}, nil
}
buf.Write(data)
}
return &gen.CborResult{Data: buf.Bytes()}, nil
}
The InspectCose node structurally decodes COSE (RFC 9052) messages, specifically single-signer/single-recipient formats such as COSE_Sign1, COSE_Mac0, and COSE_Encrypt0, into their respective components including protected headers, unprotected headers, payload, and signature or authentication tags. It performs a structural analysis without cryptographic verification and returns structured errors for malformed inputs.
View source
package nodes
import (
"context"
"fmt"
"github.com/fxamacker/cbor/v2"
"christiangeorgelucas/cbor-tools/axiom"
gen "christiangeorgelucas/cbor-tools/gen"
)
// InspectCose structurally decodes a COSE (RFC 9052) single-signer/
// single-recipient message. See the CoseInspectResult message doc-comment
// in messages/messages.proto for the exact contract.
func InspectCose(ctx context.Context, ax axiom.Context, input *gen.CborInput) (*gen.CoseInspectResult, error) {
data := input.GetData()
if err := cbor.Wellformed(data); err != nil {
return &gen.CoseInspectResult{Error: fmt.Sprintf("not well-formed CBOR: %v", err)}, nil
}
arrayRaw := cbor.RawMessage(data)
coseType := ""
var rt cbor.RawTag
if err := cbor.Unmarshal(data, &rt); err == nil {
switch rt.Number {
case 18:
coseType = "Sign1"
case 17:
coseType = "Mac0"
case 16:
coseType = "Encrypt0"
default:
return &gen.CoseInspectResult{Error: fmt.Sprintf("not a recognized COSE message: tag %d is not COSE_Sign1(18)/COSE_Mac0(17)/COSE_Encrypt0(16)", rt.Number)}, nil
}
arrayRaw = rt.Content
}
var elems []cbor.RawMessage
if err := cbor.Unmarshal(arrayRaw, &elems); err != nil {
return &gen.CoseInspectResult{Error: "not a recognized COSE message: expected a CBOR array"}, nil
}
if coseType == "" {
// Untagged input: a 3-element array is unambiguously COSE_Encrypt0
// shaped (no other single-layer COSE message has 3 elements). A
// 4-element array is structurally identical for COSE_Sign1 and
// COSE_Mac0 — RFC 9052 relies on the tag (or surrounding context)
// to tell them apart, so this node cannot guess and reports a clear
// error asking for an explicit tag instead of silently picking one.
switch len(elems) {
case 3:
coseType = "Encrypt0"
case 4:
return &gen.CoseInspectResult{Error: "untagged 4-element COSE array is ambiguous between COSE_Sign1 and COSE_Mac0 — wrap it in tag 18 (Sign1) or tag 17 (Mac0) to disambiguate"}, nil
default:
return &gen.CoseInspectResult{Error: fmt.Sprintf("not a recognized COSE message: array has %d element(s), expected 3 (Encrypt0) or 4 (Sign1/Mac0)", len(elems))}, nil
}
} else {
wantLen := 4
if coseType == "Encrypt0" {
wantLen = 3
}
if len(elems) != wantLen {
return &gen.CoseInspectResult{Error: fmt.Sprintf("COSE_%s must have %d array elements, got %d", coseType, wantLen, len(elems))}, nil
}
}
protectedJSON, err := decodeCoseProtectedHeader(elems[0])
if err != nil {
return &gen.CoseInspectResult{Error: fmt.Sprintf("protected header: %v", err)}, nil
}
unprotectedJSON, err := decodeCoseUnprotectedHeader(elems[1])
if err != nil {
return &gen.CoseInspectResult{Error: fmt.Sprintf("unprotected header: %v", err)}, nil
}
result := &gen.CoseInspectResult{
Ok: true,
CoseType: coseType,
ProtectedHeadersJson: protectedJSON,
UnprotectedHeadersJson: unprotectedJSON,
}
switch coseType {
case "Sign1", "Mac0":
payload, detached, err := decodeCoseBstrOrNil(elems[2])
if err != nil {
return &gen.CoseInspectResult{Error: fmt.Sprintf("payload: %v", err)}, nil
}
result.Payload = payload
result.PayloadDetached = detached
trailer, err := decodeCoseBstr(elems[3])
if err != nil {
label := "signature"
if coseType == "Mac0" {
label = "authentication tag"
}
return &gen.CoseInspectResult{Error: fmt.Sprintf("%s: %v", label, err)}, nil
}
if coseType == "Sign1" {
result.Signature = trailer
} else {
result.MacTag = trailer
}
case "Encrypt0":
ciphertext, detached, err := decodeCoseBstrOrNil(elems[2])
if err != nil {
return &gen.CoseInspectResult{Error: fmt.Sprintf("ciphertext: %v", err)}, nil
}
result.Ciphertext = ciphertext
result.PayloadDetached = detached
}
return result, nil
}
// decodeCoseProtectedHeader unwraps the protected-header byte string and
// decodes its content (a CBOR map, or empty) as JSON via this package's
// CBOR<->JSON convention.
func decodeCoseProtectedHeader(raw cbor.RawMessage) (string, error) {
var wrapped []byte
if err := cbor.Unmarshal(raw, &wrapped); err != nil {
return "", fmt.Errorf("expected a byte string (bstr .cbor header_map): %w", err)
}
if len(wrapped) == 0 {
return "{}", nil
}
if err := cbor.Wellformed(wrapped); err != nil {
return "", fmt.Errorf("content is not well-formed CBOR: %w", err)
}
if wrapped[0]>>5 != 5 {
return "", fmt.Errorf("content must be a CBOR map")
}
return cborToJSON(wrapped)
}
// decodeCoseUnprotectedHeader decodes the unprotected-header field (a plain
// CBOR map, not byte-string-wrapped) as JSON.
func decodeCoseUnprotectedHeader(raw cbor.RawMessage) (string, error) {
if len(raw) == 0 || raw[0]>>5 != 5 {
return "", fmt.Errorf("must be a CBOR map")
}
return cborToJSON(raw)
}
// decodeCoseBstrOrNil decodes a field that RFC 9052 allows to be either a
// byte string or CBOR null (the "detached content" convention).
func decodeCoseBstrOrNil(raw cbor.RawMessage) (data []byte, isNil bool, err error) {
var v any
if err := cbor.Unmarshal(raw, &v); err != nil {
return nil, false, err
}
switch t := v.(type) {
case nil:
return nil, true, nil
case []byte:
return t, false, nil
default:
return nil, false, fmt.Errorf("expected a byte string or null, got %T", v)
}
}
// decodeCoseBstr decodes a field that must be a byte string.
func decodeCoseBstr(raw cbor.RawMessage) ([]byte, error) {
var v []byte
if err := cbor.Unmarshal(raw, &v); err != nil {
return nil, fmt.Errorf("expected a byte string: %w", err)
}
return v, nil
}
The InspectStructure node recursively decodes a CBOR item into a schema-free structure tree, accurately reflecting the wire format of each value type and its encoded byte length. It ensures that malformed input results in a structured error rather than a crash.
View source
package nodes
import (
"context"
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
"strconv"
"github.com/fxamacker/cbor/v2"
"christiangeorgelucas/cbor-tools/axiom"
gen "christiangeorgelucas/cbor-tools/gen"
)
// InspectStructure recursively decodes a CBOR item into a generic,
// schema-free structure tree that reflects exactly what is on the wire —
// see the CborNode message doc-comment in messages/messages.proto for the
// full contract.
func InspectStructure(ctx context.Context, ax axiom.Context, input *gen.CborInput) (*gen.InspectResult, error) {
data := input.GetData()
if err := cbor.Wellformed(data); err != nil {
return &gen.InspectResult{Error: err.Error()}, nil
}
root, err := decodeCborNode(cbor.RawMessage(data))
if err != nil {
return &gen.InspectResult{Error: err.Error()}, nil
}
return &gen.InspectResult{Root: root}, nil
}
// cborRawEntry pairs a decoded map-entry's key/value CborNodes with the raw
// encoded bytes of the key, so entries can be sorted into a fixed,
// reproducible order (bytewise-lexicographic on the key's own encoding —
// the same rule RFC 8949 §4.2 uses for canonical map-key order).
type cborRawEntry struct {
keyEncoded []byte
key *gen.CborNode
value *gen.CborNode
}
// decodeCborNode decodes exactly one CBOR data item (raw must contain
// exactly one well-formed item — no trailing bytes) into a CborNode.
//
// This walks the item using the library's own typed Unmarshal for every
// piece of real parsing (finding item boundaries, decoding lengths,
// strings, numbers, floats) — it only ever reads the leading byte's top 3
// bits to pick which narrow Go type to decode into, the same "read the
// wire-format discriminant, delegate the rest" approach this package's
// neighbor asn1-tools uses for X.690 tag class/constructed-bit inspection.
func decodeCborNode(raw cbor.RawMessage) (*gen.CborNode, error) {
if len(raw) == 0 {
return nil, fmt.Errorf("empty CBOR item")
}
majorType := raw[0] >> 5
length := int32(len(raw))
switch majorType {
case 0: // unsigned integer
var v uint64
if err := cbor.Unmarshal(raw, &v); err != nil {
return nil, fmt.Errorf("decoding unsigned int: %w", err)
}
return &gen.CborNode{
MajorType: "unsigned-int",
ValueInt: strconv.FormatUint(v, 10),
EncodedLength: length,
}, nil
case 1: // negative integer
var v any
if err := cbor.Unmarshal(raw, &v); err != nil {
return nil, fmt.Errorf("decoding negative int: %w", err)
}
s, err := negativeIntToDecimalString(v)
if err != nil {
return nil, err
}
return &gen.CborNode{
MajorType: "negative-int",
ValueInt: s,
EncodedLength: length,
}, nil
case 2: // byte string
var v []byte
if err := cbor.Unmarshal(raw, &v); err != nil {
return nil, fmt.Errorf("decoding byte string: %w", err)
}
return &gen.CborNode{
MajorType: "byte-string",
ValueHex: hex.EncodeToString(v),
ValueBase64: base64.StdEncoding.EncodeToString(v),
EncodedLength: length,
}, nil
case 3: // text string
var v string
if err := cbor.Unmarshal(raw, &v); err != nil {
return nil, fmt.Errorf("decoding text string: %w", err)
}
return &gen.CborNode{
MajorType: "text-string",
ValueText: v,
EncodedLength: length,
}, nil
case 4: // array
var items []cbor.RawMessage
if err := cbor.Unmarshal(raw, &items); err != nil {
return nil, fmt.Errorf("decoding array: %w", err)
}
children := make([]*gen.CborNode, len(items))
for i, it := range items {
cn, err := decodeCborNode(it)
if err != nil {
return nil, fmt.Errorf("array element %d: %w", i, err)
}
children[i] = cn
}
return &gen.CborNode{
MajorType: "array",
Children: children,
EncodedLength: length,
}, nil
case 5: // map
var m map[any]cbor.RawMessage
if err := cbor.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("decoding map: %w", err)
}
entries := make([]cborRawEntry, 0, len(m))
for k, v := range m {
keyEncoded, err := defaultEncMode.Marshal(k)
if err != nil {
return nil, fmt.Errorf("re-encoding map key %v: %w", k, err)
}
keyNode, err := decodeCborNode(cbor.RawMessage(keyEncoded))
if err != nil {
return nil, fmt.Errorf("map key: %w", err)
}
valNode, err := decodeCborNode(v)
if err != nil {
return nil, fmt.Errorf("map value: %w", err)
}
entries = append(entries, cborRawEntry{keyEncoded: keyEncoded, key: keyNode, value: valNode})
}
sortedByEncodedKey(entries)
mapEntries := make([]*gen.CborMapEntry, len(entries))
for i, e := range entries {
mapEntries[i] = &gen.CborMapEntry{Key: e.key, Value: e.value}
}
return &gen.CborNode{
MajorType: "map",
MapEntries: mapEntries,
EncodedLength: length,
}, nil
case 6: // tag
var rt cbor.RawTag
if err := cbor.Unmarshal(raw, &rt); err != nil {
return nil, fmt.Errorf("decoding tag: %w", err)
}
content, err := decodeCborNode(rt.Content)
if err != nil {
return nil, fmt.Errorf("tag(%d) content: %w", rt.Number, err)
}
return &gen.CborNode{
MajorType: "tag",
TagNumber: rt.Number,
Children: []*gen.CborNode{content},
EncodedLength: length,
}, nil
case 7: // simple/float/bool/null/undefined
return decodeCborSimpleNode(raw, length)
default:
return nil, fmt.Errorf("unknown CBOR major type %d", majorType)
}
}
func decodeCborSimpleNode(raw cbor.RawMessage, length int32) (*gen.CborNode, error) {
switch raw[0] {
case 0xf6: // null
return &gen.CborNode{MajorType: "null", EncodedLength: length}, nil
case 0xf7: // undefined
return &gen.CborNode{MajorType: "undefined", EncodedLength: length}, nil
}
var v any
if err := cbor.Unmarshal(raw, &v); err != nil {
return nil, fmt.Errorf("decoding simple/float/bool: %w", err)
}
switch t := v.(type) {
case bool:
return &gen.CborNode{MajorType: "bool", ValueBool: t, EncodedLength: length}, nil
case float64:
return &gen.CborNode{MajorType: "float", ValueFloat: t, EncodedLength: length}, nil
case cbor.SimpleValue:
return &gen.CborNode{MajorType: "simple", SimpleValue: uint32(t), EncodedLength: length}, nil
default:
return nil, fmt.Errorf("unsupported major-type-7 decoded value %T", v)
}
}
// negativeIntToDecimalString renders a decoded CBOR major-type-1 value
// (int64 for the common case, big.Int for a magnitude beyond int64's range)
// as an exact decimal string.
func negativeIntToDecimalString(v any) (string, error) {
switch t := v.(type) {
case int64:
return strconv.FormatInt(t, 10), nil
case big.Int:
return t.String(), nil
case *big.Int:
return t.String(), nil
default:
return "", fmt.Errorf("unsupported negative-int decoded type %T", v)
}
}
The ToDiagnosticNotation node converts a CBOR data item into a human-readable Extended Diagnostic Notation (EDN) format as specified in RFC 8949 §8, preserving all CBOR-specific details for debugging purposes. It handles malformed input gracefully by returning structured error messages instead of crashing.
View source
package nodes
import (
"context"
"fmt"
"github.com/fxamacker/cbor/v2"
"christiangeorgelucas/cbor-tools/axiom"
gen "christiangeorgelucas/cbor-tools/gen"
)
// ToDiagnosticNotation renders a CBOR data item as RFC 8949 §8 Extended
// Diagnostic Notation (EDN) text.
func ToDiagnosticNotation(ctx context.Context, ax axiom.Context, input *gen.CborInput) (*gen.DiagnosticResult, error) {
notation, rest, err := cbor.DiagnoseFirst(input.GetData())
if err != nil {
return &gen.DiagnosticResult{Error: fmt.Sprintf("malformed CBOR: %v", err)}, nil
}
if len(rest) != 0 {
return &gen.DiagnosticResult{Error: fmt.Sprintf("trailing %d byte(s) after the first CBOR data item", len(rest))}, nil
}
return &gen.DiagnosticResult{Notation: notation}, nil
}
The ValidateWellFormed node checks if the provided bytes represent a single well-formed CBOR data item according to RFC 8949 §4.1, without converting it to JSON. It efficiently reports the byte length if valid or a human-readable error message if invalid.
View source
package nodes
import (
"context"
"github.com/fxamacker/cbor/v2"
"christiangeorgelucas/cbor-tools/axiom"
gen "christiangeorgelucas/cbor-tools/gen"
)
// ValidateWellFormed checks whether bytes are a single well-formed CBOR
// data item per RFC 8949 §4.1, without materializing it into JSON.
func ValidateWellFormed(ctx context.Context, ax axiom.Context, input *gen.CborInput) (*gen.ValidateResult, error) {
data := input.GetData()
if err := cbor.Wellformed(data); err != nil {
return &gen.ValidateResult{WellFormed: false, Error: err.Error()}, nil
}
return &gen.ValidateResult{WellFormed: true, ByteLength: int32(len(data))}, nil
}
Messages (12)
Download .protoThe input data in CBOR format that represents a COSE message.
The JSON text value to be encoded into CBOR format.
A boolean flag indicating whether to use RFC 8949 §4.2 Core Deterministic Encoding for the output.
A list of JSON text values to be encoded into CBOR.
A boolean indicating whether to apply canonical encoding to each item.
Use as a tool in any AI agent
christiangeorgelucas/cbor-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.
claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.
MCP server setup & other clients →Use this package
Sign in to import christiangeorgelucas/cbor-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.
Sign in to useOpen editor