Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
A
Axiom
/Marketplace/christiangeorgelucas/cbor-tools
Sign in

christiangeorgelucas/cbor-tools

v0.1.0Go

The 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.

cborjsonencodingdecodingdata-serializationbinary-datadiagnostic-notationvalidation

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)

DecodeCborunary
CborInput·DecodeCborResponse

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
}
DecodeSequenceunary
CborInput·DecodeSequenceResponse

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
}
EncodeCborunary
EncodeCborRequest·CborResult

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
}
EncodeSequenceunary
EncodeSequenceRequest·CborResult

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
}
InspectCoseunary
CborInput·CoseInspectResult

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
}
InspectStructureunary
CborInput·InspectResult

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)
	}
}
ToDiagnosticNotationunary
CborInput·DiagnosticResult

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
}
ValidateWellFormedunary
CborInput·ValidateResult

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 .proto
CborInput
data:bytes

The input data in CBOR format that represents a COSE message.

CborMapEntry
key:CborNode
value:CborNode
CborNode
major_type:string
tag_number:uint64
children:[]CborNode
map_entries:[]CborMapEntry
value_hex:string
value_base64:string
value_text:string
value_int:string
value_float:double
value_bool:bool
simple_value:uint32
encoded_length:int32
CborResult
data:bytes
error:string
CoseInspectResult
ok:bool
error:string
cose_type:string
DecodeCborResponse
json:string
error:string
DecodeSequenceResponse
items_json:[]string
count:int32
error:string
DiagnosticResult
notation:string
error:string
EncodeCborRequest
json:string

The JSON text value to be encoded into CBOR format.

canonical:bool

A boolean flag indicating whether to use RFC 8949 §4.2 Core Deterministic Encoding for the output.

EncodeSequenceRequest
items_json:[]string

A list of JSON text values to be encoded into CBOR.

canonical:bool

A boolean indicating whether to apply canonical encoding to each item.

InspectResult
root:CborNode
error:string
ValidateResult
well_formed:bool
error:string
byte_length:int32

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

Stats

Error rate (1h)0.0%
Back to marketplace