This commit is contained in:
Thomas Limoncelli 2025-12-02 16:45:52 -05:00
parent b8f2167bf9
commit 84898bb0cd
No known key found for this signature in database
8 changed files with 253 additions and 125 deletions

View file

@ -22,8 +22,7 @@ Example:
{
"bind": {
"TYPE": "BIND",
"directory": "myzones",
"filenameformat": "%U.zone"
"directory": "myzones"
}
}
```
@ -85,43 +84,60 @@ DNSControl does not handle special serial number math such as "looping through z
# filenameformat
The `filenameformat` parameter specifies the file name to be used when
writing the zone file. The default (`%U.zone`) is acceptable in most cases: the
writing the zone file. The default (`%c.zone`) is acceptable in most cases: the
file name is the name as specified in the `D()` function plus ".zone".
The filenameformat is a string with a few printf-like `%` verbs:
* The domain name without tag (the `example.com` part of `example.com!tag`):
* `%D` as specified in `D()` (no IDN conversion, but downcased)
* `%I` converted to IDN/Punycode (`xn--...`) and downcased.
* `%N` converted to Unicode (downcased first)
* `%T` the split horizon tag, or "" (the `tag` part of `example.com!tag`)
* `%?x` this returns `x` if the split horizon tag is non-null, otherwise nothing. `x` can be any printable but is usually `!`.
* `%U` short for "%I%?!%T". This is the universal, canonical, name for the domain used for comparisons within DNSControl. This is best for filenames which is why it is used in the default.
* `%%` `%`
* ordinary characters (not `%`) are copied unchanged to the output stream
* FYI: format strings must not end with an incomplete `%` or `%?`
| Verb | Description | `EXAMple.com` | `EXAMple.com!MyTag` | `рф.com!myTag` |
| ------- | ------------------------------------------------- | ------------- | ------------------- | -------------------- |
| `%T` | the tag | `` | `myTag` | `myTag` |
| `%c` | canonical name, globally unique and comparable | `example.com` | `example.com!myTag` | `xn--p1ai.com!myTag` |
| `%a` | ASCII domain (Punycode, downcased) | `example.com` | `example.com` | `xn--p1ai.com` |
| `%u` | Unicode domain (non-Unicode parts downcased) | `example.com` | `example.com` | `рф.com` |
| `%r` | Raw (unmodified) Domain from `D()` (risky!) | `EXAMple.com` | `EXAMple.com` | `рф.com` |
| `%f` | like `%c` but better for filenames (`%a%?_%T`) | `example.com` | `example.com_myTag` | `xn--p1ai.com_myTag` |
| `%F` | like `%f` but reversed order (`%T%?_%a`) | `example.com` | `myTag_example.com` | `myTag_xn--p1ai.com` |
| `%?x` | returns `x` if tag exists, otherwise "" | `` | `x` | `x` |
| `%%` | a literal percent sign | `%` | `%` | `%` |
| `a-Z./` | other printable characters are copied exactly | `a-Z./` | `a-Z./` | `a-Z./` |
| `%U` | (deprecated, use `%c`) Same as `%D%?!%T` (risky!) | `example.com` | `example.com!myTag` | `рф.com!myTag` |
| `%D` | (deprecated, use `%r`) mangles Unicode (risky!) | `example.com` | `example.com` | `рф.com` |
Typical values:
* `%?x` is typically used to generate an optional `!` or `_` if there is a tag.
* `%r` is considered "risky" because it can produce a domain name that is not
canonical. For example, if you use `D("FOO.com")` and later change it to `D("foo.com")`, your file names will change.
* Format strings must not end with an incomplete `%` or `%?`
* Generating a filename without a tag is risky. For example, if the same
`dnsconfig.js` has `D("example.com!inside", DSP_BIND)` and
`D("example.com!outside", DSP_BIND)`, both will use the same filename.
DNSControl will write both zone files to the same file, flapping between the
two. No error or warning will be output.
* `%U.zone` (The default)
* `example.com.zone` or `example.com!tag.zone`
* `%T%?_%I.zone` (optional tag and `_` + domain + `.zone`)
* `tag_example.com.zone` or `example.com.zone`
* `db_%T%?_%D`
* `db_inside_example.com` or `db_example.com`
Compatibility notes:
{% hint style="warning" %}
**Warning** DNSControl will not warn you if two zones generate the same
filename. Instead, each will write to the same place. The content would end up
flapping back and forth between the two. The best way to prevent this is to
always include the tag (`%T`) or use `%U` which includes the tag.
{% endhint %}
* `%D` should not be used. It downcases the string in a way that is probably
incompatible with Unicode characters. It is retained for compatibility with
pre-v4.28 releases. If your domain has capital Unicode characters, backwards
compatibility is not guaranteed.
* `%U` relies on `%D` which is deprecated. Use `%c` instead.
* As of v4.28 the default format string changed from `%U.zone` to `%c.zone`. This
should only matter if your `D()` statements included non-ASCII (Unicode)
runes that were capitalized.
Useful examples:
| Verb | Description | `EXAMple.com` | `EXAMple.com!MyTag` | `рф.com!myTag` |
| ------------ | ----------------------------------- | ------------------ | ------------------------ | ------------------------- |
| `%c.zone` | Default format (v4.28 and later) | `example.com.zone` | `example.com!myTag.zone` | `xn--p1ai.com!myTag.zone` |
| `%U.zone` | Default format (pre-v4.28) (risky!) | `example.com.zone` | `example.com!myTag.zone` | `рф.com!myTag.zone` |
| `db_%f` | Recommended in a popular DNS book | `db_example.com` | `db_example.com_myTag` | `db_xn--p1ai.com_myTag` |
| `db_%a%?_%T` | same as above but using `%?_` | `db_example.com` | `db_example.com_myTag` | `db_xn--p1ai.com_myTag` |
(new in v4.2.0) `dnscontrol push` will create subdirectories along the path to
the filename. This includes both the portion of the path created by the
`directory` setting and the `filenameformat` setting. The automatic creation of
subdirectories is disabled if `dnscontrol` is running as root for security
reasons.
`directory` setting and the `filenameformat` setting. For security reasons, the automatic creation of
subdirectories is disabled if `dnscontrol` is running as root.
# FYI: get-zones
@ -134,5 +150,5 @@ dnscontrol get-zones --format=nameonly - BIND all
If `filenameformat` is defined, `dnscontrol` makes a guess at which
filenames are zones but doesn't try to hard to get it right, which is
mathematically impossible in some cases. Feel free to file an issue if
mathematically impossible to do correctly in all chase. Feel free to file an issue if
your format string doesn't work. I love a challenge!

View file

@ -13,7 +13,7 @@ const (
DomainTag = "dnscontrol_tag" // A copy of DomainConfig.Tag
DomainUniqueName = "dnscontrol_uniquename" // A copy of DomainConfig.UniqueName
DomainNameRaw = "dnscontrol_nameraw" // A copy of DomainConfig.NameRaw
DomainNameIDN = "dnscontrol_nameidn" // A copy of DomainConfig.NameIDN
DomainNameASCII = "dnscontrol_nameascii" // A copy of DomainConfig.NameASCII
DomainNameUnicode = "dnscontrol_nameunicode" // A copy of DomainConfig.NameUnicode
)
@ -71,7 +71,7 @@ func (dc *DomainConfig) PostProcess() {
// Turn the user-supplied name into the fixed forms.
ff := domaintags.MakeDomainFixForms(dc.Name)
dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameIDN, ff.NameUnicode, ff.UniqueName
dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameASCII, ff.NameUnicode, ff.UniqueName
// Store the FixForms is Metadata so we don't have to change the signature of every function that might need them.
// This is a bit ugly but avoids a huge refactor. Please avoid using these to make the future refactor easier.
@ -79,7 +79,7 @@ func (dc *DomainConfig) PostProcess() {
dc.Metadata[DomainTag] = dc.Tag
}
//dc.Metadata[DomainNameRaw] = dc.NameRaw
//dc.Metadata[DomainNameIDN] = dc.Name
//dc.Metadata[DomainNameASCII] = dc.Name
//dc.Metadata[DomainNameUnicode] = dc.NameUnicode
dc.Metadata[DomainUniqueName] = dc.UniqueName
}

View file

@ -9,7 +9,7 @@ import (
// DomainFixedForms stores the various fixed forms of a domain name and tag.
type DomainFixedForms struct {
NameRaw string // "originalinput.com" (name as input by the user, lowercased (no tag))
NameIDN string // "punycode.com"
NameASCII string // "punycode.com"
NameUnicode string // "unicode.com" (converted to downcase BEFORE unicode conversion)
UniqueName string // "punycode.com!tag"
@ -25,7 +25,7 @@ type DomainFixedForms struct {
// * .UniqueName: "example.com!tag" unique across the entire config.
func MakeDomainFixForms(n string) DomainFixedForms {
var err error
var tag, nameRaw, nameIDN, nameUnicode, uniqueName string
var tag, nameRaw, nameASCII, nameUnicode, uniqueName string
var hasBang bool
// Split tag from name.
@ -38,42 +38,45 @@ func MakeDomainFixForms(n string) DomainFixedForms {
hasBang = false
}
nameRaw = strings.ToLower(p[0])
nameRaw = p[0]
if strings.HasPrefix(n, nameRaw) {
// Avoid pointless duplication.
nameRaw = n[0:len(nameRaw)]
}
nameIDN, err = idna.ToASCII(nameRaw)
nameASCII, err = idna.ToASCII(nameRaw)
if err != nil {
nameIDN = nameRaw // Fallback to raw name on error.
nameASCII = nameRaw // Fallback to raw name on error.
} else {
nameASCII = strings.ToLower(nameASCII)
// Avoid pointless duplication.
if nameIDN == nameRaw {
nameIDN = nameRaw
if strings.HasPrefix(n, nameASCII) {
// Avoid pointless duplication.
nameASCII = n[0:len(nameASCII)]
}
}
nameUnicode, err = idna.ToUnicode(nameRaw)
nameUnicode, err = idna.ToUnicode(nameASCII) // We use nameASCII since it is already lowercased.
if err != nil {
nameUnicode = nameRaw // Fallback to raw name on error.
} else {
// Avoid pointless duplication.
if nameUnicode == nameRaw {
nameUnicode = nameRaw
if strings.HasPrefix(n, nameUnicode) {
// Avoid pointless duplication.
nameUnicode = n[0:len(nameUnicode)]
}
}
if hasBang {
uniqueName = nameIDN + "!" + tag
uniqueName = nameASCII + "!" + tag
} else {
uniqueName = nameIDN
uniqueName = nameASCII
}
return DomainFixedForms{
Tag: tag,
NameRaw: nameRaw,
NameIDN: nameIDN,
NameASCII: nameASCII,
NameUnicode: nameUnicode,
UniqueName: uniqueName,
HasBang: hasBang,

View file

@ -10,7 +10,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input string
wantTag string
wantNameRaw string
wantNameIDN string
wantNameASCII string
wantNameUnicode string
wantUniqueName string
wantHasBang bool
@ -20,7 +20,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "example.com",
wantTag: "",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com",
wantHasBang: false,
@ -30,7 +30,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "example.com!mytag",
wantTag: "mytag",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com!mytag",
wantHasBang: true,
@ -40,7 +40,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "example.com!",
wantTag: "",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com!",
wantHasBang: true,
@ -50,7 +50,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "उदाहरण.com",
wantTag: "",
wantNameRaw: "उदाहरण.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com",
wantHasBang: false,
@ -60,7 +60,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "उदाहरण.com!mytag",
wantTag: "mytag",
wantNameRaw: "उदाहरण.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag",
wantHasBang: true,
@ -70,17 +70,29 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "xn--p1b6ci4b4b3a.com",
wantTag: "",
wantNameRaw: "xn--p1b6ci4b4b3a.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com",
wantHasBang: false,
},
{
// Unicode chars should be left alone (as far as case folding goes)
// Here are some Armenian characters https://tools.lgm.cl/lettercase.html
name: "mixed case unicode",
input: "fooԷէԸըԹ.com!myTag",
wantTag: "myTag",
wantNameRaw: "fooԷէԸըԹ.com",
wantNameASCII: "xn--foo-b7dfg43aja.com",
wantNameUnicode: "fooԷէԸըԹ.com",
wantUniqueName: "xn--foo-b7dfg43aja.com!myTag",
wantHasBang: true,
},
{
name: "punycode domain with tag",
input: "xn--p1b6ci4b4b3a.com!mytag",
wantTag: "mytag",
wantNameRaw: "xn--p1b6ci4b4b3a.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag",
wantHasBang: true,
@ -89,8 +101,8 @@ func Test_MakeDomainFixForms(t *testing.T) {
name: "mixed case domain",
input: "Example.COM",
wantTag: "",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameRaw: "Example.COM",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com",
wantHasBang: false,
@ -99,12 +111,34 @@ func Test_MakeDomainFixForms(t *testing.T) {
name: "mixed case domain with tag",
input: "Example.COM!MyTag",
wantTag: "MyTag",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameRaw: "Example.COM",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com!MyTag",
wantHasBang: true,
},
// This is used in the documentation for the BIND provider, thus we test
// it to make sure we got it right.
{
name: "BIND example 1",
input: "рф.com!myTag",
wantTag: "myTag",
wantNameRaw: "рф.com",
wantNameASCII: "xn--p1ai.com",
wantNameUnicode: "рф.com",
wantUniqueName: "xn--p1ai.com!myTag",
wantHasBang: true,
},
{
name: "BIND example 2",
input: "рф.com!myTag",
wantTag: "myTag",
wantNameRaw: "рф.com",
wantNameASCII: "xn--p1ai.com",
wantNameUnicode: "рф.com",
wantUniqueName: "xn--p1ai.com!myTag",
wantHasBang: true,
},
}
for _, tt := range tests {
@ -116,8 +150,8 @@ func Test_MakeDomainFixForms(t *testing.T) {
if got.NameRaw != tt.wantNameRaw {
t.Errorf("MakeDomainFixForms() gotNameRaw = %v, want %v", got.NameRaw, tt.wantNameRaw)
}
if got.NameIDN != tt.wantNameIDN {
t.Errorf("MakeDomainFixForms() gotNameIDN = %v, want %v", got.NameIDN, tt.wantNameIDN)
if got.NameASCII != tt.wantNameASCII {
t.Errorf("MakeDomainFixForms() gotNameASCII = %v, want %v", got.NameASCII, tt.wantNameASCII)
}
if got.NameUnicode != tt.wantNameUnicode {
t.Errorf("MakeDomainFixForms() gotNameUnicode = %v, want %v", got.NameUnicode, tt.wantNameUnicode)

View file

@ -24,8 +24,8 @@ func CompilePermitList(s string) PermitList {
continue
}
ff := MakeDomainFixForms(l)
if ff.HasBang && ff.NameIDN == "" { // Treat empty name as wildcard.
ff.NameIDN = "*"
if ff.HasBang && ff.NameASCII == "" { // Treat empty name as wildcard.
ff.NameASCII = "*"
}
sl.items = append(sl.items, ff)
}
@ -65,24 +65,24 @@ func (pl *PermitList) Permitted(domToCheck string) bool {
// Now that we know the tag matches, we can focus on the name.
// `*!tag` or `*` matches everything.
if filterItem.NameIDN == "*" {
if filterItem.NameASCII == "*" {
return true
}
// If the name starts with "*." then match the suffix.
if strings.HasPrefix(filterItem.NameIDN, "*.") {
if strings.HasPrefix(filterItem.NameASCII, "*.") {
// example.com matches *.example.com
if domToCheckFF.NameIDN == filterItem.NameIDN[2:] || domToCheckFF.NameUnicode == filterItem.NameUnicode[2:] {
if domToCheckFF.NameASCII == filterItem.NameASCII[2:] || domToCheckFF.NameUnicode == filterItem.NameUnicode[2:] {
return true
}
// foo.example.com matches *.example.com
if strings.HasSuffix(domToCheckFF.NameIDN, filterItem.NameIDN[1:]) || strings.HasSuffix(domToCheckFF.NameUnicode, filterItem.NameUnicode[1:]) {
if strings.HasSuffix(domToCheckFF.NameASCII, filterItem.NameASCII[1:]) || strings.HasSuffix(domToCheckFF.NameUnicode, filterItem.NameUnicode[1:]) {
return true
}
}
// No wildcards? Exact match.
if filterItem.NameIDN == domToCheckFF.NameIDN || filterItem.NameUnicode == domToCheckFF.NameUnicode {
if filterItem.NameASCII == domToCheckFF.NameASCII || filterItem.NameUnicode == domToCheckFF.NameUnicode {
return true
}
}

View file

@ -69,7 +69,7 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
api.directory = "zones"
}
if api.filenameformat == "" {
api.filenameformat = "%U.zone"
api.filenameformat = "%c.zone"
}
if len(providermeta) != 0 {
err := json.Unmarshal(providermeta, api)
@ -171,7 +171,7 @@ func (c *bindProvider) GetZoneRecords(domain string, meta map[string]string) (mo
ff := domaintags.DomainFixedForms{
Tag: meta[models.DomainTag],
NameRaw: meta[models.DomainNameRaw],
NameIDN: domain,
NameASCII: domain,
NameUnicode: meta[models.DomainNameUnicode],
UniqueName: meta[models.DomainUniqueName],
}
@ -181,9 +181,9 @@ func (c *bindProvider) GetZoneRecords(domain string, meta map[string]string) (mo
ff,
),
)
//fmt.Printf("DEBUG: Reading zonefile %q\n", zonefile)
//fmt.Printf("DEBUG: Meta %+v\n", meta)
//fmt.Printf("DEBUG: Domain Names %+v\n", ff)
fmt.Printf("DEBUG: Reading zonefile %q\n", zonefile)
fmt.Printf("DEBUG: Meta %+v\n", meta)
fmt.Printf("DEBUG: Domain Names %+v\n", ff)
content, err := os.ReadFile(zonefile)
if os.IsNotExist(err) {
@ -282,7 +282,7 @@ func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundR
domaintags.DomainFixedForms{
Tag: dc.Tag,
NameRaw: dc.NameRaw,
NameIDN: dc.Name,
NameASCII: dc.Name,
NameUnicode: dc.NameUnicode,
UniqueName: dc.UniqueName,
},

View file

@ -13,11 +13,6 @@ import (
// makeFileName uses format to generate a zone's filename. See the
func makeFileName(format string, ff domaintags.DomainFixedForms) string {
//fmt.Printf("DEBUG: makeFileName(%q, %+v)\n", format, ff)
nameRaw := ff.NameRaw
nameIDN := ff.NameIDN
nameUnicode := ff.NameUnicode
uniquename := ff.UniqueName
tag := ff.Tag
if format == "" {
panic("BUG: makeFileName called with null format")
}
@ -40,16 +35,31 @@ func makeFileName(format string, ff domaintags.DomainFixedForms) string {
pos++
tok = tokens[pos]
switch tok {
case "D":
b.WriteString(nameRaw)
case "T":
b.WriteString(tag)
case "U":
b.WriteString(uniquename)
case "I":
b.WriteString(nameIDN)
case "N":
b.WriteString(nameUnicode)
// v4.28 names
case "r": // NameRaw "originalinput.com" (i for input)
b.WriteString(ff.NameRaw)
case "a": // NameASCII "punycode.com" (a for ascii)
b.WriteString(ff.NameASCII)
case "u": // NameUnicode "unicode.com" (u for unicode)
b.WriteString(ff.NameUnicode)
case "c": // UniqueName "punycode.com!tag" or "punycode.com" if no tag (c for canonical)
b.WriteString(ff.UniqueName)
case "f": //
b.WriteString(ff.NameASCII)
if ff.Tag != "" {
b.WriteString("_")
b.WriteString(ff.Tag)
}
case "F": //
if ff.Tag != "" {
b.WriteString(ff.Tag)
b.WriteString("_")
}
b.WriteString(ff.NameASCII)
case "T": // Tag The tag portion of `example.com!tag`
b.WriteString(ff.Tag)
case "%":
b.WriteString("%")
case "?":
@ -59,9 +69,20 @@ func makeFileName(format string, ff domaintags.DomainFixedForms) string {
}
pos++
tok = tokens[pos]
if tag != "" {
if ff.Tag != "" {
b.WriteString(tok)
}
// Legacy names kept for compatibility
case "U": // the domain name as specified in `D()`
b.WriteString(strings.ToLower(ff.NameRaw))
if ff.Tag != "" {
b.WriteString("!")
b.WriteString(ff.Tag)
}
case "D": // domain (without tag) as specified in D() (no IDN conversion, but downcased)
b.WriteString(strings.ToLower(ff.NameRaw))
default:
fmt.Fprintf(&b, "%%(unknown %%verb %%%s)", tok)
}
@ -156,14 +177,14 @@ func makeExtractor(format string) (string, error) {
pos++
tok = tokens[pos]
switch tok {
case "D":
case "D", "a", "u", "r":
b.WriteString(`(.*)`)
case "T":
if pass == 0 {
// On the second pass, nothing is generated.
b.WriteString(`.*`)
}
case "U":
case "U", "c":
if pass == 0 {
b.WriteString(`(.*)!.+`)
} else {

View file

@ -1,6 +1,7 @@
package bind
import (
"fmt"
"reflect"
"testing"
@ -8,29 +9,28 @@ import (
)
func Test_makeFileName(t *testing.T) {
ff := domaintags.DomainFixedForms{
NameRaw: "raw",
NameIDN: "idn",
NameUnicode: "unicode",
UniqueName: "unique!taga",
Tag: "tagb",
}
tagless := domaintags.DomainFixedForms{
NameRaw: "raw",
NameIDN: "idn",
NameUnicode: "unicode",
UniqueName: "unique",
Tag: "",
}
fmtDefault := "%U.zone"
fmtBasic := "%U - %T - %D"
fmtBk1 := "db_%U" // Something I've seen in books on DNS
fmtBk2 := "db_%T_%D" // Something I've seen in books on DNS
fmtFancy := "%T%?_%D.zone" // Include the tag_ only if there is a tag
fmtErrorPct := "literal%"
fmtErrorOpt := "literal%?"
fmtErrorUnk := "literal%o" // Unknown % verb
ff := domaintags.DomainFixedForms{
NameRaw: "domy",
NameASCII: "idn",
NameUnicode: "uni",
UniqueName: "unique!taga",
Tag: "tagy",
}
tagless := domaintags.DomainFixedForms{
NameRaw: "domy",
NameASCII: "idn",
NameUnicode: "uni",
UniqueName: "unique",
Tag: "",
}
type args struct {
format string
ff domaintags.DomainFixedForms
@ -40,25 +40,15 @@ func Test_makeFileName(t *testing.T) {
args args
want string
}{
// Test corner cases and common cases.
{"literal", args{"literal", ff}, "literal"},
{"middle", args{"mid%Dle", ff}, "midrawle"},
{"D", args{"%D", ff}, "raw"},
{"I", args{"%I", ff}, "idn"},
{"N", args{"%N", ff}, "unicode"},
{"T", args{"%T", ff}, "tagb"},
{"x1", args{"XX%?xYY", ff}, "XXxYY"},
{"x2", args{"AA%?xBB", tagless}, "AABB"},
{"U", args{"%U", ff}, "unique!taga"},
{"percent", args{"%%", ff}, "%"},
//
{"default", args{fmtDefault, ff}, "unique!taga.zone"},
{"basic", args{fmtBasic, ff}, "unique!taga - tagb - raw"},
{"front", args{"%Daaa", ff}, "rawaaa"},
{"tail", args{"bbb%D", ff}, "bbbraw"},
{"bk1", args{fmtBk1, ff}, "db_unique!taga"},
{"bk2", args{fmtBk2, ff}, "db_tagb_raw"},
{"fanWI", args{fmtFancy, ff}, "tagb_raw.zone"},
{"fanWO", args{fmtFancy, tagless}, "raw.zone"},
{"basic", args{fmtBasic, ff}, "domy!tagy - tagy - domy"},
{"solo", args{"%D", ff}, "domy"},
{"front", args{"%Daaa", ff}, "domyaaa"},
{"tail", args{"bbb%D", ff}, "bbbdomy"},
{"def", args{fmtDefault, ff}, "domy!tagy.zone"},
{"fanWI", args{fmtFancy, ff}, "tagy_domy.zone"},
{"fanWO", args{fmtFancy, tagless}, "domy.zone"},
{"errP", args{fmtErrorPct, ff}, "literal%(format may not end in %)"},
{"errQ", args{fmtErrorOpt, ff}, "literal%(format may not end in %?)"},
{"errU", args{fmtErrorUnk, ff}, "literal%(unknown %verb %o)"},
@ -73,6 +63,66 @@ func Test_makeFileName(t *testing.T) {
}
}
func Test_makeFileName_2(t *testing.T) {
ff1 := domaintags.MakeDomainFixForms(`EXAMple.com`)
ff2 := domaintags.MakeDomainFixForms(`EXAMple.com!myTag`)
ff3 := domaintags.MakeDomainFixForms(`рф.com!myTag`)
tests := []struct {
name string
format string
want1 string
want2 string
want3 string
descr string
}{
// NOTE: "Domain" in these descriptions means the domain name without any split horizon tag. Technically the "Zone".
{`T`, `%T`, ``, `myTag`, `myTag`, `the tag`},
{`c`, `%c`, `example.com`, `example.com!myTag`, `xn--p1ai.com!myTag`, `canonical name, globally unique and comparable`},
{`a`, `%a`, `example.com`, `example.com`, `xn--p1ai.com`, `ASCII domain (Punycode, downcased)`},
{`u`, `%u`, `example.com`, `example.com`, `рф.com`, `Unicode domain (non-Unicode parts downcased)`},
{`r`, `%r`, `EXAMple.com`, `EXAMple.com`, `рф.com`, "Raw (unmodified) Domain from `D()` (risky!)"},
{`f`, `%f`, `example.com`, `example.com_myTag`, `xn--p1ai.com_myTag`, "like `%c` but better for filenames (`%a%?_%T`)"},
{`F`, `%F`, `example.com`, `myTag_example.com`, `myTag_xn--p1ai.com`, "like `%f` but reversed order (`%T%?_%a`)"},
{`%?x`, `%?x`, ``, `x`, `x`, "returns `x` if tag exists, otherwise \"\""},
{`%`, `%%`, `%`, `%`, `%`, `a literal percent sign`},
// Pre-v4.28 names kept for compatibility (note: pre v4.28 did not permit mixed case domain names, we downcased them here for the tests)
{`U`, `%U`, `example.com`, `example.com!myTag`, `рф.com!myTag`, "(deprecated, use `%c`) Same as `%D%?!%T` (risky!)"},
{`D`, `%D`, `example.com`, `example.com`, `рф.com`, "(deprecated, use `%r`) mangles Unicode (risky!)"},
{`%T%?_%D.zone`, `%T%?_%D.zone`, `example.com.zone`, `myTag_example.com.zone`, `myTag_рф.com.zone`, `mentioned in the docs`},
{`db_%T%?_%D`, `db_%T%?_%D`, `db_example.com`, `db_myTag_example.com`, `db_myTag_рф.com`, `mentioned in the docs`},
{`db_%D`, `db_%D`, `db_example.com`, `db_example.com`, `db_рф.com`, `mentioned in the docs`},
// Examples used in the documentation for the BIND provider
{`%c.zone`, `%c.zone`, "example.com.zone", "example.com!myTag.zone", "xn--p1ai.com!myTag.zone", "Default format (v4.28 and later)"},
{`%U.zone`, `%U.zone`, `example.com.zone`, `example.com!myTag.zone`, `рф.com!myTag.zone`, "Default format (pre-v4.28) (risky!)"},
{`db_%f`, `db_%f`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "Recommended in a popular DNS book"},
{`db_a%?_%T`, `db_%a%?_%T`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "same as above but using `%?_`"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got1 := makeFileName(tt.format, ff1)
if got1 != tt.want1 {
t.Errorf("makeFileName(%q) = ff1 %q, want %q", tt.format, got1, tt.want1)
}
got2 := makeFileName(tt.format, ff2)
if got2 != tt.want2 {
t.Errorf("makeFileName(%q) = ff2 %q, want %q", tt.format, got2, tt.want2)
}
got3 := makeFileName(tt.format, ff3)
if got3 != tt.want3 {
t.Errorf("makeFileName(%q) = ff3 %q, want %q", tt.format, got3, tt.want3)
}
// Uncomment to regenerate the table in the docs:
fmt.Printf("MD | `%s` | %s | `%s` | `%s` | `%s` |\n", tt.format, tt.descr, got1, got2, got3)
// Uncomment to regenerate the above test cases:
//fmt.Printf("{`%s`, `%s`, `%s`, `%s`, `%s`, %q},\n", tt.name, tt.format, got1, got2, got3, tt.descr)
})
}
}
func Test_makeExtractor(t *testing.T) {
type args struct {
format string
@ -83,6 +133,10 @@ func Test_makeExtractor(t *testing.T) {
want string
wantErr bool
}{
{"c", args{"%c.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
{"a", args{"%a.zone"}, `(.*)\.zone`, false},
{"u", args{"%u.zone"}, `(.*)\.zone`, false},
{"r", args{"%r.zone"}, `(.*)\.zone`, false},
// TODO: Add test cases.
{"u", args{"%U.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
{"d", args{"%D.zone"}, `(.*)\.zone`, false},