diff --git a/ident/ident_test.go b/ident/ident_test.go index 5edd6a5..101f300 100644 --- a/ident/ident_test.go +++ b/ident/ident_test.go @@ -88,6 +88,42 @@ func TestErrors(t *testing.T) { Data: make([]byte, 1<<10), Want: ident.EncodedSizeSystem, }, "got 1024 bytes for a 32-byte identifier"}, + + {"InvalidLabelLDH start", &ident.InvalidLabelError{ + Data: []byte{'-'}, + Label: 0xbad, + Index: 0, + Reason: ident.InvalidLabelLDH, + }, "label 2989 starts with '-'"}, + + {"InvalidLabelLDH end", &ident.InvalidLabelError{ + Data: []byte{0, '-'}, + Label: 0xbad, + Index: 1, + Reason: ident.InvalidLabelLDH, + }, "label 2989 ends with '-'"}, + + {"InvalidLabelLDH", &ident.InvalidLabelError{ + Data: []byte{0}, + Label: 0xbad, + Index: 0, + Reason: ident.InvalidLabelLDH, + }, `label 2989 contains invalid byte '\x00' at index 0`}, + + {"InvalidLabelShort", &ident.InvalidLabelError{ + Label: 0xf00d, + Reason: ident.InvalidLabelShort, + }, "label 61453 is empty"}, + + {"InvalidLabelLong", &ident.InvalidLabelError{ + Label: 0xf00d, + Reason: ident.InvalidLabelLong, + }, "label 61453 is longer than 63 bytes"}, + + {"InvalidLabelError", &ident.InvalidLabelError{ + Label: 0xcafe, + Reason: 0xbadf00d, + }, "invalid label 51966 at byte 0"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/ident/remote.go b/ident/remote.go new file mode 100644 index 0000000..6be1906 --- /dev/null +++ b/ident/remote.go @@ -0,0 +1,153 @@ +package ident + +import ( + "bytes" + "errors" + "fmt" + "strconv" +) + +// ldh are bytes that may appear in a DNS label of Remote. +var ldh = [1 << 8]bool{ + '-': true, // special: must not be the first or last byte + + '0': true, '1': true, '2': true, '3': true, '4': true, '5': true, '6': true, + '7': true, '8': true, '9': true, + + 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 'G': true, + 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 'M': true, 'N': true, + 'O': true, 'P': true, 'Q': true, 'R': true, 'S': true, 'T': true, 'U': true, + 'V': true, 'W': true, 'X': true, 'Y': true, 'Z': true, + + 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true, + 'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 'm': true, 'n': true, + 'o': true, 'p': true, 'q': true, 'r': true, 's': true, 't': true, 'u': true, + 'v': true, 'w': true, 'x': true, 'y': true, 'z': true, +} + +const ( + // RemoteMax is the maximum permissible size of [Remote]. + RemoteMax = 1<<8 - 1 + + // LabelSeparator separates labels in a [Remote]. + LabelSeparator = '.' + // LabelMax is the maximum permissible size of a label in [Remote]. + LabelMax = 1<<6 - 1 +) + +// Remote represents the remote part of [F]. +// +// A remote must contain one or more non-empty labels, separated by +// [LabelSeparator]. Each label must be no more than [LabelMax] bytes long, and +// contain only bytes permitted by the "LDH rule" described in +// [Section 2 of RFC 3696]. A remote must, in total, be no more than [RemoteMax] +// bytes long. +// +// [Section 2 of RFC 3696]: https://www.rfc-editor.org/rfc/rfc3696#section-2 +type Remote struct { + Data [RemoteMax + 1]byte + Len uint8 +} + +// Encode appends the underlying string to dst and returns the extended buffer. +func (r *Remote) Encode(dst []byte) []byte { + return append(dst, r.Data[:r.Len]...) +} + +// String returns a copy of the underlying string. +func (r *Remote) String() string { return string(r.Data[:r.Len]) } + +// MarshalText returns the result of Encode. +func (r *Remote) MarshalText() (data []byte, err error) { + return r.Encode(nil), nil +} + +var ( + // ErrShortRemote is returned validating an empty remote. + ErrShortRemote = errors.New("missing remote part") + // ErrLongRemote is returned validating a remote longer than [RemoteMax]. + ErrLongRemote = errors.New("got more than 255 bytes of remote") +) + +const ( + // InvalidLabelLDH denotes an [InvalidLabelError] describing an invalid + // label containing bytes not permitted by the LDH rule. + InvalidLabelLDH = iota + // InvalidLabelShort denotes an [InvalidLabelError] describing an empty label. + InvalidLabelShort + // InvalidLabelLong denotes an [InvalidLabelError] describing a label + // exceeding its maximum length of [LabelMax]. + InvalidLabelLong +) + +// An InvalidLabelError describes a rejected DNS label part of a [Remote]. +type InvalidLabelError struct { + Data []byte + Label int + Index int + Reason int +} + +func (e *InvalidLabelError) Error() string { + s := "label " + strconv.Itoa(e.Label) + " " + switch e.Reason { + case InvalidLabelLDH: + if e.Data[e.Index] == '-' { + if e.Index == 0 { + s += "starts" + } else { + s += "ends" + } + s += " with '-'" + break + } + s += fmt.Sprintf( + "contains invalid byte %q at index %d", + e.Data[e.Index], e.Index, + ) + + case InvalidLabelShort: + s += "is empty" + + case InvalidLabelLong: + s += "is longer than 63 bytes" + + default: + return "invalid label " + strconv.Itoa(e.Label) + + " at byte " + strconv.Itoa(e.Index) + } + return s +} + +// UnmarshalText validates and copies data to r. +func (r *Remote) UnmarshalText(data []byte) error { + if len(data) == 0 { + return ErrShortRemote + } + + r.Len = uint8(len(data)) + if copy(r.Data[:], data) == len(r.Data) { + return ErrLongRemote + } + + for i, label := range bytes.Split(r.Data[:r.Len], []byte{LabelSeparator}) { + if len(label) == 0 { + return &InvalidLabelError{label, i, -1, InvalidLabelShort} + } else if len(label) > LabelMax { + return &InvalidLabelError{label, i, LabelMax, InvalidLabelLong} + } + + if label[0] == '-' { + return &InvalidLabelError{label, i, 0, InvalidLabelLDH} + } else if label[len(label)-1] == '-' { + return &InvalidLabelError{label, i, len(label) - 1, InvalidLabelLDH} + } + for j, b := range label { + if !ldh[b] { + return &InvalidLabelError{label, i, j, InvalidLabelLDH} + } + } + } + + return nil +} diff --git a/ident/remote_test.go b/ident/remote_test.go new file mode 100644 index 0000000..0f3059a --- /dev/null +++ b/ident/remote_test.go @@ -0,0 +1,83 @@ +package ident_test + +import ( + "bytes" + "strings" + "testing" + + "git.gensokyo.uk/cofront/cof-spec/ident" +) + +// mustNewRemote validates remote and returns its corresponding [ident.Remote]. +// If remote is invalid, mustNewRemote panics. +func mustNewRemote(remote string) (r ident.Remote) { + if err := r.UnmarshalText([]byte(remote)); err != nil { + panic(err) + } + return +} + +func TestRemote(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + remote string + err error + }{ + {"short", "", ident.ErrShortRemote}, + {"long", strings.Repeat("\x00", 256), ident.ErrLongRemote}, + + {"short label", "test.label.short..empty", &ident.InvalidLabelError{ + Data: []byte{}, + Label: 3, + Index: -1, + Reason: ident.InvalidLabelShort, + }}, + + {"long label", "test.label.long." + strings.Repeat("\x00", 64), &ident.InvalidLabelError{ + Data: bytes.Repeat([]byte{0}, 64), + Label: 3, + Index: 63, + Reason: ident.InvalidLabelLong, + }}, + + {"ldh prefix", "test.prefix.-invalid-", &ident.InvalidLabelError{ + Data: []byte("-invalid-"), + Label: 2, + Index: 0, + Reason: ident.InvalidLabelLDH, + }}, + + {"ldh suffix", "test.suffix.invalid--", &ident.InvalidLabelError{ + Data: []byte("invalid--"), + Label: 2, + Index: 8, + Reason: ident.InvalidLabelLDH, + }}, + + {"ldh", "test.invalid:byte", &ident.InvalidLabelError{ + Data: []byte("invalid:byte"), + Label: 1, + Index: 7, + Reason: ident.InvalidLabelLDH, + }}, + + {"simple", "localhost", nil}, + {"valid", "gensokyo.uk", nil}, + {"full", "ABCDEFGHIJKLMNOPQRSTUVWXYZ." + + "abcdefghijklmnopqrstuvwxyz." + + "0123456789-9876543210", nil}, + } + + rtc := make(rTestCases[ident.Remote, *ident.Remote], len(testCases)) + for i, tc := range testCases { + rtc[i].name = tc.name + if tc.err == nil { + rtc[i].ident = mustNewRemote(tc.remote) + } + rtc[i].want = []byte(tc.remote) + rtc[i].err = tc.err + } + rtc.run(t) +}