diff --git a/diagnose.go b/diagnose.go new file mode 100644 index 00000000..0fbc3001 --- /dev/null +++ b/diagnose.go @@ -0,0 +1,589 @@ +// Copyright (c) Faye Amacker. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +package cbor + +import ( + "bytes" + "encoding/base32" + "encoding/base64" + "encoding/hex" + "errors" + "io" + "math" + "math/big" + "strconv" + "unicode/utf16" + "unicode/utf8" + + "github.com/x448/float16" +) + +type diagnose struct { + d *decoder + w *bytes.Buffer + opts *DiagOptions +} + +// DiagOptions specifies Diag options. +type DiagOptions struct { + // ByteStringEncoding specifies the base encoding that byte strings are notated. + // It can be set to "base16", "base32", "base32hex", "base64", default is "base16". + ByteStringEncoding string + // ByteStringHexWhitespace specifies notating with whitespace in byte string + // when ByteStringEncoding is "base16". + ByteStringHexWhitespace bool + // ByteStringText specifies notating with text in byte string + // if it is a valid UTF-8 text. + ByteStringText bool + // ByteStringEmbeddedCBOR specifies notating embedded CBOR in byte string + // if it is a valid CBOR bytes. + ByteStringEmbeddedCBOR bool + // CBORSequence specifies notating CBOR sequences. + // otherwise, it returns an error if there are more bytes after the first CBOR. + CBORSequence bool + // IndicateFloatPrecision specifies appending a suffix to indicate float precision. + // Refer to https://www.rfc-editor.org/rfc/rfc8949.html#name-encoding-indicators. + IndicateFloatPrecision bool +} + +// Diag returns a human-readable diagnostic notation bytes for the given CBOR data. +// +// Refer to https://www.rfc-editor.org/rfc/rfc8949.html#name-diagnostic-notation. +func Diag(data []byte, opts *DiagOptions) ([]byte, error) { + if opts == nil { + opts = defaultDiagOptions + } + if opts.ByteStringEncoding == "" { + opts.ByteStringEncoding = "base16" + } + + di, err := opts.new(data) + if err != nil { + return nil, err + } + + return di.diag() +} + +var diagnoseDecMode, _ = DecOptions{ + MaxNestedLevels: 256, + UTF8: UTF8DecodeInvalid, +}.decMode() + +var defaultDiagOptions = &DiagOptions{ + ByteStringEncoding: "base16", +} + +func (opts *DiagOptions) new(data []byte) (*diagnose, error) { + d := &decoder{data: data, dm: diagnoseDecMode} + off := d.off + err := d.valid(opts.CBORSequence) + d.off = off + if err != nil { + return nil, err + } + di := &diagnose{d, &bytes.Buffer{}, opts} + return di, nil +} + +func (di *diagnose) diag() ([]byte, error) { + if err := di.value(); err != nil { + return nil, err + } + + // CBOR Sequence + for { + switch err := di.valid(); err { + case nil: + if err := di.writeString(", "); err != nil { + return di.w.Bytes(), err + } + if err := di.value(); err != nil { + return di.w.Bytes(), err + } + + case io.EOF: + return di.w.Bytes(), nil + + default: + return di.w.Bytes(), err + } + } +} + +func (di *diagnose) valid() error { + off := di.d.off + err := di.d.valid(di.opts.CBORSequence) + di.d.off = off + return err +} + +func (di *diagnose) value() error { + initialByte := di.d.data[di.d.off] + switch initialByte { + case 0x5f, 0x7f: // indefinite byte string or UTF-8 string + di.d.off++ + if err := di.writeString("(_ "); err != nil { + return err + } + + i := 0 + for !di.d.foundBreak() { + if i > 0 { + if err := di.writeString(", "); err != nil { + return err + } + } + + i++ + if err := di.value(); err != nil { + return err + } + } + + return di.writeByte(')') + + case 0x9f: // indefinite array + di.d.off++ + if err := di.writeString("[_ "); err != nil { + return err + } + + i := 0 + for !di.d.foundBreak() { + if i > 0 { + if err := di.writeString(", "); err != nil { + return err + } + } + + i++ + if err := di.value(); err != nil { + return err + } + } + + return di.writeByte(']') + + case 0xbf: // indefinite map + di.d.off++ + if err := di.writeString("{_ "); err != nil { + return err + } + + i := 0 + for !di.d.foundBreak() { + if i > 0 { + if err := di.writeString(", "); err != nil { + return err + } + } + + i++ + // key + if err := di.value(); err != nil { + return err + } + + if err := di.writeString(": "); err != nil { + return err + } + + // value + if err := di.value(); err != nil { + return err + } + } + + return di.writeByte('}') + } + + t := di.d.nextCBORType() + switch t { + case cborTypePositiveInt: + _, _, val := di.d.getHead() + return di.writeString(strconv.FormatUint(val, 10)) + + case cborTypeNegativeInt: + _, _, val := di.d.getHead() + if val > math.MaxInt64 { + // CBOR negative integer overflows int64, use big.Int to store value. + bi := new(big.Int) + bi.SetUint64(val) + bi.Add(bi, big.NewInt(1)) + bi.Neg(bi) + + return di.writeString(bi.String()) + } else { + nValue := int64(-1) ^ int64(val) + return di.writeString(strconv.FormatInt(nValue, 10)) + } + + case cborTypeByteString: + b := di.d.parseByteString() + return di.encodeByteString(b) + + case cborTypeTextString: + b, err := di.d.parseTextString() + if err != nil { + return err + } + + return di.encodeTextString(string(b), '"') + + case cborTypeArray: + _, _, val := di.d.getHead() + count := int(val) + if err := di.writeByte('['); err != nil { + return err + } + + for i := 0; i < count; i++ { + if i > 0 { + if err := di.writeString(", "); err != nil { + return err + } + } + + if err := di.value(); err != nil { + return err + } + } + + if err := di.writeByte(']'); err != nil { + return err + } + + case cborTypeMap: + _, _, val := di.d.getHead() + count := int(val) + if err := di.writeByte('{'); err != nil { + return err + } + + for i := 0; i < count; i++ { + if i > 0 { + if err := di.writeString(", "); err != nil { + return err + } + } + + // key + if err := di.value(); err != nil { + return err + } + + if err := di.writeString(": "); err != nil { + return err + } + + // value + if err := di.value(); err != nil { + return err + } + } + + if err := di.writeByte('}'); err != nil { + return err + } + + case cborTypeTag: + _, _, tagNum := di.d.getHead() + switch tagNum { + case 2: + b := di.d.parseByteString() + bi := new(big.Int).SetBytes(b) + return di.writeString(bi.String()) + + case 3: + b := di.d.parseByteString() + bi := new(big.Int).SetBytes(b) + bi.Add(bi, big.NewInt(1)) + bi.Neg(bi) + return di.writeString(bi.String()) + + default: + if err := di.writeString(strconv.FormatUint(tagNum, 10)); err != nil { + return err + } + if err := di.writeByte('('); err != nil { + return err + } + if err := di.value(); err != nil { + return err + } + if err := di.writeByte(')'); err != nil { + return err + } + } + + case cborTypePrimitives: + _, ai, val := di.d.getHead() + switch ai { + case 20: + return di.writeString("false") + + case 21: + return di.writeString("true") + + case 22: + return di.writeString("null") + + case 23: + return di.writeString("undefined") + + case 25, 26, 27: + return di.encodeFloat(ai, val) + + default: + if err := di.writeString("simple("); err != nil { + return err + } + if err := di.writeString(strconv.FormatUint(val, 10)); err != nil { + return err + } + if err := di.writeByte(')'); err != nil { + return err + } + } + } + + return nil +} + +func (di *diagnose) writeByte(val byte) error { + return di.w.WriteByte(val) +} + +func (di *diagnose) writeString(val string) error { + _, err := di.w.WriteString(val) + return err +} + +// writeU16 format a rune as "\uxxxx" +func (di *diagnose) writeU16(val rune) error { + if err := di.writeString("\\u"); err != nil { + return err + } + b := make([]byte, 2) + b[0] = byte(val >> 8) + b[1] = byte(val) + return di.writeString(hex.EncodeToString(b)) +} + +var rawBase32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding) +var rawBase32HexEncoding = base32.HexEncoding.WithPadding(base32.NoPadding) + +func (di *diagnose) encodeByteString(val []byte) error { + + if len(val) > 0 { + if di.opts.ByteStringText && utf8.Valid(val) { + return di.encodeTextString(string(val), '\'') + } + + if di.opts.ByteStringEmbeddedCBOR { + if di2, err := di.opts.new(val); err == nil { + if data, err := di2.diag(); err == nil { + if err := di.writeString("<<"); err != nil { + return err + } + if err := di.writeString(string(data)); err != nil { + return err + } + + return di.writeString(">>") + } + } + } + } + + switch di.opts.ByteStringEncoding { + case "base16": + if err := di.writeString("h'"); err != nil { + return err + } + + encoder := hex.NewEncoder(di.w) + if di.opts.ByteStringHexWhitespace { + for i, b := range val { + if i > 0 { + if err := di.writeByte(' '); err != nil { + return err + } + } + if _, err := encoder.Write([]byte{b}); err != nil { + return err + } + } + } else { + if _, err := encoder.Write(val); err != nil { + return err + } + } + + case "base32": + if err := di.writeString("b32'"); err != nil { + return err + } + encoder := base32.NewEncoder(rawBase32Encoding, di.w) + if _, err := encoder.Write(val); err != nil { + return err + } + encoder.Close() + + case "base32hex": + if err := di.writeString("h32'"); err != nil { + return err + } + encoder := base32.NewEncoder(rawBase32HexEncoding, di.w) + if _, err := encoder.Write(val); err != nil { + return err + } + encoder.Close() + + case "base64": + if err := di.writeString("b64'"); err != nil { + return err + } + encoder := base64.NewEncoder(base64.RawURLEncoding, di.w) + if _, err := encoder.Write(val); err != nil { + return err + } + encoder.Close() + + default: + return errors.New("cbor: invalid ByteStringEncoding option " + strconv.Quote(di.opts.ByteStringEncoding)) + } + + return di.writeByte('\'') +} + +var utf16SurrSelf = rune(0x10000) + +// quote should be either `'` or `"` +func (di *diagnose) encodeTextString(val string, quote rune) error { + if err := di.writeByte(byte(quote)); err != nil { + return err + } + + for _, r := range val { + switch { + case r == '\t', r == '\n', r == '\r', r == '\\', r == quote: + if err := di.writeByte('\\'); err != nil { + return err + } + if err := di.writeByte(byte(r)); err != nil { + return err + } + + case r >= ' ' && r <= '~': + if err := di.writeByte(byte(r)); err != nil { + return err + } + + case r < utf16SurrSelf: + if err := di.writeU16(r); err != nil { + return err + } + + default: + r1, r2 := utf16.EncodeRune(r) + if err := di.writeU16(r1); err != nil { + return err + } + if err := di.writeU16(r2); err != nil { + return err + } + } + } + + return di.writeByte(byte(quote)) +} + +func (di *diagnose) encodeFloat(ai byte, val uint64) error { + f64 := float64(0) + switch ai { + case 25: + f16 := float16.Frombits(uint16(val)) + switch { + case f16.IsNaN(): + return di.writeString("NaN") + case f16.IsInf(1): + return di.writeString("Infinity") + case f16.IsInf(-1): + return di.writeString("-Infinity") + default: + f64 = float64(f16.Float32()) + } + + case 26: + f32 := math.Float32frombits(uint32(val)) + switch { + case f32 != f32: + return di.writeString("NaN") + case f32 > math.MaxFloat32: + return di.writeString("Infinity") + case f32 < -math.MaxFloat32: + return di.writeString("-Infinity") + default: + f64 = float64(f32) + } + + case 27: + f64 = math.Float64frombits(val) + switch { + case f64 != f64: + return di.writeString("NaN") + case f64 > math.MaxFloat64: + return di.writeString("Infinity") + case f64 < -math.MaxFloat64: + return di.writeString("-Infinity") + } + } + + // See https://github.com/golang/go/blob/4df10fba1687a6d4f51d7238a403f8f2298f6a16/src/encoding/json/encode.go#L585 + fmt := byte('f') + if abs := math.Abs(f64); abs != 0 { + if abs < 1e-6 || abs >= 1e21 { + fmt = 'e' + } + } + b := strconv.AppendFloat(nil, f64, fmt, -1, 64) + if fmt == 'e' { + // clean up e-09 to e-9 + n := len(b) + if n >= 4 && b[n-4] == 'e' && b[n-3] == '-' && b[n-2] == '0' { + b[n-2] = b[n-1] + b = b[:n-1] + } + } + + // add decimal point and trailing zero if needed + if bytes.IndexByte(b, '.') < 0 { + if i := bytes.IndexByte(b, 'e'); i < 0 { + b = append(b, '.', '0') + } else { + b = append(b[:i+2], b[i:]...) + b[i] = '.' + b[i+1] = '0' + } + } + + if err := di.writeString(string(b)); err != nil { + return err + } + + if di.opts.IndicateFloatPrecision { + switch ai { + case 25: + return di.writeString("_1") + case 26: + return di.writeString("_2") + case 27: + return di.writeString("_3") + } + } + + return nil +} diff --git a/diagnose_test.go b/diagnose_test.go new file mode 100644 index 00000000..f6bad211 --- /dev/null +++ b/diagnose_test.go @@ -0,0 +1,628 @@ +// Copyright (c) Faye Amacker. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +package cbor + +import ( + "fmt" + "strings" + "testing" +) + +func TestDiagnoseExamples(t *testing.T) { + // https://www.rfc-editor.org/rfc/rfc8949.html#name-examples-of-encoded-cbor-da + testCases := []struct { + cbor []byte + diag string + }{ + { + hexDecode("00"), + `0`, + }, + { + hexDecode("01"), + `1`, + }, + { + hexDecode("0a"), + `10`, + }, + { + hexDecode("17"), + `23`, + }, + { + hexDecode("1818"), + `24`, + }, + { + hexDecode("1819"), + `25`, + }, + { + hexDecode("1864"), + `100`, + }, + { + hexDecode("1903e8"), + `1000`, + }, + { + hexDecode("1a000f4240"), + `1000000`, + }, + { + hexDecode("1b000000e8d4a51000"), + `1000000000000`, + }, + { + hexDecode("1bffffffffffffffff"), + `18446744073709551615`, + }, + { + hexDecode("c249010000000000000000"), + `18446744073709551616`, + }, + { + hexDecode("3bffffffffffffffff"), + `-18446744073709551616`, + }, + { + hexDecode("c349010000000000000000"), + `-18446744073709551617`, + }, + { + hexDecode("20"), + `-1`, + }, + { + hexDecode("29"), + `-10`, + }, + { + hexDecode("3863"), + `-100`, + }, + { + hexDecode("3903e7"), + `-1000`, + }, + { + hexDecode("f90000"), + `0.0`, // SKIP 18 + }, + { + hexDecode("f98000"), + `-0.0`, // SKIP 19 + }, + { + hexDecode("f93c00"), + `1.0`, // SKIP 20 + }, + { + hexDecode("fb3ff199999999999a"), + `1.1`, + }, + { + hexDecode("f93e00"), + `1.5`, + }, + { + hexDecode("f97bff"), + `65504.0`, // SKIP 23 + }, + { + hexDecode("fa47c35000"), + `100000.0`, // SKIP 24 + }, + { + hexDecode("fa7f7fffff"), + `3.4028234663852886e+38`, // SKIP 25 + }, + { + hexDecode("fb7e37e43c8800759c"), + `1.0e+300`, // SKIP 26 + }, + { + hexDecode("f90001"), + `5.960464477539063e-8`, // SKIP 27 + }, + { + hexDecode("f90400"), + `0.00006103515625`, // SKIP 28 + }, + { + hexDecode("f9c400"), + `-4.0`, // SKIP 29 + }, + { + hexDecode("fbc010666666666666"), + `-4.1`, + }, + { + hexDecode("f97c00"), + `Infinity`, + }, + { + hexDecode("f97e00"), + `NaN`, + }, + { + hexDecode("f9fc00"), + `-Infinity`, + }, + { + hexDecode("fa7f800000"), + `Infinity`, + }, + { + hexDecode("fa7fc00000"), + `NaN`, + }, + { + hexDecode("faff800000"), + `-Infinity`, + }, + { + hexDecode("fb7ff0000000000000"), + `Infinity`, + }, + { + hexDecode("fb7ff8000000000000"), + `NaN`, + }, + { + hexDecode("fbfff0000000000000"), + `-Infinity`, + }, + { + hexDecode("f4"), + `false`, + }, + { + hexDecode("f5"), + `true`, + }, + { + hexDecode("f6"), + `null`, + }, + { + hexDecode("f7"), + `undefined`, + }, + { + hexDecode("f0"), + `simple(16)`, + }, + { + hexDecode("f8ff"), + `simple(255)`, + }, + { + hexDecode("c074323031332d30332d32315432303a30343a30305a"), + `0("2013-03-21T20:04:00Z")`, + }, + { + hexDecode("c11a514b67b0"), + `1(1363896240)`, + }, + { + hexDecode("c1fb41d452d9ec200000"), + `1(1363896240.5)`, // SKIP 48 + }, + { + hexDecode("d74401020304"), + `23(h'01020304')`, + }, + { + hexDecode("d818456449455446"), + `24(h'6449455446')`, + }, + { + hexDecode("d82076687474703a2f2f7777772e6578616d706c652e636f6d"), + `32("http://www.example.com")`, + }, + { + hexDecode("40"), + `h''`, + }, + { + hexDecode("4401020304"), + `h'01020304'`, + }, + { + hexDecode("60"), + `""`, + }, + { + hexDecode("6161"), + `"a"`, + }, + { + hexDecode("6449455446"), + `"IETF"`, + }, + { + hexDecode("62225c"), + `"\"\\"`, + }, + { + hexDecode("62c3bc"), + `"\u00fc"`, + }, + { + hexDecode("63e6b0b4"), + `"\u6c34"`, + }, + { + hexDecode("64f0908591"), + `"\ud800\udd51"`, + }, + { + hexDecode("80"), + `[]`, + }, + { + hexDecode("83010203"), + `[1, 2, 3]`, + }, + { + hexDecode("8301820203820405"), + `[1, [2, 3], [4, 5]]`, + }, + { + hexDecode("98190102030405060708090a0b0c0d0e0f101112131415161718181819"), + `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]`, + }, + { + hexDecode("a0"), + `{}`, + }, + { + hexDecode("a201020304"), + `{1: 2, 3: 4}`, + }, + { + hexDecode("a26161016162820203"), + `{"a": 1, "b": [2, 3]}`, + }, + { + hexDecode("826161a161626163"), + `["a", {"b": "c"}]`, + }, + { + hexDecode("a56161614161626142616361436164614461656145"), + `{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E"}`, + }, + { + hexDecode("5f42010243030405ff"), + `(_ h'0102', h'030405')`, + }, + { + hexDecode("7f657374726561646d696e67ff"), + `(_ "strea", "ming")`, + }, + { + hexDecode("9fff"), + `[_ ]`, + }, + { + hexDecode("9f018202039f0405ffff"), + `[_ 1, [2, 3], [_ 4, 5]]`, + }, + { + hexDecode("9f01820203820405ff"), + `[_ 1, [2, 3], [4, 5]]`, + }, + { + hexDecode("83018202039f0405ff"), + `[1, [2, 3], [_ 4, 5]]`, + }, + { + hexDecode("83019f0203ff820405"), + `[1, [_ 2, 3], [4, 5]]`, + }, + { + hexDecode("9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff"), + `[_ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]`, + }, + { + hexDecode("bf61610161629f0203ffff"), + `{_ "a": 1, "b": [_ 2, 3]}`, + }, + { + hexDecode("826161bf61626163ff"), + `["a", {_ "b": "c"}]`, + }, + { + hexDecode("bf6346756ef563416d7421ff"), + `{_ "Fun": true, "Amt": -2}`, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("Diagnostic %d", i), func(t *testing.T) { + data, err := Diag(tc.cbor, nil) + if err != nil { + t.Errorf("Diag(0x%x) returned error %q", tc.cbor, err) + } else if string(data) != tc.diag { + t.Errorf("Diag(0x%x) returned `%s`, want `%s`", tc.cbor, string(data), tc.diag) + } + }) + } +} + +func TestDiagnoseByteString(t *testing.T) { + testCases := []struct { + title string + cbor []byte + diag string + opts *DiagOptions + }{ + { + "base16", + hexDecode("4412345678"), + `h'12345678'`, + &DiagOptions{ + ByteStringEncoding: "base16", + }, + }, + { + "base32", + hexDecode("4412345678"), + `b32'CI2FM6A'`, + &DiagOptions{ + ByteStringEncoding: "base32", + }, + }, + { + "base32hex", + hexDecode("4412345678"), + `h32'28Q5CU0'`, + &DiagOptions{ + ByteStringEncoding: "base32hex", + }, + }, + { + "base64", + hexDecode("4412345678"), + `b64'EjRWeA'`, + &DiagOptions{ + ByteStringEncoding: "base64", + }, + }, + { + "without ByteStringHexWhitespace option", + hexDecode("4b48656c6c6f20776f726c64"), + `h'48656c6c6f20776f726c64'`, + &DiagOptions{ + ByteStringHexWhitespace: false, + }, + }, + { + "with ByteStringHexWhitespace option", + hexDecode("4b48656c6c6f20776f726c64"), + `h'48 65 6c 6c 6f 20 77 6f 72 6c 64'`, + &DiagOptions{ + ByteStringHexWhitespace: true, + }, + }, + { + "without ByteStringText option", + hexDecode("4b68656c6c6f20776f726c64"), + `h'68656c6c6f20776f726c64'`, + &DiagOptions{ + ByteStringText: false, + }, + }, + { + "with ByteStringText option", + hexDecode("4b68656c6c6f20776f726c64"), + `'hello world'`, + &DiagOptions{ + ByteStringText: true, + }, + }, + { + "without ByteStringText option and with ByteStringHexWhitespace option", + hexDecode("4b68656c6c6f20776f726c64"), + `h'68 65 6c 6c 6f 20 77 6f 72 6c 64'`, + &DiagOptions{ + ByteStringText: false, + ByteStringHexWhitespace: true, + }, + }, + { + "without ByteStringEmbeddedCBOR and CBORSequence option", + hexDecode("4101"), + `h'01'`, + &DiagOptions{ + ByteStringEmbeddedCBOR: false, + CBORSequence: false, + }, + }, + { + "with ByteStringEmbeddedCBOR and CBORSequence option", + hexDecode("4101"), + `<<1>>`, + &DiagOptions{ + ByteStringEmbeddedCBOR: true, + CBORSequence: true, + }, + }, + { + "without ByteStringEmbeddedCBOR and CBORSequence option", + hexDecode("420102"), + `h'0102'`, + &DiagOptions{ + ByteStringEmbeddedCBOR: false, + CBORSequence: false, + }, + }, + { + "with ByteStringEmbeddedCBOR and CBORSequence option", + hexDecode("420102"), + `<<1, 2>>`, + &DiagOptions{ + ByteStringEmbeddedCBOR: true, + CBORSequence: true, + }, + }, + { + "with CBORSequence option", + hexDecode("0102"), + `1, 2`, + &DiagOptions{ + CBORSequence: true, + }, + }, + { + "with ByteStringEmbeddedCBOR and CBORSequence option", + hexDecode("4563666F6FF6"), + `h'63666f6ff6'`, + &DiagOptions{ + ByteStringEmbeddedCBOR: false, + CBORSequence: false, + }, + }, + { + "with ByteStringEmbeddedCBOR and CBORSequence option", + hexDecode("4563666F6FF6"), + `<<"foo", null>>`, + &DiagOptions{ + ByteStringEmbeddedCBOR: true, + CBORSequence: true, + }, + }, + { + "with ByteStringEmbeddedCBOR and without CBORSequence option", + hexDecode("4563666F6FF6"), + `h'63666f6ff6'`, + &DiagOptions{ + ByteStringEmbeddedCBOR: true, + CBORSequence: false, + }, + }, + { + "with CBORSequence option", + hexDecode("63666F6FF6"), + `"foo", null`, + &DiagOptions{ + CBORSequence: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + + data, err := Diag(tc.cbor, tc.opts) + if err != nil { + t.Errorf("Diag(0x%x) returned error %q", tc.cbor, err) + } else if string(data) != tc.diag { + t.Errorf("Diag(0x%x) returned `%s`, want %s", tc.cbor, string(data), tc.diag) + } + }) + } + + t.Run("invalid encoding", func(t *testing.T) { + cborData := hexDecode("4b48656c6c6f20776f726c64") + _, err := Diag(cborData, &DiagOptions{ + ByteStringEncoding: "base58", + }) + if err == nil { + t.Errorf("Diag(0x%x) didn't return error", cborData) + } else if !strings.Contains(err.Error(), `base58`) { + t.Errorf("Diag(0x%x) returned error %q", cborData, err) + } + }) + + t.Run("without CBORSequence option", func(t *testing.T) { + cborData := hexDecode("63666F6FF6") + _, err := Diag(cborData, nil) + if err == nil { + t.Errorf("Diag(0x%x) didn't return error", cborData) + } else if !strings.Contains(err.Error(), `extraneous data`) { + t.Errorf("Diag(0x%x) returned error %q", cborData, err) + } + }) +} + +func TestDiagnoseFloatingPointNumber(t *testing.T) { + testCases := []struct { + title string + cbor []byte + diag string + opts *DiagOptions + }{ + { + "float16 without IndicateFloatPrecision option", + hexDecode("f93e00"), + `1.5`, + &DiagOptions{ + IndicateFloatPrecision: false, + }, + }, + { + "float16 with IndicateFloatPrecision option", + hexDecode("f93e00"), + `1.5_1`, + &DiagOptions{ + IndicateFloatPrecision: true, + }, + }, + { + "float32 without IndicateFloatPrecision option", + hexDecode("fa47c35000"), + `100000.0`, + &DiagOptions{ + IndicateFloatPrecision: false, + }, + }, + { + "float32 with IndicateFloatPrecision option", + hexDecode("fa47c35000"), + `100000.0_2`, + &DiagOptions{ + IndicateFloatPrecision: true, + }, + }, + { + "float64 without IndicateFloatPrecision option", + hexDecode("fbc010666666666666"), + `-4.1`, + &DiagOptions{ + IndicateFloatPrecision: false, + }, + }, + { + "float64 with IndicateFloatPrecision option", + hexDecode("fbc010666666666666"), + `-4.1_3`, + &DiagOptions{ + IndicateFloatPrecision: true, + }, + }, + { + "with IndicateFloatPrecision option", + hexDecode("c1fb41d452d9ec200000"), + `1(1363896240.5_3)`, + &DiagOptions{ + IndicateFloatPrecision: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + + data, err := Diag(tc.cbor, tc.opts) + if err != nil { + t.Errorf("Diag(0x%x) returned error %q", tc.cbor, err) + } else if string(data) != tc.diag { + t.Errorf("Diag(0x%x) returned `%s`, want %s", tc.cbor, string(data), tc.diag) + } + }) + } +}