From 87f59d0cf94b7303bd6c105792aa5a9bcc8ad0c6 Mon Sep 17 00:00:00 2001 From: Yonah Date: Sun, 22 Mar 2026 17:07:15 +0900 Subject: [PATCH] ident: implement full representation This can be used with both system and member identifiers. Signed-off-by: Yonah --- ident/ident.go | 60 ++++++++++++++++++++++++++++++++++++-- ident/ident_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/ident/ident.go b/ident/ident.go index b577d10..e00d6ca 100644 --- a/ident/ident.go +++ b/ident/ident.go @@ -7,10 +7,16 @@ import ( "errors" "fmt" "strconv" + "unsafe" ) -// ErrNewline is returned for identifiers found to contain newline characters. -var ErrNewline = errors.New("identifier contains newline characters") +var ( + // ErrNewline is returned for identifiers found to contain newline characters. + ErrNewline = errors.New("identifier contains newline characters") + // ErrSeparator is returned for representations of [F] missing its separator + // byte in its expected index. + ErrSeparator = errors.New("identifier has incorrect separator byte") +) // UnexpectedSizeError describes a malformed string representation of an // identifier, with unexpected length. @@ -41,3 +47,53 @@ type MS interface { // behaviour: a type is an identifier part if it has an ident method. ident() } + +// F represents a full system or member identifier. +type F[V any, T interface { + MS + *V +}] struct { + // Underlying system or member. + I T + // Split from instance suffix after [Separator] byte. + Remote Remote +} + +// Separator is the byte separating representation of [MS] from [Remote]. +const Separator = ':' + +// Encode appends the canonical string representation of full to dst and returns +// the extended buffer. +func (full *F[V, T]) Encode(dst []byte) []byte { + dst = full.I.Encode(dst) + dst = append(dst, Separator) + dst = full.Remote.Encode(dst) + return dst +} + +// String returns the canonical string representation of full. +func (full *F[V, T]) String() string { + s := full.Encode(nil) + return unsafe.String(unsafe.SliceData(s), len(s)) +} + +// MarshalText returns the result of Encode. +func (full *F[V, T]) MarshalText() (data []byte, err error) { + return full.Encode(nil), nil +} + +// UnmarshalText strictly decodes data into full. +func (full *F[V, T]) UnmarshalText(data []byte) (err error) { + sz := full.I.EncodedSize() + if len(data) < sz+1 || data[sz] != Separator { + return ErrSeparator + } + + if full.I == nil { + full.I = new(V) + } + if err = full.I.UnmarshalText(data[:sz]); err != nil { + return + } + return full.Remote.UnmarshalText(data[sz+1:]) +} diff --git a/ident/ident_test.go b/ident/ident_test.go index 101f300..bc13b5c 100644 --- a/ident/ident_test.go +++ b/ident/ident_test.go @@ -5,7 +5,9 @@ import ( "encoding/base64" "fmt" "reflect" + "strings" "testing" + "time" "git.gensokyo.uk/cofront/cof-spec/ident" ) @@ -76,6 +78,74 @@ func (testCases rTestCases[V, S]) run(t *testing.T) { } } +func TestFS(t *testing.T) { + t.Parallel() + + rTestCases[ident.F[ident.S, *ident.S], *ident.F[ident.S, *ident.S]]{ + {"separator", ident.F[ident.S, *ident.S]{}, nil, ident.ErrSeparator}, + {"bad ident", ident.F[ident.S, *ident.S]{}, []byte( + strings.Repeat("=", ident.EncodedSizeSystem) + ":", + ), base64.CorruptInputError(0)}, + + {"valid", ident.F[ident.S, *ident.S]{ + I: &ident.S{ + Site: ident.TrivialSite, + Host: ident.TrivialHost, + + Time: uint64(time.Date( + 0xfd, 7, 15, + 23, 59, 59, 0xcafe, + time.UTC, + ).UnixNano()), + + ID: 0xfee1dead0badf00d, + }, + + Remote: mustNewRemote("gensokyo.uk"), + }, []byte("_srt_r66_sr-AEPO8ZJKEA3wrQut3uH-:gensokyo.uk"), nil}, + }.run(t) +} + +func TestFM(t *testing.T) { + t.Parallel() + + rTestCases[ident.F[ident.M, *ident.M], *ident.F[ident.M, *ident.M]]{ + {"separator", ident.F[ident.M, *ident.M]{}, nil, ident.ErrSeparator}, + {"bad ident", ident.F[ident.M, *ident.M]{}, []byte( + strings.Repeat("=", ident.EncodedSizeMember) + ":", + ), base64.CorruptInputError(0)}, + + {"valid", ident.F[ident.M, *ident.M]{ + I: &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, + }, + }, + + Remote: mustNewRemote("gensokyo.uk"), + }, []byte("_f39_f39_f2rQkLO8ZJKEGRucy4AAAAA_srt_r66_sr-AEPO8ZJKEA3wrQut3uH-:gensokyo.uk"), nil}, + }.run(t) +} + func TestErrors(t *testing.T) { t.Parallel()