Files
cof-spec/ident/remote.go
Yonah ffa125b39d ident: implement remote-part validation
This is mostly equivalent to email without IP addresses.

Signed-off-by: Yonah <contrib@gensokyo.uk>
2026-03-22 16:59:31 +09:00

154 lines
4.2 KiB
Go

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
}