diff --git a/common.go b/common.go index 9cf33cd2..f0d44767 100644 --- a/common.go +++ b/common.go @@ -45,6 +45,27 @@ func (t cborType) String() string { } } +// Type is the CBOR major type of an encoded data item, as defined in +// RFC 8949 Section 3.1. It is reported by RawMessage.Type. +type Type uint8 + +const ( + TypePositiveInt Type = Type(cborTypePositiveInt) // unsigned integer (major type 0) + TypeNegativeInt Type = Type(cborTypeNegativeInt) // negative integer (major type 1) + TypeByteString Type = Type(cborTypeByteString) // byte string (major type 2) + TypeTextString Type = Type(cborTypeTextString) // UTF-8 text string (major type 3) + TypeArray Type = Type(cborTypeArray) // array (major type 4) + TypeMap Type = Type(cborTypeMap) // map (major type 5) + TypeTag Type = Type(cborTypeTag) // tagged data item (major type 6) + TypePrimitives Type = Type(cborTypePrimitives) // simple values and floating-point numbers (major type 7) +) + +// String returns a human-readable description of the CBOR major type, +// such as "array" or "UTF-8 text string". +func (t Type) String() string { + return cborType(t).String() +} + type additionalInformation uint8 const ( diff --git a/example_test.go b/example_test.go index b53e5e3f..4bfa5bea 100644 --- a/example_test.go +++ b/example_test.go @@ -569,3 +569,28 @@ func Example_webAuthn() { } fmt.Printf("%+v", v) } + +func ExampleRawMessage_Type() { + // A CBOR stream can hold data items of different major types. RawMessage.Type + // reports the outer kind of a raw value so it can be routed without a full decode. + encoded := []string{ + "83010203", // [1, 2, 3] + "6568656c6c6f", // "hello" + "a1636b657963766c61", // {"key": "vla"} + "f6", // null + } + for _, s := range encoded { + data, _ := hex.DecodeString(s) + t, err := cbor.RawMessage(data).Type() + if err != nil { + fmt.Println("error:", err) + continue + } + fmt.Println(t) + } + // Output: + // array + // UTF-8 text string + // map + // primitives +} diff --git a/stream.go b/stream.go index 282b3f7d..0a04fb56 100644 --- a/stream.go +++ b/stream.go @@ -376,3 +376,18 @@ func (m *RawMessage) UnmarshalCBOR(data []byte) error { *m = append((*m)[0:0], data...) return nil } + +// Type returns the CBOR major type of the encoded data item in m, as defined +// in RFC 8949 Section 3.1. It provides a cheap way to branch on the outer kind +// of a raw value without fully decoding it, similar to inspecting the first byte +// of a json.RawMessage. +// +// Only the initial byte is examined; Type does not verify that m holds a +// well-formed CBOR data item. Use Wellformed if you need that guarantee. +// Type returns an error if m is empty. +func (m RawMessage) Type() (Type, error) { + if len(m) == 0 { + return 0, errors.New("cbor.RawMessage: Type called on empty message") + } + return Type(getType(m[0])), nil +} diff --git a/stream_test.go b/stream_test.go index d0e9f5f9..16f43c24 100644 --- a/stream_test.go +++ b/stream_test.go @@ -1720,3 +1720,57 @@ func (r *recoverableReader) Read(b []byte) (int, error) { } return r.nBytesReader.Read(b) } + +func TestRawMessageType(t *testing.T) { + testCases := []struct { + name string + m RawMessage + want Type + wantStr string + }{ + {"positive integer", RawMessage(mustHexDecode("00")), TypePositiveInt, "positive integer"}, + {"positive integer with argument", RawMessage(mustHexDecode("1903e8")), TypePositiveInt, "positive integer"}, + {"negative integer", RawMessage(mustHexDecode("20")), TypeNegativeInt, "negative integer"}, + {"byte string", RawMessage(mustHexDecode("43010203")), TypeByteString, "byte string"}, + {"indefinite-length byte string", RawMessage(mustHexDecode("5f42010243030405ff")), TypeByteString, "byte string"}, + {"text string", RawMessage(mustHexDecode("63616263")), TypeTextString, "UTF-8 text string"}, + {"array", RawMessage(mustHexDecode("83010203")), TypeArray, "array"}, + {"indefinite-length array", RawMessage(mustHexDecode("9f010203ff")), TypeArray, "array"}, + {"map", RawMessage(mustHexDecode("a201020304")), TypeMap, "map"}, + {"tag", RawMessage(mustHexDecode("c074323031332d30332d32315432303a30343a30305a")), TypeTag, "tag"}, + {"boolean primitive", RawMessage(mustHexDecode("f5")), TypePrimitives, "primitives"}, + {"null primitive", RawMessage(mustHexDecode("f6")), TypePrimitives, "primitives"}, + {"float primitive", RawMessage(mustHexDecode("fb3ff199999999999a")), TypePrimitives, "primitives"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.m.Type() + if err != nil { + t.Fatalf("RawMessage(0x%x).Type() returned error %v", []byte(tc.m), err) + } + if got != tc.want { + t.Errorf("RawMessage(0x%x).Type() = %d, want %d", []byte(tc.m), got, tc.want) + } + if got.String() != tc.wantStr { + t.Errorf("RawMessage(0x%x).Type().String() = %q, want %q", []byte(tc.m), got.String(), tc.wantStr) + } + }) + } +} + +func TestRawMessageTypeEmpty(t *testing.T) { + for _, m := range []RawMessage{nil, {}} { + got, err := m.Type() + if err == nil { + t.Errorf("RawMessage(%v).Type() = %d with no error, want error", []byte(m), got) + } + } +} + +func TestTypeStringInvalid(t *testing.T) { + got := Type(0x01).String() + want := "Invalid type 1" + if got != want { + t.Errorf("Type(0x01).String() = %q, want %q", got, want) + } +}