package dns import ( "bufio" "fmt" "io" "io/fs" "os" "path" "path/filepath" "strconv" "strings" ) const maxTok = 512 // Token buffer start size, and growth size amount. // The maximum depth of $INCLUDE directives supported by the // ZoneParser API. const maxIncludeDepth = 7 // Tokenize a RFC 1035 zone file. The tokenizer will normalize it: // * Add ownernames if they are left blank; // * Suppress sequences of spaces; // * Make each RR fit on one line (_NEWLINE is send as last) // * Handle comments: ; // * Handle braces - anywhere. const ( // Zonefile zEOF = iota zString zBlank zQuote zNewline zRrtpe zOwner zClass zDirOrigin // $ORIGIN zDirTTL // $TTL zDirInclude // $INCLUDE zDirGenerate // $GENERATE // Privatekey file zValue zKey zExpectOwnerDir // Ownername zExpectOwnerBl // Whitespace after the ownername zExpectAny // Expect rrtype, ttl or class zExpectAnyNoClass // Expect rrtype or ttl zExpectAnyNoClassBl // The whitespace after _EXPECT_ANY_NOCLASS zExpectAnyNoTTL // Expect rrtype or class zExpectAnyNoTTLBl // Whitespace after _EXPECT_ANY_NOTTL zExpectRrtype // Expect rrtype zExpectRrtypeBl // Whitespace BEFORE rrtype zExpectRdata // The first element of the rdata zExpectDirTTLBl // Space after directive $TTL zExpectDirTTL // Directive $TTL zExpectDirOriginBl // Space after directive $ORIGIN zExpectDirOrigin // Directive $ORIGIN zExpectDirIncludeBl // Space after directive $INCLUDE zExpectDirInclude // Directive $INCLUDE zExpectDirGenerate // Directive $GENERATE zExpectDirGenerateBl // Space after directive $GENERATE ) // ParseError is a parsing error. It contains the parse error and the location in the io.Reader // where the error occurred. type ParseError struct { file string err string wrappedErr error lex lex } func (e *ParseError) Error() (s string) { if e.file != "" { s = e.file + ": " } if e.err == "" && e.wrappedErr != nil { e.err = e.wrappedErr.Error() } s += "dns: " + e.err + ": " + strconv.QuoteToASCII(e.lex.token) + " at line: " + strconv.Itoa(e.lex.line) + ":" + strconv.Itoa(e.lex.column) return } func (e *ParseError) Unwrap() error { return e.wrappedErr } type lex struct { token string // text of the token err bool // when true, token text has lexer error value uint8 // value: zString, _BLANK, etc. torc uint16 // type or class as parsed in the lexer, we only need to look this up in the grammar line int // line in the file column int // column in the file } // ttlState describes the state necessary to fill in an omitted RR TTL type ttlState struct { ttl uint32 // ttl is the current default TTL isByDirective bool // isByDirective indicates whether ttl was set by a $TTL directive } // NewRR reads a string s and returns the first RR. // If s contains no records, NewRR will return nil with no error. // // The class defaults to IN, TTL defaults to 3600, and // origin for resolving relative domain names defaults to the DNS root (.). // Full zone file syntax is supported, including directives like $TTL and $ORIGIN. // All fields of the returned RR are set from the read data, 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"), "") } return ReadRR(strings.NewReader(s), "") } // 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(r io.Reader, file string) (RR, error) { zp := NewZoneParser(r, ".", file) zp.SetDefaultTTL(defaultTtl) zp.SetIncludeAllowed(true) rr, _ := zp.Next() return rr, zp.Err() } // 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. // Note that $GENERATE's range support up to a maximum of 65535 steps. // // 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. // // Callers should not assume all returned data in an Resource Record is // syntactically correct, e.g. illegal base64 in RRSIGs will be returned as-is. 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 r io.Reader fsys fs.FS includeDepth uint8 includeAllowed bool generateDisallowed 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 { pe = &ParseError{file: file, err: "bad initial origin name"} } } 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 } // SetIncludeFS provides an [fs.FS] to use when looking for the target of // $INCLUDE directives. ($INCLUDE must still be enabled separately by calling // [ZoneParser.SetIncludeAllowed].) If fsys is nil, [os.Open] will be used. // // When fsys is an on-disk FS, the ability of $INCLUDE to reach files from // outside its root directory depends upon the FS implementation. For // instance, [os.DirFS] will refuse to open paths like "../../etc/passwd", // however it will still follow links which may point anywhere on the system. // // FS paths are slash-separated on all systems, even Windows. $INCLUDE paths // containing other characters such as backslash and colon may be accepted as // valid, but those characters will never be interpreted by an FS // implementation as path element separators. See [fs.ValidPath] for more // details. func (zp *ZoneParser) SetIncludeFS(fsys fs.FS) { zp.fsys = fsys } // 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{file: zp.file, err: err, lex: l} return nil, false } // Comment returns an optional text comment that occurred alongside // the RR. func (zp *ZoneParser) Comment() string { if zp.parseErr != nil { return "" } if zp.sub != nil { return zp.sub.Comment() } return zp.c.Comment() } func (zp *ZoneParser) subNext() (RR, bool) { if rr, ok := zp.sub.Next(); ok { return rr, true } if zp.sub.r != nil { if c, ok := zp.sub.r.(io.Closer); ok { c.Close() } zp.sub.r = 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) { 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 zp.defttl != nil { h.Ttl = zp.defttl.ttl } h.Class = ClassINET switch l.value { case zNewline: st = zExpectOwnerDir case zOwner: name, ok := toAbsoluteName(l.token, zp.origin) if !ok { return zp.setParseError("bad owner name", l) } h.Name = name st = zExpectOwnerBl case zDirTTL: st = zExpectDirTTLBl case zDirOrigin: st = zExpectDirOriginBl case zDirInclude: st = zExpectDirIncludeBl case zDirGenerate: st = zExpectDirGenerateBl case zRrtpe: h.Rrtype = l.torc st = zExpectRdata case zClass: h.Class = l.torc st = zExpectAnyNoClassBl case zBlank: // Discard, can happen when there is nothing on the // line except the RR type case zString: ttl, ok := stringToTTL(l.token) if !ok { return zp.setParseError("not a TTL", l) } h.Ttl = ttl if zp.defttl == nil || !zp.defttl.isByDirective { zp.defttl = &ttlState{ttl, false} } st = zExpectAnyNoTTLBl default: return zp.setParseError("syntax error at beginning", l) } case zExpectDirIncludeBl: if l.value != zBlank { return zp.setParseError("no blank after $INCLUDE-directive", l) } st = zExpectDirInclude case zExpectDirInclude: if l.value != zString { return zp.setParseError("expecting $INCLUDE value, not this...", l) } 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, _ := zp.c.Next() if l.value == zString { name, ok := toAbsoluteName(l.token, zp.origin) if !ok { return zp.setParseError("bad origin name", l) } neworigin = name } case zNewline, zEOF: // Ok default: 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 var r1 io.Reader var e1 error if zp.fsys != nil { // fs.FS always uses / as separator, even on Windows, so use // path instead of filepath here: if !path.IsAbs(includePath) { includePath = path.Join(path.Dir(zp.file), includePath) } // os.DirFS, and probably others, expect all paths to be // relative, so clean the path and remove leading / if // present: includePath = strings.TrimLeft(path.Clean(includePath), "/") r1, e1 = zp.fsys.Open(includePath) } else { if !filepath.IsAbs(includePath) { includePath = filepath.Join(filepath.Dir(zp.file), includePath) } r1, e1 = os.Open(includePath) } if e1 != nil { var as string if includePath != l.token { as = fmt.Sprintf(" as `%s'", includePath) } zp.parseErr = &ParseError{ file: zp.file, wrappedErr: fmt.Errorf("failed to open `%s'%s: %w", l.token, as, e1), lex: l, } return nil, false } zp.sub = NewZoneParser(r1, neworigin, includePath) zp.sub.defttl, zp.sub.includeDepth, zp.sub.r = zp.defttl, zp.includeDepth+1, r1 zp.sub.SetIncludeAllowed(true) zp.sub.SetIncludeFS(zp.fsys) return zp.subNext() case zExpectDirTTLBl: if l.value != zBlank { return zp.setParseError("no blank after $TTL-directive", l) } st = zExpectDirTTL case zExpectDirTTL: if l.value != zString { return zp.setParseError("expecting $TTL value, not this...", l) } if err := slurpRemainder(zp.c); err != nil { return zp.setParseError(err.err, err.lex) } ttl, ok := stringToTTL(l.token) if !ok { return zp.setParseError("expecting $TTL value, not this...", l) } zp.defttl = &ttlState{ttl, true} st = zExpectOwnerDir case zExpectDirOriginBl: if l.value != zBlank { return zp.setParseError("no blank after $ORIGIN-directive", l) } st = zExpectDirOrigin case zExpectDirOrigin: if l.value != zString { return zp.setParseError("expecting $ORIGIN value, not this...", l) } if err := slurpRemainder(zp.c); err != nil { return zp.setParseError(err.err, err.lex) } name, ok := toAbsoluteName(l.token, zp.origin) if !ok { return zp.setParseError("bad origin name", l) } zp.origin = name st = zExpectOwnerDir case zExpectDirGenerateBl: if l.value != zBlank { return zp.setParseError("no blank after $GENERATE-directive", l) } st = zExpectDirGenerate case zExpectDirGenerate: if zp.generateDisallowed { return zp.setParseError("nested $GENERATE directive not allowed", l) } if l.value != zString { return zp.setParseError("expecting $GENERATE value, not this...", l) } return zp.generate(l) case zExpectOwnerBl: if l.value != zBlank { return zp.setParseError("no blank after owner", l) } st = zExpectAny case zExpectAny: switch l.value { case zRrtpe: 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 { return zp.setParseError("not a TTL", l) } h.Ttl = ttl if zp.defttl == nil || !zp.defttl.isByDirective { zp.defttl = &ttlState{ttl, false} } st = zExpectAnyNoTTLBl default: return zp.setParseError("expecting RR type, TTL or class, not this...", l) } case zExpectAnyNoClassBl: if l.value != zBlank { return zp.setParseError("no blank before class", l) } st = zExpectAnyNoClass case zExpectAnyNoTTLBl: if l.value != zBlank { 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: 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 { return zp.setParseError("not a TTL", l) } h.Ttl = ttl if zp.defttl == nil || !zp.defttl.isByDirective { zp.defttl = &ttlState{ttl, false} } st = zExpectRrtypeBl case zRrtpe: h.Rrtype = l.torc st = zExpectRdata default: return zp.setParseError("expecting RR type or TTL, not this...", l) } case zExpectRrtypeBl: if l.value != zBlank { return zp.setParseError("no blank before RR type", l) } st = zExpectRrtype case zExpectRrtype: if l.value != zRrtpe { return zp.setParseError("unknown RR type", l) } h.Rrtype = l.torc st = zExpectRdata case zExpectRdata: var ( rr RR parseAsRFC3597 bool ) if newFn, ok := TypeToRR[h.Rrtype]; ok { rr = newFn() *rr.Header() = *h // We may be parsing a known RR type using the RFC3597 format. // If so, we handle that here in a generic way. // // This is also true for PrivateRR types which will have the // RFC3597 parsing done for them and the Unpack method called // to populate the RR instead of simply deferring to Parse. if zp.c.Peek().token == "\\#" { parseAsRFC3597 = true } } else { rr = &RFC3597{Hdr: *h} } _, isPrivate := rr.(*PrivateRR) if !isPrivate && zp.c.Peek().token == "" { // This is a dynamic update rr. if err := slurpRemainder(zp.c); err != nil { return zp.setParseError(err.err, err.lex) } return rr, true } else if l.value == zNewline { return zp.setParseError("unexpected newline", l) } parseAsRR := rr if parseAsRFC3597 { parseAsRR = &RFC3597{Hdr: *h} } if err := parseAsRR.parse(zp.c, zp.origin); err != nil { // err is a concrete *ParseError without the file field set. // The setParseError call below will construct a new // *ParseError with file set to zp.file. // err.lex may be nil in which case we substitute our current // lex token. if err.lex == (lex{}) { return zp.setParseError(err.err, l) } return zp.setParseError(err.err, err.lex) } if parseAsRFC3597 { err := parseAsRR.(*RFC3597).fromRFC3597(rr) if err != nil { return zp.setParseError(err.Error(), l) } } return rr, 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. return nil, false } type zlexer struct { br io.ByteReader readErr error line int column int comBuf string comment string l lex cachedL *lex brace int quote bool space bool commt bool rrtype bool owner bool nextL bool eol bool // end-of-line } func newZLexer(r io.Reader) *zlexer { br, ok := r.(io.ByteReader) if !ok { br = bufio.NewReaderSize(r, 1024) } return &zlexer{ br: br, line: 1, owner: true, } } func (zl *zlexer) Err() error { if zl.readErr == io.EOF { return nil } return zl.readErr } // readByte returns the next byte from the input func (zl *zlexer) readByte() (byte, bool) { if zl.readErr != nil { return 0, false } c, err := zl.br.ReadByte() if err != nil { zl.readErr = err return 0, false } // delay the newline handling until the next token is delivered, // fixes off-by-one errors when reporting a parse error. if zl.eol { zl.line++ zl.column = 0 zl.eol = false } if c == '\n' { zl.eol = true } else { zl.column++ } return c, true } func (zl *zlexer) Peek() lex { if zl.nextL { return zl.l } l, ok := zl.Next() if !ok { return l } if zl.nextL { // Cache l. Next returns zl.cachedL then zl.l. zl.cachedL = &l } else { // In this case l == zl.l, so we just tell Next to return zl.l. zl.nextL = true } return l } func (zl *zlexer) Next() (lex, bool) { l := &zl.l switch { case zl.cachedL != nil: l, zl.cachedL = zl.cachedL, nil return *l, true case zl.nextL: zl.nextL = false return *l, true case l.err: // Parsing errors should be sticky. return lex{value: zEOF}, false } var ( str = make([]byte, maxTok) // Hold string text com = make([]byte, maxTok) // Hold comment text stri int // Offset in str (0 means empty) comi int // Offset in com (0 means empty) escape bool ) if zl.comBuf != "" { comi = copy(com[:], zl.comBuf) zl.comBuf = "" } zl.comment = "" for x, ok := zl.readByte(); ok; x, ok = zl.readByte() { l.line, l.column = zl.line, zl.column if stri >= len(str) { // if buffer length is insufficient, increase it. str = append(str[:], make([]byte, maxTok)...) } if comi >= len(com) { // if buffer length is insufficient, increase it. com = append(com[:], make([]byte, maxTok)...) } switch x { case ' ', '\t': if escape || zl.quote { // Inside quotes or escaped this is legal. str[stri] = x stri++ escape = false break } if zl.commt { com[comi] = x comi++ break } var retL lex if stri == 0 { // Space directly in the beginning, handled in the grammar } else if zl.owner { // If we have a string and it's the first, make it an owner l.value = zOwner l.token = string(str[:stri]) // escape $... start with a \ not a $, so this will work switch strings.ToUpper(l.token) { case "$TTL": l.value = zDirTTL case "$ORIGIN": l.value = zDirOrigin case "$INCLUDE": l.value = zDirInclude case "$GENERATE": l.value = zDirGenerate } retL = *l } else { l.value = zString l.token = string(str[:stri]) if !zl.rrtype { tokenUpper := strings.ToUpper(l.token) if t, ok := StringToType[tokenUpper]; ok { l.value = zRrtpe l.torc = t zl.rrtype = true } else if strings.HasPrefix(tokenUpper, "TYPE") { t, ok := typeToInt(l.token) if !ok { l.token = "unknown RR type" l.err = true return *l, true } l.value = zRrtpe l.torc = t zl.rrtype = true } if t, ok := StringToClass[tokenUpper]; ok { l.value = zClass l.torc = t } else if strings.HasPrefix(tokenUpper, "CLASS") { t, ok := classToInt(l.token) if !ok { l.token = "unknown class" l.err = true return *l, true } l.value = zClass l.torc = t } } retL = *l } zl.owner = false if !zl.space { zl.space = true l.value = zBlank l.token = " " if retL == (lex{}) { return *l, true } zl.nextL = true } if retL != (lex{}) { return retL, true } case ';': if escape || zl.quote { // Inside quotes or escaped this is legal. str[stri] = x stri++ escape = false break } zl.commt = true zl.comBuf = "" if comi > 1 { // A newline was previously seen inside a comment that // was inside braces and we delayed adding it until now. com[comi] = ' ' // convert newline to space comi++ if comi >= len(com) { l.token = "comment length insufficient for parsing" l.err = true return *l, true } } com[comi] = ';' comi++ if stri > 0 { zl.comBuf = string(com[:comi]) l.value = zString l.token = string(str[:stri]) return *l, true } case '\r': escape = false if zl.quote { str[stri] = x stri++ } // discard if outside of quotes case '\n': escape = false // Escaped newline if zl.quote { str[stri] = x stri++ break } if zl.commt { // Reset a comment zl.commt = false zl.rrtype = false // If not in a brace this ends the comment AND the RR if zl.brace == 0 { zl.owner = true l.value = zNewline l.token = "\n" zl.comment = string(com[:comi]) return *l, true } zl.comBuf = string(com[:comi]) break } if zl.brace == 0 { // If there is previous text, we should output it here var retL lex if stri != 0 { l.value = zString l.token = string(str[:stri]) if !zl.rrtype { tokenUpper := strings.ToUpper(l.token) if t, ok := StringToType[tokenUpper]; ok { zl.rrtype = true l.value = zRrtpe l.torc = t } } retL = *l } l.value = zNewline l.token = "\n" zl.comment = zl.comBuf zl.comBuf = "" zl.rrtype = false zl.owner = true if retL != (lex{}) { zl.nextL = true return retL, true } return *l, true } case '\\': // comments do not get escaped chars, everything is copied if zl.commt { com[comi] = x comi++ break } // something already escaped must be in string if escape { str[stri] = x stri++ escape = false break } // something escaped outside of string gets added to string str[stri] = x stri++ escape = true case '"': if zl.commt { com[comi] = x comi++ break } if escape { str[stri] = x stri++ escape = false break } zl.space = false // send previous gathered text and the quote var retL lex if stri != 0 { l.value = zString l.token = string(str[:stri]) retL = *l } // send quote itself as separate token l.value = zQuote l.token = "\"" zl.quote = !zl.quote if retL != (lex{}) { zl.nextL = true return retL, true } return *l, true case '(', ')': if zl.commt { com[comi] = x comi++ break } if escape || zl.quote { // Inside quotes or escaped this is legal. str[stri] = x stri++ escape = false break } switch x { case ')': zl.brace-- if zl.brace < 0 { l.token = "extra closing brace" l.err = true return *l, true } case '(': zl.brace++ } default: escape = false if zl.commt { com[comi] = x comi++ break } str[stri] = x stri++ zl.space = false } } 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 l.value = zString l.token = string(str[:stri]) retL = *l if comi <= 0 { return retL, true } } if comi > 0 { // Send remainder of com l.value = zNewline l.token = "\n" zl.comment = string(com[:comi]) if retL != (lex{}) { zl.nextL = true return retL, true } return *l, true } if zl.brace != 0 { l.token = "unbalanced brace" l.err = true return *l, true } return lex{value: zEOF}, false } func (zl *zlexer) Comment() string { if zl.l.err { return "" } return zl.comment } // Extract the class number from CLASSxx func classToInt(token string) (uint16, bool) { offset := 5 if len(token) < offset+1 { return 0, false } class, err := strconv.ParseUint(token[offset:], 10, 16) if err != nil { return 0, false } return uint16(class), true } // Extract the rr number from TYPExxx func typeToInt(token string) (uint16, bool) { offset := 4 if len(token) < offset+1 { return 0, false } typ, err := strconv.ParseUint(token[offset:], 10, 16) if err != nil { return 0, false } return uint16(typ), true } // stringToTTL parses things like 2w, 2m, etc, and returns the time in seconds. func stringToTTL(token string) (uint32, bool) { var s, i uint32 for _, c := range token { switch c { case 's', 'S': s += i i = 0 case 'm', 'M': s += i * 60 i = 0 case 'h', 'H': s += i * 60 * 60 i = 0 case 'd', 'D': s += i * 60 * 60 * 24 i = 0 case 'w', 'W': s += i * 60 * 60 * 24 * 7 i = 0 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': i *= 10 i += uint32(c) - '0' default: return 0, false } } return s + i, true } // Parse LOC records' [.][mM] into a // mantissa exponent format. Token should contain the entire // string (i.e. no spaces allowed) func stringToCm(token string) (e, m uint8, ok bool) { if token[len(token)-1] == 'M' || token[len(token)-1] == 'm' { token = token[0 : len(token)-1] } var ( meters, cmeters, val int err error ) mStr, cmStr, hasCM := strings.Cut(token, ".") if hasCM { // There's no point in having more than 2 digits in this part, and would rather make the implementation complicated ('123' should be treated as '12'). // So we simply reject it. // We also make sure the first character is a digit to reject '+-' signs. cmeters, err = strconv.Atoi(cmStr) if err != nil || len(cmStr) > 2 || cmStr[0] < '0' || cmStr[0] > '9' { return } if len(cmStr) == 1 { // 'nn.1' must be treated as 'nn-meters and 10cm, not 1cm. cmeters *= 10 } } // This slightly ugly condition will allow omitting the 'meter' part, like .01 (meaning 0.01m = 1cm). if !hasCM || mStr != "" { meters, err = strconv.Atoi(mStr) // RFC1876 states the max value is 90000000.00. The latter two conditions enforce it. if err != nil || mStr[0] < '0' || mStr[0] > '9' || meters > 90000000 || (meters == 90000000 && cmeters != 0) { return } } if meters > 0 { e = 2 val = meters } else { e = 0 val = cmeters } for val >= 10 { e++ val /= 10 } return e, uint8(val), true } func toAbsoluteName(name, origin string) (absolute string, ok bool) { // check for an explicit origin reference if name == "@" { // require a nonempty origin if origin == "" { return "", false } return origin, true } // require a valid domain name _, ok = IsDomainName(name) if !ok || name == "" { return "", false } // check if name is already absolute if IsFqdn(name) { return name, true } // require a nonempty origin if origin == "" { return "", false } return appendOrigin(name, origin), true } func appendOrigin(name, origin string) string { if origin == "." { return name + origin } return name + "." + origin } // LOC record helper function func locCheckNorth(token string, latitude uint32) (uint32, bool) { if latitude > 90*1000*60*60 { return latitude, false } switch token { case "n", "N": return LOC_EQUATOR + latitude, true case "s", "S": return LOC_EQUATOR - latitude, true } return latitude, false } // LOC record helper function func locCheckEast(token string, longitude uint32) (uint32, bool) { if longitude > 180*1000*60*60 { return longitude, false } switch token { case "e", "E": return LOC_EQUATOR + longitude, true case "w", "W": return LOC_EQUATOR - longitude, true } return longitude, false } // "Eat" the rest of the "line" func slurpRemainder(c *zlexer) *ParseError { l, _ := c.Next() switch l.value { case zBlank: l, _ = c.Next() if l.value != zNewline && l.value != zEOF { return &ParseError{err: "garbage after rdata", lex: l} } case zNewline: case zEOF: default: return &ParseError{err: "garbage after rdata", lex: l} } return nil } // Parse a 64 bit-like ipv6 address: "0014:4fff:ff20:ee64" // Used for NID and L64 record. func stringToNodeID(l lex) (uint64, *ParseError) { if len(l.token) < 19 { return 0, &ParseError{file: l.token, err: "bad NID/L64 NodeID/Locator64", lex: l} } // There must be three colons at fixes positions, if not its a parse error if l.token[4] != ':' && l.token[9] != ':' && l.token[14] != ':' { return 0, &ParseError{file: l.token, err: "bad NID/L64 NodeID/Locator64", lex: l} } s := l.token[0:4] + l.token[5:9] + l.token[10:14] + l.token[15:19] u, err := strconv.ParseUint(s, 16, 64) if err != nil { return 0, &ParseError{file: l.token, err: "bad NID/L64 NodeID/Locator64", lex: l} } return u, nil }