ident: implement full representation
This can be used with both system and member identifiers. Signed-off-by: Yonah <contrib@gensokyo.uk>
This commit is contained in:
@@ -7,10 +7,16 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNewline is returned for identifiers found to contain newline characters.
|
var (
|
||||||
var ErrNewline = errors.New("identifier contains newline characters")
|
// 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
|
// UnexpectedSizeError describes a malformed string representation of an
|
||||||
// identifier, with unexpected length.
|
// identifier, with unexpected length.
|
||||||
@@ -41,3 +47,53 @@ type MS interface {
|
|||||||
// behaviour: a type is an identifier part if it has an ident method.
|
// behaviour: a type is an identifier part if it has an ident method.
|
||||||
ident()
|
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:])
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/cofront/cof-spec/ident"
|
"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) {
|
func TestErrors(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user