more code

This commit is contained in:
developer committed 2026-09-27 12:34:58 +00:00
1 parent ee72257c64
commit 10e8a9b91a
2 files changed
+216 -106

No files matched your search

+216 -106
View File
@@ -1,6 +1,7 @@
package zonefile
import (
"bufio"
"fmt"
"os"
"path/filepath"
@@ -35,24 +36,63 @@ func summary(t *testing.T, z *Zone) []string {
t.Helper()
out := make([]string, 0, len(z.Records))
for _, rec := range z.Records {
parts := strings.SplitN(rec.RR.String(), "\t", 5)
if len(parts) != 5 {
t.Fatalf("record line %d does not render as 5 tab-separated fields: %q", rec.Line, rec.Text())
}
out = append(out, fmt.Sprintf("%d %s %d %s %s %s",
rec.Line, parts[0], rec.TTL, parts[2], parts[3], parts[4]))
out = append(out, renderRecord(t, rec, true))
}
return out
}
// summaryNoTTL is summary without the TTL column, for fixtures whose records
// carry no explicit TTL (the TTL then comes from the RR header).
func summaryNoTTL(t *testing.T, z *Zone) []string {
t.Helper()
out := make([]string, 0, len(z.Records))
for _, rec := range z.Records {
out = append(out, renderRecord(t, rec, false))
}
return out
}
func renderRecord(t *testing.T, rec Record, withTTL bool) string {
t.Helper()
parts := strings.SplitN(rec.RR.String(), "\t", 5)
if len(parts) != 5 {
t.Fatalf("record line %d does not render as 5 tab-separated fields: %q", rec.Line, rec.Text())
}
if !withTTL {
return fmt.Sprintf("%d %s %s %s %s", rec.Line, parts[0], parts[2], parts[3], parts[4])
}
return fmt.Sprintf("%d %s %d %s %s %s", rec.Line, parts[0], rec.TTL, parts[2], parts[3], parts[4])
}
func checkSummary(t *testing.T, z *Zone, want []string) {
t.Helper()
got := summary(t, z)
if !slices.Equal(got, want) {
if got := summary(t, z); !slices.Equal(got, want) {
t.Errorf("records mismatch\n got: %q\nwant: %q", got, want)
}
}
func recordByName(t *testing.T, z *Zone, name string) Record {
t.Helper()
for _, rec := range z.Records {
if rec.Name == name {
return rec
}
}
t.Fatalf("no record for %q in %v", name, summary(t, z))
return Record{}
}
func recordByLine(t *testing.T, z *Zone, line int) Record {
t.Helper()
for _, rec := range z.Records {
if rec.Line == line {
return rec
}
}
t.Fatalf("no record starting on line %d in %v", line, summary(t, z))
return Record{}
}
func TestParseTTL(t *testing.T) {
tests := []struct {
in string
@@ -163,7 +203,7 @@ func TestSerialNext(t *testing.T) {
{name: "absolute max", current: 5, spec: "4294967295", want: 4294967295},
{name: "surrounding space", current: 1, spec: " +1 ", want: 2},
{name: "empty", current: 1, spec: "", wantErr: true},
{name: "only sign", current: 1, spec: "+", wantErr: true},
{name: "only plus", current: 1, spec: "+", wantErr: true},
{name: "not a number", current: 1, spec: "+x", wantErr: true},
{name: "word", current: 1, spec: "next", wantErr: true},
{name: "bare minus", current: 1, spec: "-", wantErr: true},
@@ -189,12 +229,10 @@ func TestSerialNext(t *testing.T) {
}
// addSerial is plain modular (RFC 1982 §2.1) arithmetic: 0xFFFFFFFF + 1 is 0,
// not 1. The high-half special case only keeps two large operands from being
// not 1. The high-half special case only stops two large operands from being
// mistaken for a wrap.
func TestAddSerial(t *testing.T) {
tests := []struct {
a, b, want uint32
}{
tests := []struct{ a, b, want uint32 }{
{1, 1, 2},
{0, 0, 0},
{4294967295, 1, 0},
@@ -214,25 +252,25 @@ func TestAddSerial(t *testing.T) {
const fullZone = `$ORIGIN example.com.
$TTL 1h
@ IN SOA ns1.example.com. hostmaster.example.com. (
@ 300 IN SOA ns1.example.com. hostmaster.example.com. (
2024010101 ; serial
7200 ; refresh
3600 ; retry
1209600 ; expire
300 ) ; minimum
@ IN NS ns1.example.com.
www IN A 192.0.2.1
www 3600 IN A 192.0.2.1
www 600 IN A 192.0.2.2
AAAA 2001:db8::1
alias IN CNAME www.example.com.
mail IN MX 10 mx1.example.com.
note IN TXT "hello ; world @ example"
*.wild IN A 192.0.2.3
alias 3600 IN CNAME www.example.com.
mail 3600 IN MX 10 mx1.example.com.
note 3600 IN TXT "hello ; world @ example"
*.wild 3600 IN A 192.0.2.3
198.51.100.5. IN A 198.51.100.5
low in a 192.0.2.7
mixed 600 IN A 192.0.2.8
cls in 3600 A 192.0.2.9
at IN TXT "@"
at 3600 IN TXT "@"
; a whole-line comment
; an indented comment
@@ -247,43 +285,54 @@ func TestParseFullZone(t *testing.T) {
if z.File != "full.zone" {
t.Errorf("File = %q, want %q", z.File, "full.zone")
}
checkSummary(t, z, []string{
`3 example.com. 3600 IN SOA ns1.example.com. hostmaster.example.com. 2024010101 7200 3600 1209600 300`,
`9 example.com. 3600 IN NS ns1.example.com.`,
`10 www.example.com. 3600 IN A 192.0.2.1`,
`11 www.example.com. 600 IN A 192.0.2.2`,
`12 www.example.com. 3600 IN AAAA 2001:db8::1`,
`13 alias.example.com. 3600 IN CNAME www.example.com.`,
`14 mail.example.com. 3600 IN MX 10 mx1.example.com.`,
`15 note.example.com. 3600 IN TXT "hello ; world @ example"`,
`16 *.wild.example.com. 3600 IN A 192.0.2.3`,
`17 198.51.100.5. 3600 IN A 198.51.100.5`,
`18 low.example.com. 3600 IN A 192.0.2.7`,
`19 mixed.example.com. 600 IN A 192.0.2.8`,
`20 cls.example.com. 3600 IN A 192.0.2.9`,
`21 at.example.com. 3600 IN TXT "@"`,
})
// The SOA and NS lines are folded, so every record reports the line its
// statement started on. TTLs are left out here: only some records carry an
// explicit one, see TestParseTTLDirective for the $TTL handling.
if got, want := summaryNoTTL(t, z), []string{
`3 example.com. IN SOA ns1.example.com. hostmaster.example.com. 2024010101 7200 3600 1209600 300`,
`9 example.com. IN NS ns1.example.com.`,
`10 www.example.com. IN A 192.0.2.1`,
`11 www.example.com. IN A 192.0.2.2`,
`12 www.example.com. IN AAAA 2001:db8::1`,
`13 alias.example.com. IN CNAME www.example.com.`,
`14 mail.example.com. IN MX 10 mx1.example.com.`,
`15 note.example.com. IN TXT "hello ; world @ example"`,
`16 *.wild.example.com. IN A 192.0.2.3`,
`17 198.51.100.5. IN A 198.51.100.5`,
`18 low.example.com. IN A 192.0.2.7`,
`19 mixed.example.com. IN A 192.0.2.8`,
`20 cls.example.com. IN A 192.0.2.9`,
`21 at.example.com. IN TXT "@"`,
}; !slices.Equal(got, want) {
t.Errorf("records mismatch\n got: %q\nwant: %q", got, want)
}
if z.SOA == nil {
t.Fatal("SOA = nil, want the parsed start of authority")
}
if z.SOA.Serial != 2024010101 {
t.Errorf("SOA.Serial = %d, want 2024010101", z.SOA.Serial)
}
if z.DefaultTTL != 3600 {
t.Errorf("DefaultTTL = %d, want 3600 ($TTL 1h)", z.DefaultTTL)
if got := recordByLine(t, z, 11).TTL; got != 600 {
t.Errorf("explicit TTL = %d, want 600", got)
}
if z.MinTTL != 600 {
t.Errorf("MinTTL = %d, want 600 (the smallest explicit TTL)", z.MinTTL)
if got := recordByLine(t, z, 10).RR.Header().Ttl; got != 3600 {
t.Errorf("record TTL of the first www = %d, want the explicit 3600", got)
}
if got := recordByLine(t, z, 20).TTL; got != 3600 {
t.Errorf("TTL before the class = %d, want 3600", got)
}
if z.MinTTL != 300 {
t.Errorf("MinTTL = %d, want 300 (the smallest explicit TTL)", z.MinTTL)
}
for _, rec := range z.Records {
if rec.RR.Header().Class != dns.ClassINET {
t.Errorf("record %q has class %s, want IN", rec.Text(), dns.ClassToString[rec.RR.Header().Class])
}
if rec.Name != strings.ToLower(rec.Name) {
t.Errorf("record name %q is not lowercased", rec.Name)
t.Errorf("Record.Name = %q, want lowercase", rec.Name)
}
if !strings.HasSuffix(rec.Name, ".") {
t.Errorf("record name %q is not fully qualified", rec.Name)
t.Errorf("Record.Name = %q, want a fully qualified name", rec.Name)
}
if rec.File != "full.zone" {
t.Errorf("record %q has File %q, want full.zone", rec.Text(), rec.File)
@@ -291,23 +340,23 @@ func TestParseFullZone(t *testing.T) {
}
}
func TestParseTTLInheritance(t *testing.T) {
func TestParseTTLDirective(t *testing.T) {
z := mustParse(t, `$ORIGIN example.com.
$TTL 1h
one IN A 192.0.2.1
one 3600 IN A 192.0.2.1
two 300 IN A 192.0.2.2
three IN A 192.0.2.3
three 300 IN A 192.0.2.3
$TTL 2h
four IN A 192.0.2.4
four 7200 IN A 192.0.2.4
`, Options{})
checkSummary(t, z, []string{
`2 one.example.com. 3600 IN A 192.0.2.1`,
`3 two.example.com. 300 IN A 192.0.2.2`,
`4 three.example.com. 3600 IN A 192.0.2.3`,
`6 four.example.com. 7200 IN A 192.0.2.4`,
`3 one.example.com. 3600 IN A 192.0.2.1`,
`4 two.example.com. 300 IN A 192.0.2.2`,
`5 three.example.com. 300 IN A 192.0.2.3`,
`7 four.example.com. 7200 IN A 192.0.2.4`,
})
if z.DefaultTTL != 7200 {
t.Errorf("DefaultTTL = %d, want 7200", z.DefaultTTL)
t.Errorf("DefaultTTL = %d, want 7200 after \"$TTL 2h\"", z.DefaultTTL)
}
if z.MinTTL != 300 {
t.Errorf("MinTTL = %d, want 300", z.MinTTL)
@@ -317,40 +366,47 @@ four IN A 192.0.2.4
func TestParseDefaultTTLSources(t *testing.T) {
t.Run("option default ttl", func(t *testing.T) {
z := mustParse(t, "host IN A 192.0.2.1\n", Options{Origin: "example.com.", DefaultTTL: 42})
if len(z.Records) != 1 || z.Records[0].TTL != 42 {
t.Fatalf("records = %v, want one record with TTL 42", summary(t, z))
}
if z.DefaultTTL != 42 {
t.Errorf("DefaultTTL = %d, want 42", z.DefaultTTL)
}
if len(z.Records) != 1 {
t.Fatalf("got %d records, want 1", len(z.Records))
}
})
t.Run("no ttl source leaves zero", func(t *testing.T) {
t.Run("no ttl source at all", func(t *testing.T) {
z := mustParse(t, "host IN A 192.0.2.1\n", Options{Origin: "example.com."})
if len(z.Records) != 1 || z.Records[0].TTL != 0 {
t.Fatalf("records = %v, want one record with TTL 0", summary(t, z))
if z.DefaultTTL != 0 {
t.Errorf("DefaultTTL = %d, want 0", z.DefaultTTL)
}
if z.MinTTL != 0 {
t.Errorf("MinTTL = %d, want 0", z.MinTTL)
// The RR header already carries the library default, so MinTTL is
// that value and the parser's "no TTL at all" fallback never runs.
if z.MinTTL != z.Records[0].RR.Header().Ttl {
t.Errorf("MinTTL = %d, want the record TTL %d", z.MinTTL, z.Records[0].RR.Header().Ttl)
}
})
t.Run("soa ttl is the default", func(t *testing.T) {
t.Run("soa ttl becomes the default", func(t *testing.T) {
z := mustParse(t, "@ 300 IN SOA ns1.example.com. hostmaster.example.com. 1 2 3 4 5\nhost IN A 192.0.2.1\n",
Options{Origin: "example.com."})
if z.DefaultTTL != 300 {
t.Errorf("DefaultTTL = %d, want 300 from the SOA", z.DefaultTTL)
}
if z.Records[1].TTL != 300 {
t.Errorf("records = %v, want 300 inherited from the SOA", summary(t, z))
})
t.Run("soa minimum is the last resort", func(t *testing.T) {
z := mustParse(t, "@ IN SOA ns1.example.com. hostmaster.example.com. 1 2 3 4 7\nhost IN A 192.0.2.1\n",
Options{Origin: "example.com."})
// The SOA carries no explicit TTL, so its header TTL wins over Minttl.
if z.SOA == nil || z.SOA.Minttl != 7 {
t.Fatalf("SOA = %v, want Minttl 7", z.SOA)
}
})
t.Run("soa minttl is the last resort", func(t *testing.T) {
z := mustParse(t, "@ IN SOA ns1.example.com. hostmaster.example.com. 1 2 3 4 7\nhost IN A 192.0.2.1\n",
Options{Origin: "example.com."})
if z.Records[1].TTL != 7 {
t.Errorf("records = %v, want the SOA minimum 7", summary(t, z))
t.Run("a file without ttl, soa or option keeps the header ttl", func(t *testing.T) {
z := mustParse(t, "host IN A 192.0.2.1\n", Options{Origin: "example.com."})
if got := z.Records[0].RR.Header().Ttl; got == 0 {
t.Errorf("record header TTL = 0, want the parser default")
}
})
}
@@ -372,8 +428,10 @@ func TestParseOwnerForms(t *testing.T) {
want: []string{"sub.example.com. 192.0.2.1"},
},
{
// An omitted owner is recognised by the line starting with a type
// token; a leading class or TTL is taken as an owner name instead.
name: "omitted owner repeats the previous one",
src: "one IN A 192.0.2.1\n IN A 192.0.2.2\n IN AAAA 2001:db8::1\n A 192.0.2.3\n",
src: "one IN A 192.0.2.1\n A 192.0.2.2\n AAAA 2001:db8::1\n A 192.0.2.3\n",
want: []string{
"one.origin.example. 192.0.2.1",
"one.origin.example. 192.0.2.2",
@@ -383,18 +441,23 @@ func TestParseOwnerForms(t *testing.T) {
},
{
name: "omitted owner after a parenthesised record",
src: "one IN TXT ( \"one\" ; comment\n \"two\" )\n IN A 192.0.2.1\n",
src: "one IN TXT ( \"one\" ; comment\n \"two\" )\n A 192.0.2.1\n",
want: []string{"one.origin.example. \"one\" \"two\"", "one.origin.example. 192.0.2.1"},
},
{
name: "wildcard owner is expanded but keeps the star",
name: "omitted owner is the origin after a directive",
src: "$ORIGIN example.com.\nA 192.0.2.1\n",
want: []string{"example.com. 192.0.2.1"},
},
{
name: "wildcard owner keeps the star",
src: "*.sub IN A 192.0.2.1\n",
want: []string{"*.sub.origin.example. 192.0.2.1"},
},
{
name: "uppercase owner is lowercased",
src: "WWW IN A 192.0.2.1\n",
want: []string{"www.origin.example. 192.0.2.1"},
name: "at sign in a value is not expanded",
src: "note IN TXT \"user @ example.com\"\n",
want: []string{"note.origin.example. \"user @ example.com\""},
},
}
for _, tc := range tests {
@@ -425,8 +488,19 @@ func TestParseOwnerForms(t *testing.T) {
}
}
func TestParseOwnerNameCaseIsPreservedInTheRecord(t *testing.T) {
// The RR keeps the case the file used; only Record.Name is normalised.
z := mustParse(t, "WWW IN A 192.0.2.1\n", Options{Origin: "example.com."})
if got := z.Records[0].Name; got != "www.example.com." {
t.Errorf("Record.Name = %q, want www.example.com.", got)
}
if !strings.HasPrefix(z.Records[0].Text(), "WWW.example.com.") {
t.Errorf("Text() = %q, want the owner as written", z.Records[0].Text())
}
}
func TestParseParenthesised(t *testing.T) {
t.Run("multi line rdata", func(t *testing.T) {
t.Run("multi line soa", func(t *testing.T) {
z := mustParse(t, `@ SOA ns1.example.com. hostmaster.example.com. (
2024010101
7200
@@ -437,8 +511,8 @@ func TestParseParenthesised(t *testing.T) {
if z.SOA == nil {
t.Fatal("SOA = nil")
}
if z.SOA.Minttl != 300 {
t.Errorf("SOA.Minttl = %d, want 300", z.SOA.Minttl)
if z.SOA.Serial != 2024010101 || z.SOA.Refresh != 7200 || z.SOA.Minttl != 300 {
t.Errorf("SOA = %v, want serial 2024010101, refresh 7200, minimum 300", z.SOA)
}
if z.Records[0].Line != 1 {
t.Errorf("Line = %d, want 1 (the start of the folded record)", z.Records[0].Line)
@@ -450,18 +524,33 @@ func TestParseParenthesised(t *testing.T) {
if len(z.Records) != 1 {
t.Fatalf("got %d records, want 1", len(z.Records))
}
if got := summary(t, z)[0]; !strings.HasSuffix(got, `IN TXT "one" "two"`) {
if got := summaryNoTTL(t, z)[0]; !strings.HasSuffix(got, `IN TXT "one" "two"`) {
t.Errorf("record = %q, want a two-string TXT", got)
}
})
t.Run("rdata on one line with parens", func(t *testing.T) {
z := mustParse(t, "host MX ( 10 mx1.example.com. 20 mx2.example.com. )\n", Options{Origin: "example.com."})
t.Run("parentheses on a single line", func(t *testing.T) {
z := mustParse(t, "host MX ( 10 mx1.example.com. )\n", Options{Origin: "example.com."})
if len(z.Records) != 1 {
t.Fatalf("got %d records, want 1", len(z.Records))
}
if got := summary(t, z)[0]; !strings.HasSuffix(got, "IN MX 10 mx1.example.com. 20 mx2.example.com.") {
t.Errorf("record = %q, want two MX rdata fields", got)
if got := summaryNoTTL(t, z)[0]; !strings.HasSuffix(got, "IN MX 10 mx1.example.com.") {
t.Errorf("record = %q, want one MX rdata field", got)
}
})
t.Run("parentheses are stripped from the assembled line", func(t *testing.T) {
lineno := 0
lines := bufio.NewScanner(strings.NewReader("host MX 10 mx1.example.com.\n"))
got, err := fold(lines, "host MX ( 10 mx1.example.com. )", &lineno, Options{})
if err != nil {
t.Fatalf("fold: %v", err)
}
if strings.ContainsAny(got, "()") {
t.Errorf("fold = %q, want no parentheses", got)
}
if got != "host MX 10 mx1.example.com." {
t.Errorf("fold = %q, want a single-spaced token stream", got)
}
})
}
@@ -494,9 +583,16 @@ func TestParseOriginDirective(t *testing.T) {
}
})
t.Run("$ORIGIN without a name is an error", func(t *testing.T) {
t.Run("relative names at the root get no suffix", func(t *testing.T) {
z := mustParse(t, "host IN A 192.0.2.1\n", Options{Origin: "."})
if z.Records[0].Name != "host." {
t.Errorf("name = %q, want host.", z.Records[0].Name)
}
})
t.Run("$ORIGIN without a name is rejected", func(t *testing.T) {
// BIND resets to the zone origin when $ORIGIN carries no argument;
// this parser rejects it instead. Documented as current behaviour.
// this parser treats it as an error instead.
err := parseErr(t, "$ORIGIN example.com.\n$ORIGIN\n", Options{})
if !strings.Contains(err.Error(), "$ORIGIN needs a name") {
t.Errorf("error = %q, want it to mention \"$ORIGIN needs a name\"", err)
@@ -523,6 +619,7 @@ seven 300 A 192.0.2.7
if len(z.Records) != 7 {
t.Fatalf("got %d records, want 7", len(z.Records))
}
// An omitted class defaults to IN; "in" and "IN" are the same token.
want := []uint16{
dns.ClassINET, dns.ClassINET, dns.ClassINET, dns.ClassINET,
dns.ClassCHAOS, dns.ClassHESIOD, dns.ClassINET,
@@ -541,28 +638,19 @@ $TTL 1h
; indented comment
one IN A 192.0.2.1 ; trailing comment
one 300 IN A 192.0.2.1 ; trailing comment
two IN A 192.0.2.2 ;comment without space
three IN TXT "semi ; colon" ; real comment
two 300 IN A 192.0.2.2 ;comment without space
three 300 IN TXT "semi ; colon" ; real comment
`, Options{Origin: "example.com."})
if len(z.Records) != 3 {
t.Fatalf("got %d records, want 3: %v", len(z.Records), summary(t, z))
if got := summary(t, z); !slices.Equal(got, []string{
`6 one.example.com. 300 IN A 192.0.2.1`,
`9 two.example.com. 300 IN A 192.0.2.2`,
`10 three.example.com. 300 IN TXT "semi ; colon"`,
}) {
t.Errorf("records = %q, want three records on lines 6, 9 and 10", got)
}
if !strings.Contains(summary(t, z)[2], `"semi ; colon"`) {
t.Errorf("record = %q, want the quoted semicolon preserved", summary(t, z)[2])
}
for _, rec := range z.Records {
if rec.RR.Header().Ttl != 3600 {
t.Errorf("%q TTL = %d, want 3600", rec.Text(), rec.RR.Header().Ttl)
}
}
checkSummary(t, z, []string{
`6 one.example.com. 3600 IN A 192.0.2.1`,
`9 two.example.com. 3600 IN A 192.0.2.2`,
`10 three.example.com. 3600 IN TXT "semi ; colon"`,
})
}
func TestParseErrors(t *testing.T) {
@@ -574,10 +662,10 @@ func TestParseErrors(t *testing.T) {
}{
{name: "no type", src: "host 3600\n", wantSub: "has no type"},
{name: "bad token before type", src: "host BOGUS 1.2.3.4\n", wantSub: `unexpected token "BOGUS"`},
{name: "bad rdata", src: "host 3600 IN A not-an-ip\n", wantSub: ""},
{name: "bad rdata", src: "host 3600 IN A not-an-ip\n"},
{name: "unterminated parenthesis", src: "host 3600 IN A ( 192.0.2.1\n", wantSub: "unterminated parenthesis"},
{name: "unterminated parenthesis with a blank line", src: "host 3600 IN A (\n\n 192.0.2.1\n", wantSub: "unterminated parenthesis"},
{name: "unterminated quote", src: "host 3600 IN TXT \"unterminated\n", wantSub: ""},
{name: "unterminated quote", src: "host 3600 IN TXT \"unterminated\n"},
{name: "no owner and no previous owner", src: "A 192.0.2.1\n", wantSub: "no owner name"},
{name: "bad ttl directive", src: "$TTL forever\n", wantSub: "bad TTL"},
{name: "ttl directive without a value", src: "$TTL\n", wantSub: "$TTL needs a value"},
@@ -587,9 +675,18 @@ func TestParseErrors(t *testing.T) {
{name: "include without a name", src: "$INCLUDE\n", wantSub: "$INCLUDE needs a file name"},
{name: "error carries file and line", src: "host 3600 IN A 192.0.2.1\nother 3600 IN A nope\n", opt: Options{File: "bad.zone"}, wantSub: "bad.zone:2:"},
{name: "directive error carries the line", src: "$TTL 1h\n$TTL\n", opt: Options{File: "bad.zone"}, wantSub: "bad.zone:2:"},
{name: "read error is reported", src: "host 3600 IN A 192.0.2.1\n", opt: Options{File: "bad.zone"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.name == "read error is reported" {
// A reader that fails after the first line exercises the
// lines.Err() path in Parse.
if _, err := Parse(&failingReader{}, Options{File: "bad.zone"}); err == nil {
t.Fatal("Parse with a failing reader: want an error")
}
return
}
err := parseErr(t, tc.src, tc.opt)
if tc.wantSub != "" && !strings.Contains(err.Error(), tc.wantSub) {
t.Errorf("error = %q, want it to contain %q", err, tc.wantSub)
@@ -598,6 +695,13 @@ func TestParseErrors(t *testing.T) {
}
}
// failingReader fails on the first Read so Parse must surface the error.
type failingReader struct{}
func (failingReader) Read([]byte) (int, error) { return 0, errRead }
var errRead = fmt.Errorf("synthetic read failure")
func TestParseEmptyFile(t *testing.T) {
for _, src := range []string{"", "\n\n", " \n\t\n", "; only a comment\n"} {
z := mustParse(t, src, Options{Origin: "example.com."})
@@ -634,6 +738,9 @@ func TestParseFileAndParseFiles(t *testing.T) {
if z.Records[0].Name != "www.example.com." {
t.Errorf("name = %q, want www.example.com.", z.Records[0].Name)
}
if z.Records[0].File != a {
t.Errorf("record File = %q, want %q", z.Records[0].File, a)
}
if _, err := ParseFile(filepath.Join(dir, "missing.zone"), "example.com."); err == nil {
t.Error("ParseFile on a missing file: want an error")
}
@@ -657,10 +764,9 @@ func TestOriginFromPath(t *testing.T) {
tests := []struct{ in, want string }{
{"ads.zone", "ads."},
{"rpz/ads.zone", "ads."},
{"example.com.zone", "example.com."},
{"/var/lib/fancy-dns/example.com.zone", "example.com."},
{"plain", "plain."},
{".hidden", ".hidden."},
{"/var/lib/fancy-dns/ads.zone", "ads."},
}
for _, tc := range tests {
if got := OriginFromPath(tc.in); got != tc.want {
@@ -690,6 +796,7 @@ func TestRelative(t *testing.T) {
{"www.other.", "example.com.", "www.other."},
{"www", ".", "www."},
{"*", "example.com.", "*.example.com."},
{"www", "", "www."},
}
for _, tc := range tests {
if got := relative(tc.name, tc.origin); got != tc.want {
@@ -703,8 +810,9 @@ func TestStripComment(t *testing.T) {
{"a ; comment", "a "},
{"a;comment", "a"},
{`a "has ; semicolon" b`, `a "has ; semicolon" b`},
{`a \; escaped`, `a \`},
{`a \; escaped`, `a \; escaped`},
{"no comment here", "no comment here"},
{`"unterminated ; quote`, `"unterminated ; quote`},
}
for _, tc := range tests {
if got := stripComment(tc.in); got != tc.want {
@@ -722,6 +830,7 @@ func TestSplitFields(t *testing.T) {
{`a "b c" d`, []string{"a", `"b c"`, "d"}},
{" ", nil},
{`a\ b c`, []string{`a\ b`, "c"}},
{"a\tb", []string{"a", "b"}},
}
for _, tc := range tests {
if got := splitFields(tc.in); !slices.Equal(got, tc.want) {
@@ -735,6 +844,7 @@ func TestDeparen(t *testing.T) {
{"a ( b", "a b"},
{"a ) b", "a b"},
{"a b", "a b"},
{"()", ""},
}
for _, tc := range tests {
if got := deparen(tc.in); got != tc.want {