mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-11-10 17:26:10 +08:00
489be2e3dc
Co-authored-by: Tom Limoncelli <tal@whatexit.org>
195 lines
4.8 KiB
Go
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
|
|
}
|