diff --git a/README.md b/README.md index 5f8f0f6..73f19a8 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,28 @@ n.GetPaddedUint8() // parses the value as uint8 and pads it if too small // all available types: bool, uint8, uint16, uint32, uint64, string, time.Time and Nodes ``` +### Supported types + +| Type | Max Length (bytes) | Notes | +|----------|-------------------:|-------------------------------------------------------------------| +| `bool` | 1 | Any **non-zero** value is treated as `true` | +| `uint8` | 1 | | +| `uint16` | 2 | | +| `uint32` | 4 | | +| `uint64` | 8 | | +| `Time` | 8 | Value is parsed as padded `uint64` and then as **Unix** (seconds) | +| `string` | **Unlimited** | Value is parsed as **UTF-8** | +| `Nodes` | **Unlimited** | | + +> If the **value** is bigger than the **max length**, only the first _n_ bytes are used. + ## Important details ### Tags are non-unique in TLV messages -When parsing a value to multiple nodes, tags can be **repeated** and will be returned by -the parser. Use `Nodes#GetByTag(tlv.Tag)` and `Nodes#GetFirstByTag(tlv.Tag)` to fetch **all** -or **one** node, respectively. +When parsing a value to multiple nodes, tags can be **repeated** and will be returned by the parser. +Use `Nodes#GetByTag(tlv.Tag)` and `Nodes#GetFirstByTag(tlv.Tag)` to fetch **all** or **one** node, +respectively. #### Example: @@ -89,30 +104,29 @@ message: ### The parser supports multiple root level messages -After reading a TLV-encoded message from a byte-array, when using `tlv.ParseBytes([]byte)` -the parser will continue reading the array until it reaches the end. The returned structure -will have **all the nodes** found in the payload. +After reading a TLV-encoded message from a byte-array, when using `tlv.ParseBytes([]byte)` the parser +will continue reading the array until it reaches the end. The returned structure will have **all the +nodes** found in the payload. -> ⚠️ The parser works in an all or none strategy when dealing with multiple messages. +> ⚠️  The parser works in an all or none strategy when dealing with multiple messages. ## Caveats ### No bit parity or checksum -The encoding scheme itself does *not* provide any **bit parity** or **checksum** to ensure -the integrity of received payloads. It is up to the upper layer or to the payload design -to add these features. +The encoding scheme itself does *not* provide **bit parity** or **checksum** to ensure the integrity +of received payloads. It is up to the upper layer or to the payload design to add these features. ### Errors with multiple messages are hard to pinpoint -The bigger the payload, more likely errors will *not* be identified by the parser. The -**only** failproof hint of a malformed payload is a mismatch between the read length and -the remaining bytes in the stream. When that happens, a reading error may have happened -*anywhere* in the payload, which means none of it can be trusted. +The bigger the payload, more likely errors will *not* be identified by the parser. The **only** +failproof hint of a malformed payload is a mismatch between the read length and the remaining bytes +in the stream. When that happens, a reading error may have happened *anywhere* in the payload, which +means none of it can be trusted. -> If by the end of the byte stream there is a mismatch between the provided length and -> the remaining bytes, the whole payload is invalidated, and the parser will return an -> error -- regardless of how many successful messages it has read. +> ⚠️  If by the end of the stream there is a mismatch between the **provided length** and the +> **remaining bytes**, the whole payload is invalidated, and the parser will return an error, +> **regardless of how many successful messages it has read**. ## Roadmap @@ -121,5 +135,6 @@ the remaining bytes in the stream. When that happens, a reading error may have h ## Changelog -* **`v1.0.0`** (2021-03-14) +* **`v1.0.0-alpha1`** (2021-03-14) * First release with basic parsing support + * ⚠️  Methods and structs may change completely diff --git a/tlv/examples_test.go b/tlv/examples_test.go index 6b561ac..71df596 100644 --- a/tlv/examples_test.go +++ b/tlv/examples_test.go @@ -71,7 +71,7 @@ func newPushNotification(nodes Nodes) pushNotification { } if timestamp, ok := nodes.GetFirstByTag(tagTimestamp); ok { ts, _ := timestamp.GetDate() - pn.Timestamp = ts.UTC() + pn.Timestamp = ts } return pn diff --git a/tlv/node.go b/tlv/node.go index cd300d4..74019e6 100644 --- a/tlv/node.go +++ b/tlv/node.go @@ -29,6 +29,36 @@ func (n Node) GetNodes() (Nodes, error) { return ParseBytes(n.Value) } +// GetBool parses the value as boolean if it has enough bytes +func (n Node) GetBool() (res, ok bool) { + if len(n.Value) < sizes.Bool { + return false, false + } + + return n.Value[0] != 0, true +} + +// GetPaddedBool parses the value as boolean regardless of its size +func (n Node) GetPaddedBool() bool { + res, _ := n.GetBool() + return res +} + +// GetString parses the value as UTF8 text +func (n Node) GetString() string { + return string(n.Value) +} + +// GetDate parses the value as date if it has enough bytes +func (n Node) GetDate() (res time.Time, ok bool) { + if len(n.Value) == 0 { + return res, false + } + + epoch := n.GetPaddedUint64() + return time.Unix(int64(epoch), 0).UTC(), true +} + // GetUint8 parses the value as uint8 func (n Node) GetUint8() (res uint8, ok bool) { if len(n.Value) < sizes.Uint8 { @@ -91,33 +121,3 @@ func (n Node) GetPaddedUint64() uint64 { padding := utils.GetPadding(sizes.Uint64, len(n.Value)) return parser.Uint64(append(padding, n.Value...)) } - -// GetString parses the value as UTF8 text -func (n Node) GetString() string { - return string(n.Value) -} - -// GetBool parses the value as boolean if it has enough bytes -func (n Node) GetBool() (res, ok bool) { - if len(n.Value) < sizes.Bool { - return false, false - } - - return n.Value[0] != 0, true -} - -// GetPaddedBool parses the value as boolean regardless of its size -func (n Node) GetPaddedBool() bool { - res, _ := n.GetBool() - return res -} - -// GetDate parses the value as date if it has enough bytes -func (n Node) GetDate() (res time.Time, ok bool) { - if len(n.Value) == 0 { - return res, false - } - - epoch := n.GetPaddedUint64() - return time.Unix(int64(epoch), 0), true -} diff --git a/tlv/node_test.go b/tlv/node_test.go new file mode 100644 index 0000000..9654751 --- /dev/null +++ b/tlv/node_test.go @@ -0,0 +1,254 @@ +package tlv + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type testScenarios map[*Node]struct { + res interface{} + ok bool +} + +func TestNode_String(t *testing.T) { + node := Node{Raw: []byte{0x01, 0x02, 0x03, 0x04, 0x05}} + + require.Equal(t, "AQIDBAU=", node.String()) +} + +func TestNode_GetNodes(t *testing.T) { + nodes, err := Node{Value: data}.GetNodes() + + require.Nil(t, err) + require.NotNil(t, nodes) + require.Equal(t, 1, len(nodes)) + require.Equal(t, tagMessage, nodes[0].Tag) +} + +func TestNode_GetNodes_WhenTheValueIsInvalid(t *testing.T) { + nodes, err := Node{Value: data[:3]}.GetNodes() + + require.NotNil(t, err) + require.Empty(t, nodes) +} + +func TestNode_GetBool(t *testing.T) { + scenarios := testScenarios{ + &Node{Value: []byte{0xff, 0x12}}: {true, true}, // bigger + &Node{Value: []byte{0xff}}: {true, true}, // exact size (>1) + &Node{Value: []byte{0x01}}: {true, true}, // exact size (=1) + &Node{Value: []byte{0x00}}: {false, true}, // exact size (=0) + &Node{Value: []byte{}}: {false, false}, // empty + &Node{Value: nil}: {false, false}, // nil + } + + for node, expected := range scenarios { + res, ok := node.GetBool() + + require.Equal(t, expected.res, res) + require.Equal(t, expected.ok, ok) + } +} + +func TestNode_GetPaddedBool(t *testing.T) { + scenarios := testScenarios{ + &Node{Value: []byte{0xff, 0x12}}: {res: true}, // bigger + &Node{Value: []byte{0xff}}: {res: true}, // exact size (>1) + &Node{Value: []byte{0x01}}: {res: true}, // exact size (=1) + &Node{Value: []byte{0x00}}: {res: false}, // exact size (=0) + &Node{Value: []byte{}}: {res: false}, // empty + &Node{Value: nil}: {res: false}, // nil + } + + for node, expected := range scenarios { + res := node.GetPaddedBool() + + require.Equal(t, expected.res, res) + } +} + +func TestNode_GetString(t *testing.T) { + scenarios := testScenarios{ + &Node{Value: []byte("abc")}: {res: "abc"}, // valid + &Node{Value: []byte{}}: {res: ""}, // empty + &Node{Value: nil}: {res: ""}, // nil + } + + for node, expected := range scenarios { + res := node.GetString() + + require.Equal(t, expected.res, res) + } +} + +func TestNode_GetDate(t *testing.T) { + date := time.Date(2021, 3, 14, 19, 26, 45, 0, time.UTC) + + res, ok := Node{Value: []byte{0x60, 0x4e, 0x63, 0x75}}.GetDate() + + require.True(t, ok) + require.Equal(t, date, res) +} + +func TestNode_GetDate_WhenValueIsEmpty(t *testing.T) { + res, ok := Node{Value: []byte{}}.GetDate() + + require.False(t, ok) + require.Empty(t, res) +} + +func TestNode_GetUint8(t *testing.T) { + scenarios := testScenarios{ + &Node{Value: []byte{0x02, 0xff}}: {uint8(2), true}, // bigger + &Node{Value: []byte{0x02}}: {uint8(2), true}, // exact size + &Node{Value: []byte{0x00}}: {uint8(0), true}, // exact size (zero) + &Node{Value: []byte{}}: {uint8(0), false}, // empty + &Node{Value: nil}: {uint8(0), false}, // nil + } + + for node, expected := range scenarios { + res, ok := node.GetUint8() + + require.Equal(t, expected.res, res) + require.Equal(t, expected.ok, ok) + } +} + +func TestNode_GetPaddedUint8(t *testing.T) { + scenarios := testScenarios{ + &Node{Value: []byte{0xff, 0x12}}: {res: uint8(255)}, // bigger + &Node{Value: []byte{0xff}}: {res: uint8(255)}, // exact size + &Node{Value: []byte{0x00}}: {res: uint8(0)}, // exact size (zero) + &Node{Value: []byte{}}: {res: uint8(0)}, // empty + &Node{Value: nil}: {res: uint8(0)}, // nil + } + + for node, expected := range scenarios { + res := node.GetPaddedUint8() + + require.Equal(t, expected.res, res) + } +} + +func TestNode_GetUint16(t *testing.T) { + scenarios := testScenarios{ + &Node{Value: []byte{0xab, 0xcd, 0xff}}: {uint16(43981), true}, // bigger + &Node{Value: []byte{0xab, 0xcd}}: {uint16(43981), true}, // exact size + &Node{Value: []byte{0x00, 0x00}}: {uint16(0), true}, // exact size (zero) + &Node{Value: []byte{0x00}}: {uint16(0), false}, // smaller + &Node{Value: []byte{}}: {uint16(0), false}, // empty + &Node{Value: nil}: {uint16(0), false}, // nil + } + + for node, expected := range scenarios { + res, ok := node.GetUint16() + + require.Equal(t, expected.res, res) + require.Equal(t, expected.ok, ok) + } +} + +func TestNode_GetPaddedUint16(t *testing.T) { + scenarios := testScenarios{ + &Node{Value: []byte{0x00, 0xf0, 0xff}}: {res: uint16(240)}, // bigger + &Node{Value: []byte{0x00, 0xf0}}: {res: uint16(240)}, // exact size + &Node{Value: []byte{0x00, 0x00}}: {res: uint16(0)}, // exact size (zero) + &Node{Value: []byte{0x00}}: {res: uint16(0)}, // smaller + &Node{Value: []byte{}}: {res: uint16(0)}, // empty + &Node{Value: nil}: {res: uint16(0)}, // nil + } + + for node, expected := range scenarios { + res := node.GetPaddedUint16() + + require.Equal(t, expected.res, res) + } +} + +func TestNode_GetUint32(t *testing.T) { + scenarios := testScenarios{ + &Node{Value: []byte{0x12, 0x34, 0x56, 0x78, 0x9a}}: {uint32(305419896), true}, // bigger + &Node{Value: []byte{0x12, 0x34, 0x56, 0x78}}: {uint32(305419896), true}, // exact size + &Node{Value: []byte{0x00, 0x00, 0x12}}: {uint32(0), false}, // smaller (3 bytes) + &Node{Value: []byte{0x00, 0x34}}: {uint32(0), false}, // smaller (2 bytes) + &Node{Value: []byte{0x56}}: {uint32(0), false}, // smaller (1 byte) + &Node{Value: []byte{}}: {uint32(0), false}, // empty + &Node{Value: nil}: {uint32(0), false}, // nil + } + + for node, expected := range scenarios { + res, ok := node.GetUint32() + + require.Equal(t, expected.res, res) + require.Equal(t, expected.ok, ok) + } +} + +func TestNode_GetPaddedUint32(t *testing.T) { + scenarios := testScenarios{ + &Node{Value: []byte{0x12, 0x34, 0x56, 0x78, 0x9a}}: {res: uint32(305419896)}, // bigger + &Node{Value: []byte{0x12, 0x34, 0x56, 0x78}}: {res: uint32(305419896)}, // exact size + &Node{Value: []byte{0x00, 0x00, 0x12}}: {res: uint32(18)}, // smaller (3 bytes) + &Node{Value: []byte{0x00, 0x34}}: {res: uint32(52)}, // smaller (2 bytes) + &Node{Value: []byte{0x56}}: {res: uint32(86)}, // smaller (1 byte) + &Node{Value: []byte{}}: {res: uint32(0)}, // empty + &Node{Value: nil}: {res: uint32(0)}, // nil + } + + for node, expected := range scenarios { + res := node.GetPaddedUint32() + + require.Equal(t, expected.res, res) + } +} + +func TestNode_GetUint64(t *testing.T) { + fullValue := []byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12} // bigger (9 bytes) + + scenarios := testScenarios{ + &Node{Value: fullValue[:9]}: {uint64(1311768467463790320), true}, // bigger + &Node{Value: fullValue[:8]}: {uint64(1311768467463790320), true}, // exact size + &Node{Value: fullValue[:7]}: {uint64(0), false}, // smaller (7 bytes) + &Node{Value: fullValue[:6]}: {uint64(0), false}, // smaller (6 bytes) + &Node{Value: fullValue[:5]}: {uint64(0), false}, // smaller (5 bytes) + &Node{Value: fullValue[:4]}: {uint64(0), false}, // smaller (4 bytes) + &Node{Value: fullValue[:3]}: {uint64(0), false}, // smaller (3 bytes) + &Node{Value: fullValue[:2]}: {uint64(0), false}, // smaller (2 bytes) + &Node{Value: fullValue[:1]}: {uint64(0), false}, // smaller (1 byte) + &Node{Value: []byte{}}: {uint64(0), false}, // empty + &Node{Value: nil}: {uint64(0), false}, // nil + } + + for node, expected := range scenarios { + res, ok := node.GetUint64() + + require.Equal(t, expected.res, res) + require.Equal(t, expected.ok, ok) + } +} + +func TestNode_GetPaddedUint64(t *testing.T) { + fullValue := []byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12} // bigger (9 bytes) + + scenarios := testScenarios{ + &Node{Value: fullValue[:9]}: {res: uint64(1311768467463790320)}, // bigger + &Node{Value: fullValue[:8]}: {res: uint64(1311768467463790320)}, // exact size + &Node{Value: fullValue[:7]}: {res: uint64(5124095576030430)}, // smaller (7 bytes) + &Node{Value: fullValue[:6]}: {res: uint64(20015998343868)}, // smaller (6 bytes) + &Node{Value: fullValue[:5]}: {res: uint64(78187493530)}, // smaller (5 bytes) + &Node{Value: fullValue[:4]}: {res: uint64(305419896)}, // smaller (4 bytes) + &Node{Value: fullValue[:3]}: {res: uint64(1193046)}, // smaller (3 bytes) + &Node{Value: fullValue[:2]}: {res: uint64(4660)}, // smaller (2 bytes) + &Node{Value: fullValue[:1]}: {res: uint64(18)}, // smaller (1 byte) + &Node{Value: []byte{}}: {res: uint64(0)}, // empty + &Node{Value: nil}: {res: uint64(0)}, // nil + } + + for node, expected := range scenarios { + res := node.GetPaddedUint64() + + require.Equal(t, expected.res, res) + } +} diff --git a/tlv/parser_test.go b/tlv/parser_test.go index cdf31bb..029b0ec 100644 --- a/tlv/parser_test.go +++ b/tlv/parser_test.go @@ -34,3 +34,24 @@ func TestParseReader_WhenTheReaderFails(t *testing.T) { require.NotNil(t, err) require.Nil(t, nodes) } + +func TestParseSingle_WhenTheDataIsCorrupted(t *testing.T) { + corrupted := data[:len(data)-5] + + node, read, err := ParseSingle(corrupted) + + require.NotNil(t, err) + require.Zero(t, read) + require.Empty(t, node) +} + +func TestParseBytes_WhenTheDataIsCorrupted(t *testing.T) { + corrupted := make([]byte, 0, len(data)*2-5) + corrupted = append(corrupted, data...) + corrupted = append(corrupted, data[:len(data)-5]...) + + node, err := ParseBytes(corrupted) + + require.NotNil(t, err) + require.Nil(t, node) +}