From 274da7d3efc4f842b8daef05c3ebb3036a12e6b4 Mon Sep 17 00:00:00 2001 From: Tom Thorogood Date: Sat, 20 Oct 2018 11:47:56 +1030 Subject: [PATCH] Add new ZoneParser API (#794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve ParseZone tests * Add new ZoneParser API * Use the ZoneParser API directly in ReadRR * Merge parseZoneHelper into ParseZone * Make generate string building slightly more efficient * Add SetDefaultTTL method to ZoneParser This makes it possible for external consumers to implement ReadRR. * Make $INCLUDE directive opt-in The $INCLUDE directive opens a user controlled file and parses it as a DNS zone file. The error messages may reveal portions of sensitive files, such as: /etc/passwd: dns: not a TTL: "root:x:0:0:root:/root:/bin/bash" at line: 1:31 /etc/shadow: dns: not a TTL: "root:$6$::0:99999:7:::" at line: 1:125 Both ParseZone and ReadRR are currently opt-in for backward compatibility. * Disable $INCLUDE support in ReadRR ReadRR and NewRR are often passed untrusted input. At the same time, $INCLUDE isn't really useful for ReadRR as it only ever returns the first record. This is a breaking change, but it currently represents a slight security risk. * Document the need to drain the ParseZone chan * Cleanup the documentation of NewRR, ReadRR and ParseZone * Document the ZoneParser API * Deprecated the ParseZone function * Add whitespace to ZoneParser.Next * Remove prevName field from ZoneParser This doesn't track anything meaningful as both zp.prevName and h.Name are only ever set at the same point and to the same value. * Use uint8 for ZoneParser.include field It has a maximum value of 7 which easily fits within uint8. This reduces the size of ZoneParser from 160 bytes to 152 bytes. * Add setParseError helper to ZoneParser * Surface $INCLUDE os.Open error in error message * Rename ZoneParser.include field to includeDepth * Make maximum $INCLUDE depth a const * Add ParseZone and ZoneParser benchmarks * Parse $GENERATE directive with a single ZoneParser This should be more efficient than calling NewRR for each generated record. * Run go fmt on generate_test.go * Add a benchmark for $GENERATE directives * Use a custom reader for generate This avoids the overhead and memory usage of building the zone string. name old time/op new time/op delta Generate-12 165µs ± 4% 157µs ± 2% -5.06% (p=0.000 n=25+25) name old alloc/op new alloc/op delta Generate-12 42.1kB ± 0% 31.8kB ± 0% -24.42% (p=0.000 n=20+23) name old allocs/op new allocs/op delta Generate-12 1.56k ± 0% 1.55k ± 0% -0.38% (p=0.000 n=25+25) * Return correct ParseError from generateReader The last commit made these regular errors while they had been ParseErrors before. * Return error message as string from modToPrintf This is slightly simpler and they don't need to be errors. * Skip setting includeDepth in generate This sub parser isn't allowed to use $INCLUDE directives anyway. Note: If generate is ever changed to allow $INCLUDE directives, then this line must be added back. Without doing that, it would be be possible to exceed maxIncludeDepth. * Make generateReader errors sticky ReadByte should not be called after an error has been returned, but this is cheap insurance. * Move file and lex fields to end of generateReader These are only used for creating a ParseError and so are unlikely to be accessed. * Don't return offset with error in modToPrintf Along for the ride, are some whitespace and style changes. * Add whitespace to generate and simplify step * Use a for loop instead of goto in generate * Support $INCLUDE directives inside $GENERATE directives This was previously supported and may be useful. This is now more rigorous as the maximum include depth is respected and relative $INCLUDE directives are now supported from within $GENERATE. * Don't return any lexer tokens after read error Without this, read errors are likely to be lost and become parse errors of the remaining str. The $GENERATE code relies on surfacing errors from the reader. * Support $INCLUDE in NewRR and ReadRR Removing $INCLUDE support from these is a breaking change and should not be included in this pull request. * Add test to ensure $GENERATE respects $INCLUDE support * Unify TestZoneParserIncludeDisallowed with other tests * Remove stray whitespace from TestGenerateSurfacesErrors * Move ZoneParser SetX methods above Err method * $GENERATE should not accept step of 0 If step is allowed to be 0, then generateReader (and the code it replaced) will get stuck in an infinite loop. This is a potential DOS vulnerability. * Fix ReadRR comment for file argument I missed this previosuly. The file argument is also used to resolve relative $INCLUDE directives. * Prevent test panics on nil error * Rework ZoneParser.subNext This is slightly cleaner and will close the underlying *os.File even if an error occurs. * Make ZoneParser.generate call subNext This also moves the calls to setParseError into generate. * Report errors when parsing rest of $GENERATE directive * Report proper error location in $GENERATE directive This makes error messages much clearer. * Simplify modToPrintf func Note: When width is 0, the leading 0 of the fmt string is now excluded. This should not alter the formatting of numbers in anyway. * Add comment explaining sub field * Remove outdated error comment from generate --- dnssec_keyscan.go | 5 + generate.go | 305 ++++++++++++++++---------- generate_test.go | 176 ++++++++++++--- scan.go | 529 +++++++++++++++++++++++++++++++--------------- scan_test.go | 92 +++++++- 5 files changed, 796 insertions(+), 311 deletions(-) diff --git a/dnssec_keyscan.go b/dnssec_keyscan.go index 24afab9f..5e654223 100644 --- a/dnssec_keyscan.go +++ b/dnssec_keyscan.go @@ -336,6 +336,11 @@ func (kl *klexer) Next() (lex, bool) { } } + if kl.readErr != nil && kl.readErr != io.EOF { + // Don't return any tokens after a read error occurs. + return lex{value: zEOF}, false + } + if str.Len() > 0 { // Send remainder l.value = zValue diff --git a/generate.go b/generate.go index 74e67020..97bc39f5 100644 --- a/generate.go +++ b/generate.go @@ -2,8 +2,8 @@ package dns import ( "bytes" - "errors" "fmt" + "io" "strconv" "strings" ) @@ -18,154 +18,225 @@ import ( // * rhs (rdata) // But we are lazy here, only the range is parsed *all* occurrences // of $ after that are interpreted. -// Any error are returned as a string value, the empty string signals -// "no error". -func generate(l lex, c *zlexer, t chan *Token, o string) string { +func (zp *ZoneParser) generate(l lex) (RR, bool) { + token := l.token step := 1 - if i := strings.IndexAny(l.token, "/"); i != -1 { - if i+1 == len(l.token) { - return "bad step in $GENERATE range" + if i := strings.IndexByte(token, '/'); i >= 0 { + if i+1 == len(token) { + return zp.setParseError("bad step in $GENERATE range", l) } - if s, err := strconv.Atoi(l.token[i+1:]); err == nil { - if s < 0 { - return "bad step in $GENERATE range" - } - step = s - } else { - return "bad step in $GENERATE range" + + s, err := strconv.Atoi(token[i+1:]) + if err != nil || s <= 0 { + return zp.setParseError("bad step in $GENERATE range", l) } - l.token = l.token[:i] + + step = s + token = token[:i] } - sx := strings.SplitN(l.token, "-", 2) + + sx := strings.SplitN(token, "-", 2) if len(sx) != 2 { - return "bad start-stop in $GENERATE range" + return zp.setParseError("bad start-stop in $GENERATE range", l) } + start, err := strconv.Atoi(sx[0]) if err != nil { - return "bad start in $GENERATE range" + return zp.setParseError("bad start in $GENERATE range", l) } + end, err := strconv.Atoi(sx[1]) if err != nil { - return "bad stop in $GENERATE range" + return zp.setParseError("bad stop in $GENERATE range", l) } if end < 0 || start < 0 || end < start { - return "bad range in $GENERATE range" + return zp.setParseError("bad range in $GENERATE range", l) } - c.Next() // _BLANK + zp.c.Next() // _BLANK + // Create a complete new string, which we then parse again. - s := "" -BuildRR: - l, _ = c.Next() - if l.value != zNewline && l.value != zEOF { - s += l.token - goto BuildRR - } - for i := start; i <= end; i += step { - var ( - escape bool - dom bytes.Buffer - mod string - err error - offset int - ) + var s string + for l, ok := zp.c.Next(); ok; l, ok = zp.c.Next() { + if l.err { + return zp.setParseError("bad data in $GENERATE directive", l) + } + if l.value == zNewline { + break + } - for j := 0; j < len(s); j++ { // No 'range' because we need to jump around - switch s[j] { - case '\\': - if escape { - dom.WriteByte('\\') - escape = false - continue - } - escape = true - case '$': - mod = "%d" - offset = 0 - if escape { - dom.WriteByte('$') - escape = false - continue - } - escape = false - if j+1 >= len(s) { // End of the string - dom.WriteString(fmt.Sprintf(mod, i+offset)) - continue - } else { - if s[j+1] == '$' { - dom.WriteByte('$') - j++ - continue - } - } - // Search for { and } - if s[j+1] == '{' { // Modifier block - sep := strings.Index(s[j+2:], "}") - if sep == -1 { - return "bad modifier in $GENERATE" - } - mod, offset, err = modToPrintf(s[j+2 : j+2+sep]) - if err != nil { - return err.Error() - } else if start+offset < 0 || end+offset > 1<<31-1 { - return "bad offset in $GENERATE" - } - j += 2 + sep // Jump to it - } - dom.WriteString(fmt.Sprintf(mod, i+offset)) - default: - if escape { // Pretty useless here - escape = false - continue - } - dom.WriteByte(s[j]) - } - } - // Re-parse the RR and send it on the current channel t - rx, err := NewRR("$ORIGIN " + o + "\n" + dom.String()) - if err != nil { - return err.Error() - } - t <- &Token{RR: rx} - // Its more efficient to first built the rrlist and then parse it in - // one go! But is this a problem? + s += l.token + } + + r := &generateReader{ + s: s, + + cur: start, + start: start, + end: end, + step: step, + + file: zp.file, + lex: &l, + } + zp.sub = NewZoneParser(r, zp.origin, zp.file) + zp.sub.includeDepth, zp.sub.includeAllowed = zp.includeDepth, zp.includeAllowed + zp.sub.SetDefaultTTL(defaultTtl) + return zp.subNext() +} + +type generateReader struct { + s string + si int + + cur int + start int + end int + step int + + mod bytes.Buffer + + escape bool + + eof bool + + file string + lex *lex +} + +func (r *generateReader) parseError(msg string, end int) *ParseError { + r.eof = true // Make errors sticky. + + l := *r.lex + l.token = r.s[r.si-1 : end] + l.column += r.si // l.column starts one zBLANK before r.s + + return &ParseError{r.file, msg, l} +} + +func (r *generateReader) Read(p []byte) (int, error) { + // NewZLexer, through NewZoneParser, should use ReadByte and + // not end up here. + + panic("not implemented") +} + +func (r *generateReader) ReadByte() (byte, error) { + if r.eof { + return 0, io.EOF + } + if r.mod.Len() > 0 { + return r.mod.ReadByte() + } + + if r.si >= len(r.s) { + r.si = 0 + r.cur += r.step + + r.eof = r.cur > r.end || r.cur < 0 + return '\n', nil + } + + si := r.si + r.si++ + + switch r.s[si] { + case '\\': + if r.escape { + r.escape = false + return '\\', nil + } + + r.escape = true + return r.ReadByte() + case '$': + if r.escape { + r.escape = false + return '$', nil + } + + mod := "%d" + + if si >= len(r.s)-1 { + // End of the string + fmt.Fprintf(&r.mod, mod, r.cur) + return r.mod.ReadByte() + } + + if r.s[si+1] == '$' { + r.si++ + return '$', nil + } + + var offset int + + // Search for { and } + if r.s[si+1] == '{' { + // Modifier block + sep := strings.Index(r.s[si+2:], "}") + if sep < 0 { + return 0, r.parseError("bad modifier in $GENERATE", len(r.s)) + } + + var errMsg string + mod, offset, errMsg = modToPrintf(r.s[si+2 : si+2+sep]) + if errMsg != "" { + return 0, r.parseError(errMsg, si+3+sep) + } + if r.start+offset < 0 || r.end+offset > 1<<31-1 { + return 0, r.parseError("bad offset in $GENERATE", si+3+sep) + } + + r.si += 2 + sep // Jump to it + } + + fmt.Fprintf(&r.mod, mod, r.cur+offset) + return r.mod.ReadByte() + default: + if r.escape { // Pretty useless here + r.escape = false + return r.ReadByte() + } + + return r.s[si], nil } - return "" } // Convert a $GENERATE modifier 0,0,d to something Printf can deal with. -func modToPrintf(s string) (string, int, error) { - xs := strings.Split(s, ",") - +func modToPrintf(s string) (string, int, string) { // Modifier is { offset [ ,width [ ,base ] ] } - provide default // values for optional width and type, if necessary. - switch len(xs) { + var offStr, widthStr, base string + switch xs := strings.Split(s, ","); len(xs) { case 1: - xs = append(xs, "0", "d") + offStr, widthStr, base = xs[0], "0", "d" case 2: - xs = append(xs, "d") + offStr, widthStr, base = xs[0], xs[1], "d" case 3: + offStr, widthStr, base = xs[0], xs[1], xs[2] default: - return "", 0, errors.New("bad modifier in $GENERATE") + return "", 0, "bad modifier in $GENERATE" } - // xs[0] is offset, xs[1] is width, xs[2] is base - if xs[2] != "o" && xs[2] != "d" && xs[2] != "x" && xs[2] != "X" { - return "", 0, errors.New("bad base in $GENERATE") + switch base { + case "o", "d", "x", "X": + default: + return "", 0, "bad base in $GENERATE" } - offset, err := strconv.Atoi(xs[0]) + + offset, err := strconv.Atoi(offStr) if err != nil { - return "", 0, errors.New("bad offset in $GENERATE") + return "", 0, "bad offset in $GENERATE" } - width, err := strconv.Atoi(xs[1]) - if err != nil || width > 255 { - return "", offset, errors.New("bad width in $GENERATE") + + width, err := strconv.Atoi(widthStr) + if err != nil || width < 0 || width > 255 { + return "", 0, "bad width in $GENERATE" } - switch { - case width < 0: - return "", offset, errors.New("bad width in $GENERATE") - case width == 0: - return "%" + xs[1] + xs[2], offset, nil + + if width == 0 { + return "%" + base, offset, "" } - return "%0" + xs[1] + xs[2], offset, nil + + return "%0" + widthStr + base, offset, "" } diff --git a/generate_test.go b/generate_test.go index e224c1b8..6ba78aa7 100644 --- a/generate_test.go +++ b/generate_test.go @@ -1,29 +1,66 @@ package dns import ( - "testing" + "fmt" + "io/ioutil" + "os" + "path/filepath" "strings" + "testing" ) func TestGenerateRangeGuard(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "dns") + if err != nil { + t.Fatalf("could not create tmpdir for test: %v", err) + } + defer os.RemoveAll(tmpdir) + + for i := 0; i <= 1; i++ { + path := filepath.Join(tmpdir, fmt.Sprintf("%04d.conf", i)) + data := []byte(fmt.Sprintf("dhcp-%04d A 10.0.0.%d", i, i)) + + if err := ioutil.WriteFile(path, data, 0644); err != nil { + t.Fatalf("could not create tmpfile for test: %v", err) + } + } + var tests = [...]struct { zone string fail bool - }{{ - `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) + }{ + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) $GENERATE 0-1 dhcp-${0,4,d} A 10.0.0.$ -`, false},{ - `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +`, false}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-1 dhcp-${0,0,x} A 10.0.0.$ +`, false}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) $GENERATE 128-129 dhcp-${-128,4,d} A 10.0.0.$ -`, false},{ - `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +`, false}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) $GENERATE 128-129 dhcp-${-129,4,d} A 10.0.0.$ -`, true},{ - `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +`, true}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) $GENERATE 0-2 dhcp-${2147483647,4,d} A 10.0.0.$ -`, true},{ - `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +`, true}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) $GENERATE 0-1 dhcp-${2147483646,4,d} A 10.0.0.$ +`, false}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-1/step dhcp-${0,4,d} A 10.0.0.$ +`, true}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-1/ dhcp-${0,4,d} A 10.0.0.$ +`, true}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-10/2 dhcp-${0,4,d} A 10.0.0.$ +`, false}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-1/0 dhcp-${0,4,d} A 10.0.0.$ +`, true}, + {`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-1 $$INCLUDE ` + tmpdir + string(filepath.Separator) + `${0,4,d}.conf `, false}, } Outer: @@ -42,6 +79,80 @@ Outer: } } +func TestGenerateIncludeDepth(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "dns") + if err != nil { + t.Fatalf("could not create tmpfile for test: %v", err) + } + defer os.Remove(tmpfile.Name()) + + zone := `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-1 $$INCLUDE ` + tmpfile.Name() + ` +` + if _, err := tmpfile.WriteString(zone); err != nil { + t.Fatalf("could not write to tmpfile for test: %v", err) + } + if err := tmpfile.Close(); err != nil { + t.Fatalf("could not close tmpfile for test: %v", err) + } + + zp := NewZoneParser(strings.NewReader(zone), ".", tmpfile.Name()) + zp.SetIncludeAllowed(true) + + for _, ok := zp.Next(); ok; _, ok = zp.Next() { + } + + const expected = "too deeply nested $INCLUDE" + if err := zp.Err(); err == nil || !strings.Contains(err.Error(), expected) { + t.Errorf("expected error to include %q, got %v", expected, err) + } +} + +func TestGenerateIncludeDisallowed(t *testing.T) { + const zone = `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-1 $$INCLUDE test.conf +` + zp := NewZoneParser(strings.NewReader(zone), ".", "") + + for _, ok := zp.Next(); ok; _, ok = zp.Next() { + } + + const expected = "$INCLUDE directive not allowed" + if err := zp.Err(); err == nil || !strings.Contains(err.Error(), expected) { + t.Errorf("expected error to include %q, got %v", expected, err) + } +} + +func TestGenerateSurfacesErrors(t *testing.T) { + const zone = `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-1 dhcp-${0,4,dd} A 10.0.0.$ +` + zp := NewZoneParser(strings.NewReader(zone), ".", "test") + + for _, ok := zp.Next(); ok; _, ok = zp.Next() { + } + + const expected = `test: dns: bad base in $GENERATE: "${0,4,dd}" at line: 2:20` + if err := zp.Err(); err == nil || err.Error() != expected { + t.Errorf("expected specific error, wanted %q, got %v", expected, err) + } +} + +func TestGenerateSurfacesLexerErrors(t *testing.T) { + const zone = `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 0-1 dhcp-${0,4,d} A 10.0.0.$ ) +` + zp := NewZoneParser(strings.NewReader(zone), ".", "test") + + for _, ok := zp.Next(); ok; _, ok = zp.Next() { + } + + const expected = `test: dns: bad data in $GENERATE directive: "extra closing brace" at line: 2:40` + if err := zp.Err(); err == nil || err.Error() != expected { + t.Errorf("expected specific error, wanted %q, got %v", expected, err) + } +} + func TestGenerateModToPrintf(t *testing.T) { tests := []struct { mod string @@ -49,26 +160,26 @@ func TestGenerateModToPrintf(t *testing.T) { wantOffset int wantErr bool }{ - {"0,0,d", "%0d", 0, false}, - {"0,0", "%0d", 0, false}, - {"0", "%0d", 0, false}, + {"0,0,d", "%d", 0, false}, + {"0,0", "%d", 0, false}, + {"0", "%d", 0, false}, {"3,2,d", "%02d", 3, false}, {"3,2", "%02d", 3, false}, - {"3", "%0d", 3, false}, - {"0,0,o", "%0o", 0, false}, - {"0,0,x", "%0x", 0, false}, - {"0,0,X", "%0X", 0, false}, + {"3", "%d", 3, false}, + {"0,0,o", "%o", 0, false}, + {"0,0,x", "%x", 0, false}, + {"0,0,X", "%X", 0, false}, {"0,0,z", "", 0, true}, {"0,0,0,d", "", 0, true}, - {"-100,0,d", "%0d", -100, false}, + {"-100,0,d", "%d", -100, false}, } for _, test := range tests { - gotFmt, gotOffset, err := modToPrintf(test.mod) + gotFmt, gotOffset, errMsg := modToPrintf(test.mod) switch { - case err != nil && !test.wantErr: - t.Errorf("modToPrintf(%q) - expected nil-error, but got %v", test.mod, err) - case err == nil && test.wantErr: - t.Errorf("modToPrintf(%q) - expected error, but got nil-error", test.mod) + case errMsg != "" && !test.wantErr: + t.Errorf("modToPrintf(%q) - expected empty-error, but got %v", test.mod, errMsg) + case errMsg == "" && test.wantErr: + t.Errorf("modToPrintf(%q) - expected error, but got empty-error", test.mod) case gotFmt != test.wantFmt: t.Errorf("modToPrintf(%q) - expected format %q, but got %q", test.mod, test.wantFmt, gotFmt) case gotOffset != test.wantOffset: @@ -76,3 +187,20 @@ func TestGenerateModToPrintf(t *testing.T) { } } } + +func BenchmarkGenerate(b *testing.B) { + const zone = `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d ) +$GENERATE 32-158 dhcp-${-32,4,d} A 10.0.0.$ +` + + for n := 0; n < b.N; n++ { + zp := NewZoneParser(strings.NewReader(zone), ".", "") + + for _, ok := zp.Next(); ok; _, ok = zp.Next() { + } + + if err := zp.Err(); err != nil { + b.Fatal(err) + } + } +} diff --git a/scan.go b/scan.go index b4fa0566..61ace121 100644 --- a/scan.go +++ b/scan.go @@ -12,6 +12,10 @@ import ( const maxTok = 2048 // Largest token we can return. +// The maximum depth of $INCLUDE directives supported by the +// ZoneParser API. +const maxIncludeDepth = 7 + // Tokinize a RFC 1035 zone file. The tokenizer will normalize it: // * Add ownernames if they are left blank; // * Suppress sequences of spaces; @@ -101,10 +105,14 @@ type ttlState struct { } // NewRR reads the RR contained in the string s. Only the first RR is -// returned. If s contains no RR, return nil with no error. The class -// defaults to IN and TTL defaults to 3600. The full zone file syntax -// like $TTL, $ORIGIN, etc. is supported. All fields of the returned -// RR are set, except RR.Header().Rdlength which is set to 0. +// returned. If s contains no records, NewRR will return nil with no +// error. +// +// The class defaults to IN and TTL defaults to 3600. The full zone +// file syntax like $TTL, $ORIGIN, etc. is supported. +// +// All fields of the returned RR are set, except RR.Header().Rdlength +// which is set to 0. func NewRR(s string) (RR, error) { if len(s) > 0 && s[len(s)-1] != '\n' { // We need a closing newline return ReadRR(strings.NewReader(s+"\n"), "") @@ -112,28 +120,31 @@ func NewRR(s string) (RR, error) { return ReadRR(strings.NewReader(s), "") } -// ReadRR reads the RR contained in q. +// ReadRR reads the RR contained in r. +// +// The string file is used in error reporting and to resolve relative +// $INCLUDE directives. +// // See NewRR for more documentation. -func ReadRR(q io.Reader, filename string) (RR, error) { - defttl := &ttlState{defaultTtl, false} - r := <-parseZoneHelper(q, ".", filename, defttl, 1) - if r == nil { - return nil, nil - } - - if r.Error != nil { - return nil, r.Error - } - return r.RR, nil +func ReadRR(r io.Reader, file string) (RR, error) { + zp := NewZoneParser(r, ".", file) + zp.SetDefaultTTL(defaultTtl) + zp.SetIncludeAllowed(true) + rr, _ := zp.Next() + return rr, zp.Err() } -// ParseZone reads a RFC 1035 style zonefile from r. It returns *Tokens on the -// returned channel, each consisting of either a parsed RR and optional comment -// or a nil RR and an error. The string file is only used -// in error reporting. The string origin is used as the initial origin, as -// if the file would start with an $ORIGIN directive. -// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are supported. -// The channel t is closed by ParseZone when the end of r is reached. +// ParseZone reads a RFC 1035 style zonefile from r. It returns +// *Tokens on the returned channel, each consisting of either a +// parsed RR and optional comment or a nil RR and an error. The +// channel is closed by ParseZone when the end of r is reached. +// +// The string file is used in error reporting and to resolve relative +// $INCLUDE directives. The string origin is used as the initial +// origin, as if the file would start with an $ORIGIN directive. +// +// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are all +// supported. // // Basic usage pattern when reading from a string (z) containing the // zone data: @@ -146,78 +157,246 @@ func ReadRR(q io.Reader, filename string) (RR, error) { // } // } // -// Comments specified after an RR (and on the same line!) are returned too: +// Comments specified after an RR (and on the same line!) are +// returned too: // // foo. IN A 10.0.0.1 ; this is a comment // -// The text "; this is comment" is returned in Token.Comment. Comments inside the -// RR are returned concatenated along with the RR. Comments on a line by themselves -// are discarded. +// The text "; this is comment" is returned in Token.Comment. +// Comments inside the RR are returned concatenated along with the +// RR. Comments on a line by themselves are discarded. +// +// To prevent memory leaks it is important to always fully drain the +// returned channel. If an error occurs, it will always be the last +// Token sent on the channel. +// +// Deprecated: New users should prefer the ZoneParser API. func ParseZone(r io.Reader, origin, file string) chan *Token { - return parseZoneHelper(r, origin, file, nil, 10000) -} - -func parseZoneHelper(r io.Reader, origin, file string, defttl *ttlState, chansize int) chan *Token { - t := make(chan *Token, chansize) - go parseZone(r, origin, file, defttl, t, 0) + t := make(chan *Token, 10000) + go parseZone(r, origin, file, t) return t } -func parseZone(r io.Reader, origin, f string, defttl *ttlState, t chan *Token, include int) { - defer func() { - if include == 0 { - close(t) +func parseZone(r io.Reader, origin, file string, t chan *Token) { + defer close(t) + + zp := NewZoneParser(r, origin, file) + zp.SetIncludeAllowed(true) + + for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { + t <- &Token{RR: rr, Comment: zp.Comment()} + } + + if err := zp.Err(); err != nil { + pe, ok := err.(*ParseError) + if !ok { + pe = &ParseError{file: file, err: err.Error()} } - }() - c := newZLexer(r) + t <- &Token{Error: pe} + } +} - // 6 possible beginnings of a line, _ is a space - // 0. zRRTYPE -> all omitted until the rrtype - // 1. zOwner _ zRrtype -> class/ttl omitted - // 2. zOwner _ zString _ zRrtype -> class omitted - // 3. zOwner _ zString _ zClass _ zRrtype -> ttl/class - // 4. zOwner _ zClass _ zRrtype -> ttl omitted - // 5. zOwner _ zClass _ zString _ zRrtype -> class/ttl (reversed) - // After detecting these, we know the zRrtype so we can jump to functions - // handling the rdata for each of these types. +// ZoneParser is a parser for an RFC 1035 style zonefile. +// +// Each parsed RR in the zone is returned sequentially from Next. An +// optional comment can be retrieved with Comment. +// +// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are all +// supported. Although $INCLUDE is disabled by default. +// +// Basic usage pattern when reading from a string (z) containing the +// zone data: +// +// zp := NewZoneParser(strings.NewReader(z), "", "") +// +// for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { +// // Do something with rr +// } +// +// if err := zp.Err(); err != nil { +// // log.Println(err) +// } +// +// Comments specified after an RR (and on the same line!) are +// returned too: +// +// foo. IN A 10.0.0.1 ; this is a comment +// +// The text "; this is comment" is returned from Comment. Comments inside +// the RR are returned concatenated along with the RR. Comments on a line +// by themselves are discarded. +type ZoneParser struct { + c *zlexer + parseErr *ParseError + + origin string + file string + + defttl *ttlState + + h RR_Header + + // sub is used to parse $INCLUDE files and $GENERATE directives. + // Next, by calling subNext, forwards the resulting RRs from this + // sub parser to the calling code. + sub *ZoneParser + osFile *os.File + + com string + + includeDepth uint8 + + includeAllowed bool +} + +// NewZoneParser returns an RFC 1035 style zonefile parser that reads +// from r. +// +// The string file is used in error reporting and to resolve relative +// $INCLUDE directives. The string origin is used as the initial +// origin, as if the file would start with an $ORIGIN directive. +func NewZoneParser(r io.Reader, origin, file string) *ZoneParser { + var pe *ParseError if origin != "" { origin = Fqdn(origin) if _, ok := IsDomainName(origin); !ok { - t <- &Token{Error: &ParseError{f, "bad initial origin name", lex{}}} - return + pe = &ParseError{file, "bad initial origin name", lex{}} } } - st := zExpectOwnerDir // initial state - var h RR_Header - var prevName string - for l, ok := c.Next(); ok; l, ok = c.Next() { - // Lexer spotted an error already - if l.err { - t <- &Token{Error: &ParseError{f, l.token, l}} - return + return &ZoneParser{ + c: newZLexer(r), + + parseErr: pe, + + origin: origin, + file: file, + } +} + +// SetDefaultTTL sets the parsers default TTL to ttl. +func (zp *ZoneParser) SetDefaultTTL(ttl uint32) { + zp.defttl = &ttlState{ttl, false} +} + +// SetIncludeAllowed controls whether $INCLUDE directives are +// allowed. $INCLUDE directives are not supported by default. +// +// The $INCLUDE directive will open and read from a user controlled +// file on the system. Even if the file is not a valid zonefile, the +// contents of the file may be revealed in error messages, such as: +// +// /etc/passwd: dns: not a TTL: "root:x:0:0:root:/root:/bin/bash" at line: 1:31 +// /etc/shadow: dns: not a TTL: "root:$6$::0:99999:7:::" at line: 1:125 +func (zp *ZoneParser) SetIncludeAllowed(v bool) { + zp.includeAllowed = v +} + +// Err returns the first non-EOF error that was encountered by the +// ZoneParser. +func (zp *ZoneParser) Err() error { + if zp.parseErr != nil { + return zp.parseErr + } + + if zp.sub != nil { + if err := zp.sub.Err(); err != nil { + return err } + } + + return zp.c.Err() +} + +func (zp *ZoneParser) setParseError(err string, l lex) (RR, bool) { + zp.parseErr = &ParseError{zp.file, err, l} + return nil, false +} + +// Comment returns an optional text comment that occurred alongside +// the RR. +func (zp *ZoneParser) Comment() string { + return zp.com +} + +func (zp *ZoneParser) subNext() (RR, bool) { + if rr, ok := zp.sub.Next(); ok { + zp.com = zp.sub.com + return rr, true + } + + if zp.sub.osFile != nil { + zp.sub.osFile.Close() + zp.sub.osFile = nil + } + + if zp.sub.Err() != nil { + // We have errors to surface. + return nil, false + } + + zp.sub = nil + return zp.Next() +} + +// Next advances the parser to the next RR in the zonefile and +// returns the (RR, true). It will return (nil, false) when the +// parsing stops, either by reaching the end of the input or an +// error. After Next returns (nil, false), the Err method will return +// any error that occurred during parsing. +func (zp *ZoneParser) Next() (RR, bool) { + zp.com = "" + + if zp.parseErr != nil { + return nil, false + } + if zp.sub != nil { + return zp.subNext() + } + + // 6 possible beginnings of a line (_ is a space): + // + // 0. zRRTYPE -> all omitted until the rrtype + // 1. zOwner _ zRrtype -> class/ttl omitted + // 2. zOwner _ zString _ zRrtype -> class omitted + // 3. zOwner _ zString _ zClass _ zRrtype -> ttl/class + // 4. zOwner _ zClass _ zRrtype -> ttl omitted + // 5. zOwner _ zClass _ zString _ zRrtype -> class/ttl (reversed) + // + // After detecting these, we know the zRrtype so we can jump to functions + // handling the rdata for each of these types. + + st := zExpectOwnerDir // initial state + h := &zp.h + + for l, ok := zp.c.Next(); ok; l, ok = zp.c.Next() { + // zlexer spotted an error already + if l.err { + return zp.setParseError(l.token, l) + } + switch st { case zExpectOwnerDir: // We can also expect a directive, like $TTL or $ORIGIN - if defttl != nil { - h.Ttl = defttl.ttl + if zp.defttl != nil { + h.Ttl = zp.defttl.ttl } + h.Class = ClassINET + switch l.value { case zNewline: st = zExpectOwnerDir case zOwner: - h.Name = l.token - name, ok := toAbsoluteName(l.token, origin) + name, ok := toAbsoluteName(l.token, zp.origin) if !ok { - t <- &Token{Error: &ParseError{f, "bad owner name", l}} - return + return zp.setParseError("bad owner name", l) } + h.Name = name - prevName = h.Name + st = zExpectOwnerBl case zDirTTL: st = zExpectDirTTLBl @@ -228,12 +407,12 @@ func parseZone(r io.Reader, origin, f string, defttl *ttlState, t chan *Token, i case zDirGenerate: st = zExpectDirGenerateBl case zRrtpe: - h.Name = prevName h.Rrtype = l.torc + st = zExpectRdata case zClass: - h.Name = prevName h.Class = l.torc + st = zExpectAnyNoClassBl case zBlank: // Discard, can happen when there is nothing on the @@ -241,239 +420,254 @@ func parseZone(r io.Reader, origin, f string, defttl *ttlState, t chan *Token, i case zString: ttl, ok := stringToTTL(l.token) if !ok { - t <- &Token{Error: &ParseError{f, "not a TTL", l}} - return + return zp.setParseError("not a TTL", l) } - h.Ttl = ttl - if defttl == nil || !defttl.isByDirective { - defttl = &ttlState{ttl, false} - } - st = zExpectAnyNoTTLBl + h.Ttl = ttl + + if zp.defttl == nil || !zp.defttl.isByDirective { + zp.defttl = &ttlState{ttl, false} + } + + st = zExpectAnyNoTTLBl default: - t <- &Token{Error: &ParseError{f, "syntax error at beginning", l}} - return + return zp.setParseError("syntax error at beginning", l) } case zExpectDirIncludeBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after $INCLUDE-directive", l}} - return + return zp.setParseError("no blank after $INCLUDE-directive", l) } + st = zExpectDirInclude case zExpectDirInclude: if l.value != zString { - t <- &Token{Error: &ParseError{f, "expecting $INCLUDE value, not this...", l}} - return + return zp.setParseError("expecting $INCLUDE value, not this...", l) } - neworigin := origin // There may be optionally a new origin set after the filename, if not use current one - switch l, _ := c.Next(); l.value { + + neworigin := zp.origin // There may be optionally a new origin set after the filename, if not use current one + switch l, _ := zp.c.Next(); l.value { case zBlank: - l, _ := c.Next() + l, _ := zp.c.Next() if l.value == zString { - name, ok := toAbsoluteName(l.token, origin) + name, ok := toAbsoluteName(l.token, zp.origin) if !ok { - t <- &Token{Error: &ParseError{f, "bad origin name", l}} - return + return zp.setParseError("bad origin name", l) } + neworigin = name } case zNewline, zEOF: // Ok default: - t <- &Token{Error: &ParseError{f, "garbage after $INCLUDE", l}} - return + return zp.setParseError("garbage after $INCLUDE", l) } + + if !zp.includeAllowed { + return zp.setParseError("$INCLUDE directive not allowed", l) + } + if zp.includeDepth >= maxIncludeDepth { + return zp.setParseError("too deeply nested $INCLUDE", l) + } + // Start with the new file includePath := l.token if !filepath.IsAbs(includePath) { - includePath = filepath.Join(filepath.Dir(f), includePath) + includePath = filepath.Join(filepath.Dir(zp.file), includePath) } + r1, e1 := os.Open(includePath) if e1 != nil { - msg := fmt.Sprintf("failed to open `%s'", l.token) + var as string if !filepath.IsAbs(l.token) { - msg += fmt.Sprintf(" as `%s'", includePath) + as = fmt.Sprintf(" as `%s'", includePath) } - t <- &Token{Error: &ParseError{f, msg, l}} - return + + msg := fmt.Sprintf("failed to open `%s'%s: %v", l.token, as, e1) + return zp.setParseError(msg, l) } - if include+1 > 7 { - t <- &Token{Error: &ParseError{f, "too deeply nested $INCLUDE", l}} - return - } - parseZone(r1, neworigin, includePath, defttl, t, include+1) - st = zExpectOwnerDir + + zp.sub = NewZoneParser(r1, neworigin, includePath) + zp.sub.defttl, zp.sub.includeDepth, zp.sub.osFile = zp.defttl, zp.includeDepth+1, r1 + zp.sub.SetIncludeAllowed(true) + return zp.subNext() case zExpectDirTTLBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after $TTL-directive", l}} - return + return zp.setParseError("no blank after $TTL-directive", l) } + st = zExpectDirTTL case zExpectDirTTL: if l.value != zString { - t <- &Token{Error: &ParseError{f, "expecting $TTL value, not this...", l}} - return + return zp.setParseError("expecting $TTL value, not this...", l) } - if e, _ := slurpRemainder(c, f); e != nil { - t <- &Token{Error: e} - return + + if e, _ := slurpRemainder(zp.c, zp.file); e != nil { + zp.parseErr = e + return nil, false } + ttl, ok := stringToTTL(l.token) if !ok { - t <- &Token{Error: &ParseError{f, "expecting $TTL value, not this...", l}} - return + return zp.setParseError("expecting $TTL value, not this...", l) } - defttl = &ttlState{ttl, true} + + zp.defttl = &ttlState{ttl, true} + st = zExpectOwnerDir case zExpectDirOriginBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after $ORIGIN-directive", l}} - return + return zp.setParseError("no blank after $ORIGIN-directive", l) } + st = zExpectDirOrigin case zExpectDirOrigin: if l.value != zString { - t <- &Token{Error: &ParseError{f, "expecting $ORIGIN value, not this...", l}} - return + return zp.setParseError("expecting $ORIGIN value, not this...", l) } - if e, _ := slurpRemainder(c, f); e != nil { - t <- &Token{Error: e} + + if e, _ := slurpRemainder(zp.c, zp.file); e != nil { + zp.parseErr = e + return nil, false } - name, ok := toAbsoluteName(l.token, origin) + + name, ok := toAbsoluteName(l.token, zp.origin) if !ok { - t <- &Token{Error: &ParseError{f, "bad origin name", l}} - return + return zp.setParseError("bad origin name", l) } - origin = name + + zp.origin = name + st = zExpectOwnerDir case zExpectDirGenerateBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after $GENERATE-directive", l}} - return + return zp.setParseError("no blank after $GENERATE-directive", l) } + st = zExpectDirGenerate case zExpectDirGenerate: if l.value != zString { - t <- &Token{Error: &ParseError{f, "expecting $GENERATE value, not this...", l}} - return + return zp.setParseError("expecting $GENERATE value, not this...", l) } - if errMsg := generate(l, c, t, origin); errMsg != "" { - t <- &Token{Error: &ParseError{f, errMsg, l}} - return - } - st = zExpectOwnerDir + + return zp.generate(l) case zExpectOwnerBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after owner", l}} - return + return zp.setParseError("no blank after owner", l) } + st = zExpectAny case zExpectAny: switch l.value { case zRrtpe: - if defttl == nil { - t <- &Token{Error: &ParseError{f, "missing TTL with no previous value", l}} - return + if zp.defttl == nil { + return zp.setParseError("missing TTL with no previous value", l) } + h.Rrtype = l.torc + st = zExpectRdata case zClass: h.Class = l.torc + st = zExpectAnyNoClassBl case zString: ttl, ok := stringToTTL(l.token) if !ok { - t <- &Token{Error: &ParseError{f, "not a TTL", l}} - return + return zp.setParseError("not a TTL", l) } + h.Ttl = ttl - if defttl == nil || !defttl.isByDirective { - defttl = &ttlState{ttl, false} + + if zp.defttl == nil || !zp.defttl.isByDirective { + zp.defttl = &ttlState{ttl, false} } + st = zExpectAnyNoTTLBl default: - t <- &Token{Error: &ParseError{f, "expecting RR type, TTL or class, not this...", l}} - return + return zp.setParseError("expecting RR type, TTL or class, not this...", l) } case zExpectAnyNoClassBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank before class", l}} - return + return zp.setParseError("no blank before class", l) } + st = zExpectAnyNoClass case zExpectAnyNoTTLBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank before TTL", l}} - return + return zp.setParseError("no blank before TTL", l) } + st = zExpectAnyNoTTL case zExpectAnyNoTTL: switch l.value { case zClass: h.Class = l.torc + st = zExpectRrtypeBl case zRrtpe: h.Rrtype = l.torc + st = zExpectRdata default: - t <- &Token{Error: &ParseError{f, "expecting RR type or class, not this...", l}} - return + return zp.setParseError("expecting RR type or class, not this...", l) } case zExpectAnyNoClass: switch l.value { case zString: ttl, ok := stringToTTL(l.token) if !ok { - t <- &Token{Error: &ParseError{f, "not a TTL", l}} - return + return zp.setParseError("not a TTL", l) } + h.Ttl = ttl - if defttl == nil || !defttl.isByDirective { - defttl = &ttlState{ttl, false} + + if zp.defttl == nil || !zp.defttl.isByDirective { + zp.defttl = &ttlState{ttl, false} } + st = zExpectRrtypeBl case zRrtpe: h.Rrtype = l.torc + st = zExpectRdata default: - t <- &Token{Error: &ParseError{f, "expecting RR type or TTL, not this...", l}} - return + return zp.setParseError("expecting RR type or TTL, not this...", l) } case zExpectRrtypeBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank before RR type", l}} - return + return zp.setParseError("no blank before RR type", l) } + st = zExpectRrtype case zExpectRrtype: if l.value != zRrtpe { - t <- &Token{Error: &ParseError{f, "unknown RR type", l}} - return + return zp.setParseError("unknown RR type", l) } + h.Rrtype = l.torc + st = zExpectRdata case zExpectRdata: - r, e, c1 := setRR(h, c, origin, f) + r, e, c1 := setRR(*h, zp.c, zp.origin, zp.file) if e != nil { // If e.lex is nil than we have encounter a unknown RR type // in that case we substitute our current lex token if e.lex.token == "" && e.lex.value == 0 { e.lex = l // Uh, dirty } - t <- &Token{Error: e} - return + + zp.parseErr = e + return nil, false } - t <- &Token{RR: r, Comment: c1} - st = zExpectOwnerDir + + zp.com = c1 + return r, true } } + // If we get here, we and the h.Rrtype is still zero, we haven't parsed anything, this // is not an error, because an empty zone file is still a zone file. - - // Surface any read errors from r. - if err := c.Err(); err != nil { - t <- &Token{Error: &ParseError{file: f, err: err.Error()}} - } + return nil, false } type zlexer struct { @@ -900,6 +1094,11 @@ func (zl *zlexer) Next() (lex, bool) { } } + if zl.readErr != nil && zl.readErr != io.EOF { + // Don't return any tokens after a read error occurs. + return lex{value: zEOF}, false + } + var retL lex if stri > 0 { // Send remainder of str diff --git a/scan_test.go b/scan_test.go index f593a865..a939d211 100644 --- a/scan_test.go +++ b/scan_test.go @@ -39,6 +39,10 @@ func TestParseZoneGenerate(t *testing.T) { } wantIdx++ } + + if wantIdx != len(wantRRs) { + t.Errorf("too few records, expected %d, got %d", len(wantRRs), wantIdx) + } } func TestParseZoneInclude(t *testing.T) { @@ -47,6 +51,7 @@ func TestParseZoneInclude(t *testing.T) { if err != nil { t.Fatalf("could not create tmpfile for test: %s", err) } + defer os.Remove(tmpfile.Name()) if _, err := tmpfile.WriteString("foo\tIN\tA\t127.0.0.1"); err != nil { t.Fatalf("unable to write content to tmpfile %q: %s", tmpfile.Name(), err) @@ -55,16 +60,24 @@ func TestParseZoneInclude(t *testing.T) { t.Fatalf("could not close tmpfile %q: %s", tmpfile.Name(), err) } - zone := "$ORIGIN example.org.\n$INCLUDE " + tmpfile.Name() + zone := "$ORIGIN example.org.\n$INCLUDE " + tmpfile.Name() + "\nbar\tIN\tA\t127.0.0.2" + var got int tok := ParseZone(strings.NewReader(zone), "", "") for x := range tok { if x.Error != nil { t.Fatalf("expected no error, but got %s", x.Error) } - if x.RR.Header().Name != "foo.example.org." { - t.Fatalf("expected %s, but got %s", "foo.example.org.", x.RR.Header().Name) + switch x.RR.Header().Name { + case "foo.example.org.", "bar.example.org.": + default: + t.Fatalf("expected foo.example.org. or bar.example.org., but got %s", x.RR.Header().Name) } + got++ + } + + if expected := 2; got != expected { + t.Errorf("failed to parse zone after include, expected %d records, got %d", expected, got) } os.Remove(tmpfile.Name()) @@ -75,12 +88,39 @@ func TestParseZoneInclude(t *testing.T) { t.Fatalf("expected first token to contain an error but it didn't") } if !strings.Contains(x.Error.Error(), "failed to open") || - !strings.Contains(x.Error.Error(), tmpfile.Name()) { - t.Fatalf(`expected error to contain: "failed to open" and %q but got: %s`, tmpfile.Name(), x.Error) + !strings.Contains(x.Error.Error(), tmpfile.Name()) || + !strings.Contains(x.Error.Error(), "no such file or directory") { + t.Fatalf(`expected error to contain: "failed to open", %q and "no such file or directory" but got: %s`, + tmpfile.Name(), x.Error) } } } +func TestZoneParserIncludeDisallowed(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "dns") + if err != nil { + t.Fatalf("could not create tmpfile for test: %s", err) + } + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.WriteString("foo\tIN\tA\t127.0.0.1"); err != nil { + t.Fatalf("unable to write content to tmpfile %q: %s", tmpfile.Name(), err) + } + if err := tmpfile.Close(); err != nil { + t.Fatalf("could not close tmpfile %q: %s", tmpfile.Name(), err) + } + + zp := NewZoneParser(strings.NewReader("$INCLUDE "+tmpfile.Name()), "example.org.", "") + + for _, ok := zp.Next(); ok; _, ok = zp.Next() { + } + + const expect = "$INCLUDE directive not allowed" + if err := zp.Err(); err == nil || !strings.Contains(err.Error(), expect) { + t.Errorf("expected error to contain %q, got %v", expect, err) + } +} + func TestParseTA(t *testing.T) { rr, err := NewRR(` Ta 0 0 0`) if err != nil { @@ -134,3 +174,45 @@ func BenchmarkReadRR(b *testing.B) { } } } + +const benchZone = ` +foo. IN A 10.0.0.1 ; this is comment 1 +foo. IN A ( + 10.0.0.2 ; this is comment 2 +) +; this is comment 3 +foo. IN A 10.0.0.3 +foo. IN A ( 10.0.0.4 ); this is comment 4 + +foo. IN A 10.0.0.5 +; this is comment 5 + +foo. IN A 10.0.0.6 + +foo. IN DNSKEY 256 3 5 AwEAAb+8l ; this is comment 6 +foo. IN NSEC miek.nl. TXT RRSIG NSEC; this is comment 7 +foo. IN TXT "THIS IS TEXT MAN"; this is comment 8 +` + +func BenchmarkParseZone(b *testing.B) { + for n := 0; n < b.N; n++ { + for tok := range ParseZone(strings.NewReader(benchZone), "example.org.", "") { + if tok.Error != nil { + b.Fatal(tok.Error) + } + } + } +} + +func BenchmarkZoneParser(b *testing.B) { + for n := 0; n < b.N; n++ { + zp := NewZoneParser(strings.NewReader(benchZone), "example.org.", "") + + for _, ok := zp.Next(); ok; _, ok = zp.Next() { + } + + if err := zp.Err(); err != nil { + b.Fatal(err) + } + } +}