diff --git a/tlv/truncated.go b/tlv/truncated.go new file mode 100644 index 00000000..b35fc166 --- /dev/null +++ b/tlv/truncated.go @@ -0,0 +1,180 @@ +package tlv + +import ( + "encoding/binary" + "errors" + "io" +) + +// ErrTUintNotMinimal signals that decoding a truncated uint failed because the +// value was not minimally encoded. +var ErrTUintNotMinimal = errors.New("truncated uint not minimally encoded") + +// numLeadingZeroBytes16 computes the number of leading zeros for a uint16. +func numLeadingZeroBytes16(v uint16) uint64 { + switch { + case v == 0: + return 2 + case v&0xff00 == 0: + return 1 + default: + return 0 + } +} + +// SizeTUint16 returns the number of bytes remaining in a uint16 after +// truncating the leading zeros. +func SizeTUint16(v uint16) uint64 { + return 2 - numLeadingZeroBytes16(v) +} + +// ETUint16 is an Encoder for truncated uint16 values, where leading zeros will +// be omitted. An error is returned if val is not a *uint16. +func ETUint16(w io.Writer, val interface{}, buf *[8]byte) error { + if t, ok := val.(*uint16); ok { + binary.BigEndian.PutUint16(buf[:2], *t) + numZeros := numLeadingZeroBytes16(*t) + _, err := w.Write(buf[numZeros:2]) + return err + } + return NewTypeForEncodingErr(val, "uint16") +} + +// DTUint16 is an Decoder for truncated uint16 values, where leading zeros will +// be resurrected. An error is returned if val is not a *uint16. +func DTUint16(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if t, ok := val.(*uint16); ok && l <= 2 { + _, err := io.ReadFull(r, buf[2-l:]) + if err != nil { + return err + } + zero(buf[:2-l]) + *t = binary.BigEndian.Uint16(buf[:2]) + if 2-numLeadingZeroBytes16(*t) != l { + return ErrTUintNotMinimal + } + return nil + } + return NewTypeForDecodingErr(val, "uint16", l, 2) +} + +// numLeadingZeroBytes16 computes the number of leading zeros for a uint32. +func numLeadingZeroBytes32(v uint32) uint64 { + switch { + case v == 0: + return 4 + case v&0xffffff00 == 0: + return 3 + case v&0xffff0000 == 0: + return 2 + case v&0xff000000 == 0: + return 1 + default: + return 0 + } +} + +// SizeTUint32 returns the number of bytes remaining in a uint32 after +// truncating the leading zeros. +func SizeTUint32(v uint32) uint64 { + return 4 - numLeadingZeroBytes32(v) +} + +// ETUint32 is an Encoder for truncated uint32 values, where leading zeros will +// be omitted. An error is returned if val is not a *uint32. +func ETUint32(w io.Writer, val interface{}, buf *[8]byte) error { + if t, ok := val.(*uint32); ok { + binary.BigEndian.PutUint32(buf[:4], *t) + numZeros := numLeadingZeroBytes32(*t) + _, err := w.Write(buf[numZeros:4]) + return err + } + return NewTypeForEncodingErr(val, "uint32") +} + +// DTUint32 is an Decoder for truncated uint32 values, where leading zeros will +// be resurrected. An error is returned if val is not a *uint32. +func DTUint32(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if t, ok := val.(*uint32); ok && l <= 4 { + _, err := io.ReadFull(r, buf[4-l:]) + if err != nil { + return err + } + zero(buf[:4-l]) + *t = binary.BigEndian.Uint32(buf[:4]) + if 4-numLeadingZeroBytes32(*t) != l { + return ErrTUintNotMinimal + } + return nil + } + return NewTypeForDecodingErr(val, "uint32", l, 4) +} + +// numLeadingZeroBytes64 computes the number of leading zeros for a uint32. +// +// TODO(conner): optimize using unrolled binary search +func numLeadingZeroBytes64(v uint64) uint64 { + switch { + case v == 0: + return 8 + case v&0xffffffffffffff00 == 0: + return 7 + case v&0xffffffffffff0000 == 0: + return 6 + case v&0xffffffffff000000 == 0: + return 5 + case v&0xffffffff00000000 == 0: + return 4 + case v&0xffffff0000000000 == 0: + return 3 + case v&0xffff000000000000 == 0: + return 2 + case v&0xff00000000000000 == 0: + return 1 + default: + return 0 + } +} + +// SizeTUint64 returns the number of bytes remaining in a uint64 after +// truncating the leading zeros. +func SizeTUint64(v uint64) uint64 { + return 8 - numLeadingZeroBytes64(v) +} + +// ETUint64 is an Encoder for truncated uint64 values, where leading zeros will +// be omitted. An error is returned if val is not a *uint64. +func ETUint64(w io.Writer, val interface{}, buf *[8]byte) error { + if t, ok := val.(*uint64); ok { + binary.BigEndian.PutUint64(buf[:], *t) + numZeros := numLeadingZeroBytes64(*t) + _, err := w.Write(buf[numZeros:]) + return err + } + return NewTypeForEncodingErr(val, "uint64") +} + +// DTUint64 is an Decoder for truncated uint64 values, where leading zeros will +// be resurrected. An error is returned if val is not a *uint64. +func DTUint64(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if t, ok := val.(*uint64); ok && l <= 8 { + _, err := io.ReadFull(r, buf[8-l:]) + if err != nil { + return err + } + zero(buf[:8-l]) + *t = binary.BigEndian.Uint64(buf[:]) + if 8-numLeadingZeroBytes64(*t) != l { + return ErrTUintNotMinimal + } + return nil + } + return NewTypeForDecodingErr(val, "uint64", l, 8) +} + +// zero clears the passed byte slice. +func zero(b []byte) { + for i := range b { + b[i] = 0x00 + } +}