From c6b857669b66fbdc344c7cd2231d0ed04105e060 Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Mon, 25 Dec 2023 18:50:47 +0100 Subject: [PATCH] simplify API --- builder.go | 467 +++----------------------------------- builder_test.go | 489 ++++++++++++++-------------------------- edns0.go | 29 +-- edns0_test.go | 59 ++--- name.go | 308 +++++++++++++++++++++++++ name_test.go | 497 ++++++++++++++++++++++++++++++++++++++++ parser.go | 441 ++++++------------------------------ parser_test.go | 588 ++++-------------------------------------------- types.go | 30 +-- 9 files changed, 1167 insertions(+), 1741 deletions(-) create mode 100644 name.go create mode 100644 name_test.go diff --git a/builder.go b/builder.go index 69c7bfb..9b89bd3 100644 --- a/builder.go +++ b/builder.go @@ -20,421 +20,6 @@ var ( ErrTruncated = errors.New("message size limit reached") ) -func MakeQuery[T RawName | ParserName | Name | SearchName](msg []byte, id uint16, flags Flags, q Question[T]) []byte { - // Header - msg = appendUint16(msg, id) - msg = appendUint16(msg, uint16(flags)) - msg = appendUint16(msg, 1) - msg = appendUint16(msg, 0) - msg = appendUint16(msg, 0) - msg = appendUint16(msg, 0) - - // Question - msg = appendName(msg, q.Name) - msg = appendUint16(msg, uint16(q.Type)) - msg = appendUint16(msg, uint16(q.Class)) - - return msg -} - -func MakeQueryWithEDNS0Header[T RawName | ParserName | Name | SearchName](msg []byte, id uint16, flags Flags, q Question[T], edns0 EDNS0Header) []byte { - // Header - msg = appendUint16(msg, id) - msg = appendUint16(msg, uint16(flags)) - msg = appendUint16(msg, 1) - msg = appendUint16(msg, 0) - msg = appendUint16(msg, 0) - msg = appendUint16(msg, 1) - - // Question - msg = appendName(msg, q.Name) - msg = appendUint16(msg, uint16(q.Type)) - msg = appendUint16(msg, uint16(q.Class)) - - // EDNS0 - msg = append(msg, 0) // root name - msg = appendUint16(msg, uint16(TypeOPT)) - msg = appendUint16(msg, uint16(edns0.Payload)) - - msg = appendUint32(msg, uint32(edns0.PartialExtendedRCode)<<24|uint32(edns0.Version)<<16|uint32(edns0.ExtendedFlags)) - msg = appendUint16(msg, 0) - return msg -} - -func isZero[T comparable](t T) bool { - return t == *new(T) -} - -func appendName[T RawName | ParserName | Name | SearchName](buf []byte, n T) []byte { - switch n := any(n).(type) { - case Name: - if isZero(n) { - panic("cannot use zero value of any name type") - } - return appendEscapedName(buf, true, n.n) - case SearchName: - if isZero(n) { - panic("cannot use zero value of any name type") - } - return appendSearchName(buf, n) - case ParserName: - if isZero(n) { - panic("cannot use zero value of any name type") - } - return n.appendRawName(buf) - case RawName: - return append(buf, n...) - default: - panic("internal error: unsupported name type") - } -} - -var errInvalidName = errors.New("invalid name") - -// Name is a wrapper around a string DNS name representation. -// Zero value of this type shouldn't be used, unless specified otherwise. -type Name struct { - n string -} - -func (n Name) AsRawName() RawName { - return appendEscapedName(make([]byte, 0, maxEncodedNameLen), true, n.n) -} - -// NewName creates a new Name. -// The name might contain escape characters like: '\DDD' and '\X', -// where D is a digit and X is any possible character, except digit. -func NewName(name string) (Name, error) { - if !isValidEscapedName(name) { - return Name{}, errInvalidName - } - return Name{n: name}, nil -} - -// MustNewName creates a new Name, panics when it is invalid. -func MustNewName(name string) Name { - n, err := NewName(name) - if err != nil { - panic("MustNewName: " + err.Error()) - } - return n -} - -func (n Name) String() string { - // TODO: the name can be "unsafe", arbitrary bytes are allowed by - // NewName, only dots need to be escaped to be treated as the label content - // maybe this method should check whether the name is unsafe, and when it it - // make it safe (add escapes properly). - return n.n -} - -func (n Name) IsRooted() bool { - if n.n[len(n.n)-1] != '.' { - return false - } - - endSlashCount := 0 - for i := len(n.n) - 2; i > 0; i-- { - v := n.n[i] - if v != '\\' { - endSlashCount++ - continue - } - break - } - - return endSlashCount%2 != 0 -} - -func (n Name) labelCount() int { - count := 0 - if !n.IsRooted() { - count++ - } - - for i := 0; i < len(n.n); i++ { - char := n.n[i] - switch char { - case '\\': - i++ - if isDigit(n.n[i]) { - i += 2 - } - case '.': - count++ - } - } - - return count -} - -func (n Name) charCount() int { - count := 0 - for i := 0; i < len(n.n); i++ { - char := n.n[i] - if char == '\\' { - i++ - if isDigit(n.n[i]) { - i += 2 - } - } - count++ - } - return count -} - -func isValidEscapedName(m string) bool { - if m == "" { - return false - } - - if m == "." { - return true - } - - labelLength := 0 - nameLength := 0 - inEscape := false - rooted := false - - for i := 0; i < len(m); i++ { - char := m[i] - rooted = false - - switch char { - case '.': - if inEscape { - labelLength++ - inEscape = false - continue - } - if labelLength == 0 || labelLength > maxLabelLength { - return false - } - rooted = true - nameLength += labelLength + 1 - labelLength = 0 - case '\\': - inEscape = !inEscape - if !inEscape { - labelLength++ - } - default: - if inEscape && isDigit(char) { - if len(m[i:]) < 3 || !isDigit(m[i+1]) || !isDigit(m[i+2]) { - return false - } - if _, ok := decodeDDD([3]byte{char, m[i+1], m[i+2]}); !ok { - return false - } - i += 2 - } - inEscape = false - labelLength++ - } - } - - if !rooted && labelLength > maxLabelLength { - return false - } - - nameLength += labelLength - - if inEscape { - return false - } - - if nameLength > 254 || nameLength == 254 && !rooted { - return false - } - - return true -} - -func appendEscapedName(buf []byte, explicitEndRoot bool, m string) []byte { - labelLength := byte(0) - - labelIndex := len(buf) - buf = append(buf, 0) - lastRoot := false - - if m == "." { - return buf - } - - for i := 0; i < len(m); i++ { - lastRoot = false - - char := m[i] - switch char { - case '.': - buf[labelIndex] = labelLength - labelLength = 0 - labelIndex = len(buf) - buf = append(buf, 0) - lastRoot = true - case '\\': - if isDigit(m[i+1]) { - labelLength++ - ddd, _ := decodeDDD([3]byte{m[i+1], m[i+2], m[i+3]}) - buf = append(buf, ddd) - i += 3 - continue - } - buf = append(buf, m[i+1]) - i += 1 - labelLength++ - default: - labelLength++ - buf = append(buf, char) - } - } - - if labelLength != 0 { - buf[labelIndex] = labelLength - } - - if explicitEndRoot && !lastRoot { - buf = append(buf, 0) - } - - return buf -} - -func isDigit(char byte) bool { - return char >= '0' && char <= '9' -} - -func decodeDDD(ddd [3]byte) (uint8, bool) { - ddd[0] -= '0' - ddd[1] -= '0' - ddd[2] -= '0' - num := uint16(ddd[0])*100 + uint16(ddd[1])*10 + uint16(ddd[2]) - if num > 255 { - return 0, false - } - return uint8(num), true -} - -type SearchNameIterator struct { - name Name - search []Name - absoluteFirst bool - absoluteDone bool - done bool -} - -func NewSearchNameIterator(name Name, search []Name, ndots int) SearchNameIterator { - if name.IsRooted() { - return SearchNameIterator{name: name} - } - return SearchNameIterator{name, search, name.labelCount() >= ndots, false, false} -} - -func (s *SearchNameIterator) Next() (SearchName, bool) { - if s.done { - return SearchName{}, false - } - - if !s.absoluteDone && s.absoluteFirst { - n, err := NewSearchName(Name{}, s.name) - if err != nil { - panic("internal error") - } - s.absoluteDone = true - return n, true - } - - for i, suffix := range s.search { - name, err := NewSearchName(s.name, suffix) - if err != nil { - continue - } - s.search = s.search[i+1:] - return name, false - } - - if !s.absoluteDone && !s.absoluteFirst { - n, err := NewSearchName(Name{}, s.name) - if err != nil { - panic("internal error") - } - s.absoluteDone = true - s.done = true - return n, true - } - - s.done = true - return SearchName{}, false -} - -// SearchName is intended for use with search domains, to avoid -// string concatenations. -// Zero value of this type shouldn't be used. -type SearchName struct { - prefix Name - suffix Name -} - -func (s SearchName) AsRawName() RawName { - return appendSearchName(make([]byte, 0, maxEncodedNameLen), s) -} - -// NewSearchName creates a new SearchName. -// prefix might be a zero value, then the suffix is -// treated as the entire name, prefix cannot be a rooted name. -func NewSearchName(prefix, suffix Name) (SearchName, error) { - if !isZero(prefix) && prefix.IsRooted() { - return SearchName{}, errInvalidName - } - nameLength := prefix.charCount() + suffix.charCount() - if nameLength > 254 || nameLength == 254 && !suffix.IsRooted() { - return SearchName{}, errInvalidName - } - return SearchName{prefix, suffix}, nil -} - -func (n SearchName) String() string { - if isZero(n) { - return "" - } - if isZero(n.prefix) { - return n.suffix.String() - } - return n.prefix.String() + "." + n.suffix.String() -} - -func appendSearchName(buf []byte, name SearchName) []byte { - return appendEscapedName(appendEscapedName(buf, false, name.prefix.n), true, name.suffix.n) -} - -type RawName []byte - -func NewRawName(name string) (RawName, error) { - var buf [maxEncodedNameLen]byte - return newRawName(&buf, name) -} - -func MustNewRawName(name string) RawName { - var buf [maxEncodedNameLen]byte - return mustNewRawName(&buf, name) -} - -func mustNewRawName(buf *[maxEncodedNameLen]byte, name string) RawName { - if !isValidEscapedName(name) { - panic("dnsmsg: MustNewName: invalid dns name") - } - return appendEscapedName(buf[:0], true, name) -} - -func newRawName(buf *[maxEncodedNameLen]byte, name string) (RawName, error) { - // TODO: merge isValid into appendEscapedName - if !isValidEscapedName(name) { - return nil, errInvalidName - } - return appendEscapedName(buf[:0], true, name), nil -} - type section uint8 const ( @@ -618,7 +203,7 @@ func (b *Builder) decResurceSection() { // It errors when the amount of questions is equal to 65535. // // The building section must be set to questions, otherwise it panics. -func (b *Builder) Question(q Question[RawName]) error { +func (b *Builder) Question(q Question) error { if b.curSection != sectionQuestions { b.panicInvalidSection() } @@ -628,7 +213,7 @@ func (b *Builder) Question(q Question[RawName]) error { } var err error - b.buf, err = b.nb.appendName(b.buf, b.maxBufSize-4, b.headerStartOffset, q.Name, true) + b.buf, err = b.nb.appendName(b.buf, b.maxBufSize-4, b.headerStartOffset, q.Name.asSlice(), true) if err != nil { return err } @@ -643,7 +228,7 @@ func (b *Builder) Question(q Question[RawName]) error { // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourceA(hdr ResourceHeader[RawName], a ResourceA) error { +func (b *Builder) ResourceA(hdr ResourceHeader, a ResourceA) error { hdr.Type = TypeA hdr.Length = 4 if err := b.appendHeader(hdr, b.maxBufSize-4); err != nil { @@ -657,7 +242,7 @@ func (b *Builder) ResourceA(hdr ResourceHeader[RawName], a ResourceA) error { // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourceAAAA(hdr ResourceHeader[RawName], aaaa ResourceAAAA) error { +func (b *Builder) ResourceAAAA(hdr ResourceHeader, aaaa ResourceAAAA) error { hdr.Type = TypeAAAA hdr.Length = 16 if err := b.appendHeader(hdr, b.maxBufSize-16); err != nil { @@ -671,13 +256,13 @@ func (b *Builder) ResourceAAAA(hdr ResourceHeader[RawName], aaaa ResourceAAAA) e // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourceNS(hdr ResourceHeader[RawName], ns ResourceNS[RawName]) error { +func (b *Builder) ResourceNS(hdr ResourceHeader, ns ResourceNS) error { hdr.Type = TypeNS f, hdrOffset, err := b.appendHeaderWithLengthFixup(hdr, b.maxBufSize) if err != nil { return err } - b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, ns.NS, true) + b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, ns.NS.asSlice(), true) if err != nil { b.removeResourceHeader(hdrOffset) return err @@ -690,13 +275,13 @@ func (b *Builder) ResourceNS(hdr ResourceHeader[RawName], ns ResourceNS[RawName] // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourceCNAME(hdr ResourceHeader[RawName], cname ResourceCNAME[RawName]) error { +func (b *Builder) ResourceCNAME(hdr ResourceHeader, cname ResourceCNAME) error { hdr.Type = TypeCNAME f, hdrOffset, err := b.appendHeaderWithLengthFixup(hdr, b.maxBufSize) if err != nil { return err } - b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, cname.CNAME, true) + b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, cname.CNAME.asSlice(), true) if err != nil { b.removeResourceHeader(hdrOffset) return err @@ -709,20 +294,20 @@ func (b *Builder) ResourceCNAME(hdr ResourceHeader[RawName], cname ResourceCNAME // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourceSOA(hdr ResourceHeader[RawName], soa ResourceSOA[RawName]) error { +func (b *Builder) ResourceSOA(hdr ResourceHeader, soa ResourceSOA) error { hdr.Type = TypeSOA f, hdrOffset, err := b.appendHeaderWithLengthFixup(hdr, b.maxBufSize) if err != nil { return err } - b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, soa.NS, true) + b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, soa.NS.asSlice(), true) if err != nil { b.removeResourceHeader(hdrOffset) return err } - b.buf, err = b.nb.appendName(b.buf, b.maxBufSize-20, b.headerStartOffset, soa.Mbox, true) + b.buf, err = b.nb.appendName(b.buf, b.maxBufSize-20, b.headerStartOffset, soa.Mbox.asSlice(), true) if err != nil { b.removeResourceHeader(hdrOffset) return err @@ -741,13 +326,13 @@ func (b *Builder) ResourceSOA(hdr ResourceHeader[RawName], soa ResourceSOA[RawNa // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourcePTR(hdr ResourceHeader[RawName], ptr ResourcePTR[RawName]) error { +func (b *Builder) ResourcePTR(hdr ResourceHeader, ptr ResourcePTR) error { hdr.Type = TypePTR f, hdrOffset, err := b.appendHeaderWithLengthFixup(hdr, b.maxBufSize) if err != nil { return err } - b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, ptr.PTR, true) + b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, ptr.PTR.asSlice(), true) if err != nil { b.removeResourceHeader(hdrOffset) return err @@ -760,14 +345,14 @@ func (b *Builder) ResourcePTR(hdr ResourceHeader[RawName], ptr ResourcePTR[RawNa // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourceMX(hdr ResourceHeader[RawName], mx ResourceMX[RawName]) error { +func (b *Builder) ResourceMX(hdr ResourceHeader, mx ResourceMX) error { hdr.Type = TypeMX f, hdrOffset, err := b.appendHeaderWithLengthFixup(hdr, b.maxBufSize-2) if err != nil { return err } b.buf = appendUint16(b.buf, mx.Pref) - b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, mx.MX, true) + b.buf, err = b.nb.appendName(b.buf, b.maxBufSize, b.headerStartOffset, mx.MX.asSlice(), true) if err != nil { b.removeResourceHeader(hdrOffset) return err @@ -782,7 +367,7 @@ var errInvalidRawTXTResource = errors.New("invalid raw txt resource") // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) RawResourceTXT(hdr ResourceHeader[RawName], txt RawResourceTXT) error { +func (b *Builder) RawResourceTXT(hdr ResourceHeader, txt RawResourceTXT) error { hdr.Type = TypeTXT if len(txt.TXT) > math.MaxUint16 || !txt.isValid() { return errInvalidRawTXTResource @@ -804,7 +389,7 @@ var errTooLongTXT = errors.New("too long txt resource") // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourceTXT(hdr ResourceHeader[RawName], txt ResourceTXT) error { +func (b *Builder) ResourceTXT(hdr ResourceHeader, txt ResourceTXT) error { hdr.Type = TypeTXT totalLength := 0 for _, str := range txt.TXT { @@ -834,12 +419,12 @@ func (b *Builder) ResourceTXT(hdr ResourceHeader[RawName], txt ResourceTXT) erro return nil } -func (b *Builder) appendHeader(hdr ResourceHeader[RawName], maxBufSize int) error { +func (b *Builder) appendHeader(hdr ResourceHeader, maxBufSize int) error { if err := b.incResurceSection(); err != nil { return err } var err error - b.buf, err = b.nb.appendName(b.buf, maxBufSize-10, b.headerStartOffset, hdr.Name, true) + b.buf, err = b.nb.appendName(b.buf, maxBufSize-10, b.headerStartOffset, hdr.Name.asSlice(), true) if err != nil { b.decResurceSection() return err @@ -865,13 +450,13 @@ func (f headerLengthFixup) currentlyStoredLength(b *Builder) uint16 { return unpackUint16(b.buf[f-2:]) } -func (b *Builder) appendHeaderWithLengthFixup(hdr ResourceHeader[RawName], maxBufSize int) (headerLengthFixup, int, error) { +func (b *Builder) appendHeaderWithLengthFixup(hdr ResourceHeader, maxBufSize int) (headerLengthFixup, int, error) { err := b.incResurceSection() if err != nil { return 0, 0, err } nameOffset := len(b.buf) - b.buf, err = b.nb.appendName(b.buf, maxBufSize-10, b.headerStartOffset, hdr.Name, true) + b.buf, err = b.nb.appendName(b.buf, maxBufSize-10, b.headerStartOffset, hdr.Name.asSlice(), true) if err != nil { b.decResurceSection() return 0, 0, err @@ -883,7 +468,7 @@ func (b *Builder) appendHeaderWithLengthFixup(hdr ResourceHeader[RawName], maxBu return headerLengthFixup(len(b.buf)), nameOffset, nil } -func (b *Builder) appendHeaderWithLengthFixupNoInc(hdr ResourceHeader[RawName], maxBufSize int) (headerLengthFixup, int, *uint16, error) { +func (b *Builder) appendHeaderWithLengthFixupNoInc(hdr ResourceHeader, maxBufSize int) (headerLengthFixup, int, *uint16, error) { var count *uint16 switch b.curSection { case sectionAnswers: @@ -902,7 +487,7 @@ func (b *Builder) appendHeaderWithLengthFixupNoInc(hdr ResourceHeader[RawName], var err error nameOffset := len(b.buf) - b.buf, err = b.nb.appendName(b.buf, maxBufSize-10, b.headerStartOffset, hdr.Name, true) + b.buf, err = b.nb.appendName(b.buf, maxBufSize-10, b.headerStartOffset, hdr.Name.asSlice(), true) if err != nil { return 0, 0, nil, err } @@ -926,7 +511,7 @@ func (b *Builder) removeResourceHeader(headerOffset int) { // Once a resource is created using the RDBuilder, attempting to use the same RDBuilder again might lead to panics. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) RDBuilder(hdr ResourceHeader[RawName]) (RDBuilder, error) { +func (b *Builder) RDBuilder(hdr ResourceHeader) (RDBuilder, error) { f, hdrOffset, count, err := b.appendHeaderWithLengthFixupNoInc(hdr, b.maxBufSize) if err != nil { return RDBuilder{}, err @@ -994,10 +579,10 @@ func (b *RDBuilder) Remove() { // is reached ([Builder.LimitMessageSize]), an error will be returned. // Note: In case of an error, the resource is not removed, and you can still use the RDBuilder safely. // The Resource can be removed via the [RDBuilder.Remove] method. -func (b *RDBuilder) Name(name RawName, compress bool) error { +func (b *RDBuilder) Name(name Name, compress bool) error { nameOffset := len(b.b.buf) var err error - b.b.buf, err = b.b.nb.appendName(b.b.buf, b.b.maxBufSize, b.b.headerStartOffset, name, compress) + b.b.buf, err = b.b.nb.appendName(b.b.buf, b.b.maxBufSize, b.b.headerStartOffset, name.asSlice(), compress) if err != nil { return err } diff --git a/builder_test.go b/builder_test.go index 3f5b8db..d714c15 100644 --- a/builder_test.go +++ b/builder_test.go @@ -7,186 +7,18 @@ import ( "math" "net/netip" "reflect" - "strings" "testing" ) -var ( - escapes = "\\.\\223\\.\\\\" - escapesCharCount = 4 - label54 = escapes + strings.Repeat("a", 54-2*escapesCharCount) + escapes - label63 = escapes + strings.Repeat("a", 63-2*escapesCharCount) + escapes - label64 = escapes + strings.Repeat("a", 64-2*escapesCharCount) + escapes -) - -var newNameTests = []struct { - name string - ok bool - diferentAsString bool -}{ - {name: "", ok: false}, - {name: "\x00", ok: true, diferentAsString: true}, - {name: ".", ok: true}, - {name: "com.", ok: true}, - {name: "com", ok: true}, - - {name: "go.dev", ok: true}, - {name: "go.dev.", ok: true}, - {name: "www.go.dev", ok: true}, - {name: "www.go.dev.", ok: true}, - - {name: "www..go.dev", ok: false}, - {name: ".www.go.dev", ok: false}, - {name: "..www.go.dev", ok: false}, - {name: "www.go.dev..", ok: false}, - - {name: "www.go.dev\\.", ok: true}, - {name: "www.go.dev\\..", ok: true}, - {name: "www.go.dev\\...", ok: false}, - {name: "www\\..go.dev", ok: true}, - {name: "www\\...go.dev", ok: false}, - - {name: "\\\\www.go.dev.", ok: true}, - {name: "\\\\www.go.dev.", ok: true}, - {name: "www.go.dev\\\\\\.", ok: true}, - {name: "www.go.dev\\\\\\.", ok: true}, - {name: "\\ww\\ w.go.dev", ok: true, diferentAsString: true}, - {name: "ww\\w.go.dev", ok: true, diferentAsString: true}, - {name: "www.go.dev\\\\", ok: true}, - - {name: "\\223www.go.dev", ok: true}, - {name: "\\000www.go.dev", ok: true}, - {name: "\\255www.go.dev", ok: true}, - - {name: "\\256www.go.dev", ok: false}, - {name: "\\999www.go.dev", ok: false}, - {name: "\\12www.go.dev", ok: false}, - {name: "\\1www.go.dev", ok: false}, - {name: "www.go.dev\\223", ok: true}, - {name: "www.go.dev\\12", ok: false}, - {name: "www.go.dev\\1", ok: false}, - {name: "www.go.dev\\", ok: false}, - - {name: label63 + ".go.dev", ok: true}, - {name: label64 + ".go.dev", ok: false}, - - {name: label63, ok: true}, - {name: label64, ok: false}, - - // 253B non-rooted name. - { - name: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%v.go.dev", label63, label54), - ok: true, - }, - - // 254B rooted name. - { - name: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%v.go.dev.", label63, label54), - ok: true, - }, - - // 254B non-rooted name. - { - name: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%va.go.dev", label63, label54), - ok: false, - }, - - // 255B rooted name. - { - name: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%va.go.dev.", label63, label54), - ok: false, - }, -} - -func TestNewName(t *testing.T) { - for _, v := range newNameTests { - _, err := NewName(v.name) - expectErr := errInvalidName - if v.ok { - expectErr = nil - } - if expectErr != err { - t.Errorf("'%v' got error: %v, expected: %v", v.name, err, expectErr) - } - } -} - -func TestAppendEscapedName(t *testing.T) { - for _, v := range newNameTests { - n, err := NewName(v.name) - if err != nil { - continue - } - - packedName := appendEscapedName(nil, true, v.name) - - p := Parser{msg: packedName} - name := ParserName{m: &p, nameStart: 0} - _, err = name.unpack() - if err != nil { - t.Errorf("'%v' failed while unpacking packed name: %v\n\traw: %v", v.name, err, packedName) - continue - } - - if !name.EqualName(n) { - t.Errorf("'%v' ParserName is not equal to name\n\traw: %v", v.name, packedName) - continue - } - - if v.diferentAsString { - continue - } - - expectName := v.name - dotAtEnd := expectName[len(expectName)-1] == '.' - if !dotAtEnd || (len(expectName) > 2 && dotAtEnd && expectName[len(expectName)-2] == '\\') { - expectName += "." - } - - if name := name.String(); name != expectName { - t.Errorf("'%v' got name: %v, expected: %v\n\traw: %v", v.name, name, expectName, packedName) - } - } -} - -func TestAppendSearchName(t *testing.T) { - n, err := NewSearchName(MustNewName("www"), MustNewName("go.dev")) - - if err != nil { - t.Fatal(err) - } - - name := appendName(nil, n) - expectName := []byte{3, 'w', 'w', 'w', 2, 'g', 'o', 3, 'd', 'e', 'v', 0} - if !bytes.Equal(name, expectName) { - t.Fatalf("expected: %v got: %v", expectName, name) - } -} - -func BenchmarkIterator(b *testing.B) { - name := MustNewName("google.com") - search := []Name{MustNewName("com"), MustNewName("com"), MustNewName("internal.google.com"), MustNewName("internal.it.google.com")} - for i := 0; i < b.N; i++ { - s := NewSearchNameIterator(name, search, 1) - for n, ok := s.Next(); ok; n, ok = s.Next() { - _ = n - } - } -} - -func mustNewRawNameValid(name string) RawName { - return appendEscapedName(make([]byte, 0, maxEncodedNameLen), true, name) -} - func BenchmarkBuilderAppendNameSameName(b *testing.B) { buf := make([]byte, headerLen, 512) b.ResetTimer() for i := 0; i < b.N; i++ { buf := buf b := nameBuilderState{} - rawName := mustNewRawNameValid("www.example.com") + rawName := MustParseName("www.example.com") for i := 0; i < 31; i++ { - buf, _ = b.appendName(buf, math.MaxInt, 0, rawName, true) + buf, _ = b.appendName(buf, math.MaxInt, 0, rawName.asSlice(), true) } } } @@ -198,15 +30,15 @@ func BenchmarkBuilderAppendNameAllPointsToFirstName(b *testing.B) { buf := buf b := nameBuilderState{} - raw1 := mustNewRawNameValid("www.example.com") - raw2 := mustNewRawNameValid("example.com") - raw3 := mustNewRawNameValid("com") + raw1 := MustParseName("www.example.com") + raw2 := MustParseName("example.com") + raw3 := MustParseName("com") - buf, _ = b.appendName(buf, math.MaxInt, 0, raw1, true) + buf, _ = b.appendName(buf, math.MaxInt, 0, raw1.asSlice(), true) for i := 0; i < 10; i++ { - buf, _ = b.appendName(buf, math.MaxInt, 0, raw1, true) - buf, _ = b.appendName(buf, math.MaxInt, 0, raw2, true) - buf, _ = b.appendName(buf, math.MaxInt, 0, raw3, true) + buf, _ = b.appendName(buf, math.MaxInt, 0, raw1.asSlice(), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, raw2.asSlice(), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, raw3.asSlice(), true) } } } @@ -216,16 +48,26 @@ func BenchmarkBuilderAppendNameAllDifferentNamesCompressable(b *testing.B) { b.ResetTimer() nb := nameBuilderState{} for i := 0; i < b.N; i++ { + names := []Name{ + MustParseName("com"), + MustParseName("example.com"), + MustParseName("www.example.com"), + MustParseName("dfd.www.example.com"), + MustParseName("aa.dfd.www.example.com"), + MustParseName("zz.aa.dfd.www.example.com"), + MustParseName("cc.zz.aa.dfd.www.example.com"), + MustParseName("aa.cc.zz.aa.dfd.www.example.com"), + } buf := buf nb.reset() - buf, _ = nb.appendName(buf, math.MaxInt, 0, mustNewRawNameValid("com"), true) - buf, _ = nb.appendName(buf, math.MaxInt, 0, mustNewRawNameValid("example.com"), true) - buf, _ = nb.appendName(buf, math.MaxInt, 0, mustNewRawNameValid("www.example.com"), true) - buf, _ = nb.appendName(buf, math.MaxInt, 0, mustNewRawNameValid("dfd.www.example.com"), true) - buf, _ = nb.appendName(buf, math.MaxInt, 0, mustNewRawNameValid("aa.dfd.www.example.com"), true) - buf, _ = nb.appendName(buf, math.MaxInt, 0, mustNewRawNameValid("zz.aa.dfd.www.example.com"), true) - buf, _ = nb.appendName(buf, math.MaxInt, 0, mustNewRawNameValid("cc.zz.aa.dfd.www.example.com"), true) - buf, _ = nb.appendName(buf, math.MaxInt, 0, mustNewRawNameValid("aa.cc.zz.aa.dfd.www.example.com"), true) + buf, _ = nb.appendName(buf, math.MaxInt, 0, names[0].asSlice(), true) + buf, _ = nb.appendName(buf, math.MaxInt, 0, names[1].asSlice(), true) + buf, _ = nb.appendName(buf, math.MaxInt, 0, names[2].asSlice(), true) + buf, _ = nb.appendName(buf, math.MaxInt, 0, names[3].asSlice(), true) + buf, _ = nb.appendName(buf, math.MaxInt, 0, names[4].asSlice(), true) + buf, _ = nb.appendName(buf, math.MaxInt, 0, names[5].asSlice(), true) + buf, _ = nb.appendName(buf, math.MaxInt, 0, names[6].asSlice(), true) + buf, _ = nb.appendName(buf, math.MaxInt, 0, names[7].asSlice(), true) } } @@ -247,11 +89,17 @@ func BenchmarkBuilderAppendNameAllDifferentNamesCompressable16Names(b *testing.B builder.reset() buf := buf for _, v := range names { - buf, _ = builder.appendName(buf, math.MaxInt, 0, mustNewRawNameValid(v), true) + n := MustParseName(v) + buf, _ = builder.appendName(buf, math.MaxInt, 0, n.asSlice(), true) } } } +func nameAsSlice(s string) []byte { + n := MustParseName(s) + return n.asSlice() +} + func TestAppendName(t *testing.T) { cases := []struct { name string @@ -262,7 +110,7 @@ func TestAppendName(t *testing.T) { name: "one name", build: func() []byte { b := nameBuilderState{} - buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, MustNewRawName("example.com."), true) + buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, nameAsSlice("example.com."), true) return buf }, expect: append( @@ -275,10 +123,10 @@ func TestAppendName(t *testing.T) { name: "four same names", build: func() []byte { b := nameBuilderState{} - buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, MustNewRawName("example.com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("example.com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("example.com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("example.com."), true) + buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, nameAsSlice("example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("example.com."), true) return buf }, expect: append( @@ -294,9 +142,9 @@ func TestAppendName(t *testing.T) { name: "three compressable names", build: func() []byte { b := nameBuilderState{} - buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, MustNewRawName("com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("example.com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("www.example.com."), true) + buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, nameAsSlice("com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("www.example.com."), true) return buf }, expect: append( @@ -311,10 +159,10 @@ func TestAppendName(t *testing.T) { name: "first root name followed by three compressable names", build: func() []byte { b := nameBuilderState{} - buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, MustNewRawName("."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("example.com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("www.example.com."), true) + buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, nameAsSlice("."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("www.example.com."), true) return buf }, expect: append( @@ -329,9 +177,9 @@ func TestAppendName(t *testing.T) { name: "compress=false", build: func() []byte { b := nameBuilderState{} - buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, MustNewRawName("com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("example.com."), false) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("www.example.com."), true) + buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, nameAsSlice("com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("example.com."), false) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("www.example.com."), true) return buf }, expect: append( @@ -345,20 +193,20 @@ func TestAppendName(t *testing.T) { name: "maxBufSize on first name", build: func() []byte { b := nameBuilderState{} - buf, err := b.appendName(make([]byte, headerLen), 30, 0, MustNewRawName("example.com."), true) + buf, err := b.appendName(make([]byte, headerLen), 30, 0, nameAsSlice("example.com."), true) if err != nil { // TODO: don't panic here. panic(err) } - buf, err = b.appendName(buf, 30, 0, MustNewRawName("example.com."), true) + buf, err = b.appendName(buf, 30, 0, nameAsSlice("example.com."), true) if err != nil { panic(err) } - buf, err = b.appendName(buf, 30, 0, MustNewRawName("example.com."), true) + buf, err = b.appendName(buf, 30, 0, nameAsSlice("example.com."), true) if err != nil { panic(err) } - buf, err = b.appendName(buf, 30, 0, MustNewRawName("example.com."), true) + buf, err = b.appendName(buf, 30, 0, nameAsSlice("example.com."), true) if err != ErrTruncated { panic(err) } @@ -375,20 +223,20 @@ func TestAppendName(t *testing.T) { name: "maxBufSize", build: func() []byte { b := nameBuilderState{} - buf, err := b.appendName(make([]byte, headerLen), 30, 0, MustNewRawName("example.com."), true) + buf, err := b.appendName(make([]byte, headerLen), 30, 0, nameAsSlice("example.com."), true) if err != nil { // TODO: don't panic here. panic(err) } - buf, err = b.appendName(buf, 30, 0, MustNewRawName("example.com."), true) + buf, err = b.appendName(buf, 30, 0, nameAsSlice("example.com."), true) if err != nil { panic(err) } - buf, err = b.appendName(buf, 30, 0, MustNewRawName("www.example.com."), true) + buf, err = b.appendName(buf, 30, 0, nameAsSlice("www.example.com."), true) if err != ErrTruncated { panic(err) } - buf, err = b.appendName(buf, 128, 0, MustNewRawName("www.example.com."), true) + buf, err = b.appendName(buf, 128, 0, nameAsSlice("www.example.com."), true) if err != nil { panic(err) } @@ -405,17 +253,17 @@ func TestAppendName(t *testing.T) { name: "maxBufSize entire not compressed second name", build: func() []byte { b := nameBuilderState{} - buf, err := b.appendName(make([]byte, headerLen), 30, 0, MustNewRawName("example.com."), true) + buf, err := b.appendName(make([]byte, headerLen), 30, 0, nameAsSlice("example.com."), true) if err != nil { // TODO: don't panic here. panic(err) } - n := MustNewRawName("example.net.") + n := nameAsSlice("example.net.") buf, err = b.appendName(buf, len(buf)+len(n)-1, 0, n, true) if err != ErrTruncated { panic(err) } - buf, err = b.appendName(buf, 128, 0, MustNewRawName("www.example.net"), true) + buf, err = b.appendName(buf, 128, 0, nameAsSlice("www.example.net"), true) if err != nil { panic(err) } @@ -436,11 +284,11 @@ func TestAppendName(t *testing.T) { name: "first name, removeNamesFromCompressionMap", build: func() []byte { b := nameBuilderState{} - buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, MustNewRawName("com."), true) + buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, nameAsSlice("com."), true) b.removeNamesFromCompressionMap(0, headerLen) - buf, _ = b.appendName(buf[:headerLen], math.MaxInt, 0, MustNewRawName("com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("example.com."), false) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("www.example.com."), true) + buf, _ = b.appendName(buf[:headerLen], math.MaxInt, 0, nameAsSlice("com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("example.com."), false) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("www.example.com."), true) return buf }, expect: append( @@ -454,13 +302,13 @@ func TestAppendName(t *testing.T) { name: "multiple names, removeNamesFromCompressionMap, with fake name", build: func() []byte { b := nameBuilderState{} - buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, MustNewRawName("smtp.example.net."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("www.example.com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("www.example.com."), true) + buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, nameAsSlice("smtp.example.net."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("www.example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("www.example.com."), true) b.removeNamesFromCompressionMap(0, headerLen) - buf, _ = b.appendName(buf[:headerLen], math.MaxInt, 0, MustNewRawName("smtp.example.net."), true) + buf, _ = b.appendName(buf[:headerLen], math.MaxInt, 0, nameAsSlice("smtp.example.net."), true) buf = append(buf, 3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("www.example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("www.example.com."), true) return buf }, expect: append( @@ -474,13 +322,13 @@ func TestAppendName(t *testing.T) { name: "after first name, removeNamesFromCompressionMap", build: func() []byte { b := nameBuilderState{} - buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, MustNewRawName("com."), true) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("example.com."), false) + buf, _ := b.appendName(make([]byte, headerLen), math.MaxInt, 0, nameAsSlice("com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("example.com."), false) offset := len(buf) - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("www.example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("www.example.com."), true) b.removeNamesFromCompressionMap(0, offset) buf = buf[:offset] - buf, _ = b.appendName(buf, math.MaxInt, 0, MustNewRawName("www.example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, 0, nameAsSlice("www.example.com."), true) return buf }, expect: append( @@ -496,14 +344,14 @@ func TestAppendName(t *testing.T) { b := nameBuilderState{} headerStartOffset := 4 buf := make([]byte, headerStartOffset+headerLen) - buf, _ = b.appendName(buf, math.MaxInt, headerStartOffset, MustNewRawName("com."), true) - buf, _ = b.appendName(buf, math.MaxInt, headerStartOffset, MustNewRawName("example.com."), false) + buf, _ = b.appendName(buf, math.MaxInt, headerStartOffset, nameAsSlice("com."), true) + buf, _ = b.appendName(buf, math.MaxInt, headerStartOffset, nameAsSlice("example.com."), false) offset := len(buf) - buf, _ = b.appendName(buf, math.MaxInt, headerStartOffset, MustNewRawName("w.example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, headerStartOffset, nameAsSlice("w.example.com."), true) b.removeNamesFromCompressionMap(headerStartOffset, offset) buf = buf[:offset] buf = append(buf, 1, 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0) - buf, _ = b.appendName(buf, math.MaxInt, headerStartOffset, MustNewRawName("w.example.com."), true) + buf, _ = b.appendName(buf, math.MaxInt, headerStartOffset, nameAsSlice("w.example.com."), true) return buf }, expect: append( @@ -524,7 +372,7 @@ func TestAppendName(t *testing.T) { } } -func testAppendCompressed(buf []byte, headerStartOffset, maxBufSize int, compression map[string]uint16, name RawName, compress bool) ([]byte, error) { +func testAppendCompressed(buf []byte, headerStartOffset, maxBufSize int, compression map[string]uint16, name []byte, compress bool) ([]byte, error) { if len(buf) < headerLen { panic("invalid use of testAppendCompressed") } @@ -614,10 +462,11 @@ func FuzzAppendName(f *testing.F) { headerStartOffset := int(r.uint16()) for _, name := range names { - n, err := NewRawName(name.name) + nn, err := ParseName(name.name) if err != nil { return } + n := nn.asSlice() if debugFuzz { encoding := "" for i := 0; i < len(n); i += int(n[i]) + 1 { @@ -660,7 +509,7 @@ func FuzzAppendName(f *testing.F) { var err error offset := len(expect) - expect, err = testAppendCompressed(expect, headerStartOffset, maxBufSize, compression, MustNewRawName(name.name), name.compress) + expect, err = testAppendCompressed(expect, headerStartOffset, maxBufSize, compression, nameAsSlice(name.name), name.compress) if debugFuzz { t.Logf("%v: offset: %v, buf[headerStartOffset:]: %v, err: %v", i, offset, expect[headerStartOffset:], err) @@ -705,7 +554,7 @@ func FuzzAppendName(f *testing.F) { if debugFuzz { t.Logf("%v: removing last name: %#v at offset: %v", i, name, offset) } - testRemoveLastlyCompressedName(expect, compression, headerStartOffset, offset, MustNewRawName(name)) + testRemoveLastlyCompressedName(expect, compression, headerStartOffset, offset, nameAsSlice(name)) expect = expect[:offset] if debugFuzz && j != 0 { t.Logf("%v: buf[headerStartOffset:]: %v", i, expect[headerStartOffset:]) @@ -732,17 +581,19 @@ func FuzzAppendName(f *testing.F) { expectedNameOffset := nameOffsets[0] - headerStartOffset nameOffsets = nameOffsets[1:] - name, n, err := p.unpackName(p.curOffset) + var name Name + offset, err := name.unpack(p.msg, p.curOffset) if err != nil { t.Fatalf("failed to unpack name at offset: %v: %v", p.curOffset, err) } - if !name.EqualName(MustNewName(expectName)) { + expect := MustParseName(expectName) + if !bytes.Equal(name.asSlice(), expect.asSlice()) { t.Fatalf("name at offset: %v, is not euqal to: %#v", p.curOffset, expectName) } if expectedNameOffset != p.curOffset { t.Fatalf("name at offset: %v, was expected to be at: %v offset", p.curOffset, expectedNameOffset) } - p.curOffset += int(n) + p.curOffset += int(offset) } got := make([]byte, headerStartOffset+headerLen, headerStartOffset+1024) @@ -763,7 +614,7 @@ func FuzzAppendName(f *testing.F) { var err error offset := len(got) - got, err = b.appendName(got, maxBufSize, headerStartOffset, MustNewRawName(name.name), name.compress) + got, err = b.appendName(got, maxBufSize, headerStartOffset, nameAsSlice(name.name), name.compress) if debugFuzz { t.Logf("%v: offset: %v, buf[headerStartOffset:]: %v, err: %v", i, offset, got[headerStartOffset:], err) } @@ -872,8 +723,8 @@ func TestBuilder(t *testing.T) { b.SetFlags(expectHeader.Flags) } - err := b.Question(Question[RawName]{ - Name: MustNewRawName("example.com"), + err := b.Question(Question{ + Name: MustParseName("example.com"), Type: TypeA, Class: ClassIN, }) @@ -883,8 +734,8 @@ func TestBuilder(t *testing.T) { testAfterAppend("Questions") - rhdr := ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + rhdr := ResourceHeader{ + Name: MustParseName("example.com"), Class: ClassIN, TTL: 3600, } @@ -892,17 +743,17 @@ func TestBuilder(t *testing.T) { var ( resourceA = ResourceA{A: [4]byte{192, 0, 2, 1}} resourceAAAA = ResourceAAAA{AAAA: netip.MustParseAddr("2001:db8::1").As16()} - resourceNS = ResourceNS[RawName]{NS: MustNewRawName("ns1.example.com")} - resourceSOA = ResourceSOA[RawName]{ - NS: MustNewRawName("ns1.example.com"), - Mbox: MustNewRawName("admin.example.com"), + resourceNS = ResourceNS{NS: MustParseName("ns1.example.com")} + resourceSOA = ResourceSOA{ + NS: MustParseName("ns1.example.com"), + Mbox: MustParseName("admin.example.com"), Serial: 2022010199, Refresh: 3948793, Retry: 34383744, Expire: 1223999999, Minimum: 123456789, } - resourcePTR = ResourcePTR[RawName]{PTR: MustNewRawName("1.2.0.192.in-addr.arpa")} + resourcePTR = ResourcePTR{PTR: MustParseName("1.2.0.192.in-addr.arpa")} resourceTXT = ResourceTXT{ TXT: [][]byte{ bytes.Repeat([]byte("a"), 209), @@ -920,8 +771,8 @@ func TestBuilder(t *testing.T) { return raw }(), } - resourceCNAME = ResourceCNAME[RawName]{CNAME: MustNewRawName("www.example.com")} - resourceMX = ResourceMX[RawName]{Pref: 54831, MX: MustNewRawName("smtp.example.com")} + resourceCNAME = ResourceCNAME{CNAME: MustParseName("www.example.com")} + resourceMX = ResourceMX{Pref: 54831, MX: MustParseName("smtp.example.com")} resourceOPT = ResourceOPT{Options: []EDNS0Option{ &EDNS0ClientSubnet{Family: AddressFamilyIPv4, SourcePrefixLength: 2, ScopePrefixLength: 3, Address: []byte{192, 0, 2, 1}}, &EDNS0Cookie{ @@ -1011,8 +862,9 @@ func TestBuilder(t *testing.T) { t.Fatalf("p.Question() unexpected error: %v", err) } - if !q.Name.EqualName(MustNewName("example.com")) { - t.Errorf(`q.Name = %v, q.Name.EqualName(MustNewName("example.com") = false, want: true`, q.Name.String()) + n := MustParseName("example.com.") + if !q.Name.Equal(&n) { + t.Errorf(`q.Name = %v, q.Name.Equal("example.com") = false, want: true`, q.Name.String()) } if q.Type != TypeA { @@ -1033,7 +885,8 @@ func TestBuilder(t *testing.T) { t.Errorf("rhdr.Name = %v, rhdr.Name.Equal(&q.Name) = false, want: true", rhdr.Name.String()) } - if !rhdr.Name.EqualName(MustNewName("example.com")) { + n := MustParseName("example.com.") + if !rhdr.Name.Equal(&n) { t.Errorf(`rhdr.Name = %v, rhdr.Name.Equal(MustNewName("example.com")) = false, want: true`, rhdr.Name.String()) } @@ -1161,12 +1014,11 @@ func equalRData(t *testing.T, name string, r1, r2 any) { r1Field := r1val.Field(i) r2Field := r2val.Field(i) - if rawName, ok := r1Field.Interface().(RawName); ok { - parserName := r2Field.Interface().(ParserName) - parserNameAsRawname := parserName.AsRawName() + if rawName, ok := r1Field.Interface().(Name); ok { + parserName := r2Field.Interface().(Name) - if !bytes.Equal(parserNameAsRawname, rawName) { - t.Errorf("%v: %v.%v = %v, want: %v ", name, r1val.Type().Name(), fieldName, parserNameAsRawname, rawName) + if !bytes.Equal(parserName.asSlice(), rawName.asSlice()) { + t.Errorf("%v: %v.%v = %v, want: %v ", name, r1val.Type().Name(), fieldName, rawName, rawName) } continue @@ -1201,8 +1053,8 @@ func TestBuilderRDBuilder(t *testing.T) { b := StartBuilder(make([]byte, 0, 512), 0, 0) b.StartAnswers() - rdb, err := b.RDBuilder(ResourceHeader[RawName]{ - Name: MustNewRawName("www.example.com"), + rdb, err := b.RDBuilder(ResourceHeader{ + Name: MustParseName("www.example.com"), Type: 54839, Class: ClassIN, }) @@ -1211,8 +1063,8 @@ func TestBuilderRDBuilder(t *testing.T) { } expectPanic("b.ResourceA", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), Class: ClassIN, }, ResourceA{}) }) @@ -1221,7 +1073,7 @@ func TestBuilderRDBuilder(t *testing.T) { t.Fatalf("changes caused by RDBuilder visible before End()") } - if err := rdb.Name(MustNewRawName("mx1.example.com"), true); err != nil { + if err := rdb.Name(MustParseName("mx1.example.com"), true); err != nil { t.Fatalf("rb.Name() unexpected error: %v", err) } @@ -1238,8 +1090,8 @@ func TestBuilderRDBuilder(t *testing.T) { } expectPanic("b.ResourceA", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), Class: ClassIN, }, ResourceA{}) }) @@ -1250,8 +1102,8 @@ func TestBuilderRDBuilder(t *testing.T) { rdb.Remove() - rdb, err = b.RDBuilder(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + rdb, err = b.RDBuilder(ResourceHeader{ + Name: MustParseName("example.com"), Type: 54839, Class: ClassIN, }) @@ -1264,13 +1116,13 @@ func TestBuilderRDBuilder(t *testing.T) { } expectPanic("b.ResourceA", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), Class: ClassIN, }, ResourceA{}) }) - if err := rdb.Name(MustNewRawName("www.example.com"), true); err != nil { + if err := rdb.Name(MustParseName("www.example.com"), true); err != nil { t.Fatalf("rb.Name() unexpected error: %v", err) } if len(b.Bytes()) != 12 || b.Length() != 12 || b.Header() != *new(Header) { @@ -1284,7 +1136,7 @@ func TestBuilderRDBuilder(t *testing.T) { t.Fatalf("changes caused by RDBuilder visible before End()") } - if err := rdb.Name(MustNewRawName("smtp.example.com"), false); err != nil { + if err := rdb.Name(MustParseName("smtp.example.com"), false); err != nil { t.Fatalf("rb.Name() unexpected error: %v", err) } if len(b.Bytes()) != 12 || b.Length() != 12 || b.Header() != *new(Header) { @@ -1320,8 +1172,8 @@ func TestBuilderRDBuilder(t *testing.T) { } expectPanic("b.ResourceA", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), Class: ClassIN, }, ResourceA{}) }) @@ -1332,8 +1184,8 @@ func TestBuilderRDBuilder(t *testing.T) { rdb.End() - if err := b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + if err := b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), Class: ClassIN, }, ResourceA{}); err != nil { t.Fatalf("b.ResourceA() unexpected error: %v", err) @@ -1376,12 +1228,13 @@ func TestBuilderRDBuilder(t *testing.T) { t.Fatalf("rdp.Name() unexpected error: %v", err) } - if !name.EqualName(MustNewName("www.example.com")) { - t.Errorf(`name = %v, name.EqualName(MustNewName("www.example.com")) = false, want: true`, name.String()) + n := MustParseName("www.example.com") + if !name.Equal(&n) { + t.Errorf(`name = %v, name.Equal("www.example.com") = false, want: true`, name.String()) } - if !name.Compressed() { - t.Errorf("name.Compressed() = false, want: true") + if name.Compression != CompressionCompressed { + t.Errorf("name.Compression = %v, want: Compressed", name.Compression) } rawBytes, err := rdp.Bytes(3) @@ -1399,12 +1252,13 @@ func TestBuilderRDBuilder(t *testing.T) { t.Fatalf("rdp.Name() unexpected error: %v", err) } - if !name2.EqualName(MustNewName("smtp.example.com")) { - t.Errorf(`name2 = %v, name2.EqualName(MustNewName("smtp.example.com")) = false, want: true`, name2.String()) + n = MustParseName("smtp.example.com.") + if !name2.Equal(&n) { + t.Errorf(`name2 = %v, name2.Equal("smtp.example.com") = false, want: true`, name2.String()) } - if name2.Compressed() { - t.Errorf("name.Compressed() = true, want: false") + if name2.Compression != CompressionNotCompressed { + t.Errorf("name.Compression = %v, want: NotCompressed", name2.Compression) } u8, err := rdp.Uint8() @@ -1459,8 +1313,8 @@ func TestBuilderRDBuilder(t *testing.T) { func TestBuilderRDBuilderRDataOverflow(t *testing.T) { b := StartBuilder(make([]byte, 0, 128), 0, 0) b.StartAnswers() - rdb, err := b.RDBuilder(ResourceHeader[RawName]{ - Name: MustNewRawName("."), + rdb, err := b.RDBuilder(ResourceHeader{ + Name: MustParseName("."), Type: 54839, Class: ClassIN, Length: 100, @@ -1472,7 +1326,7 @@ func TestBuilderRDBuilderRDataOverflow(t *testing.T) { rdb.Bytes(make([]byte, math.MaxUint16-6)) before := b.Bytes()[12:] - if err := rdb.Name(MustNewRawName("www.example.com"), true); err == nil { + if err := rdb.Name(MustParseName("www.example.com"), true); err == nil { t.Fatal("rb.Name(): unexpected success") } if !bytes.Equal(before, b.Bytes()[12:]) { @@ -1525,16 +1379,16 @@ func TestBuilderReset(t *testing.T) { b := StartBuilder(make([]byte, 0, 128), 0, 0) b.LimitMessageSize(100) - if err := b.Question(Question[RawName]{ - Name: MustNewRawName("example.net"), + if err := b.Question(Question{ + Name: MustParseName("example.net"), Type: TypeA, Class: ClassIN, }); err != nil { t.Fatalf("b.Question() returned error: %v", err) } - hdr := ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + hdr := ResourceHeader{ + Name: MustParseName("example.com"), Class: ClassIN, } @@ -1543,24 +1397,24 @@ func TestBuilderReset(t *testing.T) { t.Fatalf("b.ResourceA() returned error: %v", err) } b.StartAuthorities() - hdr.Name = MustNewRawName("www.example.com") + hdr.Name = MustParseName("www.example.com") if err := b.ResourceA(hdr, ResourceA{A: [4]byte{192, 0, 2, 1}}); err != nil { t.Fatalf("b.ResourceA() returned error: %v", err) } b.StartAdditionals() - hdr.Name = MustNewRawName("smtp.example.com") + hdr.Name = MustParseName("smtp.example.com") if err := b.ResourceA(hdr, ResourceA{A: [4]byte{192, 0, 2, 1}}); err != nil { t.Fatalf("b.ResourceA() returned error: %v", err) } - hdr.Name = MustNewRawName("internal.example.com") + hdr.Name = MustParseName("internal.example.com") if err := b.ResourceA(hdr, ResourceA{A: [4]byte{192, 0, 2, 1}}); err != ErrTruncated { t.Fatalf("b.ResourceA() returned error: %v, want: %v", err, ErrTruncated) } b.Reset(make([]byte, 0, 128), 0, 0) - if err := b.Question(Question[RawName]{ - Name: MustNewRawName("www.example.net"), + if err := b.Question(Question{ + Name: MustParseName("www.example.net"), Type: TypeA, Class: ClassIN, }); err != nil { @@ -1568,21 +1422,21 @@ func TestBuilderReset(t *testing.T) { } b.StartAnswers() - hdr.Name = MustNewRawName("internal.example.com") + hdr.Name = MustParseName("internal.example.com") if err := b.ResourceAAAA(hdr, ResourceAAAA{}); err != nil { t.Fatalf("b.ResourceA() returned error: %v", err) } b.StartAuthorities() - hdr.Name = MustNewRawName("www.example.com") + hdr.Name = MustParseName("www.example.com") if err := b.ResourceAAAA(hdr, ResourceAAAA{}); err != nil { t.Fatalf("b.ResourceA() returned error: %v", err) } b.StartAdditionals() - hdr.Name = MustNewRawName("example.com") + hdr.Name = MustParseName("example.com") if err := b.ResourceAAAA(hdr, ResourceAAAA{}); err != nil { t.Fatalf("b.ResourceA() returned error: %v", err) } - hdr.Name = MustNewRawName("www.admin.internal.example.net") + hdr.Name = MustParseName("www.admin.internal.example.net") if err := b.ResourceA(hdr, ResourceA{A: [4]byte{192, 0, 2, 1}}); err != nil { t.Fatalf("b.ResourceA() returned error: %v", err) } @@ -1597,8 +1451,9 @@ func TestBuilderReset(t *testing.T) { t.Fatalf("p.Question() returned error: %v", err) } - if !q.Name.EqualName(MustNewName("www.example.net")) { - t.Fatalf(`hdr1.Name = %v, hdr1.Name.EqualName(MustNewName("www.example.net")) = false, want: true`, q.Name.String()) + n := MustParseName("www.example.net") + if !q.Name.Equal(&n) { + t.Fatalf(`hdr1.Name = %v, hdr1.Name.Equal("www.example.net") = false, want: true`, q.Name.String()) } if q.Class != ClassIN { @@ -1618,8 +1473,9 @@ func TestBuilderReset(t *testing.T) { t.Fatalf("p.ResourceHeader() returned error: %v", err) } - if !hdr1.Name.EqualName(MustNewName("internal.example.com")) { - t.Fatalf(`hdr1.Name = %v, hdr1.Name.EqualName(MustNewName("internal.example.com")) = false, want: true`, hdr1.Name.String()) + n = MustParseName("internal.example.com") + if !hdr1.Name.Equal(&n) { + t.Fatalf(`hdr1.Name = %v, hdr1.Name.Equal("internal.example.com") = false, want: true`, hdr1.Name.String()) } if hdr1.Class != ClassIN { @@ -1640,8 +1496,9 @@ func TestBuilderReset(t *testing.T) { t.Fatalf("p.ResourceHeader() returned error: %v", err) } - if !hdr2.Name.EqualName(MustNewName("www.example.com")) { - t.Fatalf(`hdr2.Name = %v, hdr2.Name.EqualName(MustNewName("www.example.com")) = false, want: true`, hdr2.Name.String()) + n = MustParseName("www.example.com") + if !hdr2.Name.Equal(&n) { + t.Fatalf(`hdr2.Name = %v, hdr2.Name.Equal("www.example.com") = false, want: true`, hdr2.Name.String()) } if _, err := p.ResourceAAAA(); err != nil { @@ -1654,8 +1511,9 @@ func TestBuilderReset(t *testing.T) { t.Fatalf("p.ResourceHeader() returned error: %v", err) } - if !hdr3.Name.EqualName(MustNewName("example.com")) { - t.Fatalf(`hdr3.Name = %v, hdr3.Name.EqualName(MustNewName("example.com")) = false, want: true`, hdr3.Name.String()) + n = MustParseName("example.com") + if !hdr3.Name.Equal(&n) { + t.Fatalf(`hdr3.Name = %v, hdr3.Name.Equal("example.com") = false, want: true`, hdr3.Name.String()) } if _, err := p.ResourceAAAA(); err != nil { @@ -1667,8 +1525,9 @@ func TestBuilderReset(t *testing.T) { t.Fatalf("p.ResourceHeader() returned error: %v", err) } - if !hdr4.Name.EqualName(MustNewName("www.admin.internal.example.net")) { - t.Fatalf(`hdr4.Name = %v, hdr4.Name.EqualName(MustNewName("www.admin.internal.example.net")) = false, want: true`, hdr4.Name.String()) + n = MustParseName("www.admin.internal.example.net") + if !hdr4.Name.Equal(&n) { + t.Fatalf(`hdr4.Name = %v, hdr4.Name.Equal("www.admin.internal.example.net") = false, want: true`, hdr4.Name.String()) } if _, err := p.ResourceA(); err != nil { @@ -1736,8 +1595,8 @@ func (r *fuzzRand) bytes(n int) []byte { return b } -func (r *fuzzRand) rawName() RawName { - n, err := NewRawName(string(r.arbitraryAmountOfBytes())) +func (r *fuzzRand) rawName() Name { + n, err := ParseName(string(r.arbitraryAmountOfBytes())) if err != nil { r.t.SkipNow() } @@ -1787,7 +1646,7 @@ func FuzzBuilder(f *testing.F) { beforeLen := b.Length() before := b.Bytes() - q := Question[RawName]{ + q := Question{ Name: r.rawName(), Type: Type(r.uint16()), Class: Class(r.uint16()), @@ -1848,7 +1707,7 @@ func FuzzBuilder(f *testing.F) { } for r.bool() { - hdr := ResourceHeader[RawName]{ + hdr := ResourceHeader{ Name: r.rawName(), Class: Class(r.uint16()), TTL: r.uint32(), @@ -1873,13 +1732,13 @@ func FuzzBuilder(f *testing.F) { t.Logf("b.ResourceAAAA(%#v, %#v) = %v", hdr, res, err) } case 3: - res := ResourceNS[RawName]{NS: r.rawName()} + res := ResourceNS{NS: r.rawName()} err = b.ResourceNS(hdr, res) if debugFuzz { t.Logf("b.ResourceNS(%#v, %#v) = %v", hdr, res, err) } case 4: - res := ResourceSOA[RawName]{ + res := ResourceSOA{ NS: r.rawName(), Mbox: r.rawName(), Serial: r.uint32(), @@ -1917,13 +1776,13 @@ func FuzzBuilder(f *testing.F) { err = nil } case 7: - res := ResourceCNAME[RawName]{CNAME: r.rawName()} + res := ResourceCNAME{CNAME: r.rawName()} err = b.ResourceCNAME(hdr, res) if debugFuzz { t.Logf("b.ResourceCNAME(%#v, %#v) = %v", hdr, res, err) } case 8: - res := ResourceMX[RawName]{Pref: r.uint16(), MX: r.rawName()} + res := ResourceMX{Pref: r.uint16(), MX: r.rawName()} err = b.ResourceMX(hdr, res) if debugFuzz { t.Logf("b.ResourceMX(%#v, %#v) = %v", hdr, res, err) diff --git a/edns0.go b/edns0.go index b918368..3f093dc 100644 --- a/edns0.go +++ b/edns0.go @@ -55,9 +55,9 @@ type EDNS0Header struct { } // AsResourceHeader converts [EDNS0Header] into a [ResourceHeader]. -func (e EDNS0Header) AsResourceHeader() ResourceHeader[RawName] { - return ResourceHeader[RawName]{ - Name: []byte{0}, +func (e EDNS0Header) AsResourceHeader() ResourceHeader { + return ResourceHeader{ + Name: Name{Length: 1}, Type: TypeOPT, Class: Class(e.Payload), TTL: uint32(e.PartialExtendedRCode)<<24 | uint32(e.Version)<<16 | uint32(e.ExtendedFlags), @@ -69,20 +69,13 @@ var errInvalidEDNS0Header = errors.New("invalid EDNS(0) header") // AsEDNS0Header parses the ResourceHeader into an [EDNS0Header]. // // This function should only be called when the h.Type is equal to [TypeOPT]. -func (h *ResourceHeader[T]) AsEDNS0Header() (EDNS0Header, error) { +func (h *ResourceHeader) AsEDNS0Header() (EDNS0Header, error) { if h.Type != TypeOPT { return EDNS0Header{}, errInvalidOperation } - switch v := any(h.Name).(type) { - case ParserName: - if !v.isRoot() { - return EDNS0Header{}, errInvalidEDNS0Header - } - case RawName: - if len(v) != 1 { - return EDNS0Header{}, errInvalidEDNS0Header - } + if h.Name.Length != 1 { + return EDNS0Header{}, errInvalidEDNS0Header } return EDNS0Header{ @@ -193,7 +186,7 @@ func (r *ResourceOPT) EncodingLength() int { // It errors when the amount of resources in the current section is equal to 65535. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourceOPT(hdr ResourceHeader[RawName], opt ResourceOPT) error { +func (b *Builder) ResourceOPT(hdr ResourceHeader, opt ResourceOPT) error { optb, err := b.ResourceOPTBuilder(hdr) if err != nil { return err @@ -224,7 +217,7 @@ func (b *Builder) ResourceOPT(hdr ResourceHeader[RawName], opt ResourceOPT) erro // on the Builder until you call [ResourceOPTBuilder.End] or [ResourceOPTBuilder.Remove]. // // The building section must NOT be set to questions, otherwise it panics. -func (b *Builder) ResourceOPTBuilder(hdr ResourceHeader[RawName]) (ResourceOPTBuilder, error) { +func (b *Builder) ResourceOPTBuilder(hdr ResourceHeader) (ResourceOPTBuilder, error) { hdr.Type = TypeOPT fixup, hdrOffset, c, err := b.appendHeaderWithLengthFixupNoInc(hdr, b.maxBufSize) if err != nil { @@ -386,10 +379,10 @@ func (b *EDNS0OptionBuilder) Remove() { } // Name appends a DNS name to the option. -func (b *EDNS0OptionBuilder) Name(name RawName, compress bool) error { +func (b *EDNS0OptionBuilder) Name(name Name, compress bool) error { nameOffset := len(b.b.b.buf) var err error - b.b.b.buf, err = b.b.b.nb.appendName(b.b.b.buf, b.b.b.maxBufSize, b.b.b.headerStartOffset, name, compress) + b.b.b.buf, err = b.b.b.nb.appendName(b.b.b.buf, b.b.b.maxBufSize, b.b.b.headerStartOffset, name.asSlice(), compress) if err != nil { return err } @@ -703,7 +696,7 @@ func (p *EDNS0OptionParser) End() error { } // Name parses a single DNS name. -func (p *EDNS0OptionParser) Name() (ParserName, error) { +func (p *EDNS0OptionParser) Name() (Name, error) { return p.rd.Name() } diff --git a/edns0_test.go b/edns0_test.go index 4420893..1f64108 100644 --- a/edns0_test.go +++ b/edns0_test.go @@ -80,8 +80,8 @@ func TestEDNS0Header(t *testing.T) { b.StartAnswers() b.StartAuthorities() b.StartAdditionals() - rdb, err := b.RDBuilder(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + rdb, err := b.RDBuilder(ResourceHeader{ + Name: MustParseName("example.com"), Type: TypeOPT, }) if err != nil { @@ -111,7 +111,7 @@ func TestEDNS0Header(t *testing.T) { t.Fatalf("p.ResourceHeader().AsEDNS0Header() unexpected error: %v, want %v", err, errInvalidEDNS0Header) } - edns0ResHdr.Name = MustNewRawName("example.com") + edns0ResHdr.Name = MustParseName("example.com") if _, err = edns0ResHdr.AsEDNS0Header(); err != errInvalidEDNS0Header { t.Fatalf("%#v.AsEDNS0Header() unexpected error: %v, want: %v", edns0ResHdr, err, errInvalidEDNS0Header) } @@ -147,8 +147,8 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { } expectPanic("b.ResourceA()", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), }, ResourceA{A: [4]byte{192, 0, 2, 1}}) }) @@ -206,8 +206,8 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { } expectPanic("b.ResourceA()", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), }, ResourceA{A: [4]byte{192, 0, 2, 1}}) }) @@ -248,8 +248,8 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { t.Fatalf("changes caused by ResourceOPTBuilder visible before End()") } expectPanic("b.ResourceA()", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), }, ResourceA{A: [4]byte{192, 0, 2, 1}}) }) @@ -264,8 +264,8 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { t.Fatalf("changes caused by ResourceOPTBuilder visible before End()") } expectPanic("b.ResourceA()", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), }, ResourceA{A: [4]byte{192, 0, 2, 1}}) }) optb1.Remove() @@ -284,8 +284,8 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { } expectPanic("b.ResourceA()", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), }, ResourceA{A: [4]byte{192, 0, 2, 1}}) }) @@ -311,7 +311,7 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { }) }) - if err := optb2.Name(MustNewRawName("example.com"), false); err != nil { + if err := optb2.Name(MustParseName("example.com"), false); err != nil { t.Fatalf(`optb2.Name(MustNewRawName("example.com") unexpected error: %v`, err) } if l := optb2.Length(); l != 13 { @@ -323,16 +323,16 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { if l := optsb.Length(); l != beforeOptResourceLength { t.Fatalf("optsb.Length() = %v, want: %v", l, beforeOptResourceLength) } - if err := optb2.Name(MustNewRawName("example.com"), true); err != nil { + if err := optb2.Name(MustParseName("example.com"), true); err != nil { t.Fatalf(`optb2.Name(MustNewRawName("example.com", true)) unexpected error: %v`, err) } if l := optb2.Length(); l != 15 { t.Fatalf("optsb.Length() = %v, want: 15", l) } - if err := optb2.Name(MustNewRawName("www.example.com"), true); err != nil { + if err := optb2.Name(MustParseName("www.example.com"), true); err != nil { t.Fatalf(`optb2.Name(MustNewRawName("www.example.com", true)) unexpected error: %v`, err) } - if err := optb2.Name(MustNewRawName("www.example.com"), false); err != nil { + if err := optb2.Name(MustParseName("www.example.com"), false); err != nil { t.Fatalf(`optb2.Name(MustNewRawName("www.example.com", false)) unexpected error: %v`, err) } if err := optb2.Uint8(11); err != nil { @@ -357,8 +357,8 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { t.Fatalf("optsb.Length() = %v, want: %v", l, beforeOptResourceLength) } expectPanic("b.ResourceA()", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), }, ResourceA{A: [4]byte{192, 0, 2, 1}}) }) expectPanic("optsb.ClientSubnet()", func() { @@ -387,8 +387,8 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { t.Fatalf("changes caused by EDNS0OptionBuilder visible before End()") } expectPanic("b.ResourceA()", func() { - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), }, ResourceA{A: [4]byte{192, 0, 2, 1}}) }) optsb.End() @@ -603,12 +603,13 @@ func TestResourceOPTBuilderAndParser(t *testing.T) { } } -func expectParserName(t *testing.T, prefix string, name ParserName, expectNameAsStr string, comressed bool) { - if !bytes.Equal(MustNewRawName(expectNameAsStr), name.AsRawName()) { +func expectParserName(t *testing.T, prefix string, name Name, expectNameAsStr string, comressed bool) { + if !bytes.Equal(nameAsSlice(expectNameAsStr), name.asSlice()) { t.Fatalf("%v = %v, want: %v", prefix, name.String(), expectNameAsStr) } - if name.Compressed() != comressed { - t.Fatalf("%v.Compressed() = %v, want: %v", prefix, name.Compressed(), comressed) + c := name.Compression == CompressionCompressed + if c != comressed { + t.Fatalf("%v.Compressed = %v, want: %v", prefix, c, comressed) } } @@ -645,8 +646,8 @@ func TestResourceOPTEncodingLength(t *testing.T) { b.StartAnswers() for { - err := b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + err := b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), }, ResourceA{A: [4]byte{192, 0, 2, 1}}) if err != nil { if err == ErrTruncated { @@ -664,8 +665,8 @@ func TestResourceOPTEncodingLength(t *testing.T) { t.Fatalf("b.ResourceOPT() unexpected error: %v", err) } - err := b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + err := b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), }, ResourceA{A: [4]byte{192, 0, 2, 1}}) if err != ErrTruncated { t.Fatalf("b.ResourceA() unexpected error: %v, want: %v", err, ErrTruncated) diff --git a/name.go b/name.go new file mode 100644 index 0000000..51975ab --- /dev/null +++ b/name.go @@ -0,0 +1,308 @@ +package dnsmsg + +import ( + "errors" + "strings" +) + +const ( + maxEncodedNameLen = 255 + maxLabelLength = 63 +) + +type Compression uint8 + +func (c Compression) canCompress(builderAllowsCompression bool) bool { + return builderAllowsCompression && c != CompressionNever +} + +const ( + CompressionWhenPossible Compression = 0 // compress when RFC permits compression + CompressionNever Compression = 1 // never compress + + CompressionNotCompressed Compression = 128 // name was not compressed + CompressionCompressed Compression = 64 // name was compressed +) + +type Name struct { + // Name is non-comparable to prevent possible mistakes, DNS names should + // be compared in a case-insensitive way, and the Compression field might + // not be equal for the same names. + _ [0]func() + + Name [255]byte + Length uint8 + + // When [Name] is used with a [Builder], the field should be set to + // - [CompressionWhenPossible] - (default, zero value) name will be compressed + // when the RFC permits compression + // - [CompressionNever] - name will never be compressed + // + // [Parser] produces [Name]s with: + // - [CompressonNotCompressed] - name used DNS compression + // - [CompressionCompressed] - name did not use DNS compression + // + // [Builder] also permits [Name]s producted by the [Parser] with this field set to + // [CompressionNotCompressed] or [CompressionCompressed], it treats them as [CompressionWhenPossible]. + // + // This field should only be set to the constants mentioned before. + Compression Compression +} + +func (n *Name) asSlice() []byte { + return n.Name[:n.Length] +} + +var errInvalidName = errors.New("invalid name") + +func MustParseName(name string) Name { + n, err := ParseName(name) + if err != nil { + panic("dnsmsg: MustParseName: " + err.Error()) + } + return n +} + +func ParseName(name string) (Name, error) { + if name == "" { + return Name{}, errInvalidName + } + + if name == "." { + return Name{Length: 1}, nil + } + + var n Name + n.Length = 1 + + labelLengthIndex := 0 + rooted := false + + for i := 0; i < len(name); i++ { + char := name[i] + rooted = false + + labelLength := n.Length - uint8(labelLengthIndex) - 1 + if labelLength > maxLabelLength { + return Name{}, errInvalidName + } + + if n.Length == maxEncodedNameLen { + return Name{}, errInvalidName + } + + switch char { + case '.': + rooted = true + if labelLength == 0 { + return Name{}, errInvalidName + } + n.Name[labelLengthIndex] = labelLength + labelLengthIndex = int(n.Length) + n.Length++ + case '\\': + if len(name) == i+1 { + return Name{}, errInvalidName + } + i++ + if isDigit(name[i]) { + if len(name[i:]) < 3 || !isDigit(name[i+1]) || !isDigit(name[i+2]) { + return Name{}, errInvalidName + } + dddChar, ok := decodeDDD([3]byte([]byte(name[i:]))) + if !ok { + return Name{}, errInvalidName + } + i += 2 + n.Name[n.Length] = dddChar + n.Length++ + continue + } + n.Name[n.Length] = name[i] + n.Length++ + default: + n.Name[n.Length] = char + n.Length++ + } + } + + if !rooted { + if n.Length == maxEncodedNameLen { + return Name{}, errInvalidName + } + labelLength := n.Length - uint8(labelLengthIndex) - 1 + if labelLength > maxLabelLength { + return Name{}, errInvalidName + } + n.Name[labelLengthIndex] = labelLength + n.Length++ + } + + return n, nil +} + +func isDigit(char byte) bool { + return char >= '0' && char <= '9' +} + +func decodeDDD(ddd [3]byte) (uint8, bool) { + ddd[0] -= '0' + ddd[1] -= '0' + ddd[2] -= '0' + num := uint16(ddd[0])*100 + uint16(ddd[1])*10 + uint16(ddd[2]) + if num > 255 { + return 0, false + } + return uint8(num), true +} + +func (n *Name) String() string { + if n.Length == 0 { + return "" + } + + if n.Length == 1 { + return "." + } + + var b strings.Builder + b.Grow(int(n.Length)) + + i := 0 + for { + labelLength := int(n.Name[i]) + if labelLength == 0 { + break + } + i += 1 + for _, v := range n.Name[i : i+labelLength] { + switch { + case v == '.': + b.WriteString("\\.") + case v == '\\': + b.WriteString("\\\\") + case v < '!' || v > '~': + b.WriteByte('\\') + b.Write(toASCIIDecimal(v)) + default: + b.WriteByte(v) + } + } + b.WriteString(".") + i += labelLength + } + + return b.String() +} + +func toASCIIDecimal(v byte) []byte { + var d [3]byte + tmp := v / 100 + v -= tmp * 100 + d[0] = tmp + '0' + tmp = v / 10 + v -= tmp * 10 + d[1] = tmp + '0' + d[2] = v + '0' + return d[:] +} + +// Equal return true when n and other represents the same name (case-insensitively). +func (n *Name) Equal(other *Name) bool { + // Label Lengths are limited to 63, ASCII letters start at 65, so we can + // use this for our benefit and not iterate over labels separately. + return n.Length == other.Length && caseInsensitiveEqual(n.Name[:n.Length], other.Name[:other.Length]) +} + +// len(a) must be equal to len(b) +func caseInsensitiveEqual(a []byte, b []byte) bool { + for i := 0; i < len(a); i++ { + if !equalASCIICaseInsensitive(a[i], b[i]) { + return false + } + } + return true +} + +func equalASCIICaseInsensitive(a, b byte) bool { + const caseDiff = 'a' - 'A' + + if a >= 'a' && a <= 'z' { + a -= caseDiff + } + + if b >= 'a' && b <= 'z' { + b -= caseDiff + } + + return a == b +} + +// ptrLoopCount represents an upper limit of pointers that we +// accept in a single DNS name. +// There is still a poosibilitty of a false positive here, but only for names +// that are badly compressed (pointer to a pointer, pointer to a root name). +const ptrLoopCount = ((maxEncodedNameLen - 1) / 2) + +func (n *Name) unpack(msg []byte, nameStart int) (uint16, error) { + var ( + // length of the raw name, without compression pointers. + rawNameLen = uint16(0) + + // message offset, length up to the first compression pointer (if any, including it). + offset = uint16(0) + + ptrCount = uint8(0) + ) + + n.Compression = CompressionNotCompressed + for i := nameStart; i < len(msg); { + // Compression pointer + if msg[i]&0xC0 == 0xC0 { + if ptrCount++; ptrCount > ptrLoopCount { + return 0, errPtrLoop + } + + if offset == 0 { + offset = rawNameLen + 2 + } + + // Compression pointer is 2 bytes long. + if len(msg) == int(i)+1 { + return 0, errInvalidDNSName + } + + i = int(uint16(msg[i]^0xC0)<<8 | uint16(msg[i+1])) + n.Compression = CompressionCompressed + continue + } + + // Two leading bits are reserved, except for compression pointer (above). + if msg[i]&0xC0 != 0 { + return 0, errInvalidDNSName + } + + if int(msg[i]) > len(msg[i+1:]) { + return 0, errInvalidDNSName + } + + copy(n.Name[rawNameLen:], msg[i:i+1+int(msg[i])]) + + if rawNameLen++; rawNameLen > maxEncodedNameLen { + return 0, errInvalidDNSName + } + + if msg[i] == 0 { + if offset == 0 { + offset = rawNameLen + } + n.Length = uint8(rawNameLen) + return offset, nil + } + + rawNameLen += uint16(msg[i]) + i += int(msg[i]) + 1 + } + + return 0, errInvalidDNSName +} diff --git a/name_test.go b/name_test.go new file mode 100644 index 0000000..0f4c628 --- /dev/null +++ b/name_test.go @@ -0,0 +1,497 @@ +package dnsmsg + +import ( + "bytes" + "fmt" + "math" + "strings" + "testing" +) + +func TestParseName(t *testing.T) { + escapes := "\\.\\223\\.\\\\" + escapesCharCount := 4 + label54 := escapes + strings.Repeat("a", 54-escapesCharCount) + label63 := escapes + strings.Repeat("a", 63-escapesCharCount) + label64 := escapes + strings.Repeat("a", 64-escapesCharCount) + label54Encoded := append([]byte{54, '.', 223, '.', '\\'}, strings.Repeat("a", 54-escapesCharCount)...) + label63Encoded := append([]byte{63, '.', 223, '.', '\\'}, strings.Repeat("a", 63-escapesCharCount)...) + + cases := []struct { + in string + expect Name + expectErr error + }{ + {in: ".", expect: Name{Length: 1}}, + {in: "a.", expect: Name{Name: [255]byte{1, 'a', 0}, Length: 3}}, + {in: "aa.", expect: Name{Name: [255]byte{2, 'a', 'a', 0}, Length: 4}}, + {in: "aA.", expect: Name{Name: [255]byte{2, 'a', 'A', 0}, Length: 4}}, + {in: "a", expect: Name{Name: [255]byte{1, 'a', 0}, Length: 3}}, + {in: "aa", expect: Name{Name: [255]byte{2, 'a', 'a', 0}, Length: 4}}, + {in: "aA", expect: Name{Name: [255]byte{2, 'a', 'A', 0}, Length: 4}}, + + {in: "example.com.", expect: Name{Name: [255]byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 13}}, + {in: "example.com", expect: Name{Name: [255]byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 13}}, + {in: "exAMPle.cOm.", expect: Name{Name: [255]byte{7, 'e', 'x', 'A', 'M', 'P', 'l', 'e', 3, 'c', 'O', 'm', 0}, Length: 13}}, + {in: "exAMPle.cOm", expect: Name{Name: [255]byte{7, 'e', 'x', 'A', 'M', 'P', 'l', 'e', 3, 'c', 'O', 'm', 0}, Length: 13}}, + + {in: "www.example.com.", expect: Name{Name: [255]byte{3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 17}}, + {in: "www.example.com", expect: Name{Name: [255]byte{3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 17}}, + + {in: "\\..", expect: Name{Name: [255]byte{1, '.', 0}, Length: 3}}, + {in: "\\\\.", expect: Name{Name: [255]byte{1, '\\', 0}, Length: 3}}, + {in: "\\a.", expect: Name{Name: [255]byte{1, 'a', 0}, Length: 3}}, + {in: "\\-.", expect: Name{Name: [255]byte{1, '-', 0}, Length: 3}}, + {in: "\\.", expect: Name{Name: [255]byte{1, '.', 0}, Length: 3}}, + {in: "\\\\", expect: Name{Name: [255]byte{1, '\\', 0}, Length: 3}}, + {in: "\\a", expect: Name{Name: [255]byte{1, 'a', 0}, Length: 3}}, + {in: "\\-", expect: Name{Name: [255]byte{1, '-', 0}, Length: 3}}, + + {in: "\\000", expect: Name{Name: [255]byte{1, 0, 0}, Length: 3}}, + {in: "\\001", expect: Name{Name: [255]byte{1, 1, 0}, Length: 3}}, + {in: "\\041", expect: Name{Name: [255]byte{1, 41, 0}, Length: 3}}, + {in: "\\189", expect: Name{Name: [255]byte{1, 189, 0}, Length: 3}}, + {in: "\\241", expect: Name{Name: [255]byte{1, 241, 0}, Length: 3}}, + {in: "\\255", expect: Name{Name: [255]byte{1, 255, 0}, Length: 3}}, + + {in: "\\1.", expectErr: errInvalidName}, + {in: "\\12.", expectErr: errInvalidName}, + {in: "\\1a.", expectErr: errInvalidName}, + {in: "\\12a.", expectErr: errInvalidName}, + {in: "\\256.", expectErr: errInvalidName}, + {in: "\\300.", expectErr: errInvalidName}, + + { + in: "\\255\\.\\\\a\\a\\c\\..example.com.", + expect: Name{Name: [255]byte{7, 255, '.', '\\', 'a', 'a', 'c', '.', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 21}, + }, + + {in: "example.com..", expectErr: errInvalidName}, + {in: "example..com.", expectErr: errInvalidName}, + {in: "www..example.com.", expectErr: errInvalidName}, + + { + in: label63 + ".go.dev", + expect: func() (n Name) { + s := append(n.Name[:0], label63Encoded...) + s = append(s, 2, 'g', 'o', 3, 'd', 'e', 'v', 0) + n.Length = byte(len(s)) + return + }(), + }, + {in: label64 + ".go.dev", expectErr: errInvalidName}, + + { + in: label63, + expect: func() (n Name) { + s := append(n.Name[:0], label63Encoded...) + s = append(s, 0) + n.Length = byte(len(s)) + return + }(), + }, + { + in: label63 + ".", + expect: func() (n Name) { + s := append(n.Name[:0], label63Encoded...) + s = append(s, 0) + n.Length = byte(len(s)) + return + }(), + }, + {in: label64, expectErr: errInvalidName}, + {in: label64 + ".", expectErr: errInvalidName}, + + // 253B non-rooted name. + { + in: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%v.go.dev", label63, label54), + expect: func() (n Name) { + s := append(n.Name[:0], label63Encoded...) + s = append(s, label63Encoded...) + s = append(s, label63Encoded...) + s = append(s, label54Encoded...) + s = append(s, 2, 'g', 'o', 3, 'd', 'e', 'v', 0) + n.Length = byte(len(s)) + return + }(), + }, + + // 254B rooted name. + { + in: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%v.go.dev.", label63, label54), + expect: func() (n Name) { + s := append(n.Name[:0], label63Encoded...) + s = append(s, label63Encoded...) + s = append(s, label63Encoded...) + s = append(s, label54Encoded...) + s = append(s, 2, 'g', 'o', 3, 'd', 'e', 'v', 0) + n.Length = byte(len(s)) + return + }(), + }, + + // 254B non-rooted name. + { + in: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%va.go.dev", label63, label54), + expectErr: errInvalidName, + }, + + // 255B rooted name. + { + in: fmt.Sprintf("%[1]v.%[1]v.%[1]v.%va.go.dev.", label63, label54), + expectErr: errInvalidName, + }, + } + + for _, tt := range cases { + name, err := ParseName(tt.in) + if err != tt.expectErr || name.Name != tt.expect.Name || + name.Length != tt.expect.Length || name.Compression != tt.expect.Compression { + t.Errorf( + "ParseName(%q) = (%v, %v); want = (%v, %v)", + tt.in, name, err, tt.expect, tt.expectErr, + ) + } + } +} + +func TestNameString(t *testing.T) { + cases := []struct { + n Name + str string + }{ + {n: Name{}, str: ""}, + {n: Name{Length: 1}, str: "."}, + {n: Name{Name: [255]byte{1, 'a', 0}, Length: 3}, str: "a."}, + {n: Name{Name: [255]byte{2, 'a', 'A', 0}, Length: 3}, str: "aA."}, + {n: Name{Name: [255]byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 13}, str: "example.com."}, + {n: Name{Name: [255]byte{3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 13}, str: "www.example.com."}, + {n: Name{Name: [255]byte{3, 'W', 'w', 'W', 7, 'e', 'X', 'a', 'm', 'p', 'L', 'e', 3, 'c', 'O', 'm', 0}, Length: 13}, str: "WwW.eXampLe.cOm."}, + {n: Name{Name: [255]byte{2, '~', '!', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 13}, str: "~!.example.com."}, + {n: Name{Name: [255]byte{4, 0x20, 0x7F, '.', '\\', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 13}, str: "\\032\\127\\.\\\\.example.com."}, + } + + for _, tt := range cases { + if str := tt.n.String(); str != tt.str { + t.Errorf("(%v).String() = %q; want = %q", tt.n.Name[:tt.n.Length], str, tt.str) + } + } +} + +func TestNameEqual(t *testing.T) { + cases := []struct { + n1, n2 Name + eq bool + }{ + {n1: Name{}, n2: Name{}, eq: true}, + {n1: Name{Length: 1}, n2: Name{Length: 1}, eq: true}, + {n1: Name{Length: 1}, n2: Name{}, eq: false}, + { + n1: Name{Name: [255]byte{1, 'a', 0}, Length: 3}, + n2: Name{Name: [255]byte{1, 'a', 0}, Length: 3}, + eq: true, + }, + { + n1: Name{Name: [255]byte{1, 'a', 0}, Length: 3}, + n2: Name{Name: [255]byte{1, 'A', 0}, Length: 3}, + eq: true, + }, + { + n1: Name{Name: [255]byte{1, 'a', 0}, Length: 3}, + n2: Name{Name: [255]byte{1, 'b', 0}, Length: 3}, + eq: false, + }, + { + n1: Name{Name: [255]byte{1, 'a', 0}, Length: 3}, + n2: Name{Name: [255]byte{2, 'a', 'a', 0}, Length: 4}, + eq: false, + }, + + { + n1: Name{ + Name: [255]byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, + Length: 13, + }, + n2: Name{ + Name: [255]byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, + Length: 13, + }, + eq: true, + }, + { + n1: Name{ + Name: [255]byte{7, 'E', 'x', 'A', 'm', 'p', 'L', 'e', 3, 'c', 'O', 'm', 0}, + Length: 13, + }, + n2: Name{ + Name: [255]byte{7, 'E', 'X', 'a', 'm', 'p', 'l', 'E', 3, 'C', 'o', 'm', 0}, + Length: 13, + }, + eq: true, + }, + { + n1: Name{ + Name: [255]byte{3, 'w', 'w', 'w', 7, 'E', 'x', 'A', 'm', 'p', 'L', 'e', 3, 'c', 'O', 'm', 0}, + Length: 17, + }, + n2: Name{ + Name: [255]byte{7, 'E', 'X', 'a', 'm', 'p', 'l', 'E', 3, 'C', 'o', 'm', 0}, + Length: 13, + }, + eq: false, + }, + { + n1: Name{ + Name: [255]byte{3, 'w', 'w', 'w', 7, 'E', 'x', 'A', 'm', 'p', 'L', 'e', 3, 'c', 'O', 'm', 0}, + Length: 17, + }, + n2: Name{ + Name: [255]byte{4, 'i', 'm', 'a', 'p', 7, 'E', 'X', 'a', 'm', 'p', 'l', 'E', 3, 'C', 'o', 'm', 0}, + Length: 18, + }, + eq: false, + }, + { + n1: Name{ + Name: [255]byte{1, 'a', 2, 'w', 'w', 3, 'w', 'w', 'w', 0}, + Length: 10, + }, + n2: Name{ + Name: [255]byte{1, 'a', 3, 'w', 'w', 'w', 2, 'w', 'w', 0}, + Length: 10, + }, + eq: false, + }, + } + + for _, tt := range cases { + if eq := tt.n1.Equal(&tt.n2); eq != tt.eq { + t.Errorf("(%v).Equal(%v) = %v; want = %v", + tt.n1.Name[:tt.n2.Length], + tt.n2.Name[:tt.n2.Length], + eq, tt.eq, + ) + } + if eq := tt.n2.Equal(&tt.n1); eq != tt.eq { + t.Errorf("(%v).Equal(%v) = %v; want = %v", + tt.n2.Name[:tt.n2.Length], + tt.n1.Name[:tt.n1.Length], + eq, tt.eq, + ) + } + } +} + +func TestNameUnpack(t *testing.T) { + a63 := bytes.Repeat([]byte{'a'}, 63) + a61 := bytes.Repeat([]byte{'a'}, 61) + + var name255NoCompression []byte + name255NoCompression = append(name255NoCompression, byte(len(a61))) + name255NoCompression = append(name255NoCompression, a61...) + for i := 0; i < 3; i++ { + name255NoCompression = append(name255NoCompression, byte(len(a63))) + name255NoCompression = append(name255NoCompression, a63...) + } + name255NoCompression = append(name255NoCompression, 0) + + if len(name255NoCompression) != 255 { + panic("invalid name") + } + + var name255Compressed []byte + name255Compressed = append(name255Compressed, byte(len(a61))) + name255Compressed = append(name255Compressed, a61...) + name255Compressed = append(name255Compressed, 0xC0, byte(len(name255Compressed))+4) + name255Compressed = append(name255Compressed, 32, 32) // random data + for i := 0; i < 3; i++ { + name255Compressed = append(name255Compressed, byte(len(a63))) + name255Compressed = append(name255Compressed, a63...) + } + name255Compressed = append(name255Compressed, 0) + + // +4 (pointer and random data in-between") + if len(name255Compressed) != 255+4 { + panic("invalid name") + } + + cases := []struct { + msg []byte + nameStart int + + expectName Name + expectOffset uint16 + expectErr error + }{ + { + msg: []byte{0}, + expectName: Name{Length: 1, Compression: CompressionNotCompressed}, + expectOffset: 1, + }, + { + msg: []byte{0xC0, 3, 1, 0}, + expectName: Name{Length: 1, Compression: CompressionCompressed}, + expectOffset: 2, + }, + { + msg: []byte{1, 'a', 0}, + expectName: Name{Name: [255]byte{1, 'a', 0}, Length: 3, Compression: CompressionNotCompressed}, + expectOffset: 3, + }, + { + msg: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, + expectName: Name{Name: [255]byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 13, Compression: CompressionNotCompressed}, + expectOffset: 13, + }, + { + msg: []byte{7, 'E', 'x', 'a', 'M', 'p', 'l', 'E', 3, 'c', 'O', 'M', 0}, + expectName: Name{Name: [255]byte{7, 'E', 'x', 'a', 'M', 'p', 'l', 'E', 3, 'c', 'O', 'M', 0}, Length: 13, Compression: CompressionNotCompressed}, + expectOffset: 13, + }, + { + msg: []byte{3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, + expectName: Name{Name: [255]byte{3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 17, Compression: CompressionNotCompressed}, + expectOffset: 17, + }, + + { + msg: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 3, 'w', 'w', 'w', 0xC0, 0, 1, 1, 1}, + nameStart: 13, + expectName: Name{Name: [255]byte{3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 17, Compression: CompressionCompressed}, + expectOffset: 6, + }, + { + msg: []byte{88, 99, 3, 'w', 'w', 'w', 0xC0, 10, 1, 1, 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, + nameStart: 2, + expectName: Name{Name: [255]byte{3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, Length: 17, Compression: CompressionCompressed}, + expectOffset: 6, + }, + + { + // 255 Byte name without pointers + msg: name255NoCompression, + expectName: Name{Name: [255]byte(name255NoCompression), Length: 255, Compression: CompressionNotCompressed}, + expectOffset: 255, + }, + { + // 255 Byte name with one compression pointer", + msg: name255Compressed, + expectName: Name{Name: [255]byte(name255NoCompression), Length: 255, Compression: CompressionCompressed}, + expectOffset: 64, + }, + { + // 256 Byte name without compression pointers + msg: func() []byte { + var buf []byte + a63 := bytes.Repeat([]byte{'a'}, 63) + a62 := bytes.Repeat([]byte{'a'}, 62) + + for i := 0; i < 3; i++ { + buf = append(buf, byte(len(a63))) + buf = append(buf, a63...) + } + + buf = append(buf, byte(len(a62))) + buf = append(buf, a62...) + buf = append(buf, 0) + + if len(buf) != 256 { + panic("invalid name") + } + + return buf + }(), + expectErr: errInvalidDNSName, + }, + { + // 256 Byte name with one compression pointer + msg: func() []byte { + var buf []byte + a63 := bytes.Repeat([]byte{'a'}, 63) + z62 := bytes.Repeat([]byte{'z'}, 62) + + buf = append(buf, byte(len(z62))) + buf = append(buf, z62...) + buf = append(buf, 0xC0, byte(len(buf))+4) + + buf = append(buf, 32, 32) // random data + + for i := 0; i < 3; i++ { + buf = append(buf, byte(len(a63))) + buf = append(buf, a63...) + } + + buf = append(buf, 0) + + // +4 (pointer and random data in-between") + if len(buf) != 256+4 { + panic("invalid name") + } + + return buf + }(), + expectErr: errInvalidDNSName, + }, + + {msg: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 5, 'c', 'o', 'm', 0}, expectErr: errInvalidDNSName}, + {msg: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm'}, expectErr: errInvalidDNSName}, + {msg: []byte{1, 'a', 0xC0, 0}, expectErr: errPtrLoop}, + {msg: []byte{0b10000000}, expectErr: errInvalidDNSName}, + {msg: []byte{0b01000000}, expectErr: errInvalidDNSName}, + } + + for _, tt := range cases { + var n Name + offset, err := n.unpack(tt.msg, tt.nameStart) + if err != tt.expectErr || offset != tt.expectOffset { + t.Errorf( + "Name.unpack(%v, %v) = (%v, %v); want = (%v, %v)", + tt.msg, tt.nameStart, offset, err, tt.expectOffset, tt.expectErr, + ) + } + if err == nil { + if n.Name != tt.expectName.Name || n.Length != tt.expectName.Length || n.Compression != tt.expectName.Compression { + t.Errorf("Name.unpack(%v, %v) = %v, want = %v", tt.msg, tt.nameStart, n, tt.expectName) + } + } + } +} + +func TestUnpackNameCompressionPtrLoop(t *testing.T) { + nb := nameBuilderState{} + buf := make([]byte, headerLen, 1024) + + // This creates a 255b name with the maximum (sensible) pointer limit. + for i := 3; i <= maxEncodedNameLen; i += 2 { + name := make([]byte, maxEncodedNameLen)[:i] + for j := 0; j < i-1; j += 2 { + name[j] = 1 + name[j+1] = 'a' + } + buf, _ = nb.appendName(buf, math.MaxInt, 0, name, true) + // append the longest name twice, so that it is also compressed directly. + if len(name) == maxEncodedNameLen { + buf, _ = nb.appendName(buf, math.MaxInt, 0, name, true) + } + } + + offset := headerLen + + for len(buf) != offset { + var n Name + off, err := n.unpack(buf, offset) + if err != nil { + t.Fatalf("failed to unpack name at offset: %v: %v", offset, err) + } + offset += int(off) + } + + // Badly compressed name (Pointer to a Pointer). + ptrToPtrNameOffset := len(buf) + buf = appendUint16(buf, 0xC000|uint16(ptrToPtrNameOffset-2)) + var n Name + _, err := n.unpack(buf, ptrToPtrNameOffset) + if err != errPtrLoop { + t.Fatalf("unexpected error while unpacking badly packed name (ptr to ptr): %v, expected: %v", err, errPtrLoop) + } +} diff --git a/parser.go b/parser.go index 3bc96a6..fd85e18 100644 --- a/parser.go +++ b/parser.go @@ -2,12 +2,6 @@ package dnsmsg import ( "errors" - "strings" -) - -const ( - maxEncodedNameLen = 255 - maxLabelLength = 63 ) var ( @@ -157,30 +151,31 @@ func (p *Parser) End() error { // Returns [ErrSectionDone] when no more questions are available to parse. // // The parsing section must be set to questions. -func (m *Parser) Question() (Question[ParserName], error) { +func (m *Parser) Question() (Question, error) { if m.curSection != sectionQuestions { - return Question[ParserName]{}, errInvalidOperation + return Question{}, errInvalidOperation } if m.remainingQuestions == 0 { - return Question[ParserName]{}, ErrSectionDone + return Question{}, ErrSectionDone } - name, offset, err := m.unpackName(m.curOffset) + var name Name + offset, err := name.unpack(m.msg, m.curOffset) if err != nil { - return Question[ParserName]{}, err + return Question{}, err } tmpOffset := m.curOffset + int(offset) if len(m.msg)-tmpOffset < 4 { - return Question[ParserName]{}, errInvalidDNSMessage + return Question{}, errInvalidDNSMessage } m.curOffset = tmpOffset + 4 m.remainingQuestions-- - return Question[ParserName]{ + return Question{ Name: name, Type: Type(unpackUint16(m.msg[tmpOffset : tmpOffset+2])), Class: Class(unpackUint16(m.msg[tmpOffset+2 : tmpOffset+4])), @@ -199,9 +194,9 @@ func (m *Parser) Question() (Question[ParserName], error) { // current section. // // The parsing section must not be set to questions. -func (m *Parser) ResourceHeader() (ResourceHeader[ParserName], error) { +func (m *Parser) ResourceHeader() (ResourceHeader, error) { if m.resourceData { - return ResourceHeader[ParserName]{}, errInvalidOperation + return ResourceHeader{}, errInvalidOperation } var count *uint16 @@ -213,25 +208,26 @@ func (m *Parser) ResourceHeader() (ResourceHeader[ParserName], error) { case sectionAdditionals: count = &m.remainingAddtitionals default: - return ResourceHeader[ParserName]{}, errInvalidOperation + return ResourceHeader{}, errInvalidOperation } if *count == 0 { - return ResourceHeader[ParserName]{}, ErrSectionDone + return ResourceHeader{}, ErrSectionDone } - name, offset, err := m.unpackName(m.curOffset) + var name Name + offset, err := name.unpack(m.msg, m.curOffset) if err != nil { - return ResourceHeader[ParserName]{}, err + return ResourceHeader{}, err } tmpOffset := m.curOffset + int(offset) if len(m.msg)-tmpOffset < 10 { - return ResourceHeader[ParserName]{}, errInvalidDNSMessage + return ResourceHeader{}, errInvalidDNSMessage } - hdr := ResourceHeader[ParserName]{ + hdr := ResourceHeader{ Name: name, Type: Type(unpackUint16(m.msg[tmpOffset : tmpOffset+2])), Class: Class(unpackUint16(m.msg[tmpOffset+2 : tmpOffset+4])), @@ -291,72 +287,77 @@ func (m *Parser) ResourceAAAA() (ResourceAAAA, error) { // // This method can only be used after [Parser.ResourceHeader] // returns a [ResourceHeader] with a Type field equal to [TypeNS]. -func (m *Parser) ResourceNS() (ResourceNS[ParserName], error) { +func (m *Parser) ResourceNS() (ResourceNS, error) { if !m.resourceData || m.nextResourceType != TypeNS { - return ResourceNS[ParserName]{}, errInvalidOperation + return ResourceNS{}, errInvalidOperation } - ns, offset, err := m.unpackName(m.curOffset) + var ns Name + offset, err := ns.unpack(m.msg, m.curOffset) if err != nil { - return ResourceNS[ParserName]{}, err + return ResourceNS{}, err } if offset != m.nextResourceDataLength { - return ResourceNS[ParserName]{}, errInvalidDNSMessage + return ResourceNS{}, errInvalidDNSMessage } m.resourceData = false m.curOffset += int(offset) - return ResourceNS[ParserName]{ns}, nil + return ResourceNS{ns}, nil } // ResourceCNAME parses a single CNAME resouce data. // // This method can only be used after [Parser.ResourceHeader] // returns a [ResourceHeader] with a Type field equal to [TypeCNAME]. -func (m *Parser) ResourceCNAME() (ResourceCNAME[ParserName], error) { +func (m *Parser) ResourceCNAME() (ResourceCNAME, error) { if !m.resourceData || m.nextResourceType != TypeCNAME { - return ResourceCNAME[ParserName]{}, errInvalidOperation + return ResourceCNAME{}, errInvalidOperation } - name, offset, err := m.unpackName(m.curOffset) + var cname Name + offset, err := cname.unpack(m.msg, m.curOffset) if err != nil { - return ResourceCNAME[ParserName]{}, err + return ResourceCNAME{}, err } if offset != m.nextResourceDataLength { - return ResourceCNAME[ParserName]{}, errInvalidDNSMessage + return ResourceCNAME{}, errInvalidDNSMessage } m.resourceData = false m.curOffset += int(offset) - return ResourceCNAME[ParserName]{name}, nil + return ResourceCNAME{cname}, nil } // ResourceSOA parses a single SOA resouce data. // // This method can only be used after [Parser.ResourceHeader] // returns a [ResourceHeader] with a Type field equal to [TypeSOA]. -func (m *Parser) ResourceSOA() (ResourceSOA[ParserName], error) { +func (m *Parser) ResourceSOA() (ResourceSOA, error) { if !m.resourceData || m.nextResourceType != TypeSOA { - return ResourceSOA[ParserName]{}, errInvalidOperation + return ResourceSOA{}, errInvalidOperation } + var ns Name + var mbox Name + tmpOffset := m.curOffset - ns, offset, err := m.unpackName(tmpOffset) + offset, err := ns.unpack(m.msg, tmpOffset) if err != nil { - return ResourceSOA[ParserName]{}, err + return ResourceSOA{}, err } tmpOffset += int(offset) - mbox, offset, err := m.unpackName(tmpOffset) + offset, err = mbox.unpack(m.msg, tmpOffset) if err != nil { - return ResourceSOA[ParserName]{}, err + return ResourceSOA{}, err } tmpOffset += int(offset) if len(m.msg)-tmpOffset < 20 { - return ResourceSOA[ParserName]{}, errInvalidDNSMessage + return ResourceSOA{}, errInvalidDNSMessage } serial := unpackUint32(m.msg[tmpOffset:]) @@ -367,12 +368,12 @@ func (m *Parser) ResourceSOA() (ResourceSOA[ParserName], error) { tmpOffset += 20 if tmpOffset-m.curOffset != int(m.nextResourceDataLength) { - return ResourceSOA[ParserName]{}, errInvalidDNSMessage + return ResourceSOA{}, errInvalidDNSMessage } m.resourceData = false m.curOffset = tmpOffset - return ResourceSOA[ParserName]{ + return ResourceSOA{ NS: ns, Mbox: mbox, Serial: serial, @@ -387,53 +388,56 @@ func (m *Parser) ResourceSOA() (ResourceSOA[ParserName], error) { // // This method can only be used after [Parser.ResourceHeader] // returns a [ResourceHeader] with a Type field equal to [TypePTR]. -func (m *Parser) ResourcePTR() (ResourcePTR[ParserName], error) { +func (m *Parser) ResourcePTR() (ResourcePTR, error) { if !m.resourceData || m.nextResourceType != TypePTR { - return ResourcePTR[ParserName]{}, errInvalidOperation + return ResourcePTR{}, errInvalidOperation } - name, offset, err := m.unpackName(m.curOffset) + var ptr Name + offset, err := ptr.unpack(m.msg, m.curOffset) if err != nil { - return ResourcePTR[ParserName]{}, err + return ResourcePTR{}, err } if offset != m.nextResourceDataLength { - return ResourcePTR[ParserName]{}, errInvalidDNSMessage + return ResourcePTR{}, errInvalidDNSMessage } m.resourceData = false m.curOffset += int(offset) - return ResourcePTR[ParserName]{name}, nil + return ResourcePTR{ptr}, nil } // ResourceMX parses a single MX resouce data. // // This method can only be used after [Parser.ResourceHeader] // returns a [ResourceHeader] with a Type field equal to [TypeMX]. -func (m *Parser) ResourceMX() (ResourceMX[ParserName], error) { +func (m *Parser) ResourceMX() (ResourceMX, error) { if !m.resourceData || m.nextResourceType != TypeMX { - return ResourceMX[ParserName]{}, errInvalidOperation + return ResourceMX{}, errInvalidOperation } if len(m.msg)-m.curOffset < 2 { - return ResourceMX[ParserName]{}, errInvalidDNSMessage + return ResourceMX{}, errInvalidDNSMessage } pref := unpackUint16(m.msg[m.curOffset:]) - name, offset, err := m.unpackName(m.curOffset + 2) + + var mx Name + offset, err := mx.unpack(m.msg, m.curOffset+2) if err != nil { - return ResourceMX[ParserName]{}, err + return ResourceMX{}, err } if m.nextResourceDataLength != offset+2 { - return ResourceMX[ParserName]{}, errInvalidDNSMessage + return ResourceMX{}, errInvalidDNSMessage } m.resourceData = false m.curOffset += int(m.nextResourceDataLength) - return ResourceMX[ParserName]{ + return ResourceMX{ Pref: pref, - MX: name, + MX: mx, }, nil } @@ -475,12 +479,6 @@ func (m *Parser) SkipResourceData() error { return nil } -func (m *Parser) unpackName(offset int) (n ParserName, off uint16, err error) { - n = ParserName{m: m, nameStart: offset} - off, err = n.unpack() - return -} - // RDParser is a resource data parser used to parse custom resources. type RDParser struct { m *Parser @@ -504,16 +502,17 @@ func (p *RDParser) End() error { } // Name parses a single DNS name. -func (p *RDParser) Name() (ParserName, error) { - name, n, err := p.m.unpackName(p.offset) +func (p *RDParser) Name() (Name, error) { + var n Name + offset, err := n.unpack(p.m.msg, p.offset) if err != nil { - return ParserName{}, err + return Name{}, err } - if p.offset+int(n) > p.maxOffset { - return ParserName{}, errInvalidDNSMessage + if p.offset+int(offset) > p.maxOffset { + return Name{}, errInvalidDNSMessage } - p.offset += int(n) - return name, nil + p.offset += int(offset) + return n, nil } // AllBytes returns all remaining bytes in p. @@ -599,307 +598,3 @@ func (m *Parser) RDParser() (RDParser, error) { maxOffset: m.curOffset, }, nil } - -// ptrLoopCount represents an upper limit of pointers that we -// accept in a single DNS name. -// There is still a poosibilitty of a false positive here, but only for names -// that are badly compressed (pointer to a pointer, pointer to a root name). -const ptrLoopCount = ((maxEncodedNameLen - 1) / 2) - -// ParserName represents a raw DNS name. -// -// It references the underlying message referenced by the [Parser], so -// ParserName lifetime is bounded to the Parser's lifetime, which created the ParserName. -// ParserName should not be used after the underlying array of the slice passed to -// the [Parse] function gets modified. -type ParserName struct { - m *Parser - - nameStart int - rawLen uint8 - compressed bool -} - -// Compressed returns true when the name used DNS name compression. -func (m *ParserName) Compressed() bool { - return m.compressed -} - -// RawLen returns the length of the raw DNS name, -// excluding all of the compression pointers, -func (m *ParserName) RawLen() uint8 { - return m.rawLen -} - -// unpack parses the name, m.m and m.nameStart must be set accordingly -// before calling this method. -func (m *ParserName) unpack() (uint16, error) { - var ( - // length of the raw name, without compression pointers. - rawNameLen = uint16(0) - - // message offset, length up to the first compression pointer (if any, including it). - offset = uint16(0) - - ptrCount = uint8(0) - ) - - for i := int(m.nameStart); i < len(m.m.msg); { - // Compression pointer - if m.m.msg[i]&0xC0 == 0xC0 { - if ptrCount++; ptrCount > ptrLoopCount { - return 0, errPtrLoop - } - - if offset == 0 { - offset = rawNameLen + 2 - } - - // Compression pointer is 2 bytes long. - if len(m.m.msg) == int(i)+1 { - return 0, errInvalidDNSName - } - - i = int(uint16(m.m.msg[i]^0xC0)<<8 | uint16(m.m.msg[i+1])) - m.compressed = true - continue - } - - // Two leading bits are reserved, except for compression pointer (above). - if m.m.msg[i]&0xC0 != 0 { - return 0, errInvalidDNSName - } - - if rawNameLen++; rawNameLen > maxEncodedNameLen { - return 0, errInvalidDNSName - } - - if m.m.msg[i] == 0 { - if offset == 0 { - offset = rawNameLen - } - m.rawLen = uint8(rawNameLen) - return offset, nil - } - - rawNameLen += uint16(m.m.msg[i]) - i += int(m.m.msg[i]) + 1 - } - - return 0, errInvalidDNSName -} - -// Equal reports whether m and m2 represents the same name. -// It does not require identical internal representation of the name. -// Letters are compared in a case insensitive manner. -// m an m2 might be created using two different parsers. -func (m *ParserName) Equal(m2 *ParserName) bool { - im1 := m.nameStart - im2 := m2.nameStart - - for { - // Resolve all compression pointers of m - for m.m.msg[im1]&0xC0 == 0xC0 { - im1 = int(m.m.msg[im1]^0xC0)<<8 | int(m.m.msg[im1+1]) - } - - // Resolve all compression pointers of m2 - for m2.m.msg[im2]&0xC0 == 0xC0 { - im2 = int(m2.m.msg[im2]^0xC0)<<8 | int(m2.m.msg[im2+1]) - } - - // if we point to the same location in the same parser, then it is equal. - if m.m == m2.m && im1 == im2 { - return true - } - - length := int(m.m.msg[im1]) - - // different label lengths - if length != int(m2.m.msg[im2]) { - return false - } - - if length == 0 { - return true - } - - im1++ - im2++ - if !caseInsensitiveEqual(m.m.msg[im1:im1+length], m2.m.msg[im2:im2+length]) { - return false - } - im1 += length - im2 += length - } -} - -func (m *ParserName) isRoot() bool { - return m.rawLen == 1 -} - -// EqualName reports whether m and m2 represents the same name. -func (m *ParserName) EqualName(m2 Name) bool { - return m.equalName(m2, false) -} - -func (m *ParserName) equalName(m2 Name, updateNameStart bool) bool { - im1 := m.nameStart - nameOffset := 0 - - for { - // Resolve all compression pointers of m - for m.m.msg[im1]&0xC0 == 0xC0 { - im1 = int(m.m.msg[im1]^0xC0)<<8 | int(m.m.msg[im1+1]) - } - - labelLength := m.m.msg[im1] - - if labelLength == 0 { - return len(m2.n) == nameOffset || ((len(m2.n)-nameOffset) == 1 && m2.n[nameOffset] == '.') - } - - if updateNameStart && len(m2.n)-nameOffset == 0 { - m.nameStart = im1 - return true - } - - im1++ - for _, v := range m.m.msg[im1 : im1+int(labelLength)] { - if len(m2.n)-nameOffset == 0 { - return false - } - - char := m2.n[nameOffset] - nameOffset++ - if char == '\\' { - char = m2.n[nameOffset] - nameOffset++ - if isDigit(char) { - char, _ = decodeDDD([3]byte{char, m2.n[nameOffset], m2.n[nameOffset+1]}) - nameOffset += 2 - } - } - - if !equalASCIICaseInsensitive(char, v) { - return false - } - } - - if len(m2.n)-nameOffset != 0 { - if m2.n[nameOffset] != '.' { - return false - } - nameOffset++ - } - - im1 += int(labelLength) - } -} - -// EqualSearchName reports whether m and m2 represents the same name. -func (m *ParserName) EqualSearchName(m2 SearchName) bool { - c := *m - return c.equalName(m2.prefix, true) && c.equalName(m2.suffix, false) -} - -// len(a) must be caseInsensitiveEqual to len(b) -func caseInsensitiveEqual(a []byte, b []byte) bool { - for i := 0; i < len(a); i++ { - if !equalASCIICaseInsensitive(a[i], b[i]) { - return false - } - } - return true -} - -func equalASCIICaseInsensitive(a, b byte) bool { - const caseDiff = 'a' - 'A' - - if a >= 'a' && a <= 'z' { - a -= caseDiff - } - - if b >= 'a' && b <= 'z' { - b -= caseDiff - } - - return a == b -} - -// String returns the human name encoding of m. Dots inside the label -// (not separating labels) are escaped as '\.', slashes are encoded as '\\', -// other octets not in range (including) 0x21 through 0xFE are encoded using the \DDD syntax. -func (m *ParserName) String() string { - builder := strings.Builder{} - builder.Grow(int(m.RawLen() - 1)) - - i := m.nameStart - for { - if m.m.msg[i]&0xC0 == 0xC0 { - i = int(m.m.msg[i]^0xC0)<<8 | int(m.m.msg[i+1]) - continue - } - - if m.m.msg[i] == 0 { - if builder.Len() == 0 { - builder.WriteByte('.') - } - return builder.String() - } - - for _, v := range m.m.msg[i+1 : i+int(m.m.msg[i])+1] { - switch { - case v == '.': - builder.WriteString("\\.") - case v == '\\': - builder.WriteString("\\\\") - case v < '!' || v > '~': - builder.WriteByte('\\') - builder.Write(toASCIIDecimal(v)) - default: - builder.WriteByte(v) - } - } - - builder.WriteByte('.') - i += int(m.m.msg[i]) + 1 - } -} - -func toASCIIDecimal(v byte) []byte { - var d [3]byte - tmp := v / 100 - v -= tmp * 100 - d[0] = tmp + '0' - tmp = v / 10 - v -= tmp * 10 - d[1] = tmp + '0' - d[2] = v + '0' - return d[:] -} - -func (m *ParserName) appendRawName(raw []byte) []byte { - i := m.nameStart - for { - if m.m.msg[i]&0xC0 == 0xC0 { - i = int(m.m.msg[i]^0xC0)<<8 | int(m.m.msg[i+1]) - continue - } - - if m.m.msg[i] == 0 { - return append(raw, 0) - } - - raw = append(raw, m.m.msg[i:i+int(m.m.msg[i])+1]...) - i += int(m.m.msg[i]) + 1 - } -} - -func (m *ParserName) appendRawNameNoInline(raw []byte) []byte { - return m.appendRawName(raw) -} - -func (m *ParserName) AsRawName() RawName { - return m.appendRawNameNoInline(make([]byte, 0, maxEncodedNameLen)) -} diff --git a/parser_test.go b/parser_test.go index e08b2be..0ead0b8 100644 --- a/parser_test.go +++ b/parser_test.go @@ -3,531 +3,10 @@ package dnsmsg import ( "bytes" "encoding/binary" - "fmt" - "math" "net/netip" - "strconv" "testing" ) -func TestParserUnpackName(t *testing.T) { - var tests = []struct { - name string - - msg []byte - parseOffset int - - err error - offset uint8 - rawLen uint8 - }{ - { - name: "example.com", - msg: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 1, 1, 0}, - offset: 13, - rawLen: 13, - }, - { - name: "example.com", - parseOffset: 2, - msg: []byte{32, 8, 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 1, 1, 0}, - offset: 13, - rawLen: 13, - }, - { - name: "www.example.com", - msg: []byte{3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 1, 1}, - offset: 17, - rawLen: 17, - }, - { - name: "www.example.com with compression ptr backwards", - parseOffset: 16, - msg: []byte{8, 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 1, 1, 3, 'w', 'w', 'w', 0xC0, 1}, - offset: 6, - rawLen: 17, - }, - { - name: "www.example.com with compression ptr forwards", - parseOffset: 1, - msg: []byte{4, 3, 'w', 'w', 'w', 0xC0, 8, 8, 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 1, 1}, - offset: 6, - rawLen: 17, - }, - { - name: "255B", - msg: func() []byte { - var buf []byte - a63 := bytes.Repeat([]byte{'a'}, 63) - a61 := bytes.Repeat([]byte{'a'}, 61) - - for i := 0; i < 3; i++ { - buf = append(buf, byte(len(a63))) - buf = append(buf, a63...) - } - - buf = append(buf, byte(len(a61))) - buf = append(buf, a61...) - buf = append(buf, 0) - - if len(buf) != 255 { - panic("invalid name") - } - - return buf - }(), - offset: 255, - rawLen: 255, - }, - { - name: "255B with one compression pointer", - msg: func() []byte { - var buf []byte - a63 := bytes.Repeat([]byte{'a'}, 63) - z61 := bytes.Repeat([]byte{'z'}, 61) - - buf = append(buf, byte(len(z61))) - buf = append(buf, z61...) - buf = append(buf, 0xC0, byte(len(buf))+4) - - buf = append(buf, 32, 32) // random data - - for i := 0; i < 3; i++ { - buf = append(buf, byte(len(a63))) - buf = append(buf, a63...) - } - buf = append(buf, 0) - - // +4 (pointer and random data in-between") - if len(buf) != 255+4 { - panic("invalid name") - } - - return buf - }(), - offset: 64, - rawLen: 255, - }, - { - name: "256B", - msg: func() []byte { - var buf []byte - a63 := bytes.Repeat([]byte{'a'}, 63) - a62 := bytes.Repeat([]byte{'a'}, 62) - - for i := 0; i < 3; i++ { - buf = append(buf, byte(len(a63))) - buf = append(buf, a63...) - } - - buf = append(buf, byte(len(a62))) - buf = append(buf, a62...) - buf = append(buf, 0) - - if len(buf) != 256 { - panic("invalid name") - } - - return buf - }(), - err: errInvalidDNSName, - }, - { - name: "256B with one compression pointer", - msg: func() []byte { - var buf []byte - a63 := bytes.Repeat([]byte{'a'}, 63) - z62 := bytes.Repeat([]byte{'z'}, 62) - - buf = append(buf, byte(len(z62))) - buf = append(buf, z62...) - buf = append(buf, 0xC0, byte(len(buf))+4) - - buf = append(buf, 32, 32) // random data - - for i := 0; i < 3; i++ { - buf = append(buf, byte(len(a63))) - buf = append(buf, a63...) - } - - buf = append(buf, 0) - - // +4 (pointer and random data in-between") - if len(buf) != 256+4 { - panic("invalid name") - } - - return buf - }(), - err: errInvalidDNSName, - }, - - {name: "smaller name than label length", msg: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 5, 'c', 'o', 'm', 0}, err: errInvalidDNSName}, - {name: "missing root label", msg: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm'}, err: errInvalidDNSName}, - {name: "pointer loop", msg: []byte{1, 'a', 0xC0, 0}, err: errPtrLoop}, - {name: "reserved label bit(8) ", msg: []byte{0b10000000}, err: errInvalidDNSName}, - {name: "reserved label bit(7)", msg: []byte{0b01000000}, err: errInvalidDNSName}, - } - - for _, v := range tests { - msg := Parser{msg: v.msg} - n, offset, err := msg.unpackName(v.parseOffset) - if err != v.err { - t.Fatalf("%v: got err: %v, expected: %v", v.name, err, v.err) - } - - if offset != uint16(v.offset) { - t.Fatalf("%v: got offset: %v, expected: %v", v.name, offset, v.offset) - } - - if rawLen := n.RawLen(); rawLen != v.rawLen { - t.Fatalf("%v: got RawLen: %v, expected: %v", v.name, v.rawLen, rawLen) - } - } -} - -func TestUnpackNameCompressionPtrLoop(t *testing.T) { - nb := nameBuilderState{} - buf := make([]byte, headerLen, 1024) - - // This creates a 255b name with the maximum (sensible) pointer limit. - for i := 3; i <= maxEncodedNameLen; i += 2 { - name := make([]byte, maxEncodedNameLen)[:i] - for j := 0; j < i-1; j += 2 { - name[j] = 1 - name[j+1] = 'a' - } - buf, _ = nb.appendName(buf, math.MaxInt, 0, name, true) - // append the longest name twice, so that it is also compressed directly. - if len(name) == maxEncodedNameLen { - buf, _ = nb.appendName(buf, math.MaxInt, 0, name, true) - } - } - - p := Parser{msg: buf} - offset := headerLen - - for len(buf) != offset { - _, n, err := p.unpackName(offset) - if err != nil { - t.Fatalf("failed to unpack name at offset: %v: %v", offset, err) - } - offset += int(n) - } - - // Badly compressed name (Pointer to a Pointer). - ptrToPtrNameOffset := len(buf) - buf = appendUint16(buf, 0xC000|uint16(ptrToPtrNameOffset-2)) - p = Parser{msg: buf} - _, _, err := p.unpackName(ptrToPtrNameOffset) - if err != errPtrLoop { - t.Fatalf("unexpected error while unpacking badly packed name (ptr to ptr): %v, expected: %v", err, errPtrLoop) - } -} - -func TestParserNameEqual(t *testing.T) { - prepNamesSameMsg := func(t *testing.T, buf []byte, n1Start, n2Start int) [2]ParserName { - msg := Parser{msg: buf} - m1, _, err := msg.unpackName(n1Start) - if err != nil { - t.Fatal(err) - } - m2, _, err := msg.unpackName(n2Start) - if err != nil { - t.Fatal(err) - } - ret := [2]ParserName{m1, m2} - return ret - } - - prepNamesDifferentMsg := func(t *testing.T, buf1, buf2 []byte, n1Start, n2Start int) [2]ParserName { - msg1, msg2 := Parser{msg: buf1}, Parser{msg: buf2} - m1, _, err := msg1.unpackName(n1Start) - if err != nil { - t.Fatal(err) - } - m2, _, err := msg2.unpackName(n2Start) - if err != nil { - t.Fatal(err) - } - ret := [2]ParserName{m1, m2} - return ret - } - - var tests = []struct { - name string - - names [2]ParserName - equal bool - }{ - { - name: "(same msg) same nameStart", - names: prepNamesSameMsg(t, []byte{ - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, 0, 0), - equal: true, - }, - - { - name: "(same msg) second name directly points to first name", - names: prepNamesSameMsg(t, []byte{ - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - 0xC0, 0, - }, 0, 13), - equal: true, - }, - - { - name: "(same msg) two separate names, without compression pointers", - names: prepNamesSameMsg(t, []byte{ - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, 0, 13), - equal: true, - }, - - { - name: "(same msg) two separate names without compression pointers with different letter case", - names: prepNamesSameMsg(t, []byte{ - 7, 'E', 'x', 'A', 'm', 'P', 'l', 'e', 3, 'c', 'O', 'M', 0, - 7, 'E', 'X', 'a', 'm', 'P', 'l', 'E', 3, 'c', 'o', 'm', 0, - }, 0, 13), - equal: true, - }, - - { - name: "(same msg) two different names example.com != www.example.com, no pointers", - names: prepNamesSameMsg(t, []byte{ - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - 3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, 0, 13), - equal: false, - }, - - { - name: "(same msg) two different names ttt.example.com != www.example.com, no pointers", - names: prepNamesSameMsg(t, []byte{ - 3, 't', 't', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - 3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, 0, 17), - equal: false, - }, - - { - name: "(same msg) two different names example.com != www.example.com, with pointers", - names: prepNamesSameMsg(t, []byte{ - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - 3, 'w', 'w', 'w', 0xC0, 0, - }, 0, 13), - equal: false, - }, - - { - name: "(same msg) two different names example.com == example.com, with multiple pointers", - names: prepNamesSameMsg(t, []byte{ - 0xC0, 3, 99, 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - 0xC0, 0, - }, 0, 16), - equal: true, - }, - - { - name: "(different msgs) same name, no pointers", - names: prepNamesDifferentMsg(t, []byte{ - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, []byte{ - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, 0, 0), - equal: true, - }, - - { - name: "(different msgs) same names, different letter case, no pointers", - names: prepNamesDifferentMsg(t, []byte{ - 7, 'E', 'x', 'a', 'M', 'P', 'l', 'e', 3, 'c', 'O', 'm', 0, - }, []byte{ - 7, 'E', 'X', 'a', 'm', 'P', 'l', 'E', 3, 'c', 'o', 'm', 0, - }, 0, 0), - equal: true, - }, - - { - name: "(different msgs) different names, no pointers", - names: prepNamesDifferentMsg(t, []byte{ - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, []byte{ - 3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, 0, 0), - equal: false, - }, - - { - name: "(different msgs) same names, with pointers", - names: prepNamesDifferentMsg(t, []byte{ - 3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, []byte{ - 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 3, 'w', 'w', 'w', 0xC0, 0, - }, 0, 13), - equal: true, - }, - - { - name: "(different msgs) different names, with pointers", - names: prepNamesDifferentMsg(t, []byte{ - 3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, - }, []byte{ - 3, 't', 't', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 3, 'w', 'w', 'w', 0xC0, 0, - }, 0, 17), - equal: false, - }, - } - - for i, v := range tests { - for ti, tv := range []string{"n[0].Equal(n[1])", "n[1].Equal(n[0])"} { - prefix := fmt.Sprintf("%v: %v: %v:", i, v.name, tv) - - names := v.names - if ti == 1 { - names[0], names[1] = v.names[1], v.names[0] - } - - if eq := names[0].Equal(&names[1]); eq != v.equal { - t.Errorf("%v expected: %v, but: %v", prefix, v.equal, eq) - } - } - } -} - -func TestParserNameEqualToStringName(t *testing.T) { - newParserName := func(t *testing.T, buf []byte, offset int) ParserName { - msg := Parser{msg: buf} - m, _, err := msg.unpackName(offset) - if err != nil { - t.Fatal(err) - } - return m - } - - newSearchName := func(t *testing.T, n, n2 Name) SearchName { - s, err := NewSearchName(n, n2) - if err != nil { - t.Fatal(err) - } - return s - } - - tests := []struct { - parserName ParserName - name Name - searchNames []SearchName - equal bool - }{ - { - parserName: newParserName(t, []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("example.com"), - searchNames: []SearchName{newSearchName(t, MustNewName("example"), MustNewName("com"))}, - equal: true, - }, - { - parserName: newParserName(t, []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("example.com."), - searchNames: []SearchName{newSearchName(t, MustNewName("example"), MustNewName("com."))}, - equal: true, - }, - { - parserName: newParserName(t, []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("www.example.com"), - searchNames: []SearchName{ - newSearchName(t, MustNewName("www"), MustNewName("example.com")), - newSearchName(t, MustNewName("www.example"), MustNewName("com")), - }, - equal: false, - }, - { - parserName: newParserName(t, []byte{3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("example.com"), - searchNames: []SearchName{newSearchName(t, MustNewName("example"), MustNewName("com"))}, - equal: false, - }, - { - parserName: newParserName(t, []byte{7, 'E', 'X', 'A', 'M', 'p', 'l', 'E', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("eXAmPle.com"), - searchNames: []SearchName{newSearchName(t, MustNewName("eXAmPle"), MustNewName("com"))}, - equal: true, - }, - { - parserName: newParserName(t, []byte{7, 'E', 'X', 'A', 'M', 'p', 'l', 'E', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("eXAmPle.com."), - searchNames: []SearchName{ - newSearchName(t, Name{}, MustNewName("eXAmPle.com")), - newSearchName(t, MustNewName("eXAmPle"), MustNewName("com")), - }, - equal: true, - }, - { - parserName: newParserName(t, []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("\\exam\\ple.c\\om"), - searchNames: []SearchName{newSearchName(t, MustNewName("\\exam\\ple"), MustNewName("c\\om"))}, - equal: true, - }, - { - parserName: newParserName(t, []byte{3, 33, 99, 'z', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("\\033\\099\\" + strconv.Itoa('z') + ".example.com"), - searchNames: []SearchName{newSearchName(t, MustNewName("\\033\\099\\"+strconv.Itoa('z')), MustNewName("example.com"))}, - equal: true, - }, - { - parserName: newParserName(t, []byte{3, 0x33, 0x99, 'z', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("\x33\x99\\z.example.com"), - searchNames: []SearchName{newSearchName(t, MustNewName("\x33\x99\\"+strconv.Itoa('z')), MustNewName("example.com"))}, - equal: true, - }, - { - parserName: newParserName(t, []byte{3, 'w', 'w', '.', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("ww\\..example.com"), - searchNames: []SearchName{newSearchName(t, MustNewName("ww\\."), MustNewName("example.com"))}, - equal: true, - }, - { - parserName: newParserName(t, []byte{3, 'w', 'w', '.', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, 0), - name: MustNewName("ww\\.example.com"), - equal: false, - }, - { - parserName: newParserName(t, []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 2, 3, 'w', 'w', 'w', 0xC0, 0}, 14), - name: MustNewName("www.example.com"), - searchNames: []SearchName{ - newSearchName(t, MustNewName("www"), MustNewName("example.com")), - newSearchName(t, MustNewName("www.example"), MustNewName("com")), - }, - equal: true, - }, - { - parserName: newParserName(t, []byte{0xC0, 3, 9, 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 2, 3, 'w', 'w', 'w', 0xC0, 0}, 17), - name: MustNewName("www.example.com"), - searchNames: []SearchName{ - newSearchName(t, MustNewName("www"), MustNewName("example.com")), - newSearchName(t, MustNewName("www.example"), MustNewName("com")), - }, - equal: true, - }, - } - - for _, v := range tests { - eq := v.parserName.EqualName(v.name) - if eq != v.equal { - t.Errorf("ParserName(%#v) == Name(%#v) = %v", v.parserName.String(), v.name.String(), eq) - } - - for _, vv := range append(v.searchNames, newSearchName(t, Name{}, v.name)) { - eq := v.parserName.EqualSearchName(vv) - if eq != v.equal { - t.Errorf("ParserName(%#v) == %#v = %v", v.parserName.String(), vv, eq) - } - } - } -} - func TestParse(t *testing.T) { expect := Header{ ID: 43127, @@ -596,8 +75,9 @@ func TestParseQuestion(t *testing.T) { t.Fatalf("p.Question() unexpected error: %v", err) } - if !q1.Name.EqualName(MustNewName("example.com")) { - t.Errorf(`q1.Name = %v, q1.Name.EqualName(MustNewName("example.com")) = false, want: true`, q1.Name.String()) + n := MustParseName("example.com") + if !q1.Name.Equal(&n) { + t.Errorf(`q1.Name = %v, q1.Name.Equal("example.com") = false, want: true`, q1.Name.String()) } if q1.Type != TypeA { @@ -613,8 +93,9 @@ func TestParseQuestion(t *testing.T) { t.Fatalf("p.Question() unexpected error: %v", err) } - if !q2.Name.EqualName(MustNewName("www.example.com")) { - t.Errorf(`q2.Name = %v, q2.Name.EqualName(MustNewName("www.example.com")) = false, want: true`, q2.Name.String()) + n = MustParseName("www.example.com") + if !q2.Name.Equal(&n) { + t.Errorf(`q2.Name = %v, q2.Name.Equal("www.example.com") = false, want: true`, q2.Name.String()) } if q2.Type != 45938 { @@ -708,8 +189,9 @@ func TestParseResourceHeader(t *testing.T) { t.Fatalf("%v section, p.ResourceHeader(): unexpected error: %v", curSectionName, err) } - if !rhdr.Name.EqualName(MustNewName(expectNames[i])) { - t.Errorf(`%v section, rhdr.Name = %v, rhdr.Name.EqualName(MustNewName("%v")) = false, want: true`, curSectionName, rhdr.Name.String(), expectNames[i]) + n := MustParseName(expectNames[i]) + if !rhdr.Name.Equal(&n) { + t.Errorf(`%v section, rhdr.Name = %v, rhdr.Name.Equal("%v") = false, want: true`, curSectionName, rhdr.Name.String(), expectNames[i]) } if rhdr.Type != TypeA { @@ -751,8 +233,9 @@ func TestParseResourceHeader(t *testing.T) { t.Fatalf("p.ResourceHeader(): unexpected error: %v", err) } - if !rhdr2.Name.EqualName(MustNewName("smtp.example.com.")) { - t.Errorf(`rhdr2.Name = %v, rhdr2.Name.EqualName(MustNewName("smtp.example.com.")) = false, want: true`, rhdr2.Name.String()) + n := MustParseName("smtp.example.com") + if !rhdr2.Name.Equal(&n) { + t.Errorf(`rhdr2.Name = %v, rhdr2.Name.Equal("smtp.example.com.") = false, want: true`, rhdr2.Name.String()) } if rhdr2.Type != 45182 { @@ -780,8 +263,9 @@ func TestParseResourceHeader(t *testing.T) { t.Fatalf("p.ResourceHeader() unexpected error: %v", err) } - if !rhdr3.Name.EqualName(MustNewName("smtp.example.com.")) { - t.Errorf(`rhdr3.Name = %v, rhdr3.Name.EqualName(MustNewName("smtp.example.com.")) = false, want: true`, rhdr3.Name.String()) + n = MustParseName("smtp.example.com.") + if !rhdr3.Name.Equal(&n) { + t.Errorf(`rhdr3.Name = %v, rhdr3.Name.Equal("smtp.example.com.") = false, want: true`, rhdr3.Name.String()) } if rhdr3.Type != 45182 { @@ -922,8 +406,10 @@ func TestParserRDParser(t *testing.T) { if err != nil { t.Fatalf("rdp.Name() unexpected error: %v", err) } - if !name.EqualName(MustNewName("example.com")) { - t.Errorf(`rdp.Name() = %v, rdp.Name().EqualName(MustNewName("example.com")) = false, want: true`, name.String()) + + n := MustParseName("example.com") + if !name.Equal(&n) { + t.Errorf(`rdp.Name() = %v, rdp.Name().Equal("example.com") = false, want: true`, name.String()) } u8, err := rdp.Uint8() @@ -951,8 +437,10 @@ func TestParserRDParser(t *testing.T) { if err != nil { t.Fatalf("rdp.Name() unexpected error: %v", err) } - if !name.EqualName(MustNewName("www.example.com")) { - t.Errorf(`rdp.Name() = %v, rdp.Name().EqualName(MustNewName("www.example.com")) = false, want: true`, name.String()) + + n = MustParseName("www.example.com") + if !name.Equal(&n) { + t.Errorf(`rdp.Name() = %v, rdp.Name().Equal("www.example.com") = false, want: true`, name.String()) } u16, err := rdp.Uint16() @@ -1014,36 +502,36 @@ func TestParserRDParser(t *testing.T) { func TestParserInvalidOperation(t *testing.T) { b := StartBuilder(make([]byte, 0, 512), 0, 0) - b.Question(Question[RawName]{ - Name: MustNewRawName("example.com"), + b.Question(Question{ + Name: MustParseName("example.com"), Type: TypeA, Class: ClassIN, }) for _, nextSection := range []func(){b.StartAnswers, b.StartAuthorities, b.StartAdditionals} { nextSection() - hdr := ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + hdr := ResourceHeader{ + Name: MustParseName("example.com"), Class: ClassIN, TTL: 60, } b.ResourceA(hdr, ResourceA{A: [4]byte{192, 0, 2, 1}}) b.ResourceAAAA(hdr, ResourceAAAA{AAAA: netip.MustParseAddr("2001:db8::1").As16()}) - b.ResourceNS(hdr, ResourceNS[RawName]{NS: MustNewRawName("ns1.example.com")}) - b.ResourceSOA(hdr, ResourceSOA[RawName]{ - NS: MustNewRawName("ns1.example.com"), - Mbox: MustNewRawName("admin.example.com"), + b.ResourceNS(hdr, ResourceNS{NS: MustParseName("ns1.example.com")}) + b.ResourceSOA(hdr, ResourceSOA{ + NS: MustParseName("ns1.example.com"), + Mbox: MustParseName("admin.example.com"), Serial: 2022010199, Refresh: 3948793, Retry: 34383744, Expire: 1223999999, Minimum: 123456789, }) - b.ResourcePTR(hdr, ResourcePTR[RawName]{PTR: MustNewRawName("ns1.example.com")}) + b.ResourcePTR(hdr, ResourcePTR{PTR: MustParseName("ns1.example.com")}) b.ResourceTXT(hdr, ResourceTXT{TXT: [][]byte{[]byte("test"), []byte("test2")}}) b.RawResourceTXT(hdr, RawResourceTXT{[]byte{1, 'a', 2, 'b', 'a'}}) - b.ResourceCNAME(hdr, ResourceCNAME[RawName]{CNAME: MustNewRawName("www.example.com")}) - b.ResourceMX(hdr, ResourceMX[RawName]{Pref: 100, MX: MustNewRawName("smtp.example.com")}) + b.ResourceCNAME(hdr, ResourceCNAME{CNAME: MustParseName("www.example.com")}) + b.ResourceMX(hdr, ResourceMX{Pref: 100, MX: MustParseName("smtp.example.com")}) b.ResourceOPT(hdr, ResourceOPT{Options: []EDNS0Option{ &EDNS0ClientSubnet{Family: AddressFamilyIPv4, SourcePrefixLength: 2, ScopePrefixLength: 3, Address: []byte{192, 0, 2, 1}}, &EDNS0Cookie{ @@ -1235,14 +723,14 @@ func TestParserInvalidOperation(t *testing.T) { func FuzzParser(f *testing.F) { b := StartBuilder(nil, 0, 0) - b.Question(Question[RawName]{ - Name: MustNewRawName("example.com"), + b.Question(Question{ + Name: MustParseName("example.com"), Type: TypeA, Class: ClassIN, }) b.StartAnswers() - b.ResourceA(ResourceHeader[RawName]{ - Name: MustNewRawName("example.com"), + b.ResourceA(ResourceHeader{ + Name: MustParseName("example.com"), Class: ClassIN, TTL: 60, }, ResourceA{A: [4]byte{192, 0, 2, 1}}) diff --git a/types.go b/types.go index b152ee6..2d89358 100644 --- a/types.go +++ b/types.go @@ -185,14 +185,14 @@ func (h *Header) pack(msg *[headerLen]byte) { packUint16(msg[10:12], h.ARCount) } -type Question[T RawName | ParserName | Name | SearchName] struct { - Name T +type Question struct { + Name Name Type Type Class Class } -type ResourceHeader[T RawName | ParserName] struct { - Name T +type ResourceHeader struct { + Name Name Type Type Class Class TTL uint32 @@ -203,17 +203,17 @@ type ResourceA struct { A [4]byte } -type ResourceNS[T RawName | ParserName] struct { - NS T +type ResourceNS struct { + NS Name } -type ResourceCNAME[T RawName | ParserName] struct { - CNAME T +type ResourceCNAME struct { + CNAME Name } -type ResourceSOA[T RawName | ParserName] struct { - NS T - Mbox T +type ResourceSOA struct { + NS Name + Mbox Name Serial uint32 Refresh uint32 Retry uint32 @@ -221,12 +221,12 @@ type ResourceSOA[T RawName | ParserName] struct { Minimum uint32 } -type ResourcePTR[T RawName | ParserName] struct { - PTR T +type ResourcePTR struct { + PTR Name } -type ResourceMX[T RawName | ParserName] struct { - MX T +type ResourceMX struct { + MX Name Pref uint16 }