diff --git a/ident/member.go b/ident/member.go new file mode 100644 index 0000000..d4b0eb6 --- /dev/null +++ b/ident/member.go @@ -0,0 +1,84 @@ +package ident + +import ( + "encoding/base64" + "encoding/binary" + "unsafe" +) + +// M represents a unique member identifier. +type M struct { + // A per-system value incremented by some unspecified amount every time the + // metadata of a member first appears to the backend. + Serial uint64 + // An instant in time, some time after the corresponding member metadata + // first appeared to the backend, represented in nanoseconds since + // 1970-01-01. + Time uint64 + // Randomly generated value. The implementation must guarantee that the same + // value cannot be emitted for a Time value. + ID uint64 + + // Underlying system. + System S +} + +const ( + // SizeMember is the size of the binary representation of [M]. + SizeMember = SizeSystem * 2 + // EncodedSizeMember is the size of the string representation of [M]. + EncodedSizeMember = SizeMember / 3 * 4 +) + +// Encode appends the canonical string representation of mid to dst and returns +// the extended buffer. +func (mid *M) Encode(dst []byte) []byte { + var buf [SizeMember - SizeSystem]byte + + p := buf[:] + binary.LittleEndian.PutUint64(p, mid.Serial) + p = p[8:] + binary.LittleEndian.PutUint64(p, mid.Time) + p = p[8:] + binary.LittleEndian.PutUint64(p, mid.ID) + dst = base64.URLEncoding.AppendEncode(dst, buf[:]) + + return mid.System.Encode(dst) +} + +// String returns the canonical string representation of mid. +func (mid *M) String() string { + s := mid.Encode(nil) + return unsafe.String(unsafe.SliceData(s), len(s)) +} + +// MarshalText returns the result of Encode. +func (mid *M) MarshalText() (data []byte, err error) { + return mid.Encode(nil), nil +} + +// UnmarshalText strictly decodes data into mid. +func (mid *M) UnmarshalText(data []byte) error { + if len(data) != EncodedSizeMember { + return &UnexpectedSizeError{data, EncodedSizeMember} + } + + var buf [SizeMember - SizeSystem]byte + if n, err := base64.URLEncoding.Decode( + buf[:], + data[:EncodedSizeMember-EncodedSizeSystem], + ); err != nil { + return err + } else if n != SizeMember-SizeSystem { + return ErrNewline + } + + p := buf[:] + mid.Serial = binary.LittleEndian.Uint64(p) + p = p[8:] + mid.Time = binary.LittleEndian.Uint64(p) + p = p[8:] + mid.ID = binary.LittleEndian.Uint64(p) + + return mid.System.UnmarshalText(data[EncodedSizeMember-EncodedSizeSystem:]) +} diff --git a/ident/member_test.go b/ident/member_test.go new file mode 100644 index 0000000..a50ad98 --- /dev/null +++ b/ident/member_test.go @@ -0,0 +1,84 @@ +package ident_test + +import ( + "encoding/base64" + "strings" + "testing" + "time" + + "git.gensokyo.uk/cofront/cof-spec/ident" +) + +func TestM(t *testing.T) { + t.Parallel() + + rTestCases[ident.M, *ident.M]{ + {"short", ident.M{}, nil, &ident.UnexpectedSizeError{ + Data: nil, + Want: ident.EncodedSizeMember, + }}, + + {"malformed", ident.M{}, []byte{ + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + 0xfe, 0xe1, 0xde, 0xad, + }, base64.CorruptInputError(0)}, + + {"newline", ident.M{}, []byte( + "AAAA" + strings.Repeat("\n", ident.EncodedSizeMember-4), + ), ident.ErrNewline}, + + {"valid", ident.M{ + Serial: 0xfdfdfdfdfdfdfdfd, + + Time: uint64(time.Date( + 0xfd, 7, 15, + 23, 59, 59, 0xcab, + time.UTC, + ).UnixNano()), + + ID: 0x2e736e64, + + System: ident.S{ + Site: ident.TrivialSite, + Host: ident.TrivialHost, + + Time: uint64(time.Date( + 0xfd, 7, 15, + 23, 59, 59, 0xcafe, + time.UTC, + ).UnixNano()), + + ID: 0xfee1dead0badf00d, + }, + }, base64.URLEncoding.AppendEncode(nil, []byte{ + /* member */ + + /* serial: */ 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, + /* time: */ 0xab, 0x42, 0x42, 0xce, 0xf1, 0x92, 0x4a, 0x10, + /* id: */ 0x64, 0x6e, 0x73, 0x2e, 0, 0, 0, 0, + + /* system */ + + /* site: */ 0xfe, 0xca, 0xed, 0xfe, + /* host: */ 0xbe, 0xba, 0xfe, 0xca, + + /* time: */ 0xfe, 0, 0x43, 0xce, 0xf1, 0x92, 0x4a, 0x10, + /* id: */ 0xd, 0xf0, 0xad, 0xb, 0xad, 0xde, 0xe1, 0xfe, + }), nil}, + }.run(t) +}