diff --git a/construct.go b/construct.go index a6ae68f..bf72253 100644 --- a/construct.go +++ b/construct.go @@ -25,11 +25,15 @@ type Components struct { // NewFromParts builds an OrderlyID from explicit component values. // -// NewFromParts may return an error wrapping ErrInvalidPrefix. +// NewFromParts may return an error wrapping ErrInvalidPrefix or +// ErrUnsupportedVersion. func NewFromParts(c Components, withChecksum bool) (string, error) { if err := validatePrefix(c.Prefix); err != nil { return "", err } + if wireVersion(c.Flags) != supportedWireVersion { + return "", fmt.Errorf("%w: %d", ErrUnsupportedVersion, wireVersion(c.Flags)) + } // Convert absolute time to ms since 2020-01-01 UTC (epoch2020). var msSince2020 uint64 if c.TimeMs >= epoch2020 { @@ -54,8 +58,8 @@ func NewFromParts(c Components, withChecksum bool) (string, error) { // NewFromPartsHex builds an OrderlyID from explicit component values and a // big-endian random value encoded as hex. // -// NewFromPartsHex may return an error wrapping ErrInvalidPrefix or -// ErrInvalidRandomHex. +// NewFromPartsHex may return an error wrapping ErrInvalidPrefix, +// ErrInvalidRandomHex, or ErrUnsupportedVersion. func NewFromPartsHex(c Components, randomHex string, withChecksum bool) (string, error) { rb, err := hex.DecodeString(randomHex) if err != nil { diff --git a/orderlyid.go b/orderlyid.go index 83307b1..c246eb2 100644 --- a/orderlyid.go +++ b/orderlyid.go @@ -80,6 +80,8 @@ var ( ErrInvalidBase32 = errors.New("orderlyid: invalid base32") // ErrInvalidRandomHex reports invalid random hex input passed to NewFromPartsHex. ErrInvalidRandomHex = errors.New("orderlyid: invalid random hex") + // ErrUnsupportedVersion reports OrderlyIDs with unsupported wire version bits. + ErrUnsupportedVersion = errors.New("orderlyid: unsupported wire version") ) func init() { @@ -108,9 +110,9 @@ func init() { } const ( - versionBits = 0 // v1 - privacyBitMask = 1 << 5 - epoch2020 int64 = 1577836800000 // 2020-01-01T00:00:00Z in ms + supportedWireVersion = 0 // v1 + privacyBitMask = 1 << 5 + epoch2020 int64 = 1577836800000 // 2020-01-01T00:00:00Z in ms ) var ( @@ -200,7 +202,8 @@ type Parsed struct { // Parse decodes an OrderlyID string and returns its components. // // Parse may return errors wrapping ErrInvalidFormat, ErrInvalidPrefix, -// ErrInvalidChecksum, ErrInvalidPayloadLength, or ErrInvalidBase32. +// ErrInvalidChecksum, ErrInvalidPayloadLength, ErrInvalidBase32, or +// ErrUnsupportedVersion. func Parse(s string) (*Parsed, error) { s = strings.TrimSpace(s) base := s @@ -242,6 +245,9 @@ func Parse(s string) (*Parsed, error) { return nil, err } ms, flags, tenant, seq, shard, random60 := unpack(buf) + if wireVersion(flags) != supportedWireVersion { + return nil, fmt.Errorf("%w: %d", ErrUnsupportedVersion, wireVersion(flags)) + } return &Parsed{ Prefix: prefix, TimeMs: int64(ms) + epoch2020, @@ -253,6 +259,10 @@ func Parse(s string) (*Parsed, error) { }, nil } +func wireVersion(flags byte) byte { + return flags >> 6 +} + // Packing layout (big-endian) // | 48b time | 8b flags | 16b tenant | 12b seq | 16b shard | 60b random | func pack(ms uint64, flags byte, tenant uint16, seq12 uint16, shard uint16, random60 uint64) (out [20]byte) { diff --git a/orderlyid_test.go b/orderlyid_test.go index fecdd30..4db6307 100644 --- a/orderlyid_test.go +++ b/orderlyid_test.go @@ -119,6 +119,19 @@ func TestParseInvalidChecksumBaseDoesNotPanic(t *testing.T) { } } +func TestParseRejectsUnsupportedWireVersion(t *testing.T) { + body := pack(0, 0x40, 0, 0, 0, 0) + id := "order_" + b32encode(body[:]) + + _, err := Parse(id) + if err == nil { + t.Fatalf("expected unsupported version error") + } + if !errors.Is(err, ErrUnsupportedVersion) { + t.Fatalf("expected ErrUnsupportedVersion, got %v", err) + } +} + func TestNewFromPartsErrorsSupportErrorsIs(t *testing.T) { if _, err := NewFromParts(Components{Prefix: "Bad!"}, false); err == nil { t.Fatalf("expected invalid prefix error") @@ -126,6 +139,12 @@ func TestNewFromPartsErrorsSupportErrorsIs(t *testing.T) { t.Fatalf("expected ErrInvalidPrefix, got %v", err) } + if _, err := NewFromParts(Components{Prefix: "order", Flags: 0x40}, false); err == nil { + t.Fatalf("expected unsupported version error") + } else if !errors.Is(err, ErrUnsupportedVersion) { + t.Fatalf("expected ErrUnsupportedVersion, got %v", err) + } + if _, err := NewFromPartsHex(Components{Prefix: "order"}, "zz", false); err == nil { t.Fatalf("expected invalid random hex error") } else if !errors.Is(err, ErrInvalidRandomHex) {