ident: implement remote-part validation
This is mostly equivalent to email without IP addresses. Signed-off-by: Yonah <contrib@gensokyo.uk>
This commit is contained in:
@@ -88,6 +88,42 @@ func TestErrors(t *testing.T) {
|
|||||||
Data: make([]byte, 1<<10),
|
Data: make([]byte, 1<<10),
|
||||||
Want: ident.EncodedSizeSystem,
|
Want: ident.EncodedSizeSystem,
|
||||||
}, "got 1024 bytes for a 32-byte identifier"},
|
}, "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 {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|||||||
153
ident/remote.go
Normal file
153
ident/remote.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
83
ident/remote_test.go
Normal file
83
ident/remote_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user