dnscontrol/providers/bind/fnames.go
Tom Limoncelli 489be2e3dc
ROUTE53: fix R53_ZONE() handling for domains (#2306)
Co-authored-by: Tom Limoncelli <tal@whatexit.org>
2023-05-02 13:04:59 -04:00

195 lines
4.8 KiB
Go

package bind
import (
"bytes"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
// makeFileName uses format to generate a zone's filename. See the
func makeFileName(format, uniquename, domain, tag string) string {
//fmt.Printf("DEBUG: makeFileName(%q, %q, %q, %q)\n", format, uniquename, domain, tag)
if format == "" {
fmt.Fprintf(os.Stderr, "BUG: makeFileName called with null format\n")
return uniquename
}
var b bytes.Buffer
tokens := strings.Split(format, "")
lastpos := len(tokens) - 1
for pos := 0; pos < len(tokens); pos++ {
tok := tokens[pos]
if tok != "%" {
b.WriteString(tok)
continue
}
if pos == lastpos {
b.WriteString("%(format may not end in %)")
continue
}
pos++
tok = tokens[pos]
switch tok {
case "D":
b.WriteString(domain)
case "T":
b.WriteString(tag)
case "U":
b.WriteString(uniquename)
case "?":
if pos == lastpos {
b.WriteString("%(format may not end in %?)")
continue
}
pos++
tok = tokens[pos]
if tag != "" {
b.WriteString(tok)
}
default:
fmt.Fprintf(&b, "%%(unknown %%verb %%%s)", tok)
}
}
//fmt.Printf("DEBUG: makeFileName returns= %q\n", b.String())
return b.String()
}
// extractZonesFromFilenames extracts the zone names from a list of filenames
// based on the format string used to create the files. It is mathematically
// impossible to do this correctly for all format strings, but typical format
// strings are supported.
func extractZonesFromFilenames(format string, names []string) []string {
var zones []string
// Generate a regex that will extract the zonename from a filename.
extractor, err := makeExtractor(format)
if err != nil {
// Give up. Return the list of filenames.
return names
}
re := regexp.MustCompile(extractor)
//
for _, n := range names {
_, file := filepath.Split(n)
l := re.FindStringSubmatch(file)
// l[1:] is a list of matches and null strings. Pick the first non-null string.
if len(l) > 1 {
for _, s := range l[1:] {
if s != "" {
zones = append(zones, s)
break
}
}
}
}
return zones
}
// makeExtractor generates a regex that extracts domain names from filenames.
// format specifies the format string used by makeFileName to generate such
// filenames. It is mathematically impossible to do this correctly for all
// format strings, but typical format strings are supported.
func makeExtractor(format string) (string, error) {
// The algorithm works as follows.
// We generate a regex that is A or A|B.
// A is the regex that works if tag is non-null.
// B is the regex that assumes tags are "".
// If no tag-related verbs are used, A is sufficient.
// If a tag-related verb is used, we append | and generate B, which does
// Each % verb is turned into an appropriate subexpression based on pass.
// NB: This is some rather fancy CS stuff just to make the
// "get-zones all" command work for BIND. That's a lot of work for
// a feature that isn't going to be used very often, if at all.
// Therefore if this ever becomes a maintenance bother, we can just
// replace this with something more simple. For example, the
// creds.json file could specify the regex and humans can specify
// the Extractor themselves. Or, just remove this feature from the
// BIND driver.
var b bytes.Buffer
tokens := strings.Split(format, "")
lastpos := len(tokens) - 1
generateB := false
for pass := range []int{0, 1} {
for pos := 0; pos < len(tokens); pos++ {
tok := tokens[pos]
if tok == "." {
// dots are escaped
b.WriteString(`\.`)
continue
}
if tok != "%" {
// ordinary runes are passed unmodified.
b.WriteString(tok)
continue
}
if pos == lastpos {
return ``, fmt.Errorf("format may not end in %%: %q", format)
}
// Process % verbs
// Move to the next token, which is the verb name: D, U, etc.
pos++
tok = tokens[pos]
switch tok {
case "D":
b.WriteString(`(.*)`)
case "T":
if pass == 0 {
// On the second pass, nothing is generated.
b.WriteString(`.*`)
}
case "U":
if pass == 0 {
b.WriteString(`(.*)!.+`)
} else {
b.WriteString(`(.*)`)
}
generateB = true
case "?":
if pos == lastpos {
return ``, fmt.Errorf("format may not end in %%?: %q", format)
}
// Move to the next token, the tag-only char.
pos++
tok = tokens[pos]
if pass == 0 {
// On the second pass, nothing is generated.
b.WriteString(tok)
}
generateB = true
default:
return ``, fmt.Errorf("unknown %%verb %%%s: %q", tok, format)
}
}
// At the end of the first pass determine if we need the second pass.
if pass == 0 {
if generateB {
// We had a %? token. Now repeat the process
// but generate an "or" that assumes no tags.
b.WriteString(`|`)
} else {
break
}
}
}
return b.String(), nil
}