Repository: StackExchange/dnscontrol
Pull request #688 (2020-03-09 19:29:36)

get-zones output should work as input into preview
Something like this should work:

```
dnscontrol get-zone --format=js -out=foo.js bind BIND example.org 
dnscontrol preview --config foo.js
```

TTLs for NS records don't make it unharmed.  However, we're in a better situation than before.


Patch:
diff --git a/commands/getZones.go b/commands/getZones.go
index 1d7e1dd6..21384b77 100644
--- a/commands/getZones.go
+++ b/commands/getZones.go
@@ -38,17 +38,27 @@ ARGUMENTS:
    zone:     One or more zones (domains) to download; or "all".
 
 FORMATS:
-   --format=js        dnsconfig.js format (not perfect, but a decent first draft)
+   --format=js        dnsconfig.js format (not perfect, just a decent first draft)
+   --format=djs       js with disco commas
    --format=zone      BIND Zonefile format
    --format=tsv       TAB separated value (useful for AWK)
    --format=nameonly  Just print the zone names
 
+The columns in --format=tsv are:
+   FQDN (the label with the domain)
+   ShortName (just the label, "@" if it is the naked domain)
+   TTL
+   Record Type (A, AAAA, CNAME, etc.)
+   Target and arguments (quoted like in a zonefile)
+
+The --ttl flag only applies to zone/js/djs formats.
+
 EXAMPLES:
    dnscontrol get-zones myr53 ROUTE53 example.com
    dnscontrol get-zones gmain GANDI_V5 example.comn other.com
    dnscontrol get-zones cfmain CLOUDFLAREAPI all
    dnscontrol get-zones -format=tsv bind BIND example.com
-   dnscontrol get-zones -format=js -out=draft.js glcoud GCLOUD example.com`,
+   dnscontrol get-zones -format=djs -out=draft.js glcoud GCLOUD example.com`,
 	}
 }())
 
@@ -105,7 +115,7 @@ func (args *GetZoneArgs) flags() []cli.Flag {
 		Name:        "format",
 		Destination: &args.OutputFormat,
 		Value:       "zone",
-		Usage:       `Output format: js zone tsv nameonly`,
+		Usage:       `Output format: js djs zone tsv nameonly`,
 	})
 	flags = append(flags, &cli.StringFlag{
 		Name:        "out",
@@ -165,7 +175,7 @@ func GetZone(args GetZoneArgs) error {
 		return nil
 	}
 
-	// actually fetch all of the records
+	// fetch all of the records
 	zoneRecs := make([]models.Records, len(zones))
 	for i, zone := range zones {
 		recs, err := provider.GetZoneRecords(zone)
@@ -175,14 +185,15 @@ func GetZone(args GetZoneArgs) error {
 		zoneRecs[i] = recs
 	}
 
-	// Write it out:
+	// Write the heading:
 
-	if args.OutputFormat == "js" {
+	if args.OutputFormat == "js" || args.OutputFormat == "djs" {
 		fmt.Fprintf(w, `var %s = NewDnsProvider("%s", "%s");`+"\n",
 			args.CredName, args.CredName, args.ProviderName)
+		fmt.Fprintf(w, `var REG_CHANGEME = NewRegistrar("ThirdParty", "NONE");`+"\n")
 	}
 
-	// now print all zones
+	// print each zone
 	for i, recs := range zoneRecs {
 		zoneName := zones[i]
 
@@ -194,19 +205,26 @@ func GetZone(args GetZoneArgs) error {
 			prettyzone.WriteZoneFileRC(w, z.Records, zoneName, uint32(args.DefaultTTL), nil)
 			fmt.Fprintln(w)
 
-		case "js":
-			fmt.Fprintf(w, `D("%s", REG_CHANGEME,`, zoneName)
-			fmt.Fprintf(w, "\n\tDnsProvider(%s)", args.CredName)
+		case "js", "djs":
+			sep := ",\n\t" // Commas at EOL
+			if args.OutputFormat == "djs" {
+				sep = "\n\t, " // Funky comma mode
+			}
+			fmt.Fprintf(w, `D("%s", REG_CHANGEME%s`, zoneName, sep)
+			var o []string
+			o = append(o, fmt.Sprintf("DnsProvider(%s)", args.CredName))
 			defaultTTL := uint32(args.DefaultTTL)
 			if defaultTTL == 0 {
 				defaultTTL = prettyzone.MostCommonTTL(recs)
 			}
 			if defaultTTL != models.DefaultTTL && defaultTTL != 0 {
-				fmt.Fprintf(w, "\n\tDefaultTTL(%d)", defaultTTL)
+				o = append(o, fmt.Sprintf("DefaultTTL(%d)", defaultTTL))
 			}
 			for _, rec := range recs {
-				fmt.Fprint(w, formatDsl(zoneName, rec, defaultTTL))
+				o = append(o, formatDsl(zoneName, rec, defaultTTL))
 			}
+			out := strings.Join(o, sep)
+			fmt.Fprint(w, strings.ReplaceAll(out, "\n\t, //", "\n\t//, "))
 			fmt.Fprint(w, "\n)\n")
 
 		case "tsv":
@@ -233,23 +251,47 @@ func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) str
 	}
 
 	switch rec.Type { // #rtype_variations
+	case "CAA":
+		return makeCaa(rec, ttlop)
 	case "MX":
 		target = fmt.Sprintf("%d, '%s'", rec.MxPreference, rec.GetTargetField())
+	case "SSHFP":
+		target = fmt.Sprintf("%d, %d, '%s'", rec.SshfpAlgorithm, rec.SshfpFingerprint, rec.GetTargetField())
 	case "SOA":
+		rec.Type = "//SOA"
+		target = fmt.Sprintf("'%s', '%s', %d, %d, %d, %d, %d", rec.GetTargetField(), rec.SoaMbox, rec.SoaSerial, rec.SoaRefresh, rec.SoaRetry, rec.SoaExpire, rec.SoaMinttl)
+	case "SRV":
+		target = fmt.Sprintf("%d, %d, %d, '%s'", rec.SrvPriority, rec.SrvWeight, rec.SrvPort, rec.GetTargetField())
+	case "TLSA":
+		target = fmt.Sprintf("%d, %d, %d, '%s'", rec.TlsaUsage, rec.TlsaSelector, rec.TlsaMatchingType, rec.GetTargetField())
 	case "TXT":
 		if len(rec.TxtStrings) == 1 {
 			target = `'` + rec.TxtStrings[0] + `'`
 		} else {
 			target = `['` + strings.Join(rec.TxtStrings, `', '`) + `']`
 		}
+		// TODO(tlim): If this is an SPF record, generate a SPF_BUILDER().
 	case "NS":
 		// NS records at the apex should be NAMESERVER() records.
 		if rec.Name == "@" {
-			return fmt.Sprintf(",\n\tNAMESERVER('%s')", target)
+			return fmt.Sprintf("NAMESERVER('%s')", target)
 		}
+		target = "'" + target + "'"
 	default:
 		target = "'" + target + "'"
 	}
 
-	return fmt.Sprintf(",\n\t%s('%s', %s%s)", rec.Type, rec.Name, target, ttlop)
+	return fmt.Sprintf("%s('%s', %s%s)", rec.Type, rec.Name, target, ttlop)
+}
+
+func makeCaa(rec *models.RecordConfig, ttlop string) string {
+	var target string
+	if rec.CaaFlag == 128 {
+		target = fmt.Sprintf("'%s', '%s', CAA_CRITICAL", rec.CaaTag, rec.GetTargetField())
+	} else {
+		target = fmt.Sprintf("'%s', '%s'", rec.CaaTag, rec.GetTargetField())
+	}
+	return fmt.Sprintf("%s('%s', %s%s)", rec.Type, rec.Name, target, ttlop)
+
+	// TODO(tlim): Generate a CAA_BUILDER() instead?
 }
diff --git a/docs/get-zones.md b/docs/get-zones.md
index bb71e8f3..2bf07bc3 100644
--- a/docs/get-zones.md
+++ b/docs/get-zones.md
@@ -19,7 +19,7 @@ Syntax:
    dnscontrol get-zones [command options] credkey provider zone [...]
 
    --creds value   Provider credentials JSON file (default: "creds.json")
-   --format value  Output format: js zone tsv nameonly (default: "zone")
+   --format value  Output format: js djs zone tsv nameonly (default: "zone")
    --out value     Instead of stdout, write to this file
    --ttl value     Default TTL (0 picks the zone's most common TTL) (default: 0)
 
@@ -29,31 +29,34 @@ ARGUMENTS:
    zone:     One or more zones (domains) to download; or "all".
 
 FORMATS:
-   --format=js        dnsconfig.js format (not perfect, but a decent first draft)
+   --format=js        dnsconfig.js format (not perfect, just a decent first draft)
+   --format=djs       js with disco commas
    --format=zone      BIND Zonefile format
    --format=tsv       TAB separated value (useful for AWK)
    --format=nameonly  Just print the zone names
 
-When using `tsv`, the columns are:
+The columns in --format=tsv are:
    FQDN (the label with the domain)
    ShortName (just the label, "@" if it is the naked domain)
    TTL
    Record Type (A, AAAA, CNAME, etc.)
    Target and arguments (quoted like in a zonefile)
 
-The --ttl flag applies to zone and js formats.
+The --ttl flag only applies to zone/js/djs formats.
 
 EXAMPLES:
    dnscontrol get-zones myr53 ROUTE53 example.com
    dnscontrol get-zones gmain GANDI_V5 example.comn other.com
    dnscontrol get-zones cfmain CLOUDFLAREAPI all
    dnscontrol get-zones -format=tsv bind BIND example.com
-   dnscontrol get-zones -format=js -out=draft.js glcoud GCLOUD example.com`,
+   dnscontrol get-zones -format=djs -out=draft.js glcoud GCLOUD example.com`,
 
+Read a zonefile, generate a JS file, then use the JS file to see how
+different it is from the zonefile:
 
-# Example commands
+   dnscontrol get-zone --format=js -out=foo.djs bind BIND example.org 
+   dnscontrol preview --config foo.js
 
-dnscontrol get-zone
 
 # Developer Note
 
diff --git a/go.mod b/go.mod
index 4a8f9d85..e3114ffc 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
 	github.com/Azure/go-autorest/autorest/to v0.3.0
 	github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55
 	github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
+	github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
 	github.com/aws/aws-sdk-go v1.29.1
 	github.com/billputer/go-namecheap v0.0.0-20170915210158-0c7adb0710f8
 	github.com/cenkalti/backoff v2.1.1+incompatible // indirect
@@ -21,6 +22,7 @@ require (
 	github.com/go-acme/lego v2.7.2+incompatible
 	github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe
 	github.com/golang/protobuf v1.3.3 // indirect
+	github.com/google/go-cmp v0.4.0 // indirect
 	github.com/google/go-github v17.0.0+incompatible
 	github.com/google/go-querystring v1.0.1-0.20190318165438-c8c88dbee036 // indirect
 	github.com/google/uuid v1.1.2-0.20190416172445-c2e93f3ae59f
@@ -29,7 +31,6 @@ require (
 	github.com/hexonet/go-sdk v2.2.3+incompatible
 	github.com/jarcoal/httpmock v1.0.4 // indirect
 	github.com/kolo/xmlrpc v0.0.0-20150413191830-0826b98aaa29 // indirect
-	github.com/kr/pretty v0.1.0 // indirect
 	github.com/malexdev/utfutil v0.0.0-20180510171754-00c8d4a8e7a8 // indirect
 	github.com/miekg/dns v1.1.27
 	github.com/mjibson/esc v0.2.0
@@ -39,6 +40,7 @@ require (
 	github.com/prasmussen/gandi-api v0.0.0-20180224132202-58d3d4205661
 	github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 // indirect
 	github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff
+	github.com/sergi/go-diff v1.1.0 // indirect
 	github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect
 	github.com/softlayer/softlayer-go v0.0.0-20170804160555-5e1c8cccc730
 	github.com/stretchr/testify v1.4.0
@@ -49,15 +51,12 @@ require (
 	github.com/urfave/cli/v2 v2.1.1
 	github.com/vultr/govultr v0.2.0
 	golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d // indirect
-	golang.org/x/mod v0.2.0 // indirect
 	golang.org/x/net v0.0.0-20200226121028-0de0cce0169b
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
 	golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c // indirect
 	golang.org/x/tools v0.0.0-20200303202040-658b03bcd3d8 // indirect
-	golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
 	google.golang.org/api v0.17.0
 	google.golang.org/appengine v1.6.5 // indirect
-	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gopkg.in/ini.v1 v1.42.0 // indirect
 	gopkg.in/ns1/ns1-go.v2 v2.0.0-20170502175150-c563826f4cbe
 	gopkg.in/sourcemap.v1 v1.0.5 // indirect
diff --git a/go.sum b/go.sum
index 4523a686..4ebfed1d 100644
--- a/go.sum
+++ b/go.sum
@@ -40,6 +40,8 @@ github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d/go.mod h1:WML6KO
 github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
+github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/aws/aws-sdk-go v1.29.1 h1:U2vZ5WprhGAMjzb4bKVzl2QecUtZFW2BXVqa5bnd+OY=
@@ -100,6 +102,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -212,6 +216,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -275,8 +281,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -292,6 +296,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -331,8 +336,6 @@ golang.org/x/tools v0.0.0-20190624190245-7f2218787638 h1:uIfBkD8gLczr4XDgYpt/qJY
 golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212213243-2ee7536ab1cc h1:AlqcddMxhvJRMNuW1bFwyuNdm+TCkcEGKvw6FVPTuR8=
-golang.org/x/tools v0.0.0-20200212213243-2ee7536ab1cc/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200303202040-658b03bcd3d8 h1:2oWBslXKHx1vReVc1bA4mflTOcSxgXz536kSRBhwDds=
 golang.org/x/tools v0.0.0-20200303202040-658b03bcd3d8/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -373,8 +376,8 @@ google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
 gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ns1/ns1-go.v2 v2.0.0-20170502175150-c563826f4cbe h1:fuu3vZ8C6O8mk8Ich8YfkDv/Zpnx1HUotQk8JocBcSw=
@@ -385,6 +388,7 @@ gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4
 gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/vendor/github.com/andreyvit/diff/.gitignore b/vendor/github.com/andreyvit/diff/.gitignore
new file mode 100644
index 00000000..daf913b1
--- /dev/null
+++ b/vendor/github.com/andreyvit/diff/.gitignore
@@ -0,0 +1,24 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
diff --git a/vendor/github.com/andreyvit/diff/LICENSE b/vendor/github.com/andreyvit/diff/LICENSE
new file mode 100644
index 00000000..05b98e1f
--- /dev/null
+++ b/vendor/github.com/andreyvit/diff/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Andrey Tarantsov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/andreyvit/diff/README.md b/vendor/github.com/andreyvit/diff/README.md
new file mode 100644
index 00000000..5b52fea8
--- /dev/null
+++ b/vendor/github.com/andreyvit/diff/README.md
@@ -0,0 +1,28 @@
+# diff
+
+Quick'n'easy string diffing functions for Golang based on [github.com/sergi/go-diff](https://github.com/sergi/go-diff). Mainly for diffing strings in tests.
+
+See [the docs on GoDoc](https://godoc.org/github.com/andreyvit/diff).
+
+Get it:
+
+    go get -u github.com/andreyvit/diff
+
+Example:
+
+    import (
+        "strings"
+        "testing"
+        "github.com/andreyvit/diff"
+    )
+
+    const expected = `
+    ...
+    `
+
+    func TestFoo(t *testing.T) {
+        actual := Foo(...)
+        if a, e := strings.TrimSpace(actual), strings.TrimSpace(expected); a != e {
+            t.Errorf("Result not as expected:\n%v", diff.LineDiff(e, a))
+        }
+    }
diff --git a/vendor/github.com/andreyvit/diff/diff.go b/vendor/github.com/andreyvit/diff/diff.go
new file mode 100644
index 00000000..12953c83
--- /dev/null
+++ b/vendor/github.com/andreyvit/diff/diff.go
@@ -0,0 +1,128 @@
+package diff
+
+import (
+	"bytes"
+	"strings"
+
+	"github.com/sergi/go-diff/diffmatchpatch"
+)
+
+func diff(a, b string) []diffmatchpatch.Diff {
+	dmp := diffmatchpatch.New()
+	diffs := dmp.DiffMain(a, b, true)
+	if len(diffs) > 2 {
+		diffs = dmp.DiffCleanupSemantic(diffs)
+		diffs = dmp.DiffCleanupEfficiency(diffs)
+	}
+	return diffs
+}
+
+// CharacterDiff returns an inline diff between the two strings, using (++added++) and (~~deleted~~) markup.
+func CharacterDiff(a, b string) string {
+	return diffsToString(diff(a, b))
+}
+
+func diffsToString(diffs []diffmatchpatch.Diff) string {
+	var buff bytes.Buffer
+	for _, diff := range diffs {
+		text := diff.Text
+		switch diff.Type {
+		case diffmatchpatch.DiffInsert:
+			buff.WriteString("(++")
+			buff.WriteString(text)
+			buff.WriteString("++)")
+		case diffmatchpatch.DiffDelete:
+			buff.WriteString("(~~")
+			buff.WriteString(text)
+			buff.WriteString("~~)")
+		case diffmatchpatch.DiffEqual:
+			buff.WriteString(text)
+		}
+	}
+	return buff.String()
+}
+
+// LineDiff returns a normal linewise diff between the two given strings.
+func LineDiff(a, b string) string {
+	return strings.Join(LineDiffAsLines(a, b), "\n")
+}
+
+// LineDiffAsLines returns the lines of a linewise diff between the two given strings.
+func LineDiffAsLines(a, b string) []string {
+	return diffsToPatchLines(diff(a, b))
+}
+
+type patchBuilder struct {
+	output        []string
+	oldLines      []string
+	newLines      []string
+	newLineBuffer bytes.Buffer
+	oldLineBuffer bytes.Buffer
+}
+
+func (b *patchBuilder) AddCharacters(text string, op diffmatchpatch.Operation) {
+	if op == diffmatchpatch.DiffInsert || op == diffmatchpatch.DiffEqual {
+		b.newLineBuffer.WriteString(text)
+	}
+	if op == diffmatchpatch.DiffDelete || op == diffmatchpatch.DiffEqual {
+		b.oldLineBuffer.WriteString(text)
+	}
+}
+func (b *patchBuilder) AddNewline(op diffmatchpatch.Operation) {
+	oldLine := b.oldLineBuffer.String()
+	newLine := b.newLineBuffer.String()
+
+	if op == diffmatchpatch.DiffEqual && (oldLine == newLine) {
+		b.FlushChunk()
+		b.output = append(b.output, " "+newLine)
+		b.oldLineBuffer.Reset()
+		b.newLineBuffer.Reset()
+	} else {
+		if op == diffmatchpatch.DiffDelete || op == diffmatchpatch.DiffEqual {
+			b.oldLines = append(b.oldLines, "-"+oldLine)
+			b.oldLineBuffer.Reset()
+		}
+		if op == diffmatchpatch.DiffInsert || op == diffmatchpatch.DiffEqual {
+			b.newLines = append(b.newLines, "+"+newLine)
+			b.newLineBuffer.Reset()
+		}
+	}
+}
+func (b *patchBuilder) FlushChunk() {
+	if b.oldLines != nil {
+		b.output = append(b.output, b.oldLines...)
+		b.oldLines = nil
+	}
+	if b.newLines != nil {
+		b.output = append(b.output, b.newLines...)
+		b.newLines = nil
+	}
+}
+func (b *patchBuilder) Flush() {
+	if b.oldLineBuffer.Len() > 0 && b.newLineBuffer.Len() > 0 {
+		b.AddNewline(diffmatchpatch.DiffEqual)
+	} else if b.oldLineBuffer.Len() > 0 {
+		b.AddNewline(diffmatchpatch.DiffDelete)
+	} else if b.newLineBuffer.Len() > 0 {
+		b.AddNewline(diffmatchpatch.DiffInsert)
+	}
+	b.FlushChunk()
+}
+
+func diffsToPatchLines(diffs []diffmatchpatch.Diff) []string {
+	b := new(patchBuilder)
+	b.output = make([]string, 0, len(diffs))
+
+	for _, diff := range diffs {
+		lines := strings.Split(diff.Text, "\n")
+		for idx, line := range lines {
+			if idx > 0 {
+				b.AddNewline(diff.Type)
+			}
+			b.AddCharacters(line, diff.Type)
+		}
+	}
+
+	b.Flush()
+	return b.output
+}
diff --git a/vendor/github.com/andreyvit/diff/doc.go b/vendor/github.com/andreyvit/diff/doc.go
new file mode 100644
index 00000000..98e24708
--- /dev/null
+++ b/vendor/github.com/andreyvit/diff/doc.go
@@ -0,0 +1,2 @@
+// diff provides quick and easy string diffing functions based on github.com/sergi/go-diff, mainly for diffing strings in tests
+package diff
diff --git a/vendor/github.com/andreyvit/diff/trim.go b/vendor/github.com/andreyvit/diff/trim.go
new file mode 100644
index 00000000..87871f99
--- /dev/null
+++ b/vendor/github.com/andreyvit/diff/trim.go
@@ -0,0 +1,19 @@
+package diff
+
+import (
+	"strings"
+)
+
+// TrimLines applies TrimSpace to each string in the given array.
+func TrimLines(input []string) []string {
+	result := make([]string, 0, len(input))
+	for _, el := range input {
+		result = append(result, strings.TrimSpace(el))
+	}
+	return result
+}
+
+// TrimLinesInString applies TrimSpace to each line in the given string, and returns the new trimmed string. Empty lines are not removed.
+func TrimLinesInString(input string) string {
+	return strings.Join(TrimLines(strings.Split(strings.TrimSpace(input), "\n")), "\n")
+}
diff --git a/vendor/github.com/sergi/go-diff/AUTHORS b/vendor/github.com/sergi/go-diff/AUTHORS
new file mode 100644
index 00000000..2d7bb2bf
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/AUTHORS
@@ -0,0 +1,25 @@
+# This is the official list of go-diff authors for copyright purposes.
+# This file is distinct from the CONTRIBUTORS files.
+# See the latter for an explanation.
+
+# Names should be added to this file as
+#	Name or Organization <email address>
+# The email address is not required for organizations.
+
+# Please keep the list sorted.
+
+Danny Yoo <dannyyoo@google.com>
+James Kolb <jkolb@google.com>
+Jonathan Amsterdam <jba@google.com>
+Markus Zimmermann <markus.zimmermann@nethead.at> <markus.zimmermann@symflower.com> <zimmski@gmail.com>
+Matt Kovars <akaskik@gmail.com>
+Örjan Persson <orjan@spotify.com>
+Osman Masood <oamasood@gmail.com>
+Robert Carlsen <rwcarlsen@gmail.com>
+Rory Flynn <roryflynn@users.noreply.github.com>
+Sergi Mansilla <sergi.mansilla@gmail.com>
+Shatrugna Sadhu <ssadhu@apcera.com>
+Shawn Smith <shawnpsmith@gmail.com>
+Stas Maksimov <maksimov@gmail.com>
+Tor Arvid Lund <torarvid@gmail.com>
+Zac Bergquist <zbergquist99@gmail.com>
diff --git a/vendor/github.com/sergi/go-diff/CONTRIBUTORS b/vendor/github.com/sergi/go-diff/CONTRIBUTORS
new file mode 100644
index 00000000..369e3d55
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/CONTRIBUTORS
@@ -0,0 +1,32 @@
+# This is the official list of people who can contribute
+# (and typically have contributed) code to the go-diff
+# repository.
+#
+# The AUTHORS file lists the copyright holders; this file
+# lists people.  For example, ACME Inc. employees would be listed here
+# but not in AUTHORS, because ACME Inc. would hold the copyright.
+#
+# When adding J Random Contributor's name to this file,
+# either J's name or J's organization's name should be
+# added to the AUTHORS file.
+#
+# Names should be added to this file like so:
+#     Name <email address>
+#
+# Please keep the list sorted.
+
+Danny Yoo <dannyyoo@google.com>
+James Kolb <jkolb@google.com>
+Jonathan Amsterdam <jba@google.com>
+Markus Zimmermann <markus.zimmermann@nethead.at> <markus.zimmermann@symflower.com> <zimmski@gmail.com>
+Matt Kovars <akaskik@gmail.com>
+Örjan Persson <orjan@spotify.com>
+Osman Masood <oamasood@gmail.com>
+Robert Carlsen <rwcarlsen@gmail.com>
+Rory Flynn <roryflynn@users.noreply.github.com>
+Sergi Mansilla <sergi.mansilla@gmail.com>
+Shatrugna Sadhu <ssadhu@apcera.com>
+Shawn Smith <shawnpsmith@gmail.com>
+Stas Maksimov <maksimov@gmail.com>
+Tor Arvid Lund <torarvid@gmail.com>
+Zac Bergquist <zbergquist99@gmail.com>
diff --git a/vendor/github.com/sergi/go-diff/LICENSE b/vendor/github.com/sergi/go-diff/LICENSE
new file mode 100644
index 00000000..937942c2
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2012-2016 The go-diff Authors. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go
new file mode 100644
index 00000000..cb25b437
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go
@@ -0,0 +1,1345 @@
+// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
+// https://github.com/sergi/go-diff
+// See the included LICENSE file for license details.
+//
+// go-diff is a Go implementation of Google's Diff, Match, and Patch library
+// Original library is Copyright (c) 2006 Google Inc.
+// http://code.google.com/p/google-diff-match-patch/
+
+package diffmatchpatch
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"html"
+	"math"
+	"net/url"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+	"unicode/utf8"
+)
+
+// Operation defines the operation of a diff item.
+type Operation int8
+
+//go:generate stringer -type=Operation -trimprefix=Diff
+
+const (
+	// DiffDelete item represents a delete diff.
+	DiffDelete Operation = -1
+	// DiffInsert item represents an insert diff.
+	DiffInsert Operation = 1
+	// DiffEqual item represents an equal diff.
+	DiffEqual Operation = 0
+)
+
+// Diff represents one diff operation
+type Diff struct {
+	Type Operation
+	Text string
+}
+
+// splice removes amount elements from slice at index index, replacing them with elements.
+func splice(slice []Diff, index int, amount int, elements ...Diff) []Diff {
+	if len(elements) == amount {
+		// Easy case: overwrite the relevant items.
+		copy(slice[index:], elements)
+		return slice
+	}
+	if len(elements) < amount {
+		// Fewer new items than old.
+		// Copy in the new items.
+		copy(slice[index:], elements)
+		// Shift the remaining items left.
+		copy(slice[index+len(elements):], slice[index+amount:])
+		// Calculate the new end of the slice.
+		end := len(slice) - amount + len(elements)
+		// Zero stranded elements at end so that they can be garbage collected.
+		tail := slice[end:]
+		for i := range tail {
+			tail[i] = Diff{}
+		}
+		return slice[:end]
+	}
+	// More new items than old.
+	// Make room in slice for new elements.
+	// There's probably an even more efficient way to do this,
+	// but this is simple and clear.
+	need := len(slice) - amount + len(elements)
+	for len(slice) < need {
+		slice = append(slice, Diff{})
+	}
+	// Shift slice elements right to make room for new elements.
+	copy(slice[index+len(elements):], slice[index+amount:])
+	// Copy in new elements.
+	copy(slice[index:], elements)
+	return slice
+}
+
+// DiffMain finds the differences between two texts.
+// If an invalid UTF-8 sequence is encountered, it will be replaced by the Unicode replacement character.
+func (dmp *DiffMatchPatch) DiffMain(text1, text2 string, checklines bool) []Diff {
+	return dmp.DiffMainRunes([]rune(text1), []rune(text2), checklines)
+}
+
+// DiffMainRunes finds the differences between two rune sequences.
+// If an invalid UTF-8 sequence is encountered, it will be replaced by the Unicode replacement character.
+func (dmp *DiffMatchPatch) DiffMainRunes(text1, text2 []rune, checklines bool) []Diff {
+	var deadline time.Time
+	if dmp.DiffTimeout > 0 {
+		deadline = time.Now().Add(dmp.DiffTimeout)
+	}
+	return dmp.diffMainRunes(text1, text2, checklines, deadline)
+}
+
+func (dmp *DiffMatchPatch) diffMainRunes(text1, text2 []rune, checklines bool, deadline time.Time) []Diff {
+	if runesEqual(text1, text2) {
+		var diffs []Diff
+		if len(text1) > 0 {
+			diffs = append(diffs, Diff{DiffEqual, string(text1)})
+		}
+		return diffs
+	}
+	// Trim off common prefix (speedup).
+	commonlength := commonPrefixLength(text1, text2)
+	commonprefix := text1[:commonlength]
+	text1 = text1[commonlength:]
+	text2 = text2[commonlength:]
+
+	// Trim off common suffix (speedup).
+	commonlength = commonSuffixLength(text1, text2)
+	commonsuffix := text1[len(text1)-commonlength:]
+	text1 = text1[:len(text1)-commonlength]
+	text2 = text2[:len(text2)-commonlength]
+
+	// Compute the diff on the middle block.
+	diffs := dmp.diffCompute(text1, text2, checklines, deadline)
+
+	// Restore the prefix and suffix.
+	if len(commonprefix) != 0 {
+		diffs = append([]Diff{Diff{DiffEqual, string(commonprefix)}}, diffs...)
+	}
+	if len(commonsuffix) != 0 {
+		diffs = append(diffs, Diff{DiffEqual, string(commonsuffix)})
+	}
+
+	return dmp.DiffCleanupMerge(diffs)
+}
+
+// diffCompute finds the differences between two rune slices.  Assumes that the texts do not have any common prefix or suffix.
+func (dmp *DiffMatchPatch) diffCompute(text1, text2 []rune, checklines bool, deadline time.Time) []Diff {
+	diffs := []Diff{}
+	if len(text1) == 0 {
+		// Just add some text (speedup).
+		return append(diffs, Diff{DiffInsert, string(text2)})
+	} else if len(text2) == 0 {
+		// Just delete some text (speedup).
+		return append(diffs, Diff{DiffDelete, string(text1)})
+	}
+
+	var longtext, shorttext []rune
+	if len(text1) > len(text2) {
+		longtext = text1
+		shorttext = text2
+	} else {
+		longtext = text2
+		shorttext = text1
+	}
+
+	if i := runesIndex(longtext, shorttext); i != -1 {
+		op := DiffInsert
+		// Swap insertions for deletions if diff is reversed.
+		if len(text1) > len(text2) {
+			op = DiffDelete
+		}
+		// Shorter text is inside the longer text (speedup).
+		return []Diff{
+			Diff{op, string(longtext[:i])},
+			Diff{DiffEqual, string(shorttext)},
+			Diff{op, string(longtext[i+len(shorttext):])},
+		}
+	} else if len(shorttext) == 1 {
+		// Single character string.
+		// After the previous speedup, the character can't be an equality.
+		return []Diff{
+			Diff{DiffDelete, string(text1)},
+			Diff{DiffInsert, string(text2)},
+		}
+		// Check to see if the problem can be split in two.
+	} else if hm := dmp.diffHalfMatch(text1, text2); hm != nil {
+		// A half-match was found, sort out the return data.
+		text1A := hm[0]
+		text1B := hm[1]
+		text2A := hm[2]
+		text2B := hm[3]
+		midCommon := hm[4]
+		// Send both pairs off for separate processing.
+		diffsA := dmp.diffMainRunes(text1A, text2A, checklines, deadline)
+		diffsB := dmp.diffMainRunes(text1B, text2B, checklines, deadline)
+		// Merge the results.
+		diffs := diffsA
+		diffs = append(diffs, Diff{DiffEqual, string(midCommon)})
+		diffs = append(diffs, diffsB...)
+		return diffs
+	} else if checklines && len(text1) > 100 && len(text2) > 100 {
+		return dmp.diffLineMode(text1, text2, deadline)
+	}
+	return dmp.diffBisect(text1, text2, deadline)
+}
+
+// diffLineMode does a quick line-level diff on both []runes, then rediff the parts for greater accuracy. This speedup can produce non-minimal diffs.
+func (dmp *DiffMatchPatch) diffLineMode(text1, text2 []rune, deadline time.Time) []Diff {
+	// Scan the text on a line-by-line basis first.
+	text1, text2, linearray := dmp.diffLinesToRunes(text1, text2)
+
+	diffs := dmp.diffMainRunes(text1, text2, false, deadline)
+
+	// Convert the diff back to original text.
+	diffs = dmp.DiffCharsToLines(diffs, linearray)
+	// Eliminate freak matches (e.g. blank lines)
+	diffs = dmp.DiffCleanupSemantic(diffs)
+
+	// Rediff any replacement blocks, this time character-by-character.
+	// Add a dummy entry at the end.
+	diffs = append(diffs, Diff{DiffEqual, ""})
+
+	pointer := 0
+	countDelete := 0
+	countInsert := 0
+
+	// NOTE: Rune slices are slower than using strings in this case.
+	textDelete := ""
+	textInsert := ""
+
+	for pointer < len(diffs) {
+		switch diffs[pointer].Type {
+		case DiffInsert:
+			countInsert++
+			textInsert += diffs[pointer].Text
+		case DiffDelete:
+			countDelete++
+			textDelete += diffs[pointer].Text
+		case DiffEqual:
+			// Upon reaching an equality, check for prior redundancies.
+			if countDelete >= 1 && countInsert >= 1 {
+				// Delete the offending records and add the merged ones.
+				diffs = splice(diffs, pointer-countDelete-countInsert,
+					countDelete+countInsert)
+
+				pointer = pointer - countDelete - countInsert
+				a := dmp.diffMainRunes([]rune(textDelete), []rune(textInsert), false, deadline)
+				for j := len(a) - 1; j >= 0; j-- {
+					diffs = splice(diffs, pointer, 0, a[j])
+				}
+				pointer = pointer + len(a)
+			}
+
+			countInsert = 0
+			countDelete = 0
+			textDelete = ""
+			textInsert = ""
+		}
+		pointer++
+	}
+
+	return diffs[:len(diffs)-1] // Remove the dummy entry at the end.
+}
+
+// DiffBisect finds the 'middle snake' of a diff, split the problem in two and return the recursively constructed diff.
+// If an invalid UTF-8 sequence is encountered, it will be replaced by the Unicode replacement character.
+// See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+func (dmp *DiffMatchPatch) DiffBisect(text1, text2 string, deadline time.Time) []Diff {
+	// Unused in this code, but retained for interface compatibility.
+	return dmp.diffBisect([]rune(text1), []rune(text2), deadline)
+}
+
+// diffBisect finds the 'middle snake' of a diff, splits the problem in two and returns the recursively constructed diff.
+// See Myers's 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+func (dmp *DiffMatchPatch) diffBisect(runes1, runes2 []rune, deadline time.Time) []Diff {
+	// Cache the text lengths to prevent multiple calls.
+	runes1Len, runes2Len := len(runes1), len(runes2)
+
+	maxD := (runes1Len + runes2Len + 1) / 2
+	vOffset := maxD
+	vLength := 2 * maxD
+
+	v1 := make([]int, vLength)
+	v2 := make([]int, vLength)
+	for i := range v1 {
+		v1[i] = -1
+		v2[i] = -1
+	}
+	v1[vOffset+1] = 0
+	v2[vOffset+1] = 0
+
+	delta := runes1Len - runes2Len
+	// If the total number of characters is odd, then the front path will collide with the reverse path.
+	front := (delta%2 != 0)
+	// Offsets for start and end of k loop. Prevents mapping of space beyond the grid.
+	k1start := 0
+	k1end := 0
+	k2start := 0
+	k2end := 0
+	for d := 0; d < maxD; d++ {
+		// Bail out if deadline is reached.
+		if !deadline.IsZero() && d%16 == 0 && time.Now().After(deadline) {
+			break
+		}
+
+		// Walk the front path one step.
+		for k1 := -d + k1start; k1 <= d-k1end; k1 += 2 {
+			k1Offset := vOffset + k1
+			var x1 int
+
+			if k1 == -d || (k1 != d && v1[k1Offset-1] < v1[k1Offset+1]) {
+				x1 = v1[k1Offset+1]
+			} else {
+				x1 = v1[k1Offset-1] + 1
+			}
+
+			y1 := x1 - k1
+			for x1 < runes1Len && y1 < runes2Len {
+				if runes1[x1] != runes2[y1] {
+					break
+				}
+				x1++
+				y1++
+			}
+			v1[k1Offset] = x1
+			if x1 > runes1Len {
+				// Ran off the right of the graph.
+				k1end += 2
+			} else if y1 > runes2Len {
+				// Ran off the bottom of the graph.
+				k1start += 2
+			} else if front {
+				k2Offset := vOffset + delta - k1
+				if k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] != -1 {
+					// Mirror x2 onto top-left coordinate system.
+					x2 := runes1Len - v2[k2Offset]
+					if x1 >= x2 {
+						// Overlap detected.
+						return dmp.diffBisectSplit(runes1, runes2, x1, y1, deadline)
+					}
+				}
+			}
+		}
+		// Walk the reverse path one step.
+		for k2 := -d + k2start; k2 <= d-k2end; k2 += 2 {
+			k2Offset := vOffset + k2
+			var x2 int
+			if k2 == -d || (k2 != d && v2[k2Offset-1] < v2[k2Offset+1]) {
+				x2 = v2[k2Offset+1]
+			} else {
+				x2 = v2[k2Offset-1] + 1
+			}
+			var y2 = x2 - k2
+			for x2 < runes1Len && y2 < runes2Len {
+				if runes1[runes1Len-x2-1] != runes2[runes2Len-y2-1] {
+					break
+				}
+				x2++
+				y2++
+			}
+			v2[k2Offset] = x2
+			if x2 > runes1Len {
+				// Ran off the left of the graph.
+				k2end += 2
+			} else if y2 > runes2Len {
+				// Ran off the top of the graph.
+				k2start += 2
+			} else if !front {
+				k1Offset := vOffset + delta - k2
+				if k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] != -1 {
+					x1 := v1[k1Offset]
+					y1 := vOffset + x1 - k1Offset
+					// Mirror x2 onto top-left coordinate system.
+					x2 = runes1Len - x2
+					if x1 >= x2 {
+						// Overlap detected.
+						return dmp.diffBisectSplit(runes1, runes2, x1, y1, deadline)
+					}
+				}
+			}
+		}
+	}
+	// Diff took too long and hit the deadline or number of diffs equals number of characters, no commonality at all.
+	return []Diff{
+		Diff{DiffDelete, string(runes1)},
+		Diff{DiffInsert, string(runes2)},
+	}
+}
+
+func (dmp *DiffMatchPatch) diffBisectSplit(runes1, runes2 []rune, x, y int,
+	deadline time.Time) []Diff {
+	runes1a := runes1[:x]
+	runes2a := runes2[:y]
+	runes1b := runes1[x:]
+	runes2b := runes2[y:]
+
+	// Compute both diffs serially.
+	diffs := dmp.diffMainRunes(runes1a, runes2a, false, deadline)
+	diffsb := dmp.diffMainRunes(runes1b, runes2b, false, deadline)
+
+	return append(diffs, diffsb...)
+}
+
+// DiffLinesToChars splits two texts into a list of strings, and educes the texts to a string of hashes where each Unicode character represents one line.
+// It's slightly faster to call DiffLinesToRunes first, followed by DiffMainRunes.
+func (dmp *DiffMatchPatch) DiffLinesToChars(text1, text2 string) (string, string, []string) {
+	chars1, chars2, lineArray := dmp.DiffLinesToRunes(text1, text2)
+	return string(chars1), string(chars2), lineArray
+}
+
+// DiffLinesToRunes splits two texts into a list of runes. Each rune represents one line.
+func (dmp *DiffMatchPatch) DiffLinesToRunes(text1, text2 string) ([]rune, []rune, []string) {
+	// '\x00' is a valid character, but various debuggers don't like it. So we'll insert a junk entry to avoid generating a null character.
+	lineArray := []string{""}    // e.g. lineArray[4] == 'Hello\n'
+	lineHash := map[string]int{} // e.g. lineHash['Hello\n'] == 4
+
+	chars1 := dmp.diffLinesToRunesMunge(text1, &lineArray, lineHash)
+	chars2 := dmp.diffLinesToRunesMunge(text2, &lineArray, lineHash)
+
+	return chars1, chars2, lineArray
+}
+
+func (dmp *DiffMatchPatch) diffLinesToRunes(text1, text2 []rune) ([]rune, []rune, []string) {
+	return dmp.DiffLinesToRunes(string(text1), string(text2))
+}
+
+// diffLinesToRunesMunge splits a text into an array of strings, and reduces the texts to a []rune where each Unicode character represents one line.
+// We use strings instead of []runes as input mainly because you can't use []rune as a map key.
+func (dmp *DiffMatchPatch) diffLinesToRunesMunge(text string, lineArray *[]string, lineHash map[string]int) []rune {
+	// Walk the text, pulling out a substring for each line. text.split('\n') would would temporarily double our memory footprint. Modifying text would create many large strings to garbage collect.
+	lineStart := 0
+	lineEnd := -1
+	runes := []rune{}
+
+	for lineEnd < len(text)-1 {
+		lineEnd = indexOf(text, "\n", lineStart)
+
+		if lineEnd == -1 {
+			lineEnd = len(text) - 1
+		}
+
+		line := text[lineStart : lineEnd+1]
+		lineStart = lineEnd + 1
+		lineValue, ok := lineHash[line]
+
+		if ok {
+			runes = append(runes, rune(lineValue))
+		} else {
+			*lineArray = append(*lineArray, line)
+			lineHash[line] = len(*lineArray) - 1
+			runes = append(runes, rune(len(*lineArray)-1))
+		}
+	}
+
+	return runes
+}
+
+// DiffCharsToLines rehydrates the text in a diff from a string of line hashes to real lines of text.
+func (dmp *DiffMatchPatch) DiffCharsToLines(diffs []Diff, lineArray []string) []Diff {
+	hydrated := make([]Diff, 0, len(diffs))
+	for _, aDiff := range diffs {
+		chars := aDiff.Text
+		text := make([]string, len(chars))
+
+		for i, r := range chars {
+			text[i] = lineArray[r]
+		}
+
+		aDiff.Text = strings.Join(text, "")
+		hydrated = append(hydrated, aDiff)
+	}
+	return hydrated
+}
+
+// DiffCommonPrefix determines the common prefix length of two strings.
+func (dmp *DiffMatchPatch) DiffCommonPrefix(text1, text2 string) int {
+	// Unused in this code, but retained for interface compatibility.
+	return commonPrefixLength([]rune(text1), []rune(text2))
+}
+
+// DiffCommonSuffix determines the common suffix length of two strings.
+func (dmp *DiffMatchPatch) DiffCommonSuffix(text1, text2 string) int {
+	// Unused in this code, but retained for interface compatibility.
+	return commonSuffixLength([]rune(text1), []rune(text2))
+}
+
+// commonPrefixLength returns the length of the common prefix of two rune slices.
+func commonPrefixLength(text1, text2 []rune) int {
+	// Linear search. See comment in commonSuffixLength.
+	n := 0
+	for ; n < len(text1) && n < len(text2); n++ {
+		if text1[n] != text2[n] {
+			return n
+		}
+	}
+	return n
+}
+
+// commonSuffixLength returns the length of the common suffix of two rune slices.
+func commonSuffixLength(text1, text2 []rune) int {
+	// Use linear search rather than the binary search discussed at https://neil.fraser.name/news/2007/10/09/.
+	// See discussion at https://github.com/sergi/go-diff/issues/54.
+	i1 := len(text1)
+	i2 := len(text2)
+	for n := 0; ; n++ {
+		i1--
+		i2--
+		if i1 < 0 || i2 < 0 || text1[i1] != text2[i2] {
+			return n
+		}
+	}
+}
+
+// DiffCommonOverlap determines if the suffix of one string is the prefix of another.
+func (dmp *DiffMatchPatch) DiffCommonOverlap(text1 string, text2 string) int {
+	// Cache the text lengths to prevent multiple calls.
+	text1Length := len(text1)
+	text2Length := len(text2)
+	// Eliminate the null case.
+	if text1Length == 0 || text2Length == 0 {
+		return 0
+	}
+	// Truncate the longer string.
+	if text1Length > text2Length {
+		text1 = text1[text1Length-text2Length:]
+	} else if text1Length < text2Length {
+		text2 = text2[0:text1Length]
+	}
+	textLength := int(math.Min(float64(text1Length), float64(text2Length)))
+	// Quick check for the worst case.
+	if text1 == text2 {
+		return textLength
+	}
+
+	// Start by looking for a single character match and increase length until no match is found. Performance analysis: http://neil.fraser.name/news/2010/11/04/
+	best := 0
+	length := 1
+	for {
+		pattern := text1[textLength-length:]
+		found := strings.Index(text2, pattern)
+		if found == -1 {
+			break
+		}
+		length += found
+		if found == 0 || text1[textLength-length:] == text2[0:length] {
+			best = length
+			length++
+		}
+	}
+
+	return best
+}
+
+// DiffHalfMatch checks whether the two texts share a substring which is at least half the length of the longer text. This speedup can produce non-minimal diffs.
+func (dmp *DiffMatchPatch) DiffHalfMatch(text1, text2 string) []string {
+	// Unused in this code, but retained for interface compatibility.
+	runeSlices := dmp.diffHalfMatch([]rune(text1), []rune(text2))
+	if runeSlices == nil {
+		return nil
+	}
+
+	result := make([]string, len(runeSlices))
+	for i, r := range runeSlices {
+		result[i] = string(r)
+	}
+	return result
+}
+
+func (dmp *DiffMatchPatch) diffHalfMatch(text1, text2 []rune) [][]rune {
+	if dmp.DiffTimeout <= 0 {
+		// Don't risk returning a non-optimal diff if we have unlimited time.
+		return nil
+	}
+
+	var longtext, shorttext []rune
+	if len(text1) > len(text2) {
+		longtext = text1
+		shorttext = text2
+	} else {
+		longtext = text2
+		shorttext = text1
+	}
+
+	if len(longtext) < 4 || len(shorttext)*2 < len(longtext) {
+		return nil // Pointless.
+	}
+
+	// First check if the second quarter is the seed for a half-match.
+	hm1 := dmp.diffHalfMatchI(longtext, shorttext, int(float64(len(longtext)+3)/4))
+
+	// Check again based on the third quarter.
+	hm2 := dmp.diffHalfMatchI(longtext, shorttext, int(float64(len(longtext)+1)/2))
+
+	hm := [][]rune{}
+	if hm1 == nil && hm2 == nil {
+		return nil
+	} else if hm2 == nil {
+		hm = hm1
+	} else if hm1 == nil {
+		hm = hm2
+	} else {
+		// Both matched.  Select the longest.
+		if len(hm1[4]) > len(hm2[4]) {
+			hm = hm1
+		} else {
+			hm = hm2
+		}
+	}
+
+	// A half-match was found, sort out the return data.
+	if len(text1) > len(text2) {
+		return hm
+	}
+
+	return [][]rune{hm[2], hm[3], hm[0], hm[1], hm[4]}
+}
+
+// diffHalfMatchI checks if a substring of shorttext exist within longtext such that the substring is at least half the length of longtext?
+// Returns a slice containing the prefix of longtext, the suffix of longtext, the prefix of shorttext, the suffix of shorttext and the common middle, or null if there was no match.
+func (dmp *DiffMatchPatch) diffHalfMatchI(l, s []rune, i int) [][]rune {
+	var bestCommonA []rune
+	var bestCommonB []rune
+	var bestCommonLen int
+	var bestLongtextA []rune
+	var bestLongtextB []rune
+	var bestShorttextA []rune
+	var bestShorttextB []rune
+
+	// Start with a 1/4 length substring at position i as a seed.
+	seed := l[i : i+len(l)/4]
+
+	for j := runesIndexOf(s, seed, 0); j != -1; j = runesIndexOf(s, seed, j+1) {
+		prefixLength := commonPrefixLength(l[i:], s[j:])
+		suffixLength := commonSuffixLength(l[:i], s[:j])
+
+		if bestCommonLen < suffixLength+prefixLength {
+			bestCommonA = s[j-suffixLength : j]
+			bestCommonB = s[j : j+prefixLength]
+			bestCommonLen = len(bestCommonA) + len(bestCommonB)
+			bestLongtextA = l[:i-suffixLength]
+			bestLongtextB = l[i+prefixLength:]
+			bestShorttextA = s[:j-suffixLength]
+			bestShorttextB = s[j+prefixLength:]
+		}
+	}
+
+	if bestCommonLen*2 < len(l) {
+		return nil
+	}
+
+	return [][]rune{
+		bestLongtextA,
+		bestLongtextB,
+		bestShorttextA,
+		bestShorttextB,
+		append(bestCommonA, bestCommonB...),
+	}
+}
+
+// DiffCleanupSemantic reduces the number of edits by eliminating semantically trivial equalities.
+func (dmp *DiffMatchPatch) DiffCleanupSemantic(diffs []Diff) []Diff {
+	changes := false
+	// Stack of indices where equalities are found.
+	equalities := make([]int, 0, len(diffs))
+
+	var lastequality string
+	// Always equal to diffs[equalities[equalitiesLength - 1]][1]
+	var pointer int // Index of current position.
+	// Number of characters that changed prior to the equality.
+	var lengthInsertions1, lengthDeletions1 int
+	// Number of characters that changed after the equality.
+	var lengthInsertions2, lengthDeletions2 int
+
+	for pointer < len(diffs) {
+		if diffs[pointer].Type == DiffEqual {
+			// Equality found.
+			equalities = append(equalities, pointer)
+			lengthInsertions1 = lengthInsertions2
+			lengthDeletions1 = lengthDeletions2
+			lengthInsertions2 = 0
+			lengthDeletions2 = 0
+			lastequality = diffs[pointer].Text
+		} else {
+			// An insertion or deletion.
+
+			if diffs[pointer].Type == DiffInsert {
+				lengthInsertions2 += len(diffs[pointer].Text)
+			} else {
+				lengthDeletions2 += len(diffs[pointer].Text)
+			}
+			// Eliminate an equality that is smaller or equal to the edits on both sides of it.
+			difference1 := int(math.Max(float64(lengthInsertions1), float64(lengthDeletions1)))
+			difference2 := int(math.Max(float64(lengthInsertions2), float64(lengthDeletions2)))
+			if len(lastequality) > 0 &&
+				(len(lastequality) <= difference1) &&
+				(len(lastequality) <= difference2) {
+				// Duplicate record.
+				insPoint := equalities[len(equalities)-1]
+				diffs = splice(diffs, insPoint, 0, Diff{DiffDelete, lastequality})
+
+				// Change second copy to insert.
+				diffs[insPoint+1].Type = DiffInsert
+				// Throw away the equality we just deleted.
+				equalities = equalities[:len(equalities)-1]
+
+				if len(equalities) > 0 {
+					equalities = equalities[:len(equalities)-1]
+				}
+				pointer = -1
+				if len(equalities) > 0 {
+					pointer = equalities[len(equalities)-1]
+				}
+
+				lengthInsertions1 = 0 // Reset the counters.
+				lengthDeletions1 = 0
+				lengthInsertions2 = 0
+				lengthDeletions2 = 0
+				lastequality = ""
+				changes = true
+			}
+		}
+		pointer++
+	}
+
+	// Normalize the diff.
+	if changes {
+		diffs = dmp.DiffCleanupMerge(diffs)
+	}
+	diffs = dmp.DiffCleanupSemanticLossless(diffs)
+	// Find any overlaps between deletions and insertions.
+	// e.g: <del>abcxxx</del><ins>xxxdef</ins>
+	//   -> <del>abc</del>xxx<ins>def</ins>
+	// e.g: <del>xxxabc</del><ins>defxxx</ins>
+	//   -> <ins>def</ins>xxx<del>abc</del>
+	// Only extract an overlap if it is as big as the edit ahead or behind it.
+	pointer = 1
+	for pointer < len(diffs) {
+		if diffs[pointer-1].Type == DiffDelete &&
+			diffs[pointer].Type == DiffInsert {
+			deletion := diffs[pointer-1].Text
+			insertion := diffs[pointer].Text
+			overlapLength1 := dmp.DiffCommonOverlap(deletion, insertion)
+			overlapLength2 := dmp.DiffCommonOverlap(insertion, deletion)
+			if overlapLength1 >= overlapLength2 {
+				if float64(overlapLength1) >= float64(len(deletion))/2 ||
+					float64(overlapLength1) >= float64(len(insertion))/2 {
+
+					// Overlap found. Insert an equality and trim the surrounding edits.
+					diffs = splice(diffs, pointer, 0, Diff{DiffEqual, insertion[:overlapLength1]})
+					diffs[pointer-1].Text =
+						deletion[0 : len(deletion)-overlapLength1]
+					diffs[pointer+1].Text = insertion[overlapLength1:]
+					pointer++
+				}
+			} else {
+				if float64(overlapLength2) >= float64(len(deletion))/2 ||
+					float64(overlapLength2) >= float64(len(insertion))/2 {
+					// Reverse overlap found. Insert an equality and swap and trim the surrounding edits.
+					overlap := Diff{DiffEqual, deletion[:overlapLength2]}
+					diffs = splice(diffs, pointer, 0, overlap)
+					diffs[pointer-1].Type = DiffInsert
+					diffs[pointer-1].Text = insertion[0 : len(insertion)-overlapLength2]
+					diffs[pointer+1].Type = DiffDelete
+					diffs[pointer+1].Text = deletion[overlapLength2:]
+					pointer++
+				}
+			}
+			pointer++
+		}
+		pointer++
+	}
+
+	return diffs
+}
+
+// Define some regex patterns for matching boundaries.
+var (
+	nonAlphaNumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]`)
+	whitespaceRegex      = regexp.MustCompile(`\s`)
+	linebreakRegex       = regexp.MustCompile(`[\r\n]`)
+	blanklineEndRegex    = regexp.MustCompile(`\n\r?\n$`)
+	blanklineStartRegex  = regexp.MustCompile(`^\r?\n\r?\n`)
+)
+
+// diffCleanupSemanticScore computes a score representing whether the internal boundary falls on logical boundaries.
+// Scores range from 6 (best) to 0 (worst). Closure, but does not reference any external variables.
+func diffCleanupSemanticScore(one, two string) int {
+	if len(one) == 0 || len(two) == 0 {
+		// Edges are the best.
+		return 6
+	}
+
+	// Each port of this function behaves slightly differently due to subtle differences in each language's definition of things like 'whitespace'.  Since this function's purpose is largely cosmetic, the choice has been made to use each language's native features rather than force total conformity.
+	rune1, _ := utf8.DecodeLastRuneInString(one)
+	rune2, _ := utf8.DecodeRuneInString(two)
+	char1 := string(rune1)
+	char2 := string(rune2)
+
+	nonAlphaNumeric1 := nonAlphaNumericRegex.MatchString(char1)
+	nonAlphaNumeric2 := nonAlphaNumericRegex.MatchString(char2)
+	whitespace1 := nonAlphaNumeric1 && whitespaceRegex.MatchString(char1)
+	whitespace2 := nonAlphaNumeric2 && whitespaceRegex.MatchString(char2)
+	lineBreak1 := whitespace1 && linebreakRegex.MatchString(char1)
+	lineBreak2 := whitespace2 && linebreakRegex.MatchString(char2)
+	blankLine1 := lineBreak1 && blanklineEndRegex.MatchString(one)
+	blankLine2 := lineBreak2 && blanklineEndRegex.MatchString(two)
+
+	if blankLine1 || blankLine2 {
+		// Five points for blank lines.
+		return 5
+	} else if lineBreak1 || lineBreak2 {
+		// Four points for line breaks.
+		return 4
+	} else if nonAlphaNumeric1 && !whitespace1 && whitespace2 {
+		// Three points for end of sentences.
+		return 3
+	} else if whitespace1 || whitespace2 {
+		// Two points for whitespace.
+		return 2
+	} else if nonAlphaNumeric1 || nonAlphaNumeric2 {
+		// One point for non-alphanumeric.
+		return 1
+	}
+	return 0
+}
+
+// DiffCleanupSemanticLossless looks for single edits surrounded on both sides by equalities which can be shifted sideways to align the edit to a word boundary.
+// E.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
+func (dmp *DiffMatchPatch) DiffCleanupSemanticLossless(diffs []Diff) []Diff {
+	pointer := 1
+
+	// Intentionally ignore the first and last element (don't need checking).
+	for pointer < len(diffs)-1 {
+		if diffs[pointer-1].Type == DiffEqual &&
+			diffs[pointer+1].Type == DiffEqual {
+
+			// This is a single edit surrounded by equalities.
+			equality1 := diffs[pointer-1].Text
+			edit := diffs[pointer].Text
+			equality2 := diffs[pointer+1].Text
+
+			// First, shift the edit as far left as possible.
+			commonOffset := dmp.DiffCommonSuffix(equality1, edit)
+			if commonOffset > 0 {
+				commonString := edit[len(edit)-commonOffset:]
+				equality1 = equality1[0 : len(equality1)-commonOffset]
+				edit = commonString + edit[:len(edit)-commonOffset]
+				equality2 = commonString + equality2
+			}
+
+			// Second, step character by character right, looking for the best fit.
+			bestEquality1 := equality1
+			bestEdit := edit
+			bestEquality2 := equality2
+			bestScore := diffCleanupSemanticScore(equality1, edit) +
+				diffCleanupSemanticScore(edit, equality2)
+
+			for len(edit) != 0 && len(equality2) != 0 {
+				_, sz := utf8.DecodeRuneInString(edit)
+				if len(equality2) < sz || edit[:sz] != equality2[:sz] {
+					break
+				}
+				equality1 += edit[:sz]
+				edit = edit[sz:] + equality2[:sz]
+				equality2 = equality2[sz:]
+				score := diffCleanupSemanticScore(equality1, edit) +
+					diffCleanupSemanticScore(edit, equality2)
+				// The >= encourages trailing rather than leading whitespace on edits.
+				if score >= bestScore {
+					bestScore = score
+					bestEquality1 = equality1
+					bestEdit = edit
+					bestEquality2 = equality2
+				}
+			}
+
+			if diffs[pointer-1].Text != bestEquality1 {
+				// We have an improvement, save it back to the diff.
+				if len(bestEquality1) != 0 {
+					diffs[pointer-1].Text = bestEquality1
+				} else {
+					diffs = splice(diffs, pointer-1, 1)
+					pointer--
+				}
+
+				diffs[pointer].Text = bestEdit
+				if len(bestEquality2) != 0 {
+					diffs[pointer+1].Text = bestEquality2
+				} else {
+					diffs = append(diffs[:pointer+1], diffs[pointer+2:]...)
+					pointer--
+				}
+			}
+		}
+		pointer++
+	}
+
+	return diffs
+}
+
+// DiffCleanupEfficiency reduces the number of edits by eliminating operationally trivial equalities.
+func (dmp *DiffMatchPatch) DiffCleanupEfficiency(diffs []Diff) []Diff {
+	changes := false
+	// Stack of indices where equalities are found.
+	type equality struct {
+		data int
+		next *equality
+	}
+	var equalities *equality
+	// Always equal to equalities[equalitiesLength-1][1]
+	lastequality := ""
+	pointer := 0 // Index of current position.
+	// Is there an insertion operation before the last equality.
+	preIns := false
+	// Is there a deletion operation before the last equality.
+	preDel := false
+	// Is there an insertion operation after the last equality.
+	postIns := false
+	// Is there a deletion operation after the last equality.
+	postDel := false
+	for pointer < len(diffs) {
+		if diffs[pointer].Type == DiffEqual { // Equality found.
+			if len(diffs[pointer].Text) < dmp.DiffEditCost &&
+				(postIns || postDel) {
+				// Candidate found.
+				equalities = &equality{
+					data: pointer,
+					next: equalities,
+				}
+				preIns = postIns
+				preDel = postDel
+				lastequality = diffs[pointer].Text
+			} else {
+				// Not a candidate, and can never become one.
+				equalities = nil
+				lastequality = ""
+			}
+			postIns = false
+			postDel = false
+		} else { // An insertion or deletion.
+			if diffs[pointer].Type == DiffDelete {
+				postDel = true
+			} else {
+				postIns = true
+			}
+
+			// Five types to be split:
+			// <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
+			// <ins>A</ins>X<ins>C</ins><del>D</del>
+			// <ins>A</ins><del>B</del>X<ins>C</ins>
+			// <ins>A</del>X<ins>C</ins><del>D</del>
+			// <ins>A</ins><del>B</del>X<del>C</del>
+			var sumPres int
+			if preIns {
+				sumPres++
+			}
+			if preDel {
+				sumPres++
+			}
+			if postIns {
+				sumPres++
+			}
+			if postDel {
+				sumPres++
+			}
+			if len(lastequality) > 0 &&
+				((preIns && preDel && postIns && postDel) ||
+					((len(lastequality) < dmp.DiffEditCost/2) && sumPres == 3)) {
+
+				insPoint := equalities.data
+
+				// Duplicate record.
+				diffs = splice(diffs, insPoint, 0, Diff{DiffDelete, lastequality})
+
+				// Change second copy to insert.
+				diffs[insPoint+1].Type = DiffInsert
+				// Throw away the equality we just deleted.
+				equalities = equalities.next
+				lastequality = ""
+
+				if preIns && preDel {
+					// No changes made which could affect previous entry, keep going.
+					postIns = true
+					postDel = true
+					equalities = nil
+				} else {
+					if equalities != nil {
+						equalities = equalities.next
+					}
+					if equalities != nil {
+						pointer = equalities.data
+					} else {
+						pointer = -1
+					}
+					postIns = false
+					postDel = false
+				}
+				changes = true
+			}
+		}
+		pointer++
+	}
+
+	if changes {
+		diffs = dmp.DiffCleanupMerge(diffs)
+	}
+
+	return diffs
+}
+
+// DiffCleanupMerge reorders and merges like edit sections. Merge equalities.
+// Any edit section can move as long as it doesn't cross an equality.
+func (dmp *DiffMatchPatch) DiffCleanupMerge(diffs []Diff) []Diff {
+	// Add a dummy entry at the end.
+	diffs = append(diffs, Diff{DiffEqual, ""})
+	pointer := 0
+	countDelete := 0
+	countInsert := 0
+	commonlength := 0
+	textDelete := []rune(nil)
+	textInsert := []rune(nil)
+
+	for pointer < len(diffs) {
+		switch diffs[pointer].Type {
+		case DiffInsert:
+			countInsert++
+			textInsert = append(textInsert, []rune(diffs[pointer].Text)...)
+			pointer++
+			break
+		case DiffDelete:
+			countDelete++
+			textDelete = append(textDelete, []rune(diffs[pointer].Text)...)
+			pointer++
+			break
+		case DiffEqual:
+			// Upon reaching an equality, check for prior redundancies.
+			if countDelete+countInsert > 1 {
+				if countDelete != 0 && countInsert != 0 {
+					// Factor out any common prefixies.
+					commonlength = commonPrefixLength(textInsert, textDelete)
+					if commonlength != 0 {
+						x := pointer - countDelete - countInsert
+						if x > 0 && diffs[x-1].Type == DiffEqual {
+							diffs[x-1].Text += string(textInsert[:commonlength])
+						} else {
+							diffs = append([]Diff{Diff{DiffEqual, string(textInsert[:commonlength])}}, diffs...)
+							pointer++
+						}
+						textInsert = textInsert[commonlength:]
+						textDelete = textDelete[commonlength:]
+					}
+					// Factor out any common suffixies.
+					commonlength = commonSuffixLength(textInsert, textDelete)
+					if commonlength != 0 {
+						insertIndex := len(textInsert) - commonlength
+						deleteIndex := len(textDelete) - commonlength
+						diffs[pointer].Text = string(textInsert[insertIndex:]) + diffs[pointer].Text
+						textInsert = textInsert[:insertIndex]
+						textDelete = textDelete[:deleteIndex]
+					}
+				}
+				// Delete the offending records and add the merged ones.
+				if countDelete == 0 {
+					diffs = splice(diffs, pointer-countInsert,
+						countDelete+countInsert,
+						Diff{DiffInsert, string(textInsert)})
+				} else if countInsert == 0 {
+					diffs = splice(diffs, pointer-countDelete,
+						countDelete+countInsert,
+						Diff{DiffDelete, string(textDelete)})
+				} else {
+					diffs = splice(diffs, pointer-countDelete-countInsert,
+						countDelete+countInsert,
+						Diff{DiffDelete, string(textDelete)},
+						Diff{DiffInsert, string(textInsert)})
+				}
+
+				pointer = pointer - countDelete - countInsert + 1
+				if countDelete != 0 {
+					pointer++
+				}
+				if countInsert != 0 {
+					pointer++
+				}
+			} else if pointer != 0 && diffs[pointer-1].Type == DiffEqual {
+				// Merge this equality with the previous one.
+				diffs[pointer-1].Text += diffs[pointer].Text
+				diffs = append(diffs[:pointer], diffs[pointer+1:]...)
+			} else {
+				pointer++
+			}
+			countInsert = 0
+			countDelete = 0
+			textDelete = nil
+			textInsert = nil
+			break
+		}
+	}
+
+	if len(diffs[len(diffs)-1].Text) == 0 {
+		diffs = diffs[0 : len(diffs)-1] // Remove the dummy entry at the end.
+	}
+
+	// Second pass: look for single edits surrounded on both sides by equalities which can be shifted sideways to eliminate an equality. E.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
+	changes := false
+	pointer = 1
+	// Intentionally ignore the first and last element (don't need checking).
+	for pointer < (len(diffs) - 1) {
+		if diffs[pointer-1].Type == DiffEqual &&
+			diffs[pointer+1].Type == DiffEqual {
+			// This is a single edit surrounded by equalities.
+			if strings.HasSuffix(diffs[pointer].Text, diffs[pointer-1].Text) {
+				// Shift the edit over the previous equality.
+				diffs[pointer].Text = diffs[pointer-1].Text +
+					diffs[pointer].Text[:len(diffs[pointer].Text)-len(diffs[pointer-1].Text)]
+				diffs[pointer+1].Text = diffs[pointer-1].Text + diffs[pointer+1].Text
+				diffs = splice(diffs, pointer-1, 1)
+				changes = true
+			} else if strings.HasPrefix(diffs[pointer].Text, diffs[pointer+1].Text) {
+				// Shift the edit over the next equality.
+				diffs[pointer-1].Text += diffs[pointer+1].Text
+				diffs[pointer].Text =
+					diffs[pointer].Text[len(diffs[pointer+1].Text):] + diffs[pointer+1].Text
+				diffs = splice(diffs, pointer+1, 1)
+				changes = true
+			}
+		}
+		pointer++
+	}
+
+	// If shifts were made, the diff needs reordering and another shift sweep.
+	if changes {
+		diffs = dmp.DiffCleanupMerge(diffs)
+	}
+
+	return diffs
+}
+
+// DiffXIndex returns the equivalent location in s2.
+func (dmp *DiffMatchPatch) DiffXIndex(diffs []Diff, loc int) int {
+	chars1 := 0
+	chars2 := 0
+	lastChars1 := 0
+	lastChars2 := 0
+	lastDiff := Diff{}
+	for i := 0; i < len(diffs); i++ {
+		aDiff := diffs[i]
+		if aDiff.Type != DiffInsert {
+			// Equality or deletion.
+			chars1 += len(aDiff.Text)
+		}
+		if aDiff.Type != DiffDelete {
+			// Equality or insertion.
+			chars2 += len(aDiff.Text)
+		}
+		if chars1 > loc {
+			// Overshot the location.
+			lastDiff = aDiff
+			break
+		}
+		lastChars1 = chars1
+		lastChars2 = chars2
+	}
+	if lastDiff.Type == DiffDelete {
+		// The location was deleted.
+		return lastChars2
+	}
+	// Add the remaining character length.
+	return lastChars2 + (loc - lastChars1)
+}
+
+// DiffPrettyHtml converts a []Diff into a pretty HTML report.
+// It is intended as an example from which to write one's own display functions.
+func (dmp *DiffMatchPatch) DiffPrettyHtml(diffs []Diff) string {
+	var buff bytes.Buffer
+	for _, diff := range diffs {
+		text := strings.Replace(html.EscapeString(diff.Text), "\n", "&para;<br>", -1)
+		switch diff.Type {
+		case DiffInsert:
+			_, _ = buff.WriteString("<ins style=\"background:#e6ffe6;\">")
+			_, _ = buff.WriteString(text)
+			_, _ = buff.WriteString("</ins>")
+		case DiffDelete:
+			_, _ = buff.WriteString("<del style=\"background:#ffe6e6;\">")
+			_, _ = buff.WriteString(text)
+			_, _ = buff.WriteString("</del>")
+		case DiffEqual:
+			_, _ = buff.WriteString("<span>")
+			_, _ = buff.WriteString(text)
+			_, _ = buff.WriteString("</span>")
+		}
+	}
+	return buff.String()
+}
+
+// DiffPrettyText converts a []Diff into a colored text report.
+func (dmp *DiffMatchPatch) DiffPrettyText(diffs []Diff) string {
+	var buff bytes.Buffer
+	for _, diff := range diffs {
+		text := diff.Text
+
+		switch diff.Type {
+		case DiffInsert:
+			_, _ = buff.WriteString("\x1b[32m")
+			_, _ = buff.WriteString(text)
+			_, _ = buff.WriteString("\x1b[0m")
+		case DiffDelete:
+			_, _ = buff.WriteString("\x1b[31m")
+			_, _ = buff.WriteString(text)
+			_, _ = buff.WriteString("\x1b[0m")
+		case DiffEqual:
+			_, _ = buff.WriteString(text)
+		}
+	}
+
+	return buff.String()
+}
+
+// DiffText1 computes and returns the source text (all equalities and deletions).
+func (dmp *DiffMatchPatch) DiffText1(diffs []Diff) string {
+	//StringBuilder text = new StringBuilder()
+	var text bytes.Buffer
+
+	for _, aDiff := range diffs {
+		if aDiff.Type != DiffInsert {
+			_, _ = text.WriteString(aDiff.Text)
+		}
+	}
+	return text.String()
+}
+
+// DiffText2 computes and returns the destination text (all equalities and insertions).
+func (dmp *DiffMatchPatch) DiffText2(diffs []Diff) string {
+	var text bytes.Buffer
+
+	for _, aDiff := range diffs {
+		if aDiff.Type != DiffDelete {
+			_, _ = text.WriteString(aDiff.Text)
+		}
+	}
+	return text.String()
+}
+
+// DiffLevenshtein computes the Levenshtein distance that is the number of inserted, deleted or substituted characters.
+func (dmp *DiffMatchPatch) DiffLevenshtein(diffs []Diff) int {
+	levenshtein := 0
+	insertions := 0
+	deletions := 0
+
+	for _, aDiff := range diffs {
+		switch aDiff.Type {
+		case DiffInsert:
+			insertions += utf8.RuneCountInString(aDiff.Text)
+		case DiffDelete:
+			deletions += utf8.RuneCountInString(aDiff.Text)
+		case DiffEqual:
+			// A deletion and an insertion is one substitution.
+			levenshtein += max(insertions, deletions)
+			insertions = 0
+			deletions = 0
+		}
+	}
+
+	levenshtein += max(insertions, deletions)
+	return levenshtein
+}
+
+// DiffToDelta crushes the diff into an encoded string which describes the operations required to transform text1 into text2.
+// E.g. =3\t-2\t+ing  -> Keep 3 chars, delete 2 chars, insert 'ing'. Operations are tab-separated.  Inserted text is escaped using %xx notation.
+func (dmp *DiffMatchPatch) DiffToDelta(diffs []Diff) string {
+	var text bytes.Buffer
+	for _, aDiff := range diffs {
+		switch aDiff.Type {
+		case DiffInsert:
+			_, _ = text.WriteString("+")
+			_, _ = text.WriteString(strings.Replace(url.QueryEscape(aDiff.Text), "+", " ", -1))
+			_, _ = text.WriteString("\t")
+			break
+		case DiffDelete:
+			_, _ = text.WriteString("-")
+			_, _ = text.WriteString(strconv.Itoa(utf8.RuneCountInString(aDiff.Text)))
+			_, _ = text.WriteString("\t")
+			break
+		case DiffEqual:
+			_, _ = text.WriteString("=")
+			_, _ = text.WriteString(strconv.Itoa(utf8.RuneCountInString(aDiff.Text)))
+			_, _ = text.WriteString("\t")
+			break
+		}
+	}
+	delta := text.String()
+	if len(delta) != 0 {
+		// Strip off trailing tab character.
+		delta = delta[0 : utf8.RuneCountInString(delta)-1]
+		delta = unescaper.Replace(delta)
+	}
+	return delta
+}
+
+// DiffFromDelta given the original text1, and an encoded string which describes the operations required to transform text1 into text2, comAdde the full diff.
+func (dmp *DiffMatchPatch) DiffFromDelta(text1 string, delta string) (diffs []Diff, err error) {
+	i := 0
+	runes := []rune(text1)
+
+	for _, token := range strings.Split(delta, "\t") {
+		if len(token) == 0 {
+			// Blank tokens are ok (from a trailing \t).
+			continue
+		}
+
+		// Each token begins with a one character parameter which specifies the operation of this token (delete, insert, equality).
+		param := token[1:]
+
+		switch op := token[0]; op {
+		case '+':
+			// Decode would Diff all "+" to " "
+			param = strings.Replace(param, "+", "%2b", -1)
+			param, err = url.QueryUnescape(param)
+			if err != nil {
+				return nil, err
+			}
+			if !utf8.ValidString(param) {
+				return nil, fmt.Errorf("invalid UTF-8 token: %q", param)
+			}
+
+			diffs = append(diffs, Diff{DiffInsert, param})
+		case '=', '-':
+			n, err := strconv.ParseInt(param, 10, 0)
+			if err != nil {
+				return nil, err
+			} else if n < 0 {
+				return nil, errors.New("Negative number in DiffFromDelta: " + param)
+			}
+
+			i += int(n)
+			// Break out if we are out of bounds, go1.6 can't handle this very well
+			if i > len(runes) {
+				break
+			}
+			// Remember that string slicing is by byte - we want by rune here.
+			text := string(runes[i-int(n) : i])
+
+			if op == '=' {
+				diffs = append(diffs, Diff{DiffEqual, text})
+			} else {
+				diffs = append(diffs, Diff{DiffDelete, text})
+			}
+		default:
+			// Anything else is an error.
+			return nil, errors.New("Invalid diff operation in DiffFromDelta: " + string(token[0]))
+		}
+	}
+
+	if i != len(runes) {
+		return nil, fmt.Errorf("Delta length (%v) is different from source text length (%v)", i, len(text1))
+	}
+
+	return diffs, nil
+}
diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/diffmatchpatch.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/diffmatchpatch.go
new file mode 100644
index 00000000..d3acc32c
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/diffmatchpatch.go
@@ -0,0 +1,46 @@
+// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
+// https://github.com/sergi/go-diff
+// See the included LICENSE file for license details.
+//
+// go-diff is a Go implementation of Google's Diff, Match, and Patch library
+// Original library is Copyright (c) 2006 Google Inc.
+// http://code.google.com/p/google-diff-match-patch/
+
+// Package diffmatchpatch offers robust algorithms to perform the operations required for synchronizing plain text.
+package diffmatchpatch
+
+import (
+	"time"
+)
+
+// DiffMatchPatch holds the configuration for diff-match-patch operations.
+type DiffMatchPatch struct {
+	// Number of seconds to map a diff before giving up (0 for infinity).
+	DiffTimeout time.Duration
+	// Cost of an empty edit operation in terms of edit characters.
+	DiffEditCost int
+	// How far to search for a match (0 = exact location, 1000+ = broad match). A match this many characters away from the expected location will add 1.0 to the score (0.0 is a perfect match).
+	MatchDistance int
+	// When deleting a large block of text (over ~64 characters), how close do the contents have to be to match the expected contents. (0.0 = perfection, 1.0 = very loose).  Note that MatchThreshold controls how closely the end points of a delete need to match.
+	PatchDeleteThreshold float64
+	// Chunk size for context length.
+	PatchMargin int
+	// The number of bits in an int.
+	MatchMaxBits int
+	// At what point is no match declared (0.0 = perfection, 1.0 = very loose).
+	MatchThreshold float64
+}
+
+// New creates a new DiffMatchPatch object with default parameters.
+func New() *DiffMatchPatch {
+	// Defaults.
+	return &DiffMatchPatch{
+		DiffTimeout:          time.Second,
+		DiffEditCost:         4,
+		MatchThreshold:       0.5,
+		MatchDistance:        1000,
+		PatchDeleteThreshold: 0.5,
+		PatchMargin:          4,
+		MatchMaxBits:         32,
+	}
+}
diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/match.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/match.go
new file mode 100644
index 00000000..17374e10
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/match.go
@@ -0,0 +1,160 @@
+// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
+// https://github.com/sergi/go-diff
+// See the included LICENSE file for license details.
+//
+// go-diff is a Go implementation of Google's Diff, Match, and Patch library
+// Original library is Copyright (c) 2006 Google Inc.
+// http://code.google.com/p/google-diff-match-patch/
+
+package diffmatchpatch
+
+import (
+	"math"
+)
+
+// MatchMain locates the best instance of 'pattern' in 'text' near 'loc'.
+// Returns -1 if no match found.
+func (dmp *DiffMatchPatch) MatchMain(text, pattern string, loc int) int {
+	// Check for null inputs not needed since null can't be passed in C#.
+
+	loc = int(math.Max(0, math.Min(float64(loc), float64(len(text)))))
+	if text == pattern {
+		// Shortcut (potentially not guaranteed by the algorithm)
+		return 0
+	} else if len(text) == 0 {
+		// Nothing to match.
+		return -1
+	} else if loc+len(pattern) <= len(text) && text[loc:loc+len(pattern)] == pattern {
+		// Perfect match at the perfect spot!  (Includes case of null pattern)
+		return loc
+	}
+	// Do a fuzzy compare.
+	return dmp.MatchBitap(text, pattern, loc)
+}
+
+// MatchBitap locates the best instance of 'pattern' in 'text' near 'loc' using the Bitap algorithm.
+// Returns -1 if no match was found.
+func (dmp *DiffMatchPatch) MatchBitap(text, pattern string, loc int) int {
+	// Initialise the alphabet.
+	s := dmp.MatchAlphabet(pattern)
+
+	// Highest score beyond which we give up.
+	scoreThreshold := dmp.MatchThreshold
+	// Is there a nearby exact match? (speedup)
+	bestLoc := indexOf(text, pattern, loc)
+	if bestLoc != -1 {
+		scoreThreshold = math.Min(dmp.matchBitapScore(0, bestLoc, loc,
+			pattern), scoreThreshold)
+		// What about in the other direction? (speedup)
+		bestLoc = lastIndexOf(text, pattern, loc+len(pattern))
+		if bestLoc != -1 {
+			scoreThreshold = math.Min(dmp.matchBitapScore(0, bestLoc, loc,
+				pattern), scoreThreshold)
+		}
+	}
+
+	// Initialise the bit arrays.
+	matchmask := 1 << uint((len(pattern) - 1))
+	bestLoc = -1
+
+	var binMin, binMid int
+	binMax := len(pattern) + len(text)
+	lastRd := []int{}
+	for d := 0; d < len(pattern); d++ {
+		// Scan for the best match; each iteration allows for one more error. Run a binary search to determine how far from 'loc' we can stray at this error level.
+		binMin = 0
+		binMid = binMax
+		for binMin < binMid {
+			if dmp.matchBitapScore(d, loc+binMid, loc, pattern) <= scoreThreshold {
+				binMin = binMid
+			} else {
+				binMax = binMid
+			}
+			binMid = (binMax-binMin)/2 + binMin
+		}
+		// Use the result from this iteration as the maximum for the next.
+		binMax = binMid
+		start := int(math.Max(1, float64(loc-binMid+1)))
+		finish := int(math.Min(float64(loc+binMid), float64(len(text))) + float64(len(pattern)))
+
+		rd := make([]int, finish+2)
+		rd[finish+1] = (1 << uint(d)) - 1
+
+		for j := finish; j >= start; j-- {
+			var charMatch int
+			if len(text) <= j-1 {
+				// Out of range.
+				charMatch = 0
+			} else if _, ok := s[text[j-1]]; !ok {
+				charMatch = 0
+			} else {
+				charMatch = s[text[j-1]]
+			}
+
+			if d == 0 {
+				// First pass: exact match.
+				rd[j] = ((rd[j+1] << 1) | 1) & charMatch
+			} else {
+				// Subsequent passes: fuzzy match.
+				rd[j] = ((rd[j+1]<<1)|1)&charMatch | (((lastRd[j+1] | lastRd[j]) << 1) | 1) | lastRd[j+1]
+			}
+			if (rd[j] & matchmask) != 0 {
+				score := dmp.matchBitapScore(d, j-1, loc, pattern)
+				// This match will almost certainly be better than any existing match.  But check anyway.
+				if score <= scoreThreshold {
+					// Told you so.
+					scoreThreshold = score
+					bestLoc = j - 1
+					if bestLoc > loc {
+						// When passing loc, don't exceed our current distance from loc.
+						start = int(math.Max(1, float64(2*loc-bestLoc)))
+					} else {
+						// Already passed loc, downhill from here on in.
+						break
+					}
+				}
+			}
+		}
+		if dmp.matchBitapScore(d+1, loc, loc, pattern) > scoreThreshold {
+			// No hope for a (better) match at greater error levels.
+			break
+		}
+		lastRd = rd
+	}
+	return bestLoc
+}
+
+// matchBitapScore computes and returns the score for a match with e errors and x location.
+func (dmp *DiffMatchPatch) matchBitapScore(e, x, loc int, pattern string) float64 {
+	accuracy := float64(e) / float64(len(pattern))
+	proximity := math.Abs(float64(loc - x))
+	if dmp.MatchDistance == 0 {
+		// Dodge divide by zero error.
+		if proximity == 0 {
+			return accuracy
+		}
+
+		return 1.0
+	}
+	return accuracy + (proximity / float64(dmp.MatchDistance))
+}
+
+// MatchAlphabet initialises the alphabet for the Bitap algorithm.
+func (dmp *DiffMatchPatch) MatchAlphabet(pattern string) map[byte]int {
+	s := map[byte]int{}
+	charPattern := []byte(pattern)
+	for _, c := range charPattern {
+		_, ok := s[c]
+		if !ok {
+			s[c] = 0
+		}
+	}
+	i := 0
+
+	for _, c := range charPattern {
+		value := s[c] | int(uint(1)<<uint((len(pattern)-i-1)))
+		s[c] = value
+		i++
+	}
+	return s
+}
diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/mathutil.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/mathutil.go
new file mode 100644
index 00000000..aed242bc
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/mathutil.go
@@ -0,0 +1,23 @@
+// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
+// https://github.com/sergi/go-diff
+// See the included LICENSE file for license details.
+//
+// go-diff is a Go implementation of Google's Diff, Match, and Patch library
+// Original library is Copyright (c) 2006 Google Inc.
+// http://code.google.com/p/google-diff-match-patch/
+
+package diffmatchpatch
+
+func min(x, y int) int {
+	if x < y {
+		return x
+	}
+	return y
+}
+
+func max(x, y int) int {
+	if x > y {
+		return x
+	}
+	return y
+}
diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/operation_string.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/operation_string.go
new file mode 100644
index 00000000..533ec0da
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/operation_string.go
@@ -0,0 +1,17 @@
+// Code generated by "stringer -type=Operation -trimprefix=Diff"; DO NOT EDIT.
+
+package diffmatchpatch
+
+import "fmt"
+
+const _Operation_name = "DeleteEqualInsert"
+
+var _Operation_index = [...]uint8{0, 6, 11, 17}
+
+func (i Operation) String() string {
+	i -= -1
+	if i < 0 || i >= Operation(len(_Operation_index)-1) {
+		return fmt.Sprintf("Operation(%d)", i+-1)
+	}
+	return _Operation_name[_Operation_index[i]:_Operation_index[i+1]]
+}
diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go
new file mode 100644
index 00000000..223c43c4
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go
@@ -0,0 +1,556 @@
+// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
+// https://github.com/sergi/go-diff
+// See the included LICENSE file for license details.
+//
+// go-diff is a Go implementation of Google's Diff, Match, and Patch library
+// Original library is Copyright (c) 2006 Google Inc.
+// http://code.google.com/p/google-diff-match-patch/
+
+package diffmatchpatch
+
+import (
+	"bytes"
+	"errors"
+	"math"
+	"net/url"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+// Patch represents one patch operation.
+type Patch struct {
+	diffs   []Diff
+	Start1  int
+	Start2  int
+	Length1 int
+	Length2 int
+}
+
+// String emulates GNU diff's format.
+// Header: @@ -382,8 +481,9 @@
+// Indices are printed as 1-based, not 0-based.
+func (p *Patch) String() string {
+	var coords1, coords2 string
+
+	if p.Length1 == 0 {
+		coords1 = strconv.Itoa(p.Start1) + ",0"
+	} else if p.Length1 == 1 {
+		coords1 = strconv.Itoa(p.Start1 + 1)
+	} else {
+		coords1 = strconv.Itoa(p.Start1+1) + "," + strconv.Itoa(p.Length1)
+	}
+
+	if p.Length2 == 0 {
+		coords2 = strconv.Itoa(p.Start2) + ",0"
+	} else if p.Length2 == 1 {
+		coords2 = strconv.Itoa(p.Start2 + 1)
+	} else {
+		coords2 = strconv.Itoa(p.Start2+1) + "," + strconv.Itoa(p.Length2)
+	}
+
+	var text bytes.Buffer
+	_, _ = text.WriteString("@@ -" + coords1 + " +" + coords2 + " @@\n")
+
+	// Escape the body of the patch with %xx notation.
+	for _, aDiff := range p.diffs {
+		switch aDiff.Type {
+		case DiffInsert:
+			_, _ = text.WriteString("+")
+		case DiffDelete:
+			_, _ = text.WriteString("-")
+		case DiffEqual:
+			_, _ = text.WriteString(" ")
+		}
+
+		_, _ = text.WriteString(strings.Replace(url.QueryEscape(aDiff.Text), "+", " ", -1))
+		_, _ = text.WriteString("\n")
+	}
+
+	return unescaper.Replace(text.String())
+}
+
+// PatchAddContext increases the context until it is unique, but doesn't let the pattern expand beyond MatchMaxBits.
+func (dmp *DiffMatchPatch) PatchAddContext(patch Patch, text string) Patch {
+	if len(text) == 0 {
+		return patch
+	}
+
+	pattern := text[patch.Start2 : patch.Start2+patch.Length1]
+	padding := 0
+
+	// Look for the first and last matches of pattern in text.  If two different matches are found, increase the pattern length.
+	for strings.Index(text, pattern) != strings.LastIndex(text, pattern) &&
+		len(pattern) < dmp.MatchMaxBits-2*dmp.PatchMargin {
+		padding += dmp.PatchMargin
+		maxStart := max(0, patch.Start2-padding)
+		minEnd := min(len(text), patch.Start2+patch.Length1+padding)
+		pattern = text[maxStart:minEnd]
+	}
+	// Add one chunk for good luck.
+	padding += dmp.PatchMargin
+
+	// Add the prefix.
+	prefix := text[max(0, patch.Start2-padding):patch.Start2]
+	if len(prefix) != 0 {
+		patch.diffs = append([]Diff{Diff{DiffEqual, prefix}}, patch.diffs...)
+	}
+	// Add the suffix.
+	suffix := text[patch.Start2+patch.Length1 : min(len(text), patch.Start2+patch.Length1+padding)]
+	if len(suffix) != 0 {
+		patch.diffs = append(patch.diffs, Diff{DiffEqual, suffix})
+	}
+
+	// Roll back the start points.
+	patch.Start1 -= len(prefix)
+	patch.Start2 -= len(prefix)
+	// Extend the lengths.
+	patch.Length1 += len(prefix) + len(suffix)
+	patch.Length2 += len(prefix) + len(suffix)
+
+	return patch
+}
+
+// PatchMake computes a list of patches.
+func (dmp *DiffMatchPatch) PatchMake(opt ...interface{}) []Patch {
+	if len(opt) == 1 {
+		diffs, _ := opt[0].([]Diff)
+		text1 := dmp.DiffText1(diffs)
+		return dmp.PatchMake(text1, diffs)
+	} else if len(opt) == 2 {
+		text1 := opt[0].(string)
+		switch t := opt[1].(type) {
+		case string:
+			diffs := dmp.DiffMain(text1, t, true)
+			if len(diffs) > 2 {
+				diffs = dmp.DiffCleanupSemantic(diffs)
+				diffs = dmp.DiffCleanupEfficiency(diffs)
+			}
+			return dmp.PatchMake(text1, diffs)
+		case []Diff:
+			return dmp.patchMake2(text1, t)
+		}
+	} else if len(opt) == 3 {
+		return dmp.PatchMake(opt[0], opt[2])
+	}
+	return []Patch{}
+}
+
+// patchMake2 computes a list of patches to turn text1 into text2.
+// text2 is not provided, diffs are the delta between text1 and text2.
+func (dmp *DiffMatchPatch) patchMake2(text1 string, diffs []Diff) []Patch {
+	// Check for null inputs not needed since null can't be passed in C#.
+	patches := []Patch{}
+	if len(diffs) == 0 {
+		return patches // Get rid of the null case.
+	}
+
+	patch := Patch{}
+	charCount1 := 0 // Number of characters into the text1 string.
+	charCount2 := 0 // Number of characters into the text2 string.
+	// Start with text1 (prepatchText) and apply the diffs until we arrive at text2 (postpatchText). We recreate the patches one by one to determine context info.
+	prepatchText := text1
+	postpatchText := text1
+
+	for i, aDiff := range diffs {
+		if len(patch.diffs) == 0 && aDiff.Type != DiffEqual {
+			// A new patch starts here.
+			patch.Start1 = charCount1
+			patch.Start2 = charCount2
+		}
+
+		switch aDiff.Type {
+		case DiffInsert:
+			patch.diffs = append(patch.diffs, aDiff)
+			patch.Length2 += len(aDiff.Text)
+			postpatchText = postpatchText[:charCount2] +
+				aDiff.Text + postpatchText[charCount2:]
+		case DiffDelete:
+			patch.Length1 += len(aDiff.Text)
+			patch.diffs = append(patch.diffs, aDiff)
+			postpatchText = postpatchText[:charCount2] + postpatchText[charCount2+len(aDiff.Text):]
+		case DiffEqual:
+			if len(aDiff.Text) <= 2*dmp.PatchMargin &&
+				len(patch.diffs) != 0 && i != len(diffs)-1 {
+				// Small equality inside a patch.
+				patch.diffs = append(patch.diffs, aDiff)
+				patch.Length1 += len(aDiff.Text)
+				patch.Length2 += len(aDiff.Text)
+			}
+			if len(aDiff.Text) >= 2*dmp.PatchMargin {
+				// Time for a new patch.
+				if len(patch.diffs) != 0 {
+					patch = dmp.PatchAddContext(patch, prepatchText)
+					patches = append(patches, patch)
+					patch = Patch{}
+					// Unlike Unidiff, our patch lists have a rolling context. http://code.google.com/p/google-diff-match-patch/wiki/Unidiff Update prepatch text & pos to reflect the application of the just completed patch.
+					prepatchText = postpatchText
+					charCount1 = charCount2
+				}
+			}
+		}
+
+		// Update the current character count.
+		if aDiff.Type != DiffInsert {
+			charCount1 += len(aDiff.Text)
+		}
+		if aDiff.Type != DiffDelete {
+			charCount2 += len(aDiff.Text)
+		}
+	}
+
+	// Pick up the leftover patch if not empty.
+	if len(patch.diffs) != 0 {
+		patch = dmp.PatchAddContext(patch, prepatchText)
+		patches = append(patches, patch)
+	}
+
+	return patches
+}
+
+// PatchDeepCopy returns an array that is identical to a given an array of patches.
+func (dmp *DiffMatchPatch) PatchDeepCopy(patches []Patch) []Patch {
+	patchesCopy := []Patch{}
+	for _, aPatch := range patches {
+		patchCopy := Patch{}
+		for _, aDiff := range aPatch.diffs {
+			patchCopy.diffs = append(patchCopy.diffs, Diff{
+				aDiff.Type,
+				aDiff.Text,
+			})
+		}
+		patchCopy.Start1 = aPatch.Start1
+		patchCopy.Start2 = aPatch.Start2
+		patchCopy.Length1 = aPatch.Length1
+		patchCopy.Length2 = aPatch.Length2
+		patchesCopy = append(patchesCopy, patchCopy)
+	}
+	return patchesCopy
+}
+
+// PatchApply merges a set of patches onto the text.  Returns a patched text, as well as an array of true/false values indicating which patches were applied.
+func (dmp *DiffMatchPatch) PatchApply(patches []Patch, text string) (string, []bool) {
+	if len(patches) == 0 {
+		return text, []bool{}
+	}
+
+	// Deep copy the patches so that no changes are made to originals.
+	patches = dmp.PatchDeepCopy(patches)
+
+	nullPadding := dmp.PatchAddPadding(patches)
+	text = nullPadding + text + nullPadding
+	patches = dmp.PatchSplitMax(patches)
+
+	x := 0
+	// delta keeps track of the offset between the expected and actual location of the previous patch.  If there are patches expected at positions 10 and 20, but the first patch was found at 12, delta is 2 and the second patch has an effective expected position of 22.
+	delta := 0
+	results := make([]bool, len(patches))
+	for _, aPatch := range patches {
+		expectedLoc := aPatch.Start2 + delta
+		text1 := dmp.DiffText1(aPatch.diffs)
+		var startLoc int
+		endLoc := -1
+		if len(text1) > dmp.MatchMaxBits {
+			// PatchSplitMax will only provide an oversized pattern in the case of a monster delete.
+			startLoc = dmp.MatchMain(text, text1[:dmp.MatchMaxBits], expectedLoc)
+			if startLoc != -1 {
+				endLoc = dmp.MatchMain(text,
+					text1[len(text1)-dmp.MatchMaxBits:], expectedLoc+len(text1)-dmp.MatchMaxBits)
+				if endLoc == -1 || startLoc >= endLoc {
+					// Can't find valid trailing context.  Drop this patch.
+					startLoc = -1
+				}
+			}
+		} else {
+			startLoc = dmp.MatchMain(text, text1, expectedLoc)
+		}
+		if startLoc == -1 {
+			// No match found.  :(
+			results[x] = false
+			// Subtract the delta for this failed patch from subsequent patches.
+			delta -= aPatch.Length2 - aPatch.Length1
+		} else {
+			// Found a match.  :)
+			results[x] = true
+			delta = startLoc - expectedLoc
+			var text2 string
+			if endLoc == -1 {
+				text2 = text[startLoc:int(math.Min(float64(startLoc+len(text1)), float64(len(text))))]
+			} else {
+				text2 = text[startLoc:int(math.Min(float64(endLoc+dmp.MatchMaxBits), float64(len(text))))]
+			}
+			if text1 == text2 {
+				// Perfect match, just shove the Replacement text in.
+				text = text[:startLoc] + dmp.DiffText2(aPatch.diffs) + text[startLoc+len(text1):]
+			} else {
+				// Imperfect match.  Run a diff to get a framework of equivalent indices.
+				diffs := dmp.DiffMain(text1, text2, false)
+				if len(text1) > dmp.MatchMaxBits && float64(dmp.DiffLevenshtein(diffs))/float64(len(text1)) > dmp.PatchDeleteThreshold {
+					// The end points match, but the content is unacceptably bad.
+					results[x] = false
+				} else {
+					diffs = dmp.DiffCleanupSemanticLossless(diffs)
+					index1 := 0
+					for _, aDiff := range aPatch.diffs {
+						if aDiff.Type != DiffEqual {
+							index2 := dmp.DiffXIndex(diffs, index1)
+							if aDiff.Type == DiffInsert {
+								// Insertion
+								text = text[:startLoc+index2] + aDiff.Text + text[startLoc+index2:]
+							} else if aDiff.Type == DiffDelete {
+								// Deletion
+								startIndex := startLoc + index2
+								text = text[:startIndex] +
+									text[startIndex+dmp.DiffXIndex(diffs, index1+len(aDiff.Text))-index2:]
+							}
+						}
+						if aDiff.Type != DiffDelete {
+							index1 += len(aDiff.Text)
+						}
+					}
+				}
+			}
+		}
+		x++
+	}
+	// Strip the padding off.
+	text = text[len(nullPadding) : len(nullPadding)+(len(text)-2*len(nullPadding))]
+	return text, results
+}
+
+// PatchAddPadding adds some padding on text start and end so that edges can match something.
+// Intended to be called only from within patchApply.
+func (dmp *DiffMatchPatch) PatchAddPadding(patches []Patch) string {
+	paddingLength := dmp.PatchMargin
+	nullPadding := ""
+	for x := 1; x <= paddingLength; x++ {
+		nullPadding += string(x)
+	}
+
+	// Bump all the patches forward.
+	for i := range patches {
+		patches[i].Start1 += paddingLength
+		patches[i].Start2 += paddingLength
+	}
+
+	// Add some padding on start of first diff.
+	if len(patches[0].diffs) == 0 || patches[0].diffs[0].Type != DiffEqual {
+		// Add nullPadding equality.
+		patches[0].diffs = append([]Diff{Diff{DiffEqual, nullPadding}}, patches[0].diffs...)
+		patches[0].Start1 -= paddingLength // Should be 0.
+		patches[0].Start2 -= paddingLength // Should be 0.
+		patches[0].Length1 += paddingLength
+		patches[0].Length2 += paddingLength
+	} else if paddingLength > len(patches[0].diffs[0].Text) {
+		// Grow first equality.
+		extraLength := paddingLength - len(patches[0].diffs[0].Text)
+		patches[0].diffs[0].Text = nullPadding[len(patches[0].diffs[0].Text):] + patches[0].diffs[0].Text
+		patches[0].Start1 -= extraLength
+		patches[0].Start2 -= extraLength
+		patches[0].Length1 += extraLength
+		patches[0].Length2 += extraLength
+	}
+
+	// Add some padding on end of last diff.
+	last := len(patches) - 1
+	if len(patches[last].diffs) == 0 || patches[last].diffs[len(patches[last].diffs)-1].Type != DiffEqual {
+		// Add nullPadding equality.
+		patches[last].diffs = append(patches[last].diffs, Diff{DiffEqual, nullPadding})
+		patches[last].Length1 += paddingLength
+		patches[last].Length2 += paddingLength
+	} else if paddingLength > len(patches[last].diffs[len(patches[last].diffs)-1].Text) {
+		// Grow last equality.
+		lastDiff := patches[last].diffs[len(patches[last].diffs)-1]
+		extraLength := paddingLength - len(lastDiff.Text)
+		patches[last].diffs[len(patches[last].diffs)-1].Text += nullPadding[:extraLength]
+		patches[last].Length1 += extraLength
+		patches[last].Length2 += extraLength
+	}
+
+	return nullPadding
+}
+
+// PatchSplitMax looks through the patches and breaks up any which are longer than the maximum limit of the match algorithm.
+// Intended to be called only from within patchApply.
+func (dmp *DiffMatchPatch) PatchSplitMax(patches []Patch) []Patch {
+	patchSize := dmp.MatchMaxBits
+	for x := 0; x < len(patches); x++ {
+		if patches[x].Length1 <= patchSize {
+			continue
+		}
+		bigpatch := patches[x]
+		// Remove the big old patch.
+		patches = append(patches[:x], patches[x+1:]...)
+		x--
+
+		Start1 := bigpatch.Start1
+		Start2 := bigpatch.Start2
+		precontext := ""
+		for len(bigpatch.diffs) != 0 {
+			// Create one of several smaller patches.
+			patch := Patch{}
+			empty := true
+			patch.Start1 = Start1 - len(precontext)
+			patch.Start2 = Start2 - len(precontext)
+			if len(precontext) != 0 {
+				patch.Length1 = len(precontext)
+				patch.Length2 = len(precontext)
+				patch.diffs = append(patch.diffs, Diff{DiffEqual, precontext})
+			}
+			for len(bigpatch.diffs) != 0 && patch.Length1 < patchSize-dmp.PatchMargin {
+				diffType := bigpatch.diffs[0].Type
+				diffText := bigpatch.diffs[0].Text
+				if diffType == DiffInsert {
+					// Insertions are harmless.
+					patch.Length2 += len(diffText)
+					Start2 += len(diffText)
+					patch.diffs = append(patch.diffs, bigpatch.diffs[0])
+					bigpatch.diffs = bigpatch.diffs[1:]
+					empty = false
+				} else if diffType == DiffDelete && len(patch.diffs) == 1 && patch.diffs[0].Type == DiffEqual && len(diffText) > 2*patchSize {
+					// This is a large deletion.  Let it pass in one chunk.
+					patch.Length1 += len(diffText)
+					Start1 += len(diffText)
+					empty = false
+					patch.diffs = append(patch.diffs, Diff{diffType, diffText})
+					bigpatch.diffs = bigpatch.diffs[1:]
+				} else {
+					// Deletion or equality.  Only take as much as we can stomach.
+					diffText = diffText[:min(len(diffText), patchSize-patch.Length1-dmp.PatchMargin)]
+
+					patch.Length1 += len(diffText)
+					Start1 += len(diffText)
+					if diffType == DiffEqual {
+						patch.Length2 += len(diffText)
+						Start2 += len(diffText)
+					} else {
+						empty = false
+					}
+					patch.diffs = append(patch.diffs, Diff{diffType, diffText})
+					if diffText == bigpatch.diffs[0].Text {
+						bigpatch.diffs = bigpatch.diffs[1:]
+					} else {
+						bigpatch.diffs[0].Text =
+							bigpatch.diffs[0].Text[len(diffText):]
+					}
+				}
+			}
+			// Compute the head context for the next patch.
+			precontext = dmp.DiffText2(patch.diffs)
+			precontext = precontext[max(0, len(precontext)-dmp.PatchMargin):]
+
+			postcontext := ""
+			// Append the end context for this patch.
+			if len(dmp.DiffText1(bigpatch.diffs)) > dmp.PatchMargin {
+				postcontext = dmp.DiffText1(bigpatch.diffs)[:dmp.PatchMargin]
+			} else {
+				postcontext = dmp.DiffText1(bigpatch.diffs)
+			}
+
+			if len(postcontext) != 0 {
+				patch.Length1 += len(postcontext)
+				patch.Length2 += len(postcontext)
+				if len(patch.diffs) != 0 && patch.diffs[len(patch.diffs)-1].Type == DiffEqual {
+					patch.diffs[len(patch.diffs)-1].Text += postcontext
+				} else {
+					patch.diffs = append(patch.diffs, Diff{DiffEqual, postcontext})
+				}
+			}
+			if !empty {
+				x++
+				patches = append(patches[:x], append([]Patch{patch}, patches[x:]...)...)
+			}
+		}
+	}
+	return patches
+}
+
+// PatchToText takes a list of patches and returns a textual representation.
+func (dmp *DiffMatchPatch) PatchToText(patches []Patch) string {
+	var text bytes.Buffer
+	for _, aPatch := range patches {
+		_, _ = text.WriteString(aPatch.String())
+	}
+	return text.String()
+}
+
+// PatchFromText parses a textual representation of patches and returns a List of Patch objects.
+func (dmp *DiffMatchPatch) PatchFromText(textline string) ([]Patch, error) {
+	patches := []Patch{}
+	if len(textline) == 0 {
+		return patches, nil
+	}
+	text := strings.Split(textline, "\n")
+	textPointer := 0
+	patchHeader := regexp.MustCompile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$")
+
+	var patch Patch
+	var sign uint8
+	var line string
+	for textPointer < len(text) {
+
+		if !patchHeader.MatchString(text[textPointer]) {
+			return patches, errors.New("Invalid patch string: " + text[textPointer])
+		}
+
+		patch = Patch{}
+		m := patchHeader.FindStringSubmatch(text[textPointer])
+
+		patch.Start1, _ = strconv.Atoi(m[1])
+		if len(m[2]) == 0 {
+			patch.Start1--
+			patch.Length1 = 1
+		} else if m[2] == "0" {
+			patch.Length1 = 0
+		} else {
+			patch.Start1--
+			patch.Length1, _ = strconv.Atoi(m[2])
+		}
+
+		patch.Start2, _ = strconv.Atoi(m[3])
+
+		if len(m[4]) == 0 {
+			patch.Start2--
+			patch.Length2 = 1
+		} else if m[4] == "0" {
+			patch.Length2 = 0
+		} else {
+			patch.Start2--
+			patch.Length2, _ = strconv.Atoi(m[4])
+		}
+		textPointer++
+
+		for textPointer < len(text) {
+			if len(text[textPointer]) > 0 {
+				sign = text[textPointer][0]
+			} else {
+				textPointer++
+				continue
+			}
+
+			line = text[textPointer][1:]
+			line = strings.Replace(line, "+", "%2b", -1)
+			line, _ = url.QueryUnescape(line)
+			if sign == '-' {
+				// Deletion.
+				patch.diffs = append(patch.diffs, Diff{DiffDelete, line})
+			} else if sign == '+' {
+				// Insertion.
+				patch.diffs = append(patch.diffs, Diff{DiffInsert, line})
+			} else if sign == ' ' {
+				// Minor equality.
+				patch.diffs = append(patch.diffs, Diff{DiffEqual, line})
+			} else if sign == '@' {
+				// Start of next patch.
+				break
+			} else {
+				// WTF?
+				return patches, errors.New("Invalid patch mode '" + string(sign) + "' in: " + string(line))
+			}
+			textPointer++
+		}
+
+		patches = append(patches, patch)
+	}
+	return patches, nil
+}
diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go
new file mode 100644
index 00000000..265f29cc
--- /dev/null
+++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go
@@ -0,0 +1,88 @@
+// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
+// https://github.com/sergi/go-diff
+// See the included LICENSE file for license details.
+//
+// go-diff is a Go implementation of Google's Diff, Match, and Patch library
+// Original library is Copyright (c) 2006 Google Inc.
+// http://code.google.com/p/google-diff-match-patch/
+
+package diffmatchpatch
+
+import (
+	"strings"
+	"unicode/utf8"
+)
+
+// unescaper unescapes selected chars for compatibility with JavaScript's encodeURI.
+// In speed critical applications this could be dropped since the receiving application will certainly decode these fine. Note that this function is case-sensitive.  Thus "%3F" would not be unescaped.  But this is ok because it is only called with the output of HttpUtility.UrlEncode which returns lowercase hex. Example: "%3f" -> "?", "%24" -> "$", etc.
+var unescaper = strings.NewReplacer(
+	"%21", "!", "%7E", "~", "%27", "'",
+	"%28", "(", "%29", ")", "%3B", ";",
+	"%2F", "/", "%3F", "?", "%3A", ":",
+	"%40", "@", "%26", "&", "%3D", "=",
+	"%2B", "+", "%24", "$", "%2C", ",", "%23", "#", "%2A", "*")
+
+// indexOf returns the first index of pattern in str, starting at str[i].
+func indexOf(str string, pattern string, i int) int {
+	if i > len(str)-1 {
+		return -1
+	}
+	if i <= 0 {
+		return strings.Index(str, pattern)
+	}
+	ind := strings.Index(str[i:], pattern)
+	if ind == -1 {
+		return -1
+	}
+	return ind + i
+}
+
+// lastIndexOf returns the last index of pattern in str, starting at str[i].
+func lastIndexOf(str string, pattern string, i int) int {
+	if i < 0 {
+		return -1
+	}
+	if i >= len(str) {
+		return strings.LastIndex(str, pattern)
+	}
+	_, size := utf8.DecodeRuneInString(str[i:])
+	return strings.LastIndex(str[:i+size], pattern)
+}
+
+// runesIndexOf returns the index of pattern in target, starting at target[i].
+func runesIndexOf(target, pattern []rune, i int) int {
+	if i > len(target)-1 {
+		return -1
+	}
+	if i <= 0 {
+		return runesIndex(target, pattern)
+	}
+	ind := runesIndex(target[i:], pattern)
+	if ind == -1 {
+		return -1
+	}
+	return ind + i
+}
+
+func runesEqual(r1, r2 []rune) bool {
+	if len(r1) != len(r2) {
+		return false
+	}
+	for i, c := range r1 {
+		if c != r2[i] {
+			return false
+		}
+	}
+	return true
+}
+
+// runesIndex is the equivalent of strings.Index for rune slices.
+func runesIndex(r1, r2 []rune) int {
+	last := len(r1) - len(r2)
+	for i := 0; i <= last; i++ {
+		if runesEqual(r1[i:i+len(r2)], r2) {
+			return i
+		}
+	}
+	return -1
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 1f4d65db..a569dc39 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -31,6 +31,9 @@ github.com/DisposaBoy/JsonConfigReader
 # github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
 ## explicit
 github.com/TomOnTime/utfutil
+# github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
+## explicit
+github.com/andreyvit/diff
 # github.com/aws/aws-sdk-go v1.29.1
 ## explicit
 github.com/aws/aws-sdk-go/aws
@@ -135,6 +138,8 @@ github.com/golang/protobuf/ptypes/duration
 github.com/golang/protobuf/ptypes/timestamp
 # github.com/golang/snappy v0.0.1
 github.com/golang/snappy
+# github.com/google/go-cmp v0.4.0
+## explicit
 # github.com/google/go-github v17.0.0+incompatible
 ## explicit
 github.com/google/go-github/github
@@ -204,8 +209,6 @@ github.com/jmespath/go-jmespath
 # github.com/kolo/xmlrpc v0.0.0-20150413191830-0826b98aaa29
 ## explicit
 github.com/kolo/xmlrpc
-# github.com/kr/pretty v0.1.0
-## explicit
 # github.com/malexdev/utfutil v0.0.0-20180510171754-00c8d4a8e7a8
 ## explicit
 # github.com/miekg/dns v1.1.27
@@ -265,6 +268,9 @@ github.com/robertkrimen/otto/underscore
 github.com/russross/blackfriday/v2
 # github.com/ryanuber/go-glob v1.0.0
 github.com/ryanuber/go-glob
+# github.com/sergi/go-diff v1.1.0
+## explicit
+github.com/sergi/go-diff/diffmatchpatch
 # github.com/shurcooL/sanitized_anchor_name v1.0.0
 github.com/shurcooL/sanitized_anchor_name
 # github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a
@@ -330,7 +336,6 @@ golang.org/x/crypto/pbkdf2
 golang.org/x/crypto/pkcs12
 golang.org/x/crypto/pkcs12/internal/rc2
 # golang.org/x/mod v0.2.0
-## explicit
 golang.org/x/mod/module
 golang.org/x/mod/semver
 # golang.org/x/net v0.0.0-20200226121028-0de0cce0169b
@@ -383,7 +388,6 @@ golang.org/x/tools/internal/gocommand
 golang.org/x/tools/internal/gopathwalk
 golang.org/x/tools/internal/imports
 # golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
-## explicit
 golang.org/x/xerrors
 golang.org/x/xerrors/internal
 # google.golang.org/api v0.17.0
@@ -448,8 +452,6 @@ google.golang.org/grpc/serviceconfig
 google.golang.org/grpc/stats
 google.golang.org/grpc/status
 google.golang.org/grpc/tap
-# gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127
-## explicit
 # gopkg.in/ini.v1 v1.42.0
 ## explicit
 gopkg.in/ini.v1


Test patch:
diff --git a/commands/gz_test.go b/commands/gz_test.go
new file mode 100644
index 00000000..f395562b
--- /dev/null
+++ b/commands/gz_test.go
@@ -0,0 +1,80 @@
+package commands
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"testing"
+
+	"github.com/andreyvit/diff"
+
+	_ "github.com/StackExchange/dnscontrol/v2/providers/_all"
+)
+
+func TestFormatTypes(t *testing.T) {
+	/*
+	  Input:                   Converted to:   Should match contents of:
+	  test_data/$DOMAIN.zone   js              test_data/$DOMAIN.zone.js
+	  test_data/$DOMAIN.zone   tsv             test_data/$DOMAIN.zone.tsv
+	  test_data/$DOMAIN.zone   zone            test_data/$DOMAIN.zone.zone
+	*/
+
+	for _, domain := range []string{"simple.com", "example.org"} {
+		t.Run(domain+"/js", func(t *testing.T) { testFormat(t, domain, "js") })
+		t.Run(domain+"/tsv", func(t *testing.T) { testFormat(t, domain, "tsv") })
+		t.Run(domain+"/zone", func(t *testing.T) { testFormat(t, domain, "zone") })
+	}
+}
+
+func testFormat(t *testing.T, domain, format string) {
+	t.Helper()
+
+	expectedFilename := fmt.Sprintf("test_data/%s.zone.%s", domain, format)
+	outputFiletmpl := fmt.Sprintf("%s.zone.%s.*.txt", domain, format)
+
+	outfile, err := ioutil.TempFile("", outputFiletmpl)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer os.Remove(outfile.Name())
+
+	// Convert test data to the experiment output.
+	gzargs := GetZoneArgs{
+		ZoneNames:    []string{domain},
+		OutputFormat: format,
+		OutputFile:   outfile.Name(),
+		CredName:     "bind",
+		ProviderName: "BIND",
+	}
+	gzargs.CredsFile = "test_data/creds.json"
+
+	// Read the zonefile and convert
+	err = GetZone(gzargs)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Read the actual result:
+	got, err := ioutil.ReadFile(outfile.Name())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Read the expected result
+	want, err := ioutil.ReadFile(expectedFilename)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	//	// Update got -> want
+	//	err = ioutil.WriteFile(expectedFilename, got, 0644)
+	//	if err != nil {
+	//		log.Fatal(err)
+	//	}
+
+	if w, g := string(want), string(got); w != g {
+		t.Errorf("testFormat mismatch (-got +want):\n%s", diff.LineDiff(g, w))
+	}
+
+}
diff --git a/commands/test_data/example.org.zone b/commands/test_data/example.org.zone
new file mode 100644
index 00000000..8e22339d
--- /dev/null
+++ b/commands/test_data/example.org.zone
@@ -0,0 +1,1150 @@
+; Copyright © 2006-2020 Phil Pennock
+; No warranty: if it breaks, you get to cut your hands picking up the shards.
+; Permission granted to duplicate in its entirety with authorship preserved,
+; or redact as needed for test frameworks, or copy fragments for use in your
+; own domains, or pretty much anything else except presenting it as your own
+; creation.
+
+$TTL 7200
+$ORIGIN example.org.
+
+; This is adapted from a real zonefile.
+; All IPs and DNS should be using address-spaces reserved for documentation or
+; otherwise available for this usage.
+; The original had a /28 routed to an onlink /32 in IPv4, and a /48 for IPv6.
+;
+; A couple of entries come from a different zone, to round out the examples.
+;
+; Some information has been redacted, eg Let's Encrypt account numbers.
+; Some public keys are the same as they were in the original zone-file, so if
+; you have tooling which parses ASN structure or whatever, they should still be
+; good.
+; If you have a DNS Observatory, you can probably identify the original zone
+; through some of these.  Good luck!
+;
+; Some of the IPv6 stuff shows a H:EX encoding scheme; that's real, not
+; invented for the example.org redaction.  I just changed the prefix.
+
+; {{{ SOA and NS records
+example.org.	43200 SOA	ns1.example.org. hostmaster.example.org. (
+			2020030700	; serial
+			7200		; refresh period
+			3600		; retry interval
+			10d		; expire time
+			7200		; default/negative TTL
+			)
+		NS	ns1.example.org.
+		NS	ns2.example.org.
+		NS	ns-a.example.net.
+		NS	friend-dns.example.com.
+; }}} SOA and NS records
+
+;;; ======================================================================
+; EMAIL FEDERATION {{{
+; These are not for clients within our domain.
+; These are for sending and receiving email, and telling other mail-systems
+; what can send email as us, or claim to be us in HELO, etc.
+;
+; For SPF: this affects "domains which aren't the domain in question"; we can
+; ignore it for our own domains from systems under our control, as the
+; mail-server we list here is the one which all mail is channeled through, so
+; is what the outside world sees, so is the one listed.
+
+
+		MX	10 mx
+		TXT	"v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"
+; CSA Priority/Weight/Port;
+;   Priority for CSA version, so 1
+;   Weight: 1 unauthorized, 2 authorized, 3 unknown  (bit-field)
+;   Port: assertions: 0 no assertion; 1 subdomains must use CSA
+;   Target: hostname for which sender IP must be in the RRsets of address records
+; Top-level assertion: we list _all_ hostnames which send email out, so EHLO/HELO claiming to be us must be covered here.
+; Because almost all mail routes through the mail-hub, we can make this claim; foo is exception
+_client._smtp		SRV	1 1 1 @
+_client._smtp.mx	SRV	1 2 1 mx
+_client._smtp.foo	SRV	1 2 1 foo
+
+; }}}
+
+;;; ======================================================================
+; Authentication and directory services {{{
+
+; _Service._Proto.Name TTL Class SRV Priority Weight Port Target
+_kerberos._tcp		SRV	10 1 88 kerb-service
+_kerberos._udp		SRV	10 1 88 kerb-service
+_kpasswd._udp		SRV	10 1 464 kerb-service
+_kerberos-adm._tcp	SRV	10 1 749 kerb-service
+_kerberos		TXT	"EXAMPLE.ORG"
+
+; No LDAP service, and tell people that.
+_ldap._tcp		SRV	0 0 0 .
+_ldap._udp		SRV	0 0 0 .
+
+; PGP Universal & GnuPG use keys.$domain to find PGP keys, via LDAP
+
+; }}}
+
+;;; ======================================================================
+; Chat services {{{
+
+; XMPP we divide up the hostnames for federation vs client access.
+; Federation on xmpp-s2s, client access on xmpp.
+; They are the same host, but we architect to be able to split.
+_jabber._tcp		SRV	10 2 5269 xmpp-s2s
+_xmpp-server._tcp	SRV	10 2 5269 xmpp-s2s
+_xmpp-client._tcp	SRV	10 2 5222 xmpp
+; I don't think we need _im._xmpp and _pres._xmpp for a real present server, so
+; skip them.  If setting up a domain without XMPP, publish `0 0 0 .` for these too.
+; _im._xmpp		SRV ...
+; _pres._xmpp		SRV ...
+
+; RFC 3832 "Remote Discovery in SLP via DNS SRV" {exp}: _slpda.{_tcp,_udp}
+; RFC 3861: _im.<proto> and _pres.<proto> for IM protocols;
+;   Instant Messaging SRV Protocol Label registry  http://www.iana.org/assignments/im-srv-labels
+;   Presence SRV Protocol Label registry           http://www.iana.org/assignments/pres-srv-labels/pres-srv-labels.xhtml
+;  So far, both only contain: _xmpp
+;  Thus _im._xmpp _pres._xmpp
+;  draft-loreto-simple-im-srv-label-02.txt adds _sip: _im._sip _pres._sip
+; RFC 3921: _im._xmpp _pres._xmpp _xmpp._tcp
+; SIP SRV: http://www.iana.org/assignments/sip-table
+;   _sip+d2t._tcp _sips+d2t._tcp _sip+d2u._udp _sip+d2s._sctp _sips+d2s._sctp
+; RFC 4386: http://www.iana.org/assignments/pkix-parameters/pkix-parameters.xhtml
+;   _pkixrep.<proto> -> _ldap _http _ocsp
+; Also possibly:  _sip _sips _sipfederation _msrps
+
+_im._sip		SRV	0 0 0 .
+_pres._sip		SRV	0 0 0 .
+_sip+d2t._tcp		SRV	0 0 0 .
+_sips+d2t._tcp		SRV	0 0 0 .
+_sip+d2u._udp		SRV	0 0 0 .
+_sip+d2s._sctp		SRV	0 0 0 .
+_sips+d2s._sctp		SRV	0 0 0 .
+
+; }}}
+
+;;; ======================================================================
+; Email service for use by clients within the domain. {{{
+; RFC 6186 (was: draft-daboo-srv-email-05.txt)
+; RFC 8314 updates for submissions
+; RFC 8314 updates for submissions (obsoletes all cleartext)
+; RFC 8552 sets up a registry for all the underscore services and 8553 backfills it
+
+_submission._tcp	SRV	10 10 587 smtp
+_submissions._tcp	SRV	10 10 465 smtp
+_imap._tcp		SRV	10 10 143 imap
+_imaps._tcp		SRV	10 10 993 imap
+_pop3._tcp		SRV	0 0 0 .
+_pop3s._tcp		SRV	0 0 0 .
+_sieve._tcp		SRV	10 10 4190 imap
+
+; }}}
+
+;;; ======================================================================
+; Other zone-top services {{{
+
+; Where can people send questions or get more information
+;;;@		RP	hostmaster.example.org. dns-moreinfo.example.org.
+
+dns-moreinfo		TXT (
+		"Fred Bloggs, TZ=America/New_York"
+		"Chat-Service-X: @handle1"
+		"Chat-Service-Y: federated-handle@example.org"
+		)
+
+; SKS withdrawn
+_pgpkey-http._tcp	SRV	0 0 0 .
+_pgpkey-https._tcp	SRV	0 0 0 .
+_hkp._tcp		SRV	0 0 0 .
+
+; WKD <https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/?include_text=1>
+; The SRV record should be disappearing because it was removed from the spec.
+; In the meantime, point it to the host while being cognizant that GnuPG will
+; use the hostname from the target of the SRV for the Host header.
+_openpgpkey._tcp	SRV	10 10 443 openpgpkey.example.org.
+
+; Not sure anything actually uses this, but ah well
+; Avoid pointing at a CNAME, just use barbican directly
+_finger._tcp		SRV	10 10 79 barbican
+
+; https://wiki.libravatar.org/api/
+_avatars-sec._tcp	SRV	10 10 443 avatars
+
+; For bare domain as a hostname, we try to avoid it as much as possible.
+; Sometimes, it's necessary, eg finger.
+; Once it exists in DNS though, HTTP requests will try to hit it when people
+; don't type `www.`
+; So we point the bare domain at a minimal jail with "not much" in it, and
+; use packet filters to redirect the traffic to the correct places.
+; For HTTP, we'll issue redirects to avoid this.  For finger, we just respond.
+@			A	192.0.2.1
+@			AAAA	2001:db8::1:1
+
+; }}}
+
+;;; ======================================================================
+; Email cryptographic authentication {{{
+
+; RFC5585 DomainKeys Identified Mail (DKIM) Service Overview
+_adsp._domainkey	TXT "dkim=all"  ; RFC5617 unknown | all | discardable
+; http://dmarc.org/draft-dmarc-base-00-01.html DMARC DKIM policy
+;
+; DMARC: beware that some validators require v/p with no intermediate values,
+; which one reading of RFC7489 can support.  So keep those together at the front.
+_dmarc			TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+
+; [This is a good place to have a link to documentation on local processes for
+;  updating DKIM keys]
+;
+; These are the real current (March 2020) values for the domain which this
+; example was drawn from.
+; Note that the domain dual-signs email with both RSA and Ed25519 keys.
+; The domain also cycles DKIM keys every three months, but it's manual and the
+; calendar reminder was missed in February.  Oops.
+;
+d201911._domainkey TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks" "6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB"
+d201911e2._domainkey TXT "v=DKIM1; k=ed25519; p=GBt2k2L39KUb39fg5brOppXDHXvISy0+ECGgPld/bIo="
+;
+d202003._domainkey TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jo" "pv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB"
+d202003e2._domainkey TXT "v=DKIM1; k=ed25519; p=DQI5d9sNMrr0SLDoAi071IFOyKnlbR29hAQdqVQecQg="
+;
+
+; http://tools.ietf.org/html/draft-ietf-marf-reporting-discovery-01
+_report			TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+; These are for RFC8460 and the sender needs to support MTA-STS or DANE; if
+; they only support one, we might get error complaints.
+; SMTP TLS Reporting used `_smtp-tlsrpt` in <https://tools.ietf.org/html/draft-ietf-uta-smtp-tlsrpt-17>
+; but `_smtp._tls` by the time RFC 8460 was published.
+_smtp._tls		TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp-tlsrpt		TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+
+; RFC 7489 § 7.1
+; Any domains which point their DMARC records to email in this domain need
+; to be authorized by having a record here, saying "yes, it's known, go ahead
+; and send their DMARC reports to us."
+;
+; This is not needed for our sub-domains, because they're the same
+; "organizational domain" as us (organizational domain is the thing which is
+; publicly registered, ie where the parent domain is in the public suffix
+; list).
+;
+$ORIGIN _report._dmarc.example.org.
+example.net			TXT	"v=DMARC1"
+example.com			TXT	"v=DMARC1"
+xn--2j5b.xn--9t4b11yi5a		TXT	"v=DMARC1"
+special.test			TXT	"v=DMARC1"
+xn--qck5b9a5eml3bze.xn--zckzah	TXT	"v=DMARC1"
+$ORIGIN example.org.
+
+; https://datatracker.ietf.org/doc/draft-ietf-dane-smime/?include_text=1
+; Using Secure DNS to Associate Certificates with Domain Names For S/MIME
+;  draft-ietf-dane-smime-15
+
+*._smimecert		CNAME	_ourca-smimea
+; Whatever the LHS, it all uses the one set of CAs, mine, which is ECC.
+
+; }}}
+
+;;; ======================================================================
+; Zeroconf Delegation {{{
+
+; Point clients at a sub-domain for Wide Area Bonjour
+; "b" = browse domain
+; "lb" = legacy browse domain (include domain in empty-string browses)
+; "r" = registration domain
+b._dns-sd._udp		PTR field
+lb._dns-sd._udp		PTR field
+r._dns-sd._udp		PTR field
+
+field		NS	ns1.example.org.
+		NS	ns2.example.org.
+;;;field		DS	50664 7 1 8AA19AF49BFBAE7103E3450FB19E7C4B88FA556A
+;;;field		DS	50664 7 2 D4EEDAE5EC46C3C1A3A6DC6BC4404F36BA00E4025562A9BC8F3261A9 D0F08F96
+
+; }}}
+
+;;; ======================================================================
+; Hosts and SSH Fingerprints {{{
+
+; SSH fingerprints
+; RFC4255 Using DNS to Securely Publish Secure Shell (SSH) Key Fingerprints
+; RFC6594 Use of the SHA-256 Algorithm with RSA, Digital Signature Algorithm (DSA), and Elliptic Curve DSA (ECDSA) in SSHFP Resource Records
+; http://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xml
+;
+; On a host with a suitably up-to-date ssh-keygen, using the .pub files:
+;   ssh-keygen -r $hostname -f /etc/ssh/ssh_host_$type_key.pub
+;
+; Records are:  SSHFP ${keytype_alg} ${hash_alg} ${hash_data}
+;  keytype_alg:  1/RSA 2/DSA 3/ECDSA 4/Ed25519 6/Ed448
+;  hash_alg:     1/SHA-1  2/SHA-256
+;
+;nb: SSH DSA currently constrained to 1024-bit, far too short, so dropping alg=2 from DNS
+;nb: SHA-1 is dead, let's stop publishing it.
+
+; barbican: perimeter defense {{{
+; barbican runs perimeter general services, such as recursive DNS
+; This is also the IP address used for NAT/RDR for private jails
+; This is also now @ for example.org too.
+; Deliberately no SSH here.
+barbican	A	192.0.2.1
+barbican	AAAA	2001:db8::1:1
+barbican.ipv4	A	192.0.2.1
+barbican.ipv6	AAAA	2001:db8::1:1
+; }}}
+
+; megalomaniac: Onlink-address for main Colo box {{{
+megalomaniac		A	198.51.100.254
+			AAAA	2001:db8:ffef::254
+megalomaniac.ipv4	A	198.51.100.254
+megalomaniac.ipv6	AAAA	2001:db8:ffef::254
+
+megalomaniac SSHFP 1 2 4e9ced94d3caf2ce915f85a63ce7279d5118a79ea03dac59cf4859b825d2f619
+megalomaniac SSHFP 3 2 d3556a3db83ab9ccec39dc6693dd2f3e28b178c9bba61880924821c426cc61eb
+megalomaniac SSHFP 4 2 c60c9d9d4728668f5f46986ff0c5b416c5e913862c4970cbfe211a6f44a111b4
+
+megalomaniac.ipv4 SSHFP 1 2 4e9ced94d3caf2ce915f85a63ce7279d5118a79ea03dac59cf4859b825d2f619
+megalomaniac.ipv4 SSHFP 3 2 d3556a3db83ab9ccec39dc6693dd2f3e28b178c9bba61880924821c426cc61eb
+megalomaniac.ipv4 SSHFP 4 2 c60c9d9d4728668f5f46986ff0c5b416c5e913862c4970cbfe211a6f44a111b4
+
+megalomaniac.ipv6 SSHFP 1 2 4e9ced94d3caf2ce915f85a63ce7279d5118a79ea03dac59cf4859b825d2f619
+megalomaniac.ipv6 SSHFP 3 2 d3556a3db83ab9ccec39dc6693dd2f3e28b178c9bba61880924821c426cc61eb
+megalomaniac.ipv6 SSHFP 4 2 c60c9d9d4728668f5f46986ff0c5b416c5e913862c4970cbfe211a6f44a111b4
+; }}}
+
+; tower is the main login jail {{{
+tower		A	192.0.2.42
+tower		AAAA	2001:db8::1:42
+tower.ipv4	A	192.0.2.42
+tower.ipv6	AAAA	2001:db8::1:42
+
+tower SSHFP 1 2 0f211d236e94768911a294f38653c4af6fa935a5b06c975d8162f59142571451
+tower SSHFP 3 2 88bf7b7401c11fa2e84871efb06cd73d8fc409154605b354db2dda0b82fe1160
+tower SSHFP 4 2 6d30900be0faaae73568fc007a87b4d076cf9a351ecacc1106aef726c34ad61d
+tower.ipv4 SSHFP 1 2 0f211d236e94768911a294f38653c4af6fa935a5b06c975d8162f59142571451
+tower.ipv4 SSHFP 3 2 88bf7b7401c11fa2e84871efb06cd73d8fc409154605b354db2dda0b82fe1160
+tower.ipv4 SSHFP 4 2 6d30900be0faaae73568fc007a87b4d076cf9a351ecacc1106aef726c34ad61d
+tower.ipv6 SSHFP 1 2 0f211d236e94768911a294f38653c4af6fa935a5b06c975d8162f59142571451
+tower.ipv6 SSHFP 3 2 88bf7b7401c11fa2e84871efb06cd73d8fc409154605b354db2dda0b82fe1160
+tower.ipv6 SSHFP 4 2 6d30900be0faaae73568fc007a87b4d076cf9a351ecacc1106aef726c34ad61d
+; }}}
+
+; vcs for svn and git {{{
+vcs		A	192.0.2.228
+vcs		AAAA	2001:db8::48:4558:4456:4353
+vcs.ipv4	A	192.0.2.228
+vcs.ipv6	AAAA	2001:db8::48:4558:4456:4353
+git		CNAME	vcs
+git.ipv4	CNAME	vcs.ipv4
+git.ipv6	CNAME	vcs.ipv6
+; svn is kerberized so has its own hostname and is only IPv6-accessible because
+; we don't have IPv4 to spare for this.
+svn		AAAA	2001:db8::48:4558:73:766e
+
+vcs SSHFP 1 2 b518be390babdf43cb2d598aa6befa6ce6878546bf107b829d0cfc65253a97d4
+vcs SSHFP 3 2 e92545dc0bf501f72333ddeb7a37afc2c5b408ce39a3ad95fbc66236f0077323
+vcs SSHFP 4 2 02289441124a487095a6cda2e946c6a8ed9087faf3592ec4135536c3e615521c
+vcs.ipv4 SSHFP 1 2 b518be390babdf43cb2d598aa6befa6ce6878546bf107b829d0cfc65253a97d4
+vcs.ipv4 SSHFP 3 2 e92545dc0bf501f72333ddeb7a37afc2c5b408ce39a3ad95fbc66236f0077323
+vcs.ipv4 SSHFP 4 2 02289441124a487095a6cda2e946c6a8ed9087faf3592ec4135536c3e615521c
+vcs.ipv6 SSHFP 1 2 b518be390babdf43cb2d598aa6befa6ce6878546bf107b829d0cfc65253a97d4
+vcs.ipv6 SSHFP 3 2 e92545dc0bf501f72333ddeb7a37afc2c5b408ce39a3ad95fbc66236f0077323
+vcs.ipv6 SSHFP 4 2 02289441124a487095a6cda2e946c6a8ed9087faf3592ec4135536c3e615521c
+; }}}
+
+; nsauth is the authoritative DNS server {{{
+nsauth		A	192.0.2.53
+nsauth		AAAA	2001:db8::53:1
+nsauth.ipv4	A	192.0.2.53
+nsauth.ipv6	AAAA	2001:db8::53:1
+
+nsauth SSHFP 1 2 895804ae022fff643b2677563cb850607c5bb564d9919896c521098c8abc40f2
+nsauth SSHFP 3 2 28a65470badae611375747e1a803211c41e3d71e97741fa92ccbdf7b01f34e42
+nsauth SSHFP 4 2 6e10445c0649c03fa83e18b1873e5b89b3a20893ecb48d01e7cedb3dd563ecf0
+nsauth.ipv4 SSHFP 1 2 895804ae022fff643b2677563cb850607c5bb564d9919896c521098c8abc40f2
+nsauth.ipv4 SSHFP 3 2 28a65470badae611375747e1a803211c41e3d71e97741fa92ccbdf7b01f34e42
+nsauth.ipv4 SSHFP 4 2 6e10445c0649c03fa83e18b1873e5b89b3a20893ecb48d01e7cedb3dd563ecf0
+nsauth.ipv6 SSHFP 1 2 895804ae022fff643b2677563cb850607c5bb564d9919896c521098c8abc40f2
+nsauth.ipv6 SSHFP 3 2 28a65470badae611375747e1a803211c41e3d71e97741fa92ccbdf7b01f34e42
+nsauth.ipv6 SSHFP 4 2 6e10445c0649c03fa83e18b1873e5b89b3a20893ecb48d01e7cedb3dd563ecf0
+
+; These are the entries which appear in glue
+ns1		A	192.0.2.53
+ns1		AAAA	2001:db8::53:1
+ns2		A	203.0.113.53
+ns2		AAAA	2001:db8:113::53
+
+; }}}
+
+; hermes for mail {{{
+; The raw IPv6 for this match the SMTP and IMAP _submission_ aliases
+; for various reasons, but not the MX IPv6 (which also goes there).
+hermes		A	192.0.2.25
+hermes		AAAA	2001:db8::48:4558:736d:7470
+hermes		AAAA	2001:db8::48:4558:696d:6170
+hermes.ipv4	A	192.0.2.25
+hermes.ipv6	AAAA	2001:db8::48:4558:736d:7470
+hermes.ipv6	AAAA	2001:db8::48:4558:696d:6170
+hermes SSHFP 1 2 4472ff5bd0528cd49216af4503ba6a1c48f121d0292a31d6af193e5000af4966
+hermes SSHFP 3 2 eaba20c1565676a5229184ccfcf82d0ee408f91757a67d9fa51a0b6f3db4a33b
+hermes SSHFP 4 2 a9d89920e599d04363c8b35a4ce66c1ed257ea1d16981f060b6aed080bbb7a7c
+hermes.ipv4 SSHFP 1 2 4472ff5bd0528cd49216af4503ba6a1c48f121d0292a31d6af193e5000af4966
+hermes.ipv4 SSHFP 3 2 eaba20c1565676a5229184ccfcf82d0ee408f91757a67d9fa51a0b6f3db4a33b
+hermes.ipv4 SSHFP 4 2 a9d89920e599d04363c8b35a4ce66c1ed257ea1d16981f060b6aed080bbb7a7c
+hermes.ipv6 SSHFP 1 2 4472ff5bd0528cd49216af4503ba6a1c48f121d0292a31d6af193e5000af4966
+hermes.ipv6 SSHFP 3 2 eaba20c1565676a5229184ccfcf82d0ee408f91757a67d9fa51a0b6f3db4a33b
+hermes.ipv6 SSHFP 4 2 a9d89920e599d04363c8b35a4ce66c1ed257ea1d16981f060b6aed080bbb7a7c
+; }}}
+
+; other top-level base service hostnames (no SSH) {{{
+; other IPv4 and IPv6 is routed to unredoubted and then configured up locally
+kerb-service	A	192.0.2.88
+kerb-service	AAAA	2001:db8::48:4558:6b65:7262
+security	A	192.0.2.92
+security	AAAA	2001:db8::48:4558:53:4543
+security.ipv4	A	192.0.2.92
+security.ipv6	AAAA	2001:db8::48:4558:53:4543
+services	A	192.0.2.93
+services	AAAA	2001:db8::48:4558:5345:5256
+services.ipv4	A	192.0.2.93
+services.ipv6	AAAA	2001:db8::48:4558:5345:5256
+; }}}
+
+; CNAMEs and things we wish were ALIAS or auto-made: {{{
+
+; https://www.ietf.org/id/draft-koch-openpgp-webkey-service-07.txt
+; (thru -06 it was SRV records); this is constructed from the email address,
+; so we don't need .ipvX variants.
+; I don't want CNAME as target of SRV; so grep-bait: CNAME	security
+openpgpkey	A	192.0.2.92
+openpgpkey	AAAA	2001:db8::48:4558:53:4543
+
+finger		CNAME	barbican
+finger.ipv4	CNAME	barbican.ipv4
+finger.ipv6	CNAME	barbican.ipv6
+
+; avatars can't be a CNAME and the target of an SRV.
+; greb-bait: CNAME	services
+avatars		A	192.0.2.93
+		AAAA	2001:db8::48:4558:5345:5256
+dict		CNAME	services
+people		CNAME	services
+people.ipv4	CNAME	services.ipv4
+people.ipv6	CNAME	services.ipv6
+wpad		CNAME	services
+www		CNAME	services
+www.ipv4	CNAME	services.ipv4
+www.ipv6	CNAME	services.ipv6
+
+; }}}
+
+; Hosts and SSH Fingerprints }}}
+
+;;; ======================================================================
+; Zone Identity Services {{{
+
+; CAA: originally:RFC 6844 ; currently: RFC 8659, with extensions in RFC 8657
+;
+; issue/issuewild tags use values which are "rest of data" (not Strings, no 255
+; limit) and which are a domain name, followed optionally by parameters, each
+; individual parameter preceded by a `;` semi-colon; parameters are defined
+; per-Issuer.  A missing domain-name is allowed, no parameters defined for that
+; case.
+;
+; Even though RFC 8659 says "The semantics of parameters to the issue Property
+; Tag are determined by the Issuer alone.", RFC 8657 proceeds to define
+; semantics for some parameters.  I think this is more "if folks do stick to
+; one common schema then it's easier for others to build tools, but still each
+; Issuer can set their own meanings and ignore this if they really want".
+; This is backed by 8657 IANA Considerations.
+; But then 8657 then imposes MUST requirements upon ACME-using systems.
+;
+; RFC 8657 adds: `accounturi`, `validationmethods`; can repeat the CAA record
+; with the same domain but a different accounturi each time.
+
+; https://docs.aws.amazon.com/acm/latest/userguide/setup-caa.html
+;   Any of:  "amazon.com" "amazontrust.com" "awstrust.com" "amazonaws.com"
+;   No info re RFC8657 as of 2019-12-31
+;
+; https://letsencrypt.org/docs/caa/
+;   "Let’s Encrypt’s identifying domain name for CAA is letsencrypt.org. This is officially documented in our Certification Practice Statement (CPS), section 4.2.1."
+;   https://letsencrypt.org/documents/isrg-cps-v2.0/
+;     ISRG checks for relevant CAA records prior to issuing certificates. The CA acts in accordance with CAA records if present. The CA’s CAA identifying domain is ‘letsencrypt.org’.
+;   2019-12-31: can find no docs re accounturi, but community forum posts
+;   showing that the account from the ACME client data works (after a bug was
+;   fixed re ACMEv2 URLs)
+;
+; https://www.digicert.com/dns-caa-rr-check.htm
+;   Equivalent: "digicert.com" "www.digicert.com" "digicert.ne.jp" "cybertrust.ne.jp" "symantec.com" "thawte.com" "geotrust.com" "rapidssl.com"
+;   2019-12-31: no accounturi docs
+;
+; https://ccadb-public.secure.force.com/ccadb/AllCAAIdentifiersReport
+;   HTML page, all CAA identifiers known to the Common CA Database; <https://www.ccadb.org/resources>
+
+; 2019-12-31: I am not using Amazon inside example.org domain; that's on the blog at bridge.example.com
+;  <https://crt.sh/?q=%25.example.org> only lists LE.
+; Beware that our LetsEncrypt automation tooling uses multiple email addresses
+; for different purposes, but all hostnames within example.org are using the
+; noc@example.org address, so that's the only one we need.
+
+; Let's Encrypt:
+;    Prod/1234567 == tower / noc@example.org
+;    Stag/23456789 = chat2 / noc+chat2@example.net
+;    Prod/76543210 = chat2 / noc+chat2@example.net
+
+@		CAA	0	issue "example.net"
+@		CAA	0	issue "letsencrypt.org\; accounturi=https://acme-v01.api.letsencrypt.org/acme/reg/1234567"
+@		CAA	0	issue "letsencrypt.org\; accounturi=https://acme-staging-v02.api.letsencrypt.org/acme/acct/23456789"
+@		CAA	0	issue "letsencrypt.org\; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/76543210"
+;@		CAA	0	issue "amazonaws.com"
+@		CAA	0	issuewild ";"
+@		CAA	0	iodef "mailto:security@example.org"
+
+; Zone Identity Services }}}
+
+;;; ======================================================================
+; TLSA Records {{{
+
+; TLSA: Usage Selector Matching-Type CAdata
+; Usage==2    : CA anchor, no PKIX requirement
+; Selector==0 : match entire cert
+; Selector==1 : match public key
+; MT: 0=exact match, 1=sha256, 2=sha512
+; RFCs: 6698 6394
+; Naming from RFC 7218 (draft-ietf-dane-registry-acronyms-03.txt):
+;   * Usages: PKIX-TA(0) PKIX-EE(1) DANE-TA(2) DANE-EE(3)  PrivCert(255)
+;   * Selectors: Cert(0) SPKI(1)  PrivSel(255)
+;   * Matching Types: Full(0) SHA2-256(1) SHA2-512(2)  PrivMatch(255)
+
+; We don't use -example-tlsa-full any more; that was our v3 CA, and in the meantime
+; the client tooling for TLSA has matured and we can use better matching.
+;
+; _example-tlsa-full	TLSA	( 2 0 0 30.... )	; OurCA3 PKIX-less trust anchor
+;
+; openssl x509 -inform pem -outform der -in OurCA3.pem| perl -pe 's/(.)/sprintf "%02x", ord $1/aesg';echo
+; openssl x509 -inform pem -outform der -in OurCA3.pem| perl -pe 's/(.)/sprintf "%02x", ord $1/aesg' | perl -pe 's/(.{72})/$1\n/g';echo
+
+; For our own CAs, we use Selector 0 to match the entire cert, because we
+; control the horizontal and the vertical.
+; For CAs we use, we use Selector 1, so that if they reissue their signing cert
+; then our DNS records remain valid: as long the as public key is unchanged,
+; the 2/1/1 will be unchanged too.
+;
+; We typically avoid 3/x/x for individual certs or public keys directly in DNS:
+; we don't want to have to update a static zonefile for cert re-issuance, and
+; even with a dynamic zonefile, any complicated pre-issuance schemes or timing
+; dances to deal with DNS TTLs fall down when there's an emergency revocation
+; or replacement.  Just pin the issuing CA with 2/x/x and be done with it.
+
+; OurCA {{{
+_ourcaca4-tlsa	TLSA	(	; OurCA4 PKIX-less trust anchor
+	02 00 01 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+	; TLSA DANE-TA CERT SHA2-256
+	)
+; danetool --tlsa-rr --host=foo --ca --x509 --load-certificate=OurCA4.pem
+
+_ourcaca5-tlsa	TLSA	(	; OurCA5 PKIX-less trust anchor
+	02 00 01 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+	; TLSA DANE-TA CERT SHA2-256
+	)
+; danetool --tlsa-rr --host=foo --ca --x509 --load-certificate=OurCA5.pem
+
+; SMIMEA {{{
+
+; Note that the SMIME email signatures include the CA usually; mutt certainly does,
+; so folks getting signed email from me _have_ the CA.  So no need to use 2/0/0 here.
+;
+; That _was_:
+; openssl x509 -inform pem -outform der -in OurCA5.pem | perl -pe 's/(.)/sprintf "%02x", ord $1/aesg' | perl -pe 's/(.{72})/$1\n/g';echo
+;
+; but instead we can just use the signatures.
+; Since it's just an X.509 cert and almost the same as a TLSA record, use danetool.
+; Heck, copy the _ourcaca5-tlsa RRset and change the RRtypes.
+
+;;;_ourcaca5-smimea	SMIMEA	(	; OurCA5 PKIX-less trust anchor
+;;;	02 00 01 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+;;;	; SMIMEA DANE-TA CERT SHA2-256
+;;;	)
+; danetool --tlsa-rr --host=foo --ca --x509 --load-certificate=OurCA5.pem
+
+; This one should be a _set_ of CAs, letting me add OurCA6 (Ed25519) when the time comes.
+;;;_ourca-smimea		SMIMEA	(	; OurCA5 PKIX-less trust anchor
+;;;	02 00 01 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+;;;	; SMIMEA DANE-TA CERT SHA2-256
+;;;	)
+
+; OurCA }}}
+
+; SMIMEA }}}
+
+; Other CAs, standalone {{{
+_cacert-c3-tlsa	TLSA	(	; CACert Class 3 PKIX-less trust anchor
+	02 00 01 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8
+	; TLSA DANE-TA CERT SHA2-256
+	)
+; danetool --tlsa-rr --host=foo --ca --x509 --load-certificate=cacert-class3.pem
+
+; For Let's Encrypt, where they have multiple signing paths, we use public-key
+; hashing, not certificate hashing.
+; This avoids breakage given, eg, IdenTrust vs other authority paths
+;
+;_letsencrypt-tlsa TLSA ( 02 01 01 0b9fa5a59eed715c26c1020c711b4f6ec42d58b0015e14337a39dad301c5afc3 ) ; ISRG Root X1
+_letsencrypt-tlsa TLSA ( 02 01 01 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18 ) ; X1 & X3
+_letsencrypt-tlsa TLSA ( 02 01 01 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b ) ; X2 & X4
+
+; danetool --tlsa-rr --host=foo --ca --load-certificate=letsencrypt_rsa_authority_x1.pem
+
+; edit results of: for F in AmazonRootCA*; danetool --tlsa-rr --host=$F --ca --x509 --load-certificate=$F
+_amazon-tlsa TLSA ( 02 00 01 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e ) ; AmazonRootCA1
+_amazon-tlsa TLSA ( 02 00 01 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4 ) ; AmazonRootCA2
+_amazon-tlsa TLSA ( 02 00 01 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4 ) ; AmazonRootCA3
+_amazon-tlsa TLSA ( 02 00 01 e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092 ) ; AmazonRootCA4
+
+; Other CAs, standalone }}}
+
+; Combined CA TLSA records {{{
+; These let us migrate between CAs without any outage, or have contingency plans.
+
+; _ourca-tlsa is a pairing anchor, handling both CA4 and CA5.
+_ourca-tlsa	TLSA ( 02 00 01 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488 ) ; OurCA4
+_ourca-tlsa	TLSA ( 02 00 01 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1 ) ; OurCA5
+
+; Used for migrations CACert -> OurCA4
+_ourca-cacert-tlsa TLSA 02 00 01 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488 ; OurCA4
+_ourca-cacert-tlsa TLSA 02 00 01 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1 ; OurCA5
+_ourca-cacert-tlsa TLSA 02 00 01 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8 ; cacert-c3
+
+; This mostly means "stuff which used to be on our own CA but for which we now use Let's Encrypt because others might see it"
+_ourca-le-tlsa TLSA ( 02 00 01 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488 ) ; OurCA4
+_ourca-le-tlsa TLSA ( 02 00 01 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1 ) ; OurCA5
+_ourca-le-tlsa TLSA ( 02 01 01 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18 ) ; letsencrypt X1 & X3
+_ourca-le-tlsa TLSA ( 02 01 01 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b ) ; letsencrypt X2 & X4
+
+; Don't ask
+_ourca-cacert-le-tlsa TLSA ( 02 00 01 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488 ) ; OurCA4
+_ourca-cacert-le-tlsa TLSA ( 02 00 01 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1 ) ; OurCA5
+_ourca-cacert-le-tlsa TLSA ( 02 00 01 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8 ) ; cacert-c3
+_ourca-cacert-le-tlsa TLSA ( 02 01 01 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18 ) ; letsencrypt X1 & X3
+_ourca-cacert-le-tlsa TLSA ( 02 01 01 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b ) ; letsencrypt X2 & X4
+
+; Stuff we migrate from CACert to Let's Encrypt; was never private CA, we
+; always wanted others to be able to validate.
+_cacert-le-tlsa TLSA ( 02 00 01 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8 ) ; cacert-c3
+_cacert-le-tlsa TLSA ( 02 01 01 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18 ) ; letsencrypt X1 & X3
+_cacert-le-tlsa TLSA ( 02 01 01 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b ) ; letsencrypt X2 & X4
+
+; Some stuff we move between LE and AWS CloudFront in front of an S3 bucket
+_le-amazon-tlsa TLSA ( 02 01 01 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18 ) ; letsencrypt X1 & X3
+_le-amazon-tlsa TLSA ( 02 01 01 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b ) ; letsencrypt X2 & X4
+_le-amazon-tlsa TLSA ( 02 00 01 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e ) ; AmazonRootCA1
+_le-amazon-tlsa TLSA ( 02 00 01 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4 ) ; AmazonRootCA2
+_le-amazon-tlsa TLSA ( 02 00 01 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4 ) ; AmazonRootCA3
+_le-amazon-tlsa TLSA ( 02 00 01 e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092 ) ; AmazonRootCA4
+
+; All The Current Bases
+_ourca-le-amazon-tlsa TLSA ( 02 00 01 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488 ) ; OurCA4
+_ourca-le-amazon-tlsa TLSA ( 02 00 01 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1 ) ; OurCA5
+_ourca-le-amazon-tlsa TLSA ( 02 01 01 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18 ) ; letsencrypt X1 & X3
+_ourca-le-amazon-tlsa TLSA ( 02 01 01 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b ) ; letsencrypt X2 & X4
+_ourca-le-amazon-tlsa TLSA ( 02 00 01 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e ) ; AmazonRootCA1
+_ourca-le-amazon-tlsa TLSA ( 02 00 01 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4 ) ; AmazonRootCA2
+_ourca-le-amazon-tlsa TLSA ( 02 00 01 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4 ) ; AmazonRootCA3
+_ourca-le-amazon-tlsa TLSA ( 02 00 01 e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092 ) ; AmazonRootCA4
+
+; Combined CA TLSA records }}}
+
+; TLSA Records }}}
+
+;;; ======================================================================
+; TLSA Referencing {{{
+
+; full:
+; openssl x509 -inform pem -outform der -in OurCA3.pem| perl -pe 's/(.)/sprintf "%02x", ord $1/aesg' | perl -pe 's/(.{72})/$1\n/g';echo
+; short:
+; danetool --tlsa-rr --host=foo --ca --x509 --load-certificate=some-company.classN.server.shaN.ca.pem
+
+; Web and other HTTP Transport
+_443._tcp.www		CNAME	_ourca-le-tlsa
+_443._tcp.www.ipv4	CNAME	_ourca-le-tlsa
+_443._tcp.www.ipv6	CNAME	_ourca-le-tlsa
+_443._tcp.people	CNAME	_ourca-le-tlsa
+_443._tcp.people.ipv4	CNAME	_ourca-le-tlsa
+_443._tcp.people.ipv6	CNAME	_ourca-le-tlsa
+
+_443._tcp.git		CNAME	_ourca-le-tlsa
+_443._tcp.svn		CNAME	_ourca-le-tlsa
+
+; XMPP / Jabber
+_5222._tcp.xmpp		CNAME	_ourca-le-tlsa
+_5223._tcp.xmpp		CNAME	_ourca-le-tlsa
+_5269._tcp.xmpp-s2s	CNAME	_ourca-le-tlsa
+
+; Email
+_25._tcp.mx		CNAME	_ourca-le-tlsa
+_26._tcp.mx		CNAME	_ourca-le-tlsa
+_27._tcp.mx		CNAME	_ourca-le-tlsa
+; the ....46 are the SRV results, the non-46 may be manually configured
+; the 1xxx are for potential debugging
+; there's too much historical cruft here, from when I had one IPv4 address and
+; a bazillion IPv6 addresses.
+_465._tcp.smtp46	CNAME	_ourca-le-tlsa
+_587._tcp.smtp46	CNAME	_ourca-le-tlsa
+_1465._tcp.smtp46	CNAME	_ourca-le-tlsa
+_1587._tcp.smtp46	CNAME	_ourca-le-tlsa
+_465._tcp.smtp		CNAME	_ourca-le-tlsa
+_587._tcp.smtp		CNAME	_ourca-le-tlsa
+_1465._tcp.smtp		CNAME	_ourca-le-tlsa
+_1587._tcp.smtp		CNAME	_ourca-le-tlsa
+_143._tcp.imap46	CNAME	_ourca-le-tlsa
+_993._tcp.imap46	CNAME	_ourca-le-tlsa
+_143._tcp.imap		CNAME	_ourca-le-tlsa
+_993._tcp.imap		CNAME	_ourca-le-tlsa
+; except for sieve, where there's only IPv6
+_4190._tcp.imap		CNAME	_ourca-le-tlsa
+;
+
+www.security		CNAME	security
+www.security.ipv4	CNAME	security.ipv4
+www.security.ipv6	CNAME	security.ipv6
+_443._tcp.www.security		CNAME	_ourca-le-tlsa
+_443._tcp.www.security.ipv4	CNAME	_ourca-le-tlsa
+_443._tcp.www.security.ipv6	CNAME	_ourca-le-tlsa
+_443._tcp.security		CNAME	_ourca-le-tlsa
+_443._tcp.security.ipv4		CNAME	_ourca-le-tlsa
+_443._tcp.security.ipv6		CNAME	_ourca-le-tlsa
+
+; _acme-challenge.www.security.example.org. 120 TXT "..."
+; _acme-challenge.security.example.org. 120 TXT "..."
+; beware SAN : one each
+; rather low TTL because some things might be using this as their backoff determination, buggily.
+;
+; This does require that the ACME client correctly remap domains to update in
+; DNS before asking for a validation of the unmapped domain.  The tool I used
+; in 2020-02 claimed such support but it was buggy and I had to abandon this
+; approach.  Instead, we have the nginx vhost which handles a bare
+; "example.org" handle /.well-known/acme-challenge/ by trying local files
+; first, else HTTP proxying to the chat server.
+; I'd like to get rid of that proxying and switch back to DNS updating;
+; "d.example.net" is delegated to a commercial service which has an API with
+; near-instant live updates.
+
+_acme-challenge			15 CNAME                 _acme-challenge.chat-acme.d.example.net.
+_acme-challenge.xmpp		15 CNAME            _acme-challenge.xmpp.chat-acme.d.example.net.
+_acme-challenge.chat		15 CNAME            _acme-challenge.chat.chat-acme.d.example.net.
+_acme-challenge.conference	15 CNAME      _acme-challenge.conference.chat-acme.d.example.net.
+_acme-challenge.proxy-chatfiles	15 CNAME _acme-challenge.proxy-chatfiles.chat-acme.d.example.net.
+_acme-challenge.pubsub.xmpp	15 CNAME     _acme-challenge.pubsub.xmpp.chat-acme.d.example.net.
+
+; TLSA Referencing }}}
+
+;;; ======================================================================
+; Mail server hostnames {{{
+
+; Willing to sacrifice Kerberos portability and rely upon disabling reverse DNS
+; hostname canonicalization.  Sucking it up.  Still no reliable IPv6 at home.
+; :(
+; For /etc/krb5.conf: [libdefaults] dns_canonicalize_hostname = false
+;
+imap		AAAA	2001:db8::48:4558:696d:6170
+		A	192.0.2.25
+smtp		AAAA	2001:db8::48:4558:736d:7470
+		A	192.0.2.25
+;
+smtp46		A	192.0.2.25	; old alias pre-dating IPv4 in smtp
+		AAAA	2001:db8::48:4558:736d:7470
+imap46		A	192.0.2.25	; old alias pre-dating IPv4 in imap
+		AAAA	2001:db8::48:4558:696d:6170
+
+; If changing this, then also update the SPF record; it hard-codes these for
+; efficiency of remote systems, saving them some lookups.
+; Really, we should be constructing the SPF record via a macro of some kind.
+; I'm trying to avoid using M4 to make this zonefile, but sometimes with some
+; good whiskey, that looks mighty tempting.
+mx		A	192.0.2.25
+		AAAA	2001:db8::48:4558:736d:7470
+mx.ipv4		A	192.0.2.25
+mx.ipv6		AAAA	2001:db8::48:4558:736d:7470
+;
+; RFC 7208 section 10.1.3
+; HELO hostnames need to have SPF to allow processing for bounces
+; In short: there is no envelope sender for bounces, so the only thing which
+; can be checked in the HELO name.
+;
+; This needs to be tied to the exim.conf setup for outbound `helo_data` on
+; transports.  At this time, that's "mx.example.org"; this needs to also
+; match the `interface` IP address list.
+; We do not need to allow other email service providers to claim to be this
+; host, so we use "a" (for A/AAAA matching), a catchall for our IPs, and a
+; *hard* reject on any other IP.
+; Because we never send email from mx.example.org and it's only used by HELO
+; checks, we can be firm and not have to worry about forwarding or other
+; legitimate shenanigans.
+mx		TXT	"v=spf1 a include:_spflarge.example.net -all"
+
+; RFC 8461 SMTP MTA Strict Transport Security (MTA-STS)
+; Final: `_mta-sts` TXT record, `v=STSv1`, with host records on `mta-sts`:
+;   https://mta-sts.example.com/.well-known/mta-sts.txt
+;
+;; History:
+;;; https://tools.ietf.org/html/draft-ietf-uta-mta-sts-02
+;;; SMTP MTA Strict Transport Security (MTA-STS)
+;;; Draft -03 renames to "mta-sts", to match the hostname which has to resolve anyway,
+;;; which makes sense: NXDOMAIN can skip a second lookup.
+;;; https://tools.ietf.org/html/draft-ietf-uta-mta-sts-03
+;;; NB: draft -15 in now in last call, and by this point:
+;;;  + it's .txt, not .json
+;;;  + the allowed fields for the mode have changed (`report` -> `testing`)
+;;;  + DNS is back to _mta-sts
+;
+; IP addresses should match `services`; this is ALIAS record stuff, can't use
+; CNAME because need TXT record too, for the draft, but can switch back to CNAME
+; if ignoring the draft.
+;
+; If updating version, don't forget to check globnix.org DNS too!
+_mta-sts	TXT	"v=STSv1; id=20191231r1;"
+mta-sts		TXT	"v=STSv1; id=20191231r1;"
+mta-sts		A	192.0.2.93
+mta-sts		AAAA	2001:db8::48:4558:5345:5256
+
+; Mail server hostnames }}}
+
+;;; ======================================================================
+; Chat server hostnames {{{
+
+; $HostingProvider chat2.example.net
+; ipv4: 203.0.113.175
+; ipv6: 2001:db8::f0ab:cdef:1234:f00f
+;
+xmpp.ipv6	AAAA	2001:db8::f0ab:cdef:1234:f00f
+xmpp-s2s.ipv6	AAAA	2001:db8::f0ab:cdef:1234:f00f
+xmpp		A	203.0.113.175
+		AAAA	2001:db8::f0ab:cdef:1234:f00f
+xmpp-s2s	A	203.0.113.175
+		AAAA	2001:db8::f0ab:cdef:1234:f00f
+proxy-chatfiles			CNAME	xmpp
+fileproxy.xmpp			CNAME	xmpp
+; Federated services which are offered under their own hostnames (even if on
+; the same server instance and port) federate with the same mechanisms, so you
+; need an SRV or just an address record if on the default ports.
+; Which remote servers will actually use SRV is more open to question.
+conference			CNAME	xmpp-s2s
+_xmpp-server._tcp.conference	SRV	10 2 5269 xmpp-s2s
+; Services which are modules and need their own hostnames, but are not
+; federated within XMPP, don't need the SRV record
+pubsub.xmpp			CNAME	xmpp-s2s
+
+chat			A	203.0.113.175
+			AAAA	2001:db8::f0ab:cdef:1234:f00f
+proxy-chatfiles.chat	CNAME	chat
+fileproxy.chat		CNAME	chat
+conference.chat		CNAME	chat
+pubsub.chat		CNAME	chat
+_xmpp-server._tcp.conference	SRV 10 2 5269 chat
+
+; Chat server hostnames }}}
+
+;;; ======================================================================
+; Random other hostnames {{{
+
+auth		AAAA	2001:db8::48:4558:6175:7468
+kpeople		AAAA	2001:db8::48:4558:6b70:706c
+ocsp.security	AAAA	2001:db8::48:4558:6f63:7370
+webauth		AAAA	2001:db8::48:4558:7765:6261
+
+news-feed	A	192.0.2.93
+		AAAA	2001:db8::48:4558:6e6e:7470
+
+; This is for Go package downloads, keeping the canonical names of modules
+; in domains under our control, so that we're not locked into one code-hosting
+; provider.
+go		CNAME	abcdefghijklmn.cloudfront.net.
+; Do *not* deploy TLSA until cloudfront.net is signed
+; (or we use ALIAS-type records)
+;_443._tcp.go	CNAME	_amazon-tlsa
+
+foo		A	192.0.2.200
+
+; Random other hostnames }}}
+
+;;; ======================================================================
+; Per-person email sub-domains {{{
+; Household dedicated mail domains where *@person.example.org can receive email
+
+; Gladys receives email here but never sends from this address
+; (Gladys has probably forgotten this exists and will roll her eyes at it.)
+gladys		MX	10 mx
+$ORIGIN gladys.example.org.
+_adsp._domainkey	TXT "dkim=all"
+_dmarc			TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+_report			TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp._tls		TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp-tlsrpt		TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+$ORIGIN example.org.
+
+; Fred sends and receives email in this domain
+fred		MX	10 mx
+$ORIGIN fred.example.org.
+; Also have a web-server for some old OpenID stuff
+@		A	192.0.2.93 ; services
+@		AAAA	2001:db8::48:4558:5345:5256 ; services
+		TXT	"v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"
+;
+d201911._domainkey TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/Tlz" "P2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB"
+d201911e2._domainkey TXT "v=DKIM1; k=ed25519; p=rQNsV9YcPJn/WYI1EDLjNbN/VuX1Hqq/oe4htbnhv+A="
+;
+d202003._domainkey TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYj" "c0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB"
+d202003e2._domainkey TXT "v=DKIM1; k=ed25519; p=0DAPp/IRLYFI/Z4YSgJRi4gr7xcu1/EfJ5mjVn10aAw="
+;
+_adsp._domainkey	TXT "dkim=all"
+_dmarc			TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+_report			TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp._tls		TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp-tlsrpt		TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+$ORIGIN example.org.
+
+; Per-person email sub-domains }}}
+
+;;; ======================================================================
+; {{{PGP-Keys-And-Fingerprints
+
+; RFC4398 Storing Certificates in the Domain Name System (DNS)
+; type tag algorithm <data>
+; type 3 = PGP
+; type 6 = IPGP
+;   IPGP fingerprint length, fingerprint, URL; either FP or URL may be empty
+; gnupg comes with tools/make-dns-cert.c
+;
+; There's a lack of clarity about what clients do given a dot in the email LHS,
+; so I solve it by publishing both with the dot escaped to be part of the label,
+; and the dot introducing DNS hierarchy.
+;
+;;;fred		CERT	6 0 0 FKy7QyQ5Ot41Fdot2k0ekA4UwcwEaHR0cHM6Ly93d3cuc2VjdXJpdHkuc3BvZGh1aXMub3JnL1BHUC9rZXlzLzB4NEQxRTkwMEUxNEMxQ0MwNC5hc2M=
+;;;fred.bloggs	CERT	6 0 0 FKy7QyQ5Ot41Fdot2k0ekA4UwcwEaHR0cHM6Ly93d3cuc2VjdXJpdHkuc3BvZGh1aXMub3JnL1BHUC9rZXlzLzB4NEQxRTkwMEUxNEMxQ0MwNC5hc2M=
+;;;fred\.bloggs	CERT	6 0 0 FKy7QyQ5Ot41Fdot2k0ekA4UwcwEaHR0cHM6Ly93d3cuc2VjdXJpdHkuc3BvZGh1aXMub3JnL1BHUC9rZXlzLzB4NEQxRTkwMEUxNEMxQ0MwNC5hc2M=
+
+;
+; I used:
+; perl -MMIME::Base64 -le '$_ = $ARGV[0]; s/\s+//g; s/(..)/chr(hex($1))/eg;
+;   $_=chr(length($_)) . $_ . $ARGV[1]; print encode_base64($_, "")' \
+;   "$(gpg --with-colons --fingerprint $gpg_key | perl -F: -lane 'print $F[9] if $F[0] eq "fpr"')" \
+;   https://www.security.example.org/PGP/keys/0x0123456789ABCDEF.asc
+;
+; Note that my first attempt omitted the length.  Oops.  The first octet of
+; the base64 data is the length; following octets, for that length, is the
+; fingerprint, in raw binary form; any remaining octets are the URL.
+; With the URL in, we might have with key 0x0123456789ABCDEF :
+;  FKy7QyQ5Ot41Fdot2k0ekA4UwcwEaHR0cHM6Ly93d3cuc2VjdXJpdHkuc3BvZGh1aXMub3JnL1BHUC9rZXlzLzB4NEQxRTkwMEUxNEMxQ0MwNC5hc2M=
+
+;; WKD is https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/?include_text=1
+;; and included above with SRV records
+
+;; https://datatracker.ietf.org/doc/draft-ietf-dane-openpgpkey/?include_text=1
+;; rev 00: so draft-ietf-dane-openpgpkey-00.txt
+;; NOW: RFC 7929 (experimental)
+
+; Generated by my gnupg-exportsof-mykey tooling; this is from output file
+; dns-examplecom-pgp.openpgpkey.db
+; This is the 2020 Ed25519 key.  It's smaller, so it's marginally less insane
+; to have it in DNS.
+;
+; For example.org redaction: leaving this intact, original PGP key, there
+; are email addresses buried in the key, and human names.  But changing
+; the DNS labels and comments for zone consistency.
+; You can get the original name just by looking up the fingerprint below
+; the ORIGIN:
+$ORIGIN _openpgpkey.example.org.
+; 4833892924C60A7AE666D32A1DA3E68F41CEECAC
+; Fred Bloggs <fred.bloggs@example.org>
+;;;6555f290b6e653629d9d582660dd2fe730320e43e252f8e2e5ee1bdd TYPE61 \# 1239 (
+;;;	9833045e2504f516092b06010401da470f0101074043c292df50b30dcaadd549
+;;;	cf48fd9cbc06e43c3a9ca1cb788da0bd3c00ffc750b4245068696c2050656e6e
+;;;	6f636b203c7068696c4070656e6e6f636b2d746563682e636f6d3e888e041316
+;;;	080036021b03030b090703150a080416030201021e0102178002190116210448
+;;;	33892924c60a7ae666d32a1da3e68f41ceecac05025e250723000a09101da3e6
+;;;	8f41ceecac2b46010081e1c09a255705b3fdda16a5485e6ec25edeabac78c92d
+;;;	aa5f6c1af2b71cc93800fe364bd0438dd5120585a034327e69757aa58ca0890a
+;;;	d7691cba343c55cf957806b41c5068696c2050656e6e6f636b203c7064704067
+;;;	6e7570672e6e65743e888b041316080033021b03030b090703150a0804160302
+;;;	01021e010217801621044833892924c60a7ae666d32a1da3e68f41ceecac0502
+;;;	5e250723000a09101da3e68f41ceecac4faf00ff4bc1c3a4389cb5bb9cb90026
+;;;	c416f2ad99be2a6e0fe2ceea0bfa8e175e02c0eb0100eb8a11ec88b21ffa08c3
+;;;	0a71afd04dc43d093fabcb9c5ae2de0b9c627e88670eb4285068696c2050656e
+;;;	6e6f636b203c7068696c2e70656e6e6f636b4073706f64687569732e6f72673e
+;;;	888b041316080033021b03030b090703150a080416030201021e010217801621
+;;;	044833892924c60a7ae666d32a1da3e68f41ceecac05025e250724000a09101d
+;;;	a3e68f41ceecacc40800ff6702abcf89ddfe3ed0670953ca6a0a33ff4d6d83bf
+;;;	8b4496cc299abedfb4947401008ab97f09df18813ad998de3fe3c55085b49f56
+;;;	ee1f68cac298be4be6d4f34906b41b5068696c2050656e6e6f636b203c706470
+;;;	406578696d2e6f72673e888b041316080033021b03030b090703150a08041603
+;;;	0201021e010217801621044833892924c60a7ae666d32a1da3e68f41ceecac05
+;;;	025e250724000a09101da3e68f41ceecac9dbf0100f3eac82b9ef48d4cabc522
+;;;	44cbd52ed9392a2ad4111d8f6b0aa33f8859e956c301008653862e15970d7601
+;;;	57764064209c2ca8a9a616b1441e0e670b96458fd1a90eb838045e2504f5120a
+;;;	2b0601040197550105010107401ea56299a543466023db5f4d4f1452450a393a
+;;;	fcb9039ada0c27e2dc7f59752f03010807887e04181608002616210448338929
+;;;	24c60a7ae666d32a1da3e68f41ceecac05025e2504f5021b0c05090966018000
+;;;	0a09101da3e68f41ceecac624d00fe30c7b6e6bbe930e899c270f4f17de5bdae
+;;;	55612f9d69cb7490f6a4a4d04f261600ff58e26ae9fc3324de9bd51a77ff65d4
+;;;	2af60294f55a03fe0f1ec316e4f5b8e70bb833045e25054016092b06010401da
+;;;	470f010107406df5a87cd9b51890f84f7e597ab17e549f1ba093844178ea61ce
+;;;	ac484b4a58e988ef0418160800201621044833892924c60a7ae666d32a1da3e6
+;;;	8f41ceecac05025e250540021b02008109101da3e68f41ceecac762004191608
+;;;	001d16210436bea421261c40a54fc9261c2e7665110f8a56ff05025e25054000
+;;;	0a09102e7665110f8a56ff0bb500ff4aee429d5659915336511a3b744c4c25fd
+;;;	d09fb8af2962c57e279b1906ad5e040100a8616e0d4def13d7910c30a595e7fc
+;;;	92308de87404b96fa17325b6ef6cd7e9029f260100c2fe3edf9bbfce3c42317c
+;;;	a93bdbc4e52dd98bd2f782eb708edca1fbd7c345e50100dc69c0cc5a2481dc79
+;;;	08162d7c6d185c8866498eb87d159e0eb3877d68de2b02
+;;;	)
+
+; {{{PGP-PKA-Records
+
+; Phil notes, since the actual records below are "as output by gpg" for easier maintenance.
+; TYPE37 == CERT
+; During early GnuPG 2.1.x, GnuPG switched from v1 PKA to v2; details at:
+;   <https://incenp.org/notes/2015/keys-in-dns.html>
+; => local-part is zooko's base32 of LHS (as seen from src), type is CERT/IPGP, no URL
+;    so _very_ similar to the records already above where I chose to use CERT/IPGP ages ago.
+;
+; pip install zbase32 ; python    # zbase32 is python2-only
+;   import hashlib, zbase32
+;   lhs = 'fob'   # fred o bloggs
+;   print(zbase32.zbase32.b2a(hashlib.sha1(lhs.lower()).digest()))
+; That gets us the left-hand-side.
+;
+; Skimming code, it looks as though the PKA lookup really does just re-use the older IPGP
+; code, which can return a URL; `gpg2.1.15 --auto-key-locate pka` got a fingerprint but then
+; errored on getting the key.  So instead, let's experiment by grabbing our own CERT from above,
+; shoving it into this namespace and changing the LHS to use the zb32 encoded variants
+; (which also handles `.` vs `\.` issues).
+;
+; Discovery from log.dirmngr : tries to access
+; <https://www.security.example.org:443/pks/lookup?op=get&options=mr&search=0x0123456789ABCDEF0123456789ABCDEF01234567>
+; Okay, so change the hostname to `sks.example.org` and we should be good, as the PGP keyserver will get it
+; % perl -MMIME::Base64 -le '$_ = $ARGV[0]; s/\s+//g; s/(..)/chr(hex($1))/eg;
+;     $_=chr(length($_)) . $_ . $ARGV[1]; print encode_base64($_, "")' \
+;     "$(gpg --with-colons --fingerprint $pgp_key_main | perl -F: -lane 'print $F[9] if $F[0] eq "fpr"')" \
+;     https://sks.example.org
+; FKy7QyQ5Ot41Fdot2k0ekA4UwcwEaHR0cHM6Ly9za3Muc3BvZGh1aXMub3Jn
+;
+; 2018-07-14 after turning down sks.example.org:
+; % perl -MMIME::Base64 -le '$_ = $ARGV[0]; s/\s+//g; s/(..)/chr(hex($1))/eg; $_=chr(length($_)) . $_ . $ARGV[1]; print encode_base64($_, "")' \
+;   "$(gpg --with-colons --fingerprint $pgp_key_main | perl -F: -lane 'if ($F[0] eq "fpr") {print $F[9]; exit}')" \
+;   http://ha.pool.sks-keyservers.net
+; FKy7QyQ5Ot41Fdot2k0ekA4UwcwEaHR0cDovL2hhLnBvb2wuc2tzLWtleXNlcnZlcnMubmV0
+; ...
+
+; But that's not working with GnuPG.  Just junk all of the above.
+; Use my pka.py script and drop the keyserver entirely.
+
+; gpg2.1 --export-options export-minimal,export-pka --export 0x0123456789ABCDEF
+$ORIGIN _pka.example.org.
+
+; Fingerprint: 0123456789ABCDEF0123456789ABCDEF01234567
+; <fred.bloggs@example.org>
+;;;kwj7zzgek39st5d5q81hq517e3iuzbr4   CERT  6 0 0 FKy7QyQ5Ot41Fdot2k0ekA4UwcwE
+
+; Fingerprint: 0123456789ABCDEF0123456789ABCDEF01234567
+; <fob@example.org>
+;;;ajh79yijhx3xmeadhc79zrje7ret3qzr   CERT  6 0 0 FKy7QyQ5Ot41Fdot2k0ekA4UwcwE
+
+$ORIGIN example.org.
+; }}}PGP-PKA-Records
+
+
+; }}}PGP-Keys-And-Fingerprints
+
+; Removed localhost entries: cookie stealing etc
+
+;;; ======================================================================
+; Sub-domains with email and other services {{{
+
+mailtest			MX	10 mx
+$ORIGIN mailtest.example.org.
+;
+d201911._domainkey TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn" "0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB"
+d201911e2._domainkey TXT "v=DKIM1; k=ed25519; p=afulDDnhaTzdqKQN0jtWV04eOhAcyBk3NCyVheOf53Y="
+;
+d202003._domainkey TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KN" "aS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB"
+d202003e2._domainkey TXT "v=DKIM1; k=ed25519; p=iqwH/hhozFdeo1xnuldr8KUi7O7g+DzmC+f0SYMKVDc="
+;
+_adsp._domainkey	TXT "dkim=all"
+_dmarc			TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+_report			TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp._tls		TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp-tlsrpt		TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+$ORIGIN example.org.
+
+; Stub to show that SKS has been withdrawn; we no longer have email in this
+; domain (it was used for PGP exchange with two non-SKS keyservers).
+$ORIGIN sks.example.org.
+_pgpkey-http._tcp	SRV	0 0 0 .
+_pgpkey-https._tcp	SRV	0 0 0 .
+_hkp._tcp		SRV	0 0 0 .
+
+$ORIGIN sks-peer.example.org.
+_pgpkey-http._tcp	SRV	0 0 0 .
+_pgpkey-https._tcp	SRV	0 0 0 .
+_hkp._tcp		SRV	0 0 0 .
+
+; This is used for a dynamic DNS entry for my home router; at one point the
+; router I was using supported various HTTP-based updates but not Dynamic DNS
+; updates, so I switched to HE and have never switched that one back.
+$ORIGIN yoyo.example.org.
+@	NS	ns5.he.net.
+@	NS	ns4.he.net.
+@	NS	ns3.he.net.
+@	NS	ns2.he.net.
+@	NS	ns1.he.net.
+
+$ORIGIN example.org.
+
+; khard: kubernetes the hard way, Google, `xyz-2` project
+; gcloud dns managed-zones create khard --description="Kubernetes The Hard Way HD2" --dns-name="khard.example.org."
+; gcloud dns managed-zones describe khard
+khard			IN	NS	ns-cloud-d1.googledomains.com.
+khard			IN	NS	ns-cloud-d2.googledomains.com.
+khard			IN	NS	ns-cloud-d3.googledomains.com.
+khard			IN	NS	ns-cloud-d4.googledomains.com.
+;;;khard			IN	DS	15247 8 2 C7CAF8A6EFF0E6E7E811D68CDD87AD2EC135CB501A959FACBA3BC13F93F59864
+
+; Sub-domains with email and other services }}}
+
+;;; ======================================================================
+; Stubs to reject email to some hosts {{{
+; Hosts which should not appear as senders but are abused by spammers because
+; of parsing message-id headers:
+realhost		MX	0 .
+realhost		TXT	"v=spf1 -all"
+_25._tcp.realhost	TLSA	03 00 00 0000000000000000000000000000000000000000000000000000000000000000
+; Stubs to reject email to some hosts }}}
+
+;;; ======================================================================
+; Domain verifications for some tools {{{
+
+; <https://help.github.com/en/articles/verifying-your-organizations-domain>
+; "Optionally, once the "Verified" badge is visible on your organization's
+; profile page, you can delete the TXT entry from the DNS record at your domain
+; hosting service."
+;_github-challenge-ExampleOrg  TXT     "" ; dropped value when commented out
+
+; <https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html>
+; "ACM automatically renews your certificate for as long as the certificate is
+; in use and the CNAME record that ACM created for you remains in place in your
+; DNS database. You can stop automatic renewal by removing the certificate from
+; the AWS service with which it is associated or by deleting the CNAME record."
+_fedcba9876543210fedcba9876543210.go.example.org.  CNAME _45678901234abcdef45678901234abcd.ggedgsdned.acm-validations.aws.
+; -- added 2019-10-09; leave in place, no scheduled removal.
+
+; Google Webmaster
+; added 2020-01-26 login fred@example.net
+; to allow usage in oauth consent screens.
+; Must be left in place.
+opqrstuvwxyz		CNAME	gv-abcdefghijklmn.dv.googlehosted.com.
+; postmaster.google.com (CNAME alternative to TXT on top-level domain):
+zyxwvutsrqpo		CNAME	gv-nmlkjihgfedcba.dv.googlehosted.com.
+
+; Bing (I didn't record the policy back when I added this)
+0123456789abcdef0123456789abcdef		CNAME	verify.bing.com.
+
+; Domain verifications for some tools }}}
+
+; vim: set filetype=bindzone foldmethod=marker :
diff --git a/commands/test_data/example.org.zone.js b/commands/test_data/example.org.zone.js
new file mode 100644
index 00000000..a75d969d
--- /dev/null
+++ b/commands/test_data/example.org.zone.js
@@ -0,0 +1,355 @@
+var bind = NewDnsProvider("bind", "BIND");
+var REG_CHANGEME = NewRegistrar("ThirdParty", "NONE");
+D("example.org", REG_CHANGEME,
+	DnsProvider(bind),
+	DefaultTTL(7200),
+	//SOA('@', 'ns1.example.org.', 'hostmaster.example.org.', 2020030700, 7200, 3600, 864000, 7200, TTL(43200)),
+	NAMESERVER('ns1.example.org.'),
+	NAMESERVER('ns2.example.org.'),
+	NAMESERVER('ns-a.example.net.'),
+	NAMESERVER('friend-dns.example.com.'),
+	MX('@', 10, 'mx.example.org.'),
+	TXT('@', 'v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all'),
+	SRV('_client._smtp', 1, 1, 1, 'example.org.'),
+	SRV('_client._smtp.mx', 1, 2, 1, 'mx.example.org.'),
+	SRV('_client._smtp.foo', 1, 2, 1, 'foo.example.org.'),
+	SRV('_kerberos._tcp', 10, 1, 88, 'kerb-service.example.org.'),
+	SRV('_kerberos._udp', 10, 1, 88, 'kerb-service.example.org.'),
+	SRV('_kpasswd._udp', 10, 1, 464, 'kerb-service.example.org.'),
+	SRV('_kerberos-adm._tcp', 10, 1, 749, 'kerb-service.example.org.'),
+	TXT('_kerberos', 'EXAMPLE.ORG'),
+	SRV('_ldap._tcp', 0, 0, 0, '.'),
+	SRV('_ldap._udp', 0, 0, 0, '.'),
+	SRV('_jabber._tcp', 10, 2, 5269, 'xmpp-s2s.example.org.'),
+	SRV('_xmpp-server._tcp', 10, 2, 5269, 'xmpp-s2s.example.org.'),
+	SRV('_xmpp-client._tcp', 10, 2, 5222, 'xmpp.example.org.'),
+	SRV('_im._sip', 0, 0, 0, '.'),
+	SRV('_pres._sip', 0, 0, 0, '.'),
+	SRV('_sip+d2t._tcp', 0, 0, 0, '.'),
+	SRV('_sips+d2t._tcp', 0, 0, 0, '.'),
+	SRV('_sip+d2u._udp', 0, 0, 0, '.'),
+	SRV('_sip+d2s._sctp', 0, 0, 0, '.'),
+	SRV('_sips+d2s._sctp', 0, 0, 0, '.'),
+	SRV('_submission._tcp', 10, 10, 587, 'smtp.example.org.'),
+	SRV('_submissions._tcp', 10, 10, 465, 'smtp.example.org.'),
+	SRV('_imap._tcp', 10, 10, 143, 'imap.example.org.'),
+	SRV('_imaps._tcp', 10, 10, 993, 'imap.example.org.'),
+	SRV('_pop3._tcp', 0, 0, 0, '.'),
+	SRV('_pop3s._tcp', 0, 0, 0, '.'),
+	SRV('_sieve._tcp', 10, 10, 4190, 'imap.example.org.'),
+	TXT('dns-moreinfo', ['Fred Bloggs, TZ=America/New_York', 'Chat-Service-X: @handle1', 'Chat-Service-Y: federated-handle@example.org']),
+	SRV('_pgpkey-http._tcp', 0, 0, 0, '.'),
+	SRV('_pgpkey-https._tcp', 0, 0, 0, '.'),
+	SRV('_hkp._tcp', 0, 0, 0, '.'),
+	SRV('_openpgpkey._tcp', 10, 10, 443, 'openpgpkey.example.org.'),
+	SRV('_finger._tcp', 10, 10, 79, 'barbican.example.org.'),
+	SRV('_avatars-sec._tcp', 10, 10, 443, 'avatars.example.org.'),
+	A('@', '192.0.2.1'),
+	AAAA('@', '2001:db8::1:1'),
+	TXT('_adsp._domainkey', 'dkim=all'),
+	TXT('_dmarc', 'v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s'),
+	TXT('d201911._domainkey', ['v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks', '6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB']),
+	TXT('d201911e2._domainkey', 'v=DKIM1; k=ed25519; p=GBt2k2L39KUb39fg5brOppXDHXvISy0+ECGgPld/bIo='),
+	TXT('d202003._domainkey', ['v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jo', 'pv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB']),
+	TXT('d202003e2._domainkey', 'v=DKIM1; k=ed25519; p=DQI5d9sNMrr0SLDoAi071IFOyKnlbR29hAQdqVQecQg='),
+	TXT('_report', 'r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;'),
+	TXT('_smtp._tls', 'v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org'),
+	TXT('_smtp-tlsrpt', 'v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org'),
+	TXT('example.net._report._dmarc', 'v=DMARC1'),
+	TXT('example.com._report._dmarc', 'v=DMARC1'),
+	TXT('xn--2j5b.xn--9t4b11yi5a._report._dmarc', 'v=DMARC1'),
+	TXT('special.test._report._dmarc', 'v=DMARC1'),
+	TXT('xn--qck5b9a5eml3bze.xn--zckzah._report._dmarc', 'v=DMARC1'),
+	CNAME('*._smimecert', '_ourca-smimea.example.org.'),
+	PTR('b._dns-sd._udp', 'field.example.org.'),
+	PTR('lb._dns-sd._udp', 'field.example.org.'),
+	PTR('r._dns-sd._udp', 'field.example.org.'),
+	NS('field', 'ns1.example.org.'),
+	NS('field', 'ns2.example.org.'),
+	A('barbican', '192.0.2.1'),
+	AAAA('barbican', '2001:db8::1:1'),
+	A('barbican.ipv4', '192.0.2.1'),
+	AAAA('barbican.ipv6', '2001:db8::1:1'),
+	A('megalomaniac', '198.51.100.254'),
+	AAAA('megalomaniac', '2001:db8:ffef::254'),
+	A('megalomaniac.ipv4', '198.51.100.254'),
+	AAAA('megalomaniac.ipv6', '2001:db8:ffef::254'),
+	SSHFP('megalomaniac', 1, 2, '4e9ced94d3caf2ce915f85a63ce7279d5118a79ea03dac59cf4859b825d2f619'),
+	SSHFP('megalomaniac', 3, 2, 'd3556a3db83ab9ccec39dc6693dd2f3e28b178c9bba61880924821c426cc61eb'),
+	SSHFP('megalomaniac', 4, 2, 'c60c9d9d4728668f5f46986ff0c5b416c5e913862c4970cbfe211a6f44a111b4'),
+	SSHFP('megalomaniac.ipv4', 1, 2, '4e9ced94d3caf2ce915f85a63ce7279d5118a79ea03dac59cf4859b825d2f619'),
+	SSHFP('megalomaniac.ipv4', 3, 2, 'd3556a3db83ab9ccec39dc6693dd2f3e28b178c9bba61880924821c426cc61eb'),
+	SSHFP('megalomaniac.ipv4', 4, 2, 'c60c9d9d4728668f5f46986ff0c5b416c5e913862c4970cbfe211a6f44a111b4'),
+	SSHFP('megalomaniac.ipv6', 1, 2, '4e9ced94d3caf2ce915f85a63ce7279d5118a79ea03dac59cf4859b825d2f619'),
+	SSHFP('megalomaniac.ipv6', 3, 2, 'd3556a3db83ab9ccec39dc6693dd2f3e28b178c9bba61880924821c426cc61eb'),
+	SSHFP('megalomaniac.ipv6', 4, 2, 'c60c9d9d4728668f5f46986ff0c5b416c5e913862c4970cbfe211a6f44a111b4'),
+	A('tower', '192.0.2.42'),
+	AAAA('tower', '2001:db8::1:42'),
+	A('tower.ipv4', '192.0.2.42'),
+	AAAA('tower.ipv6', '2001:db8::1:42'),
+	SSHFP('tower', 1, 2, '0f211d236e94768911a294f38653c4af6fa935a5b06c975d8162f59142571451'),
+	SSHFP('tower', 3, 2, '88bf7b7401c11fa2e84871efb06cd73d8fc409154605b354db2dda0b82fe1160'),
+	SSHFP('tower', 4, 2, '6d30900be0faaae73568fc007a87b4d076cf9a351ecacc1106aef726c34ad61d'),
+	SSHFP('tower.ipv4', 1, 2, '0f211d236e94768911a294f38653c4af6fa935a5b06c975d8162f59142571451'),
+	SSHFP('tower.ipv4', 3, 2, '88bf7b7401c11fa2e84871efb06cd73d8fc409154605b354db2dda0b82fe1160'),
+	SSHFP('tower.ipv4', 4, 2, '6d30900be0faaae73568fc007a87b4d076cf9a351ecacc1106aef726c34ad61d'),
+	SSHFP('tower.ipv6', 1, 2, '0f211d236e94768911a294f38653c4af6fa935a5b06c975d8162f59142571451'),
+	SSHFP('tower.ipv6', 3, 2, '88bf7b7401c11fa2e84871efb06cd73d8fc409154605b354db2dda0b82fe1160'),
+	SSHFP('tower.ipv6', 4, 2, '6d30900be0faaae73568fc007a87b4d076cf9a351ecacc1106aef726c34ad61d'),
+	A('vcs', '192.0.2.228'),
+	AAAA('vcs', '2001:db8::48:4558:4456:4353'),
+	A('vcs.ipv4', '192.0.2.228'),
+	AAAA('vcs.ipv6', '2001:db8::48:4558:4456:4353'),
+	CNAME('git', 'vcs.example.org.'),
+	CNAME('git.ipv4', 'vcs.ipv4.example.org.'),
+	CNAME('git.ipv6', 'vcs.ipv6.example.org.'),
+	AAAA('svn', '2001:db8::48:4558:73:766e'),
+	SSHFP('vcs', 1, 2, 'b518be390babdf43cb2d598aa6befa6ce6878546bf107b829d0cfc65253a97d4'),
+	SSHFP('vcs', 3, 2, 'e92545dc0bf501f72333ddeb7a37afc2c5b408ce39a3ad95fbc66236f0077323'),
+	SSHFP('vcs', 4, 2, '02289441124a487095a6cda2e946c6a8ed9087faf3592ec4135536c3e615521c'),
+	SSHFP('vcs.ipv4', 1, 2, 'b518be390babdf43cb2d598aa6befa6ce6878546bf107b829d0cfc65253a97d4'),
+	SSHFP('vcs.ipv4', 3, 2, 'e92545dc0bf501f72333ddeb7a37afc2c5b408ce39a3ad95fbc66236f0077323'),
+	SSHFP('vcs.ipv4', 4, 2, '02289441124a487095a6cda2e946c6a8ed9087faf3592ec4135536c3e615521c'),
+	SSHFP('vcs.ipv6', 1, 2, 'b518be390babdf43cb2d598aa6befa6ce6878546bf107b829d0cfc65253a97d4'),
+	SSHFP('vcs.ipv6', 3, 2, 'e92545dc0bf501f72333ddeb7a37afc2c5b408ce39a3ad95fbc66236f0077323'),
+	SSHFP('vcs.ipv6', 4, 2, '02289441124a487095a6cda2e946c6a8ed9087faf3592ec4135536c3e615521c'),
+	A('nsauth', '192.0.2.53'),
+	AAAA('nsauth', '2001:db8::53:1'),
+	A('nsauth.ipv4', '192.0.2.53'),
+	AAAA('nsauth.ipv6', '2001:db8::53:1'),
+	SSHFP('nsauth', 1, 2, '895804ae022fff643b2677563cb850607c5bb564d9919896c521098c8abc40f2'),
+	SSHFP('nsauth', 3, 2, '28a65470badae611375747e1a803211c41e3d71e97741fa92ccbdf7b01f34e42'),
+	SSHFP('nsauth', 4, 2, '6e10445c0649c03fa83e18b1873e5b89b3a20893ecb48d01e7cedb3dd563ecf0'),
+	SSHFP('nsauth.ipv4', 1, 2, '895804ae022fff643b2677563cb850607c5bb564d9919896c521098c8abc40f2'),
+	SSHFP('nsauth.ipv4', 3, 2, '28a65470badae611375747e1a803211c41e3d71e97741fa92ccbdf7b01f34e42'),
+	SSHFP('nsauth.ipv4', 4, 2, '6e10445c0649c03fa83e18b1873e5b89b3a20893ecb48d01e7cedb3dd563ecf0'),
+	SSHFP('nsauth.ipv6', 1, 2, '895804ae022fff643b2677563cb850607c5bb564d9919896c521098c8abc40f2'),
+	SSHFP('nsauth.ipv6', 3, 2, '28a65470badae611375747e1a803211c41e3d71e97741fa92ccbdf7b01f34e42'),
+	SSHFP('nsauth.ipv6', 4, 2, '6e10445c0649c03fa83e18b1873e5b89b3a20893ecb48d01e7cedb3dd563ecf0'),
+	A('ns1', '192.0.2.53'),
+	AAAA('ns1', '2001:db8::53:1'),
+	A('ns2', '203.0.113.53'),
+	AAAA('ns2', '2001:db8:113::53'),
+	A('hermes', '192.0.2.25'),
+	AAAA('hermes', '2001:db8::48:4558:736d:7470'),
+	AAAA('hermes', '2001:db8::48:4558:696d:6170'),
+	A('hermes.ipv4', '192.0.2.25'),
+	AAAA('hermes.ipv6', '2001:db8::48:4558:736d:7470'),
+	AAAA('hermes.ipv6', '2001:db8::48:4558:696d:6170'),
+	SSHFP('hermes', 1, 2, '4472ff5bd0528cd49216af4503ba6a1c48f121d0292a31d6af193e5000af4966'),
+	SSHFP('hermes', 3, 2, 'eaba20c1565676a5229184ccfcf82d0ee408f91757a67d9fa51a0b6f3db4a33b'),
+	SSHFP('hermes', 4, 2, 'a9d89920e599d04363c8b35a4ce66c1ed257ea1d16981f060b6aed080bbb7a7c'),
+	SSHFP('hermes.ipv4', 1, 2, '4472ff5bd0528cd49216af4503ba6a1c48f121d0292a31d6af193e5000af4966'),
+	SSHFP('hermes.ipv4', 3, 2, 'eaba20c1565676a5229184ccfcf82d0ee408f91757a67d9fa51a0b6f3db4a33b'),
+	SSHFP('hermes.ipv4', 4, 2, 'a9d89920e599d04363c8b35a4ce66c1ed257ea1d16981f060b6aed080bbb7a7c'),
+	SSHFP('hermes.ipv6', 1, 2, '4472ff5bd0528cd49216af4503ba6a1c48f121d0292a31d6af193e5000af4966'),
+	SSHFP('hermes.ipv6', 3, 2, 'eaba20c1565676a5229184ccfcf82d0ee408f91757a67d9fa51a0b6f3db4a33b'),
+	SSHFP('hermes.ipv6', 4, 2, 'a9d89920e599d04363c8b35a4ce66c1ed257ea1d16981f060b6aed080bbb7a7c'),
+	A('kerb-service', '192.0.2.88'),
+	AAAA('kerb-service', '2001:db8::48:4558:6b65:7262'),
+	A('security', '192.0.2.92'),
+	AAAA('security', '2001:db8::48:4558:53:4543'),
+	A('security.ipv4', '192.0.2.92'),
+	AAAA('security.ipv6', '2001:db8::48:4558:53:4543'),
+	A('services', '192.0.2.93'),
+	AAAA('services', '2001:db8::48:4558:5345:5256'),
+	A('services.ipv4', '192.0.2.93'),
+	AAAA('services.ipv6', '2001:db8::48:4558:5345:5256'),
+	A('openpgpkey', '192.0.2.92'),
+	AAAA('openpgpkey', '2001:db8::48:4558:53:4543'),
+	CNAME('finger', 'barbican.example.org.'),
+	CNAME('finger.ipv4', 'barbican.ipv4.example.org.'),
+	CNAME('finger.ipv6', 'barbican.ipv6.example.org.'),
+	A('avatars', '192.0.2.93'),
+	AAAA('avatars', '2001:db8::48:4558:5345:5256'),
+	CNAME('dict', 'services.example.org.'),
+	CNAME('people', 'services.example.org.'),
+	CNAME('people.ipv4', 'services.ipv4.example.org.'),
+	CNAME('people.ipv6', 'services.ipv6.example.org.'),
+	CNAME('wpad', 'services.example.org.'),
+	CNAME('www', 'services.example.org.'),
+	CNAME('www.ipv4', 'services.ipv4.example.org.'),
+	CNAME('www.ipv6', 'services.ipv6.example.org.'),
+	CAA('@', 'issue', 'example.net'),
+	CAA('@', 'issue', 'letsencrypt.org\; accounturi=https://acme-v01.api.letsencrypt.org/acme/reg/1234567'),
+	CAA('@', 'issue', 'letsencrypt.org\; accounturi=https://acme-staging-v02.api.letsencrypt.org/acme/acct/23456789'),
+	CAA('@', 'issue', 'letsencrypt.org\; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/76543210'),
+	CAA('@', 'issuewild', ';'),
+	CAA('@', 'iodef', 'mailto:security@example.org'),
+	TLSA('_ourcaca4-tlsa', 2, 0, 1, 'ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488'),
+	TLSA('_ourcaca5-tlsa', 2, 0, 1, '11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1'),
+	TLSA('_cacert-c3-tlsa', 2, 0, 1, '4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8'),
+	TLSA('_letsencrypt-tlsa', 2, 1, 1, '60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18'),
+	TLSA('_letsencrypt-tlsa', 2, 1, 1, 'b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b'),
+	TLSA('_amazon-tlsa', 2, 0, 1, '8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e'),
+	TLSA('_amazon-tlsa', 2, 0, 1, '1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4'),
+	TLSA('_amazon-tlsa', 2, 0, 1, '18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4'),
+	TLSA('_amazon-tlsa', 2, 0, 1, 'e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092'),
+	TLSA('_ourca-tlsa', 2, 0, 1, 'ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488'),
+	TLSA('_ourca-tlsa', 2, 0, 1, '11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1'),
+	TLSA('_ourca-cacert-tlsa', 2, 0, 1, 'ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488'),
+	TLSA('_ourca-cacert-tlsa', 2, 0, 1, '11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1'),
+	TLSA('_ourca-cacert-tlsa', 2, 0, 1, '4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8'),
+	TLSA('_ourca-le-tlsa', 2, 0, 1, 'ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488'),
+	TLSA('_ourca-le-tlsa', 2, 0, 1, '11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1'),
+	TLSA('_ourca-le-tlsa', 2, 1, 1, '60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18'),
+	TLSA('_ourca-le-tlsa', 2, 1, 1, 'b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b'),
+	TLSA('_ourca-cacert-le-tlsa', 2, 0, 1, 'ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488'),
+	TLSA('_ourca-cacert-le-tlsa', 2, 0, 1, '11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1'),
+	TLSA('_ourca-cacert-le-tlsa', 2, 0, 1, '4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8'),
+	TLSA('_ourca-cacert-le-tlsa', 2, 1, 1, '60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18'),
+	TLSA('_ourca-cacert-le-tlsa', 2, 1, 1, 'b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b'),
+	TLSA('_cacert-le-tlsa', 2, 0, 1, '4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8'),
+	TLSA('_cacert-le-tlsa', 2, 1, 1, '60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18'),
+	TLSA('_cacert-le-tlsa', 2, 1, 1, 'b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b'),
+	TLSA('_le-amazon-tlsa', 2, 1, 1, '60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18'),
+	TLSA('_le-amazon-tlsa', 2, 1, 1, 'b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b'),
+	TLSA('_le-amazon-tlsa', 2, 0, 1, '8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e'),
+	TLSA('_le-amazon-tlsa', 2, 0, 1, '1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4'),
+	TLSA('_le-amazon-tlsa', 2, 0, 1, '18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4'),
+	TLSA('_le-amazon-tlsa', 2, 0, 1, 'e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092'),
+	TLSA('_ourca-le-amazon-tlsa', 2, 0, 1, 'ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488'),
+	TLSA('_ourca-le-amazon-tlsa', 2, 0, 1, '11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1'),
+	TLSA('_ourca-le-amazon-tlsa', 2, 1, 1, '60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18'),
+	TLSA('_ourca-le-amazon-tlsa', 2, 1, 1, 'b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b'),
+	TLSA('_ourca-le-amazon-tlsa', 2, 0, 1, '8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e'),
+	TLSA('_ourca-le-amazon-tlsa', 2, 0, 1, '1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4'),
+	TLSA('_ourca-le-amazon-tlsa', 2, 0, 1, '18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4'),
+	TLSA('_ourca-le-amazon-tlsa', 2, 0, 1, 'e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092'),
+	CNAME('_443._tcp.www', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.www.ipv4', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.www.ipv6', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.people', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.people.ipv4', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.people.ipv6', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.git', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.svn', '_ourca-le-tlsa.example.org.'),
+	CNAME('_5222._tcp.xmpp', '_ourca-le-tlsa.example.org.'),
+	CNAME('_5223._tcp.xmpp', '_ourca-le-tlsa.example.org.'),
+	CNAME('_5269._tcp.xmpp-s2s', '_ourca-le-tlsa.example.org.'),
+	CNAME('_25._tcp.mx', '_ourca-le-tlsa.example.org.'),
+	CNAME('_26._tcp.mx', '_ourca-le-tlsa.example.org.'),
+	CNAME('_27._tcp.mx', '_ourca-le-tlsa.example.org.'),
+	CNAME('_465._tcp.smtp46', '_ourca-le-tlsa.example.org.'),
+	CNAME('_587._tcp.smtp46', '_ourca-le-tlsa.example.org.'),
+	CNAME('_1465._tcp.smtp46', '_ourca-le-tlsa.example.org.'),
+	CNAME('_1587._tcp.smtp46', '_ourca-le-tlsa.example.org.'),
+	CNAME('_465._tcp.smtp', '_ourca-le-tlsa.example.org.'),
+	CNAME('_587._tcp.smtp', '_ourca-le-tlsa.example.org.'),
+	CNAME('_1465._tcp.smtp', '_ourca-le-tlsa.example.org.'),
+	CNAME('_1587._tcp.smtp', '_ourca-le-tlsa.example.org.'),
+	CNAME('_143._tcp.imap46', '_ourca-le-tlsa.example.org.'),
+	CNAME('_993._tcp.imap46', '_ourca-le-tlsa.example.org.'),
+	CNAME('_143._tcp.imap', '_ourca-le-tlsa.example.org.'),
+	CNAME('_993._tcp.imap', '_ourca-le-tlsa.example.org.'),
+	CNAME('_4190._tcp.imap', '_ourca-le-tlsa.example.org.'),
+	CNAME('www.security', 'security.example.org.'),
+	CNAME('www.security.ipv4', 'security.ipv4.example.org.'),
+	CNAME('www.security.ipv6', 'security.ipv6.example.org.'),
+	CNAME('_443._tcp.www.security', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.www.security.ipv4', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.www.security.ipv6', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.security', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.security.ipv4', '_ourca-le-tlsa.example.org.'),
+	CNAME('_443._tcp.security.ipv6', '_ourca-le-tlsa.example.org.'),
+	CNAME('_acme-challenge', '_acme-challenge.chat-acme.d.example.net.', TTL(15)),
+	CNAME('_acme-challenge.xmpp', '_acme-challenge.xmpp.chat-acme.d.example.net.', TTL(15)),
+	CNAME('_acme-challenge.chat', '_acme-challenge.chat.chat-acme.d.example.net.', TTL(15)),
+	CNAME('_acme-challenge.conference', '_acme-challenge.conference.chat-acme.d.example.net.', TTL(15)),
+	CNAME('_acme-challenge.proxy-chatfiles', '_acme-challenge.proxy-chatfiles.chat-acme.d.example.net.', TTL(15)),
+	CNAME('_acme-challenge.pubsub.xmpp', '_acme-challenge.pubsub.xmpp.chat-acme.d.example.net.', TTL(15)),
+	AAAA('imap', '2001:db8::48:4558:696d:6170'),
+	A('imap', '192.0.2.25'),
+	AAAA('smtp', '2001:db8::48:4558:736d:7470'),
+	A('smtp', '192.0.2.25'),
+	A('smtp46', '192.0.2.25'),
+	AAAA('smtp46', '2001:db8::48:4558:736d:7470'),
+	A('imap46', '192.0.2.25'),
+	AAAA('imap46', '2001:db8::48:4558:696d:6170'),
+	A('mx', '192.0.2.25'),
+	AAAA('mx', '2001:db8::48:4558:736d:7470'),
+	A('mx.ipv4', '192.0.2.25'),
+	AAAA('mx.ipv6', '2001:db8::48:4558:736d:7470'),
+	TXT('mx', 'v=spf1 a include:_spflarge.example.net -all'),
+	TXT('_mta-sts', 'v=STSv1; id=20191231r1;'),
+	TXT('mta-sts', 'v=STSv1; id=20191231r1;'),
+	A('mta-sts', '192.0.2.93'),
+	AAAA('mta-sts', '2001:db8::48:4558:5345:5256'),
+	AAAA('xmpp.ipv6', '2001:db8::f0ab:cdef:1234:f00f'),
+	AAAA('xmpp-s2s.ipv6', '2001:db8::f0ab:cdef:1234:f00f'),
+	A('xmpp', '203.0.113.175'),
+	AAAA('xmpp', '2001:db8::f0ab:cdef:1234:f00f'),
+	A('xmpp-s2s', '203.0.113.175'),
+	AAAA('xmpp-s2s', '2001:db8::f0ab:cdef:1234:f00f'),
+	CNAME('proxy-chatfiles', 'xmpp.example.org.'),
+	CNAME('fileproxy.xmpp', 'xmpp.example.org.'),
+	CNAME('conference', 'xmpp-s2s.example.org.'),
+	SRV('_xmpp-server._tcp.conference', 10, 2, 5269, 'xmpp-s2s.example.org.'),
+	CNAME('pubsub.xmpp', 'xmpp-s2s.example.org.'),
+	A('chat', '203.0.113.175'),
+	AAAA('chat', '2001:db8::f0ab:cdef:1234:f00f'),
+	CNAME('proxy-chatfiles.chat', 'chat.example.org.'),
+	CNAME('fileproxy.chat', 'chat.example.org.'),
+	CNAME('conference.chat', 'chat.example.org.'),
+	CNAME('pubsub.chat', 'chat.example.org.'),
+	SRV('_xmpp-server._tcp.conference', 10, 2, 5269, 'chat.example.org.'),
+	AAAA('auth', '2001:db8::48:4558:6175:7468'),
+	AAAA('kpeople', '2001:db8::48:4558:6b70:706c'),
+	AAAA('ocsp.security', '2001:db8::48:4558:6f63:7370'),
+	AAAA('webauth', '2001:db8::48:4558:7765:6261'),
+	A('news-feed', '192.0.2.93'),
+	AAAA('news-feed', '2001:db8::48:4558:6e6e:7470'),
+	CNAME('go', 'abcdefghijklmn.cloudfront.net.'),
+	A('foo', '192.0.2.200'),
+	MX('gladys', 10, 'mx.example.org.'),
+	TXT('_adsp._domainkey.gladys', 'dkim=all'),
+	TXT('_dmarc.gladys', 'v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s'),
+	TXT('_report.gladys', 'r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;'),
+	TXT('_smtp._tls.gladys', 'v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org'),
+	TXT('_smtp-tlsrpt.gladys', 'v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org'),
+	MX('fred', 10, 'mx.example.org.'),
+	A('fred', '192.0.2.93'),
+	AAAA('fred', '2001:db8::48:4558:5345:5256'),
+	TXT('fred', 'v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all'),
+	TXT('d201911._domainkey.fred', ['v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/Tlz', 'P2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB']),
+	TXT('d201911e2._domainkey.fred', 'v=DKIM1; k=ed25519; p=rQNsV9YcPJn/WYI1EDLjNbN/VuX1Hqq/oe4htbnhv+A='),
+	TXT('d202003._domainkey.fred', ['v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYj', 'c0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB']),
+	TXT('d202003e2._domainkey.fred', 'v=DKIM1; k=ed25519; p=0DAPp/IRLYFI/Z4YSgJRi4gr7xcu1/EfJ5mjVn10aAw='),
+	TXT('_adsp._domainkey.fred', 'dkim=all'),
+	TXT('_dmarc.fred', 'v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s'),
+	TXT('_report.fred', 'r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;'),
+	TXT('_smtp._tls.fred', 'v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org'),
+	TXT('_smtp-tlsrpt.fred', 'v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org'),
+	MX('mailtest', 10, 'mx.example.org.'),
+	TXT('d201911._domainkey.mailtest', ['v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn', '0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB']),
+	TXT('d201911e2._domainkey.mailtest', 'v=DKIM1; k=ed25519; p=afulDDnhaTzdqKQN0jtWV04eOhAcyBk3NCyVheOf53Y='),
+	TXT('d202003._domainkey.mailtest', ['v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KN', 'aS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB']),
+	TXT('d202003e2._domainkey.mailtest', 'v=DKIM1; k=ed25519; p=iqwH/hhozFdeo1xnuldr8KUi7O7g+DzmC+f0SYMKVDc='),
+	TXT('_adsp._domainkey.mailtest', 'dkim=all'),
+	TXT('_dmarc.mailtest', 'v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s'),
+	TXT('_report.mailtest', 'r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;'),
+	TXT('_smtp._tls.mailtest', 'v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org'),
+	TXT('_smtp-tlsrpt.mailtest', 'v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org'),
+	SRV('_pgpkey-http._tcp.sks', 0, 0, 0, '.'),
+	SRV('_pgpkey-https._tcp.sks', 0, 0, 0, '.'),
+	SRV('_hkp._tcp.sks', 0, 0, 0, '.'),
+	SRV('_pgpkey-http._tcp.sks-peer', 0, 0, 0, '.'),
+	SRV('_pgpkey-https._tcp.sks-peer', 0, 0, 0, '.'),
+	SRV('_hkp._tcp.sks-peer', 0, 0, 0, '.'),
+	NS('yoyo', 'ns5.he.net.'),
+	NS('yoyo', 'ns4.he.net.'),
+	NS('yoyo', 'ns3.he.net.'),
+	NS('yoyo', 'ns2.he.net.'),
+	NS('yoyo', 'ns1.he.net.'),
+	NS('khard', 'ns-cloud-d1.googledomains.com.'),
+	NS('khard', 'ns-cloud-d2.googledomains.com.'),
+	NS('khard', 'ns-cloud-d3.googledomains.com.'),
+	NS('khard', 'ns-cloud-d4.googledomains.com.'),
+	MX('realhost', 0, '.'),
+	TXT('realhost', 'v=spf1 -all'),
+	TLSA('_25._tcp.realhost', 3, 0, 0, '0000000000000000000000000000000000000000000000000000000000000000'),
+	CNAME('_fedcba9876543210fedcba9876543210.go', '_45678901234abcdef45678901234abcd.ggedgsdned.acm-validations.aws.'),
+	CNAME('opqrstuvwxyz', 'gv-abcdefghijklmn.dv.googlehosted.com.'),
+	CNAME('zyxwvutsrqpo', 'gv-nmlkjihgfedcba.dv.googlehosted.com.'),
+	CNAME('0123456789abcdef0123456789abcdef', 'verify.bing.com.')
+)
diff --git a/commands/test_data/example.org.zone.tsv b/commands/test_data/example.org.zone.tsv
new file mode 100644
index 00000000..ca15d2fc
--- /dev/null
+++ b/commands/test_data/example.org.zone.tsv
@@ -0,0 +1,349 @@
+example.org	@	43200	IN	SOA	ns1.example.org. hostmaster.example.org. 2020030700 7200 3600 864000 7200
+example.org	@	7200	IN	NS	ns1.example.org.
+example.org	@	7200	IN	NS	ns2.example.org.
+example.org	@	7200	IN	NS	ns-a.example.net.
+example.org	@	7200	IN	NS	friend-dns.example.com.
+example.org	@	7200	IN	MX	10 mx.example.org.
+example.org	@	7200	IN	TXT	"v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"
+_client._smtp.example.org	_client._smtp	7200	IN	SRV	1 1 1 example.org.
+_client._smtp.mx.example.org	_client._smtp.mx	7200	IN	SRV	1 2 1 mx.example.org.
+_client._smtp.foo.example.org	_client._smtp.foo	7200	IN	SRV	1 2 1 foo.example.org.
+_kerberos._tcp.example.org	_kerberos._tcp	7200	IN	SRV	10 1 88 kerb-service.example.org.
+_kerberos._udp.example.org	_kerberos._udp	7200	IN	SRV	10 1 88 kerb-service.example.org.
+_kpasswd._udp.example.org	_kpasswd._udp	7200	IN	SRV	10 1 464 kerb-service.example.org.
+_kerberos-adm._tcp.example.org	_kerberos-adm._tcp	7200	IN	SRV	10 1 749 kerb-service.example.org.
+_kerberos.example.org	_kerberos	7200	IN	TXT	"EXAMPLE.ORG"
+_ldap._tcp.example.org	_ldap._tcp	7200	IN	SRV	0 0 0 .
+_ldap._udp.example.org	_ldap._udp	7200	IN	SRV	0 0 0 .
+_jabber._tcp.example.org	_jabber._tcp	7200	IN	SRV	10 2 5269 xmpp-s2s.example.org.
+_xmpp-server._tcp.example.org	_xmpp-server._tcp	7200	IN	SRV	10 2 5269 xmpp-s2s.example.org.
+_xmpp-client._tcp.example.org	_xmpp-client._tcp	7200	IN	SRV	10 2 5222 xmpp.example.org.
+_im._sip.example.org	_im._sip	7200	IN	SRV	0 0 0 .
+_pres._sip.example.org	_pres._sip	7200	IN	SRV	0 0 0 .
+_sip+d2t._tcp.example.org	_sip+d2t._tcp	7200	IN	SRV	0 0 0 .
+_sips+d2t._tcp.example.org	_sips+d2t._tcp	7200	IN	SRV	0 0 0 .
+_sip+d2u._udp.example.org	_sip+d2u._udp	7200	IN	SRV	0 0 0 .
+_sip+d2s._sctp.example.org	_sip+d2s._sctp	7200	IN	SRV	0 0 0 .
+_sips+d2s._sctp.example.org	_sips+d2s._sctp	7200	IN	SRV	0 0 0 .
+_submission._tcp.example.org	_submission._tcp	7200	IN	SRV	10 10 587 smtp.example.org.
+_submissions._tcp.example.org	_submissions._tcp	7200	IN	SRV	10 10 465 smtp.example.org.
+_imap._tcp.example.org	_imap._tcp	7200	IN	SRV	10 10 143 imap.example.org.
+_imaps._tcp.example.org	_imaps._tcp	7200	IN	SRV	10 10 993 imap.example.org.
+_pop3._tcp.example.org	_pop3._tcp	7200	IN	SRV	0 0 0 .
+_pop3s._tcp.example.org	_pop3s._tcp	7200	IN	SRV	0 0 0 .
+_sieve._tcp.example.org	_sieve._tcp	7200	IN	SRV	10 10 4190 imap.example.org.
+dns-moreinfo.example.org	dns-moreinfo	7200	IN	TXT	"Fred Bloggs, TZ=America/New_York" "Chat-Service-X: @handle1" "Chat-Service-Y: federated-handle@example.org"
+_pgpkey-http._tcp.example.org	_pgpkey-http._tcp	7200	IN	SRV	0 0 0 .
+_pgpkey-https._tcp.example.org	_pgpkey-https._tcp	7200	IN	SRV	0 0 0 .
+_hkp._tcp.example.org	_hkp._tcp	7200	IN	SRV	0 0 0 .
+_openpgpkey._tcp.example.org	_openpgpkey._tcp	7200	IN	SRV	10 10 443 openpgpkey.example.org.
+_finger._tcp.example.org	_finger._tcp	7200	IN	SRV	10 10 79 barbican.example.org.
+_avatars-sec._tcp.example.org	_avatars-sec._tcp	7200	IN	SRV	10 10 443 avatars.example.org.
+example.org	@	7200	IN	A	192.0.2.1
+example.org	@	7200	IN	AAAA	2001:db8::1:1
+_adsp._domainkey.example.org	_adsp._domainkey	7200	IN	TXT	"dkim=all"
+_dmarc.example.org	_dmarc	7200	IN	TXT	"v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+d201911._domainkey.example.org	d201911._domainkey	7200	IN	TXT	"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks" "6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB"
+d201911e2._domainkey.example.org	d201911e2._domainkey	7200	IN	TXT	"v=DKIM1; k=ed25519; p=GBt2k2L39KUb39fg5brOppXDHXvISy0+ECGgPld/bIo="
+d202003._domainkey.example.org	d202003._domainkey	7200	IN	TXT	"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jo" "pv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB"
+d202003e2._domainkey.example.org	d202003e2._domainkey	7200	IN	TXT	"v=DKIM1; k=ed25519; p=DQI5d9sNMrr0SLDoAi071IFOyKnlbR29hAQdqVQecQg="
+_report.example.org	_report	7200	IN	TXT	"r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp._tls.example.org	_smtp._tls	7200	IN	TXT	"v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp-tlsrpt.example.org	_smtp-tlsrpt	7200	IN	TXT	"v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+example.net._report._dmarc.example.org	example.net._report._dmarc	7200	IN	TXT	"v=DMARC1"
+example.com._report._dmarc.example.org	example.com._report._dmarc	7200	IN	TXT	"v=DMARC1"
+xn--2j5b.xn--9t4b11yi5a._report._dmarc.example.org	xn--2j5b.xn--9t4b11yi5a._report._dmarc	7200	IN	TXT	"v=DMARC1"
+special.test._report._dmarc.example.org	special.test._report._dmarc	7200	IN	TXT	"v=DMARC1"
+xn--qck5b9a5eml3bze.xn--zckzah._report._dmarc.example.org	xn--qck5b9a5eml3bze.xn--zckzah._report._dmarc	7200	IN	TXT	"v=DMARC1"
+*._smimecert.example.org	*._smimecert	7200	IN	CNAME	_ourca-smimea.example.org.
+b._dns-sd._udp.example.org	b._dns-sd._udp	7200	IN	PTR	field.example.org.
+lb._dns-sd._udp.example.org	lb._dns-sd._udp	7200	IN	PTR	field.example.org.
+r._dns-sd._udp.example.org	r._dns-sd._udp	7200	IN	PTR	field.example.org.
+field.example.org	field	7200	IN	NS	ns1.example.org.
+field.example.org	field	7200	IN	NS	ns2.example.org.
+barbican.example.org	barbican	7200	IN	A	192.0.2.1
+barbican.example.org	barbican	7200	IN	AAAA	2001:db8::1:1
+barbican.ipv4.example.org	barbican.ipv4	7200	IN	A	192.0.2.1
+barbican.ipv6.example.org	barbican.ipv6	7200	IN	AAAA	2001:db8::1:1
+megalomaniac.example.org	megalomaniac	7200	IN	A	198.51.100.254
+megalomaniac.example.org	megalomaniac	7200	IN	AAAA	2001:db8:ffef::254
+megalomaniac.ipv4.example.org	megalomaniac.ipv4	7200	IN	A	198.51.100.254
+megalomaniac.ipv6.example.org	megalomaniac.ipv6	7200	IN	AAAA	2001:db8:ffef::254
+megalomaniac.example.org	megalomaniac	7200	IN	SSHFP	1 2 4E9CED94D3CAF2CE915F85A63CE7279D5118A79EA03DAC59CF4859B825D2F619
+megalomaniac.example.org	megalomaniac	7200	IN	SSHFP	3 2 D3556A3DB83AB9CCEC39DC6693DD2F3E28B178C9BBA61880924821C426CC61EB
+megalomaniac.example.org	megalomaniac	7200	IN	SSHFP	4 2 C60C9D9D4728668F5F46986FF0C5B416C5E913862C4970CBFE211A6F44A111B4
+megalomaniac.ipv4.example.org	megalomaniac.ipv4	7200	IN	SSHFP	1 2 4E9CED94D3CAF2CE915F85A63CE7279D5118A79EA03DAC59CF4859B825D2F619
+megalomaniac.ipv4.example.org	megalomaniac.ipv4	7200	IN	SSHFP	3 2 D3556A3DB83AB9CCEC39DC6693DD2F3E28B178C9BBA61880924821C426CC61EB
+megalomaniac.ipv4.example.org	megalomaniac.ipv4	7200	IN	SSHFP	4 2 C60C9D9D4728668F5F46986FF0C5B416C5E913862C4970CBFE211A6F44A111B4
+megalomaniac.ipv6.example.org	megalomaniac.ipv6	7200	IN	SSHFP	1 2 4E9CED94D3CAF2CE915F85A63CE7279D5118A79EA03DAC59CF4859B825D2F619
+megalomaniac.ipv6.example.org	megalomaniac.ipv6	7200	IN	SSHFP	3 2 D3556A3DB83AB9CCEC39DC6693DD2F3E28B178C9BBA61880924821C426CC61EB
+megalomaniac.ipv6.example.org	megalomaniac.ipv6	7200	IN	SSHFP	4 2 C60C9D9D4728668F5F46986FF0C5B416C5E913862C4970CBFE211A6F44A111B4
+tower.example.org	tower	7200	IN	A	192.0.2.42
+tower.example.org	tower	7200	IN	AAAA	2001:db8::1:42
+tower.ipv4.example.org	tower.ipv4	7200	IN	A	192.0.2.42
+tower.ipv6.example.org	tower.ipv6	7200	IN	AAAA	2001:db8::1:42
+tower.example.org	tower	7200	IN	SSHFP	1 2 0F211D236E94768911A294F38653C4AF6FA935A5B06C975D8162F59142571451
+tower.example.org	tower	7200	IN	SSHFP	3 2 88BF7B7401C11FA2E84871EFB06CD73D8FC409154605B354DB2DDA0B82FE1160
+tower.example.org	tower	7200	IN	SSHFP	4 2 6D30900BE0FAAAE73568FC007A87B4D076CF9A351ECACC1106AEF726C34AD61D
+tower.ipv4.example.org	tower.ipv4	7200	IN	SSHFP	1 2 0F211D236E94768911A294F38653C4AF6FA935A5B06C975D8162F59142571451
+tower.ipv4.example.org	tower.ipv4	7200	IN	SSHFP	3 2 88BF7B7401C11FA2E84871EFB06CD73D8FC409154605B354DB2DDA0B82FE1160
+tower.ipv4.example.org	tower.ipv4	7200	IN	SSHFP	4 2 6D30900BE0FAAAE73568FC007A87B4D076CF9A351ECACC1106AEF726C34AD61D
+tower.ipv6.example.org	tower.ipv6	7200	IN	SSHFP	1 2 0F211D236E94768911A294F38653C4AF6FA935A5B06C975D8162F59142571451
+tower.ipv6.example.org	tower.ipv6	7200	IN	SSHFP	3 2 88BF7B7401C11FA2E84871EFB06CD73D8FC409154605B354DB2DDA0B82FE1160
+tower.ipv6.example.org	tower.ipv6	7200	IN	SSHFP	4 2 6D30900BE0FAAAE73568FC007A87B4D076CF9A351ECACC1106AEF726C34AD61D
+vcs.example.org	vcs	7200	IN	A	192.0.2.228
+vcs.example.org	vcs	7200	IN	AAAA	2001:db8::48:4558:4456:4353
+vcs.ipv4.example.org	vcs.ipv4	7200	IN	A	192.0.2.228
+vcs.ipv6.example.org	vcs.ipv6	7200	IN	AAAA	2001:db8::48:4558:4456:4353
+git.example.org	git	7200	IN	CNAME	vcs.example.org.
+git.ipv4.example.org	git.ipv4	7200	IN	CNAME	vcs.ipv4.example.org.
+git.ipv6.example.org	git.ipv6	7200	IN	CNAME	vcs.ipv6.example.org.
+svn.example.org	svn	7200	IN	AAAA	2001:db8::48:4558:73:766e
+vcs.example.org	vcs	7200	IN	SSHFP	1 2 B518BE390BABDF43CB2D598AA6BEFA6CE6878546BF107B829D0CFC65253A97D4
+vcs.example.org	vcs	7200	IN	SSHFP	3 2 E92545DC0BF501F72333DDEB7A37AFC2C5B408CE39A3AD95FBC66236F0077323
+vcs.example.org	vcs	7200	IN	SSHFP	4 2 02289441124A487095A6CDA2E946C6A8ED9087FAF3592EC4135536C3E615521C
+vcs.ipv4.example.org	vcs.ipv4	7200	IN	SSHFP	1 2 B518BE390BABDF43CB2D598AA6BEFA6CE6878546BF107B829D0CFC65253A97D4
+vcs.ipv4.example.org	vcs.ipv4	7200	IN	SSHFP	3 2 E92545DC0BF501F72333DDEB7A37AFC2C5B408CE39A3AD95FBC66236F0077323
+vcs.ipv4.example.org	vcs.ipv4	7200	IN	SSHFP	4 2 02289441124A487095A6CDA2E946C6A8ED9087FAF3592EC4135536C3E615521C
+vcs.ipv6.example.org	vcs.ipv6	7200	IN	SSHFP	1 2 B518BE390BABDF43CB2D598AA6BEFA6CE6878546BF107B829D0CFC65253A97D4
+vcs.ipv6.example.org	vcs.ipv6	7200	IN	SSHFP	3 2 E92545DC0BF501F72333DDEB7A37AFC2C5B408CE39A3AD95FBC66236F0077323
+vcs.ipv6.example.org	vcs.ipv6	7200	IN	SSHFP	4 2 02289441124A487095A6CDA2E946C6A8ED9087FAF3592EC4135536C3E615521C
+nsauth.example.org	nsauth	7200	IN	A	192.0.2.53
+nsauth.example.org	nsauth	7200	IN	AAAA	2001:db8::53:1
+nsauth.ipv4.example.org	nsauth.ipv4	7200	IN	A	192.0.2.53
+nsauth.ipv6.example.org	nsauth.ipv6	7200	IN	AAAA	2001:db8::53:1
+nsauth.example.org	nsauth	7200	IN	SSHFP	1 2 895804AE022FFF643B2677563CB850607C5BB564D9919896C521098C8ABC40F2
+nsauth.example.org	nsauth	7200	IN	SSHFP	3 2 28A65470BADAE611375747E1A803211C41E3D71E97741FA92CCBDF7B01F34E42
+nsauth.example.org	nsauth	7200	IN	SSHFP	4 2 6E10445C0649C03FA83E18B1873E5B89B3A20893ECB48D01E7CEDB3DD563ECF0
+nsauth.ipv4.example.org	nsauth.ipv4	7200	IN	SSHFP	1 2 895804AE022FFF643B2677563CB850607C5BB564D9919896C521098C8ABC40F2
+nsauth.ipv4.example.org	nsauth.ipv4	7200	IN	SSHFP	3 2 28A65470BADAE611375747E1A803211C41E3D71E97741FA92CCBDF7B01F34E42
+nsauth.ipv4.example.org	nsauth.ipv4	7200	IN	SSHFP	4 2 6E10445C0649C03FA83E18B1873E5B89B3A20893ECB48D01E7CEDB3DD563ECF0
+nsauth.ipv6.example.org	nsauth.ipv6	7200	IN	SSHFP	1 2 895804AE022FFF643B2677563CB850607C5BB564D9919896C521098C8ABC40F2
+nsauth.ipv6.example.org	nsauth.ipv6	7200	IN	SSHFP	3 2 28A65470BADAE611375747E1A803211C41E3D71E97741FA92CCBDF7B01F34E42
+nsauth.ipv6.example.org	nsauth.ipv6	7200	IN	SSHFP	4 2 6E10445C0649C03FA83E18B1873E5B89B3A20893ECB48D01E7CEDB3DD563ECF0
+ns1.example.org	ns1	7200	IN	A	192.0.2.53
+ns1.example.org	ns1	7200	IN	AAAA	2001:db8::53:1
+ns2.example.org	ns2	7200	IN	A	203.0.113.53
+ns2.example.org	ns2	7200	IN	AAAA	2001:db8:113::53
+hermes.example.org	hermes	7200	IN	A	192.0.2.25
+hermes.example.org	hermes	7200	IN	AAAA	2001:db8::48:4558:736d:7470
+hermes.example.org	hermes	7200	IN	AAAA	2001:db8::48:4558:696d:6170
+hermes.ipv4.example.org	hermes.ipv4	7200	IN	A	192.0.2.25
+hermes.ipv6.example.org	hermes.ipv6	7200	IN	AAAA	2001:db8::48:4558:736d:7470
+hermes.ipv6.example.org	hermes.ipv6	7200	IN	AAAA	2001:db8::48:4558:696d:6170
+hermes.example.org	hermes	7200	IN	SSHFP	1 2 4472FF5BD0528CD49216AF4503BA6A1C48F121D0292A31D6AF193E5000AF4966
+hermes.example.org	hermes	7200	IN	SSHFP	3 2 EABA20C1565676A5229184CCFCF82D0EE408F91757A67D9FA51A0B6F3DB4A33B
+hermes.example.org	hermes	7200	IN	SSHFP	4 2 A9D89920E599D04363C8B35A4CE66C1ED257EA1D16981F060B6AED080BBB7A7C
+hermes.ipv4.example.org	hermes.ipv4	7200	IN	SSHFP	1 2 4472FF5BD0528CD49216AF4503BA6A1C48F121D0292A31D6AF193E5000AF4966
+hermes.ipv4.example.org	hermes.ipv4	7200	IN	SSHFP	3 2 EABA20C1565676A5229184CCFCF82D0EE408F91757A67D9FA51A0B6F3DB4A33B
+hermes.ipv4.example.org	hermes.ipv4	7200	IN	SSHFP	4 2 A9D89920E599D04363C8B35A4CE66C1ED257EA1D16981F060B6AED080BBB7A7C
+hermes.ipv6.example.org	hermes.ipv6	7200	IN	SSHFP	1 2 4472FF5BD0528CD49216AF4503BA6A1C48F121D0292A31D6AF193E5000AF4966
+hermes.ipv6.example.org	hermes.ipv6	7200	IN	SSHFP	3 2 EABA20C1565676A5229184CCFCF82D0EE408F91757A67D9FA51A0B6F3DB4A33B
+hermes.ipv6.example.org	hermes.ipv6	7200	IN	SSHFP	4 2 A9D89920E599D04363C8B35A4CE66C1ED257EA1D16981F060B6AED080BBB7A7C
+kerb-service.example.org	kerb-service	7200	IN	A	192.0.2.88
+kerb-service.example.org	kerb-service	7200	IN	AAAA	2001:db8::48:4558:6b65:7262
+security.example.org	security	7200	IN	A	192.0.2.92
+security.example.org	security	7200	IN	AAAA	2001:db8::48:4558:53:4543
+security.ipv4.example.org	security.ipv4	7200	IN	A	192.0.2.92
+security.ipv6.example.org	security.ipv6	7200	IN	AAAA	2001:db8::48:4558:53:4543
+services.example.org	services	7200	IN	A	192.0.2.93
+services.example.org	services	7200	IN	AAAA	2001:db8::48:4558:5345:5256
+services.ipv4.example.org	services.ipv4	7200	IN	A	192.0.2.93
+services.ipv6.example.org	services.ipv6	7200	IN	AAAA	2001:db8::48:4558:5345:5256
+openpgpkey.example.org	openpgpkey	7200	IN	A	192.0.2.92
+openpgpkey.example.org	openpgpkey	7200	IN	AAAA	2001:db8::48:4558:53:4543
+finger.example.org	finger	7200	IN	CNAME	barbican.example.org.
+finger.ipv4.example.org	finger.ipv4	7200	IN	CNAME	barbican.ipv4.example.org.
+finger.ipv6.example.org	finger.ipv6	7200	IN	CNAME	barbican.ipv6.example.org.
+avatars.example.org	avatars	7200	IN	A	192.0.2.93
+avatars.example.org	avatars	7200	IN	AAAA	2001:db8::48:4558:5345:5256
+dict.example.org	dict	7200	IN	CNAME	services.example.org.
+people.example.org	people	7200	IN	CNAME	services.example.org.
+people.ipv4.example.org	people.ipv4	7200	IN	CNAME	services.ipv4.example.org.
+people.ipv6.example.org	people.ipv6	7200	IN	CNAME	services.ipv6.example.org.
+wpad.example.org	wpad	7200	IN	CNAME	services.example.org.
+www.example.org	www	7200	IN	CNAME	services.example.org.
+www.ipv4.example.org	www.ipv4	7200	IN	CNAME	services.ipv4.example.org.
+www.ipv6.example.org	www.ipv6	7200	IN	CNAME	services.ipv6.example.org.
+example.org	@	7200	IN	CAA	0 issue "example.net"
+example.org	@	7200	IN	CAA	0 issue "letsencrypt.org; accounturi=https://acme-v01.api.letsencrypt.org/acme/reg/1234567"
+example.org	@	7200	IN	CAA	0 issue "letsencrypt.org; accounturi=https://acme-staging-v02.api.letsencrypt.org/acme/acct/23456789"
+example.org	@	7200	IN	CAA	0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/76543210"
+example.org	@	7200	IN	CAA	0 issuewild ";"
+example.org	@	7200	IN	CAA	0 iodef "mailto:security@example.org"
+_ourcaca4-tlsa.example.org	_ourcaca4-tlsa	7200	IN	TLSA	2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+_ourcaca5-tlsa.example.org	_ourcaca5-tlsa	7200	IN	TLSA	2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+_cacert-c3-tlsa.example.org	_cacert-c3-tlsa	7200	IN	TLSA	2 0 1 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8
+_letsencrypt-tlsa.example.org	_letsencrypt-tlsa	7200	IN	TLSA	2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+_letsencrypt-tlsa.example.org	_letsencrypt-tlsa	7200	IN	TLSA	2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_amazon-tlsa.example.org	_amazon-tlsa	7200	IN	TLSA	2 0 1 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e
+_amazon-tlsa.example.org	_amazon-tlsa	7200	IN	TLSA	2 0 1 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4
+_amazon-tlsa.example.org	_amazon-tlsa	7200	IN	TLSA	2 0 1 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4
+_amazon-tlsa.example.org	_amazon-tlsa	7200	IN	TLSA	2 0 1 e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092
+_ourca-tlsa.example.org	_ourca-tlsa	7200	IN	TLSA	2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+_ourca-tlsa.example.org	_ourca-tlsa	7200	IN	TLSA	2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+_ourca-cacert-tlsa.example.org	_ourca-cacert-tlsa	7200	IN	TLSA	2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+_ourca-cacert-tlsa.example.org	_ourca-cacert-tlsa	7200	IN	TLSA	2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+_ourca-cacert-tlsa.example.org	_ourca-cacert-tlsa	7200	IN	TLSA	2 0 1 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8
+_ourca-le-tlsa.example.org	_ourca-le-tlsa	7200	IN	TLSA	2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+_ourca-le-tlsa.example.org	_ourca-le-tlsa	7200	IN	TLSA	2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+_ourca-le-tlsa.example.org	_ourca-le-tlsa	7200	IN	TLSA	2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+_ourca-le-tlsa.example.org	_ourca-le-tlsa	7200	IN	TLSA	2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_ourca-cacert-le-tlsa.example.org	_ourca-cacert-le-tlsa	7200	IN	TLSA	2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+_ourca-cacert-le-tlsa.example.org	_ourca-cacert-le-tlsa	7200	IN	TLSA	2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+_ourca-cacert-le-tlsa.example.org	_ourca-cacert-le-tlsa	7200	IN	TLSA	2 0 1 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8
+_ourca-cacert-le-tlsa.example.org	_ourca-cacert-le-tlsa	7200	IN	TLSA	2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+_ourca-cacert-le-tlsa.example.org	_ourca-cacert-le-tlsa	7200	IN	TLSA	2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_cacert-le-tlsa.example.org	_cacert-le-tlsa	7200	IN	TLSA	2 0 1 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8
+_cacert-le-tlsa.example.org	_cacert-le-tlsa	7200	IN	TLSA	2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+_cacert-le-tlsa.example.org	_cacert-le-tlsa	7200	IN	TLSA	2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_le-amazon-tlsa.example.org	_le-amazon-tlsa	7200	IN	TLSA	2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+_le-amazon-tlsa.example.org	_le-amazon-tlsa	7200	IN	TLSA	2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_le-amazon-tlsa.example.org	_le-amazon-tlsa	7200	IN	TLSA	2 0 1 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e
+_le-amazon-tlsa.example.org	_le-amazon-tlsa	7200	IN	TLSA	2 0 1 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4
+_le-amazon-tlsa.example.org	_le-amazon-tlsa	7200	IN	TLSA	2 0 1 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4
+_le-amazon-tlsa.example.org	_le-amazon-tlsa	7200	IN	TLSA	2 0 1 e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092
+_ourca-le-amazon-tlsa.example.org	_ourca-le-amazon-tlsa	7200	IN	TLSA	2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+_ourca-le-amazon-tlsa.example.org	_ourca-le-amazon-tlsa	7200	IN	TLSA	2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+_ourca-le-amazon-tlsa.example.org	_ourca-le-amazon-tlsa	7200	IN	TLSA	2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+_ourca-le-amazon-tlsa.example.org	_ourca-le-amazon-tlsa	7200	IN	TLSA	2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_ourca-le-amazon-tlsa.example.org	_ourca-le-amazon-tlsa	7200	IN	TLSA	2 0 1 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e
+_ourca-le-amazon-tlsa.example.org	_ourca-le-amazon-tlsa	7200	IN	TLSA	2 0 1 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4
+_ourca-le-amazon-tlsa.example.org	_ourca-le-amazon-tlsa	7200	IN	TLSA	2 0 1 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4
+_ourca-le-amazon-tlsa.example.org	_ourca-le-amazon-tlsa	7200	IN	TLSA	2 0 1 e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092
+_443._tcp.www.example.org	_443._tcp.www	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.www.ipv4.example.org	_443._tcp.www.ipv4	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.www.ipv6.example.org	_443._tcp.www.ipv6	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.people.example.org	_443._tcp.people	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.people.ipv4.example.org	_443._tcp.people.ipv4	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.people.ipv6.example.org	_443._tcp.people.ipv6	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.git.example.org	_443._tcp.git	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.svn.example.org	_443._tcp.svn	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_5222._tcp.xmpp.example.org	_5222._tcp.xmpp	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_5223._tcp.xmpp.example.org	_5223._tcp.xmpp	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_5269._tcp.xmpp-s2s.example.org	_5269._tcp.xmpp-s2s	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_25._tcp.mx.example.org	_25._tcp.mx	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_26._tcp.mx.example.org	_26._tcp.mx	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_27._tcp.mx.example.org	_27._tcp.mx	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_465._tcp.smtp46.example.org	_465._tcp.smtp46	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_587._tcp.smtp46.example.org	_587._tcp.smtp46	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_1465._tcp.smtp46.example.org	_1465._tcp.smtp46	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_1587._tcp.smtp46.example.org	_1587._tcp.smtp46	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_465._tcp.smtp.example.org	_465._tcp.smtp	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_587._tcp.smtp.example.org	_587._tcp.smtp	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_1465._tcp.smtp.example.org	_1465._tcp.smtp	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_1587._tcp.smtp.example.org	_1587._tcp.smtp	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_143._tcp.imap46.example.org	_143._tcp.imap46	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_993._tcp.imap46.example.org	_993._tcp.imap46	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_143._tcp.imap.example.org	_143._tcp.imap	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_993._tcp.imap.example.org	_993._tcp.imap	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_4190._tcp.imap.example.org	_4190._tcp.imap	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+www.security.example.org	www.security	7200	IN	CNAME	security.example.org.
+www.security.ipv4.example.org	www.security.ipv4	7200	IN	CNAME	security.ipv4.example.org.
+www.security.ipv6.example.org	www.security.ipv6	7200	IN	CNAME	security.ipv6.example.org.
+_443._tcp.www.security.example.org	_443._tcp.www.security	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.www.security.ipv4.example.org	_443._tcp.www.security.ipv4	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.www.security.ipv6.example.org	_443._tcp.www.security.ipv6	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.security.example.org	_443._tcp.security	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.security.ipv4.example.org	_443._tcp.security.ipv4	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_443._tcp.security.ipv6.example.org	_443._tcp.security.ipv6	7200	IN	CNAME	_ourca-le-tlsa.example.org.
+_acme-challenge.example.org	_acme-challenge	15	IN	CNAME	_acme-challenge.chat-acme.d.example.net.
+_acme-challenge.xmpp.example.org	_acme-challenge.xmpp	15	IN	CNAME	_acme-challenge.xmpp.chat-acme.d.example.net.
+_acme-challenge.chat.example.org	_acme-challenge.chat	15	IN	CNAME	_acme-challenge.chat.chat-acme.d.example.net.
+_acme-challenge.conference.example.org	_acme-challenge.conference	15	IN	CNAME	_acme-challenge.conference.chat-acme.d.example.net.
+_acme-challenge.proxy-chatfiles.example.org	_acme-challenge.proxy-chatfiles	15	IN	CNAME	_acme-challenge.proxy-chatfiles.chat-acme.d.example.net.
+_acme-challenge.pubsub.xmpp.example.org	_acme-challenge.pubsub.xmpp	15	IN	CNAME	_acme-challenge.pubsub.xmpp.chat-acme.d.example.net.
+imap.example.org	imap	7200	IN	AAAA	2001:db8::48:4558:696d:6170
+imap.example.org	imap	7200	IN	A	192.0.2.25
+smtp.example.org	smtp	7200	IN	AAAA	2001:db8::48:4558:736d:7470
+smtp.example.org	smtp	7200	IN	A	192.0.2.25
+smtp46.example.org	smtp46	7200	IN	A	192.0.2.25
+smtp46.example.org	smtp46	7200	IN	AAAA	2001:db8::48:4558:736d:7470
+imap46.example.org	imap46	7200	IN	A	192.0.2.25
+imap46.example.org	imap46	7200	IN	AAAA	2001:db8::48:4558:696d:6170
+mx.example.org	mx	7200	IN	A	192.0.2.25
+mx.example.org	mx	7200	IN	AAAA	2001:db8::48:4558:736d:7470
+mx.ipv4.example.org	mx.ipv4	7200	IN	A	192.0.2.25
+mx.ipv6.example.org	mx.ipv6	7200	IN	AAAA	2001:db8::48:4558:736d:7470
+mx.example.org	mx	7200	IN	TXT	"v=spf1 a include:_spflarge.example.net -all"
+_mta-sts.example.org	_mta-sts	7200	IN	TXT	"v=STSv1; id=20191231r1;"
+mta-sts.example.org	mta-sts	7200	IN	TXT	"v=STSv1; id=20191231r1;"
+mta-sts.example.org	mta-sts	7200	IN	A	192.0.2.93
+mta-sts.example.org	mta-sts	7200	IN	AAAA	2001:db8::48:4558:5345:5256
+xmpp.ipv6.example.org	xmpp.ipv6	7200	IN	AAAA	2001:db8::f0ab:cdef:1234:f00f
+xmpp-s2s.ipv6.example.org	xmpp-s2s.ipv6	7200	IN	AAAA	2001:db8::f0ab:cdef:1234:f00f
+xmpp.example.org	xmpp	7200	IN	A	203.0.113.175
+xmpp.example.org	xmpp	7200	IN	AAAA	2001:db8::f0ab:cdef:1234:f00f
+xmpp-s2s.example.org	xmpp-s2s	7200	IN	A	203.0.113.175
+xmpp-s2s.example.org	xmpp-s2s	7200	IN	AAAA	2001:db8::f0ab:cdef:1234:f00f
+proxy-chatfiles.example.org	proxy-chatfiles	7200	IN	CNAME	xmpp.example.org.
+fileproxy.xmpp.example.org	fileproxy.xmpp	7200	IN	CNAME	xmpp.example.org.
+conference.example.org	conference	7200	IN	CNAME	xmpp-s2s.example.org.
+_xmpp-server._tcp.conference.example.org	_xmpp-server._tcp.conference	7200	IN	SRV	10 2 5269 xmpp-s2s.example.org.
+pubsub.xmpp.example.org	pubsub.xmpp	7200	IN	CNAME	xmpp-s2s.example.org.
+chat.example.org	chat	7200	IN	A	203.0.113.175
+chat.example.org	chat	7200	IN	AAAA	2001:db8::f0ab:cdef:1234:f00f
+proxy-chatfiles.chat.example.org	proxy-chatfiles.chat	7200	IN	CNAME	chat.example.org.
+fileproxy.chat.example.org	fileproxy.chat	7200	IN	CNAME	chat.example.org.
+conference.chat.example.org	conference.chat	7200	IN	CNAME	chat.example.org.
+pubsub.chat.example.org	pubsub.chat	7200	IN	CNAME	chat.example.org.
+_xmpp-server._tcp.conference.example.org	_xmpp-server._tcp.conference	7200	IN	SRV	10 2 5269 chat.example.org.
+auth.example.org	auth	7200	IN	AAAA	2001:db8::48:4558:6175:7468
+kpeople.example.org	kpeople	7200	IN	AAAA	2001:db8::48:4558:6b70:706c
+ocsp.security.example.org	ocsp.security	7200	IN	AAAA	2001:db8::48:4558:6f63:7370
+webauth.example.org	webauth	7200	IN	AAAA	2001:db8::48:4558:7765:6261
+news-feed.example.org	news-feed	7200	IN	A	192.0.2.93
+news-feed.example.org	news-feed	7200	IN	AAAA	2001:db8::48:4558:6e6e:7470
+go.example.org	go	7200	IN	CNAME	abcdefghijklmn.cloudfront.net.
+foo.example.org	foo	7200	IN	A	192.0.2.200
+gladys.example.org	gladys	7200	IN	MX	10 mx.example.org.
+_adsp._domainkey.gladys.example.org	_adsp._domainkey.gladys	7200	IN	TXT	"dkim=all"
+_dmarc.gladys.example.org	_dmarc.gladys	7200	IN	TXT	"v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+_report.gladys.example.org	_report.gladys	7200	IN	TXT	"r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp._tls.gladys.example.org	_smtp._tls.gladys	7200	IN	TXT	"v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp-tlsrpt.gladys.example.org	_smtp-tlsrpt.gladys	7200	IN	TXT	"v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+fred.example.org	fred	7200	IN	MX	10 mx.example.org.
+fred.example.org	fred	7200	IN	A	192.0.2.93
+fred.example.org	fred	7200	IN	AAAA	2001:db8::48:4558:5345:5256
+fred.example.org	fred	7200	IN	TXT	"v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"
+d201911._domainkey.fred.example.org	d201911._domainkey.fred	7200	IN	TXT	"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/Tlz" "P2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB"
+d201911e2._domainkey.fred.example.org	d201911e2._domainkey.fred	7200	IN	TXT	"v=DKIM1; k=ed25519; p=rQNsV9YcPJn/WYI1EDLjNbN/VuX1Hqq/oe4htbnhv+A="
+d202003._domainkey.fred.example.org	d202003._domainkey.fred	7200	IN	TXT	"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYj" "c0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB"
+d202003e2._domainkey.fred.example.org	d202003e2._domainkey.fred	7200	IN	TXT	"v=DKIM1; k=ed25519; p=0DAPp/IRLYFI/Z4YSgJRi4gr7xcu1/EfJ5mjVn10aAw="
+_adsp._domainkey.fred.example.org	_adsp._domainkey.fred	7200	IN	TXT	"dkim=all"
+_dmarc.fred.example.org	_dmarc.fred	7200	IN	TXT	"v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+_report.fred.example.org	_report.fred	7200	IN	TXT	"r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp._tls.fred.example.org	_smtp._tls.fred	7200	IN	TXT	"v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp-tlsrpt.fred.example.org	_smtp-tlsrpt.fred	7200	IN	TXT	"v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+mailtest.example.org	mailtest	7200	IN	MX	10 mx.example.org.
+d201911._domainkey.mailtest.example.org	d201911._domainkey.mailtest	7200	IN	TXT	"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn" "0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB"
+d201911e2._domainkey.mailtest.example.org	d201911e2._domainkey.mailtest	7200	IN	TXT	"v=DKIM1; k=ed25519; p=afulDDnhaTzdqKQN0jtWV04eOhAcyBk3NCyVheOf53Y="
+d202003._domainkey.mailtest.example.org	d202003._domainkey.mailtest	7200	IN	TXT	"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KN" "aS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB"
+d202003e2._domainkey.mailtest.example.org	d202003e2._domainkey.mailtest	7200	IN	TXT	"v=DKIM1; k=ed25519; p=iqwH/hhozFdeo1xnuldr8KUi7O7g+DzmC+f0SYMKVDc="
+_adsp._domainkey.mailtest.example.org	_adsp._domainkey.mailtest	7200	IN	TXT	"dkim=all"
+_dmarc.mailtest.example.org	_dmarc.mailtest	7200	IN	TXT	"v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+_report.mailtest.example.org	_report.mailtest	7200	IN	TXT	"r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp._tls.mailtest.example.org	_smtp._tls.mailtest	7200	IN	TXT	"v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp-tlsrpt.mailtest.example.org	_smtp-tlsrpt.mailtest	7200	IN	TXT	"v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_pgpkey-http._tcp.sks.example.org	_pgpkey-http._tcp.sks	7200	IN	SRV	0 0 0 .
+_pgpkey-https._tcp.sks.example.org	_pgpkey-https._tcp.sks	7200	IN	SRV	0 0 0 .
+_hkp._tcp.sks.example.org	_hkp._tcp.sks	7200	IN	SRV	0 0 0 .
+_pgpkey-http._tcp.sks-peer.example.org	_pgpkey-http._tcp.sks-peer	7200	IN	SRV	0 0 0 .
+_pgpkey-https._tcp.sks-peer.example.org	_pgpkey-https._tcp.sks-peer	7200	IN	SRV	0 0 0 .
+_hkp._tcp.sks-peer.example.org	_hkp._tcp.sks-peer	7200	IN	SRV	0 0 0 .
+yoyo.example.org	yoyo	7200	IN	NS	ns5.he.net.
+yoyo.example.org	yoyo	7200	IN	NS	ns4.he.net.
+yoyo.example.org	yoyo	7200	IN	NS	ns3.he.net.
+yoyo.example.org	yoyo	7200	IN	NS	ns2.he.net.
+yoyo.example.org	yoyo	7200	IN	NS	ns1.he.net.
+khard.example.org	khard	7200	IN	NS	ns-cloud-d1.googledomains.com.
+khard.example.org	khard	7200	IN	NS	ns-cloud-d2.googledomains.com.
+khard.example.org	khard	7200	IN	NS	ns-cloud-d3.googledomains.com.
+khard.example.org	khard	7200	IN	NS	ns-cloud-d4.googledomains.com.
+realhost.example.org	realhost	7200	IN	MX	0 .
+realhost.example.org	realhost	7200	IN	TXT	"v=spf1 -all"
+_25._tcp.realhost.example.org	_25._tcp.realhost	7200	IN	TLSA	3 0 0 0000000000000000000000000000000000000000000000000000000000000000
+_fedcba9876543210fedcba9876543210.go.example.org	_fedcba9876543210fedcba9876543210.go	7200	IN	CNAME	_45678901234abcdef45678901234abcd.ggedgsdned.acm-validations.aws.
+opqrstuvwxyz.example.org	opqrstuvwxyz	7200	IN	CNAME	gv-abcdefghijklmn.dv.googlehosted.com.
+zyxwvutsrqpo.example.org	zyxwvutsrqpo	7200	IN	CNAME	gv-nmlkjihgfedcba.dv.googlehosted.com.
+0123456789abcdef0123456789abcdef.example.org	0123456789abcdef0123456789abcdef	7200	IN	CNAME	verify.bing.com.
diff --git a/commands/test_data/example.org.zone.zone b/commands/test_data/example.org.zone.zone
new file mode 100644
index 00000000..0c484811
--- /dev/null
+++ b/commands/test_data/example.org.zone.zone
@@ -0,0 +1,352 @@
+$ORIGIN example.org.
+$TTL 7200
+@          43200 IN SOA   ns1.example.org. hostmaster.example.org. 2020030700 7200 3600 864000 7200
+                 IN NS    friend-dns.example.com.
+                 IN NS    ns-a.example.net.
+                 IN NS    ns1.example.org.
+                 IN NS    ns2.example.org.
+                 IN A     192.0.2.1
+                 IN AAAA  2001:db8::1:1
+                 IN MX    10 mx.example.org.
+                 IN TXT   "v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"
+                 IN CAA   0 iodef "mailto:security@example.org"
+                 IN CAA   0 issue "example.net"
+                 IN CAA   0 issue "letsencrypt.org; accounturi=https://acme-staging-v02.api.letsencrypt.org/acme/acct/23456789"
+                 IN CAA   0 issue "letsencrypt.org; accounturi=https://acme-v01.api.letsencrypt.org/acme/reg/1234567"
+                 IN CAA   0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/76543210"
+                 IN CAA   0 issuewild ";"
+0123456789abcdef0123456789abcdef IN CNAME verify.bing.com.
+_acme-challenge 15 IN CNAME _acme-challenge.chat-acme.d.example.net.
+_amazon-tlsa     IN TLSA  2 0 1 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4
+                 IN TLSA  2 0 1 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4
+                 IN TLSA  2 0 1 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e
+                 IN TLSA  2 0 1 e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092
+_cacert-c3-tlsa  IN TLSA  2 0 1 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8
+_cacert-le-tlsa  IN TLSA  2 0 1 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8
+                 IN TLSA  2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+                 IN TLSA  2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_dmarc           IN TXT   "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+example.com._report._dmarc IN TXT "v=DMARC1"
+example.net._report._dmarc IN TXT "v=DMARC1"
+special.test._report._dmarc IN TXT "v=DMARC1"
+xn--2j5b.xn--9t4b11yi5a._report._dmarc IN TXT "v=DMARC1"
+xn--qck5b9a5eml3bze.xn--zckzah._report._dmarc IN TXT "v=DMARC1"
+_adsp._domainkey IN TXT   "dkim=all"
+d201911._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks" "6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB"
+d201911e2._domainkey IN TXT "v=DKIM1; k=ed25519; p=GBt2k2L39KUb39fg5brOppXDHXvISy0+ECGgPld/bIo="
+d202003._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jo" "pv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB"
+d202003e2._domainkey IN TXT "v=DKIM1; k=ed25519; p=DQI5d9sNMrr0SLDoAi071IFOyKnlbR29hAQdqVQecQg="
+_kerberos        IN TXT   "EXAMPLE.ORG"
+_le-amazon-tlsa  IN TLSA  2 0 1 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4
+                 IN TLSA  2 0 1 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4
+                 IN TLSA  2 0 1 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e
+                 IN TLSA  2 0 1 e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092
+                 IN TLSA  2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+                 IN TLSA  2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_letsencrypt-tlsa IN TLSA 2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+                 IN TLSA  2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_mta-sts         IN TXT   "v=STSv1; id=20191231r1;"
+_ourca-cacert-le-tlsa IN TLSA 2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+                 IN TLSA  2 0 1 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8
+                 IN TLSA  2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+                 IN TLSA  2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+                 IN TLSA  2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_ourca-cacert-tlsa IN TLSA 2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+                 IN TLSA  2 0 1 4edde9e55ca453b388887caa25d5c5c5bccf2891d73b87495808293d5fac83c8
+                 IN TLSA  2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+_ourca-le-amazon-tlsa IN TLSA 2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+                 IN TLSA  2 0 1 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4
+                 IN TLSA  2 0 1 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4
+                 IN TLSA  2 0 1 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e
+                 IN TLSA  2 0 1 e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092
+                 IN TLSA  2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+                 IN TLSA  2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+                 IN TLSA  2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_ourca-le-tlsa   IN TLSA  2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+                 IN TLSA  2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+                 IN TLSA  2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
+                 IN TLSA  2 1 1 b111dd8a1c2091a89bd4fd60c57f0716cce50feeff8137cdbee0326e02cf362b
+_ourca-tlsa      IN TLSA  2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+                 IN TLSA  2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+_ourcaca4-tlsa   IN TLSA  2 0 1 ea99063a0a3bda9727032cf82da238698b90ba729300703d3956943635f96488
+_ourcaca5-tlsa   IN TLSA  2 0 1 11f058f61f97b8adc66ef4801f918c71b10e5c1e3d39afde10408b3026647ef1
+_report          IN TXT   "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_sip+d2s._sctp   IN SRV   0 0 0 .
+_sips+d2s._sctp  IN SRV   0 0 0 .
+_im._sip         IN SRV   0 0 0 .
+_pres._sip       IN SRV   0 0 0 .
+*._smimecert     IN CNAME _ourca-smimea.example.org.
+_client._smtp    IN SRV   1 1 1 example.org.
+_smtp-tlsrpt     IN TXT   "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_avatars-sec._tcp IN SRV  10 10 443 avatars.example.org.
+_finger._tcp     IN SRV   10 10 79 barbican.example.org.
+_hkp._tcp        IN SRV   0 0 0 .
+_imap._tcp       IN SRV   10 10 143 imap.example.org.
+_imaps._tcp      IN SRV   10 10 993 imap.example.org.
+_jabber._tcp     IN SRV   10 2 5269 xmpp-s2s.example.org.
+_kerberos._tcp   IN SRV   10 1 88 kerb-service.example.org.
+_kerberos-adm._tcp IN SRV 10 1 749 kerb-service.example.org.
+_ldap._tcp       IN SRV   0 0 0 .
+_openpgpkey._tcp IN SRV   10 10 443 openpgpkey.example.org.
+_pgpkey-http._tcp IN SRV  0 0 0 .
+_pgpkey-https._tcp IN SRV 0 0 0 .
+_pop3._tcp       IN SRV   0 0 0 .
+_pop3s._tcp      IN SRV   0 0 0 .
+_sieve._tcp      IN SRV   10 10 4190 imap.example.org.
+_sip+d2t._tcp    IN SRV   0 0 0 .
+_sips+d2t._tcp   IN SRV   0 0 0 .
+_submission._tcp IN SRV   10 10 587 smtp.example.org.
+_submissions._tcp IN SRV  10 10 465 smtp.example.org.
+_xmpp-client._tcp IN SRV  10 2 5222 xmpp.example.org.
+_xmpp-server._tcp IN SRV  10 2 5269 xmpp-s2s.example.org.
+_smtp._tls       IN TXT   "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+b._dns-sd._udp   IN PTR   field.example.org.
+lb._dns-sd._udp  IN PTR   field.example.org.
+r._dns-sd._udp   IN PTR   field.example.org.
+_kerberos._udp   IN SRV   10 1 88 kerb-service.example.org.
+_kpasswd._udp    IN SRV   10 1 464 kerb-service.example.org.
+_ldap._udp       IN SRV   0 0 0 .
+_sip+d2u._udp    IN SRV   0 0 0 .
+auth             IN AAAA  2001:db8::48:4558:6175:7468
+avatars          IN A     192.0.2.93
+                 IN AAAA  2001:db8::48:4558:5345:5256
+barbican         IN A     192.0.2.1
+                 IN AAAA  2001:db8::1:1
+chat             IN A     203.0.113.175
+                 IN AAAA  2001:db8::f0ab:cdef:1234:f00f
+_acme-challenge.chat 15 IN CNAME _acme-challenge.chat.chat-acme.d.example.net.
+conference.chat  IN CNAME chat.example.org.
+fileproxy.chat   IN CNAME chat.example.org.
+proxy-chatfiles.chat IN CNAME chat.example.org.
+pubsub.chat      IN CNAME chat.example.org.
+conference       IN CNAME xmpp-s2s.example.org.
+_acme-challenge.conference 15 IN CNAME _acme-challenge.conference.chat-acme.d.example.net.
+_xmpp-server._tcp.conference IN SRV 10 2 5269 chat.example.org.
+                 IN SRV   10 2 5269 xmpp-s2s.example.org.
+dict             IN CNAME services.example.org.
+dns-moreinfo     IN TXT   "Fred Bloggs, TZ=America/New_York" "Chat-Service-X: @handle1" "Chat-Service-Y: federated-handle@example.org"
+field            IN NS    ns1.example.org.
+                 IN NS    ns2.example.org.
+finger           IN CNAME barbican.example.org.
+foo              IN A     192.0.2.200
+_client._smtp.foo IN SRV  1 2 1 foo.example.org.
+fred             IN A     192.0.2.93
+                 IN AAAA  2001:db8::48:4558:5345:5256
+                 IN MX    10 mx.example.org.
+                 IN TXT   "v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"
+_dmarc.fred      IN TXT   "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+_adsp._domainkey.fred IN TXT "dkim=all"
+d201911._domainkey.fred IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/Tlz" "P2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB"
+d201911e2._domainkey.fred IN TXT "v=DKIM1; k=ed25519; p=rQNsV9YcPJn/WYI1EDLjNbN/VuX1Hqq/oe4htbnhv+A="
+d202003._domainkey.fred IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYj" "c0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB"
+d202003e2._domainkey.fred IN TXT "v=DKIM1; k=ed25519; p=0DAPp/IRLYFI/Z4YSgJRi4gr7xcu1/EfJ5mjVn10aAw="
+_report.fred     IN TXT   "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp-tlsrpt.fred IN TXT  "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp._tls.fred  IN TXT   "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+git              IN CNAME vcs.example.org.
+_443._tcp.git    IN CNAME _ourca-le-tlsa.example.org.
+gladys           IN MX    10 mx.example.org.
+_dmarc.gladys    IN TXT   "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+_adsp._domainkey.gladys IN TXT "dkim=all"
+_report.gladys   IN TXT   "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp-tlsrpt.gladys IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp._tls.gladys IN TXT  "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+go               IN CNAME abcdefghijklmn.cloudfront.net.
+_fedcba9876543210fedcba9876543210.go IN CNAME _45678901234abcdef45678901234abcd.ggedgsdned.acm-validations.aws.
+hermes           IN A     192.0.2.25
+                 IN AAAA  2001:db8::48:4558:696d:6170
+                 IN AAAA  2001:db8::48:4558:736d:7470
+                 IN SSHFP 1 2 4472FF5BD0528CD49216AF4503BA6A1C48F121D0292A31D6AF193E5000AF4966
+                 IN SSHFP 3 2 EABA20C1565676A5229184CCFCF82D0EE408F91757A67D9FA51A0B6F3DB4A33B
+                 IN SSHFP 4 2 A9D89920E599D04363C8B35A4CE66C1ED257EA1D16981F060B6AED080BBB7A7C
+imap             IN A     192.0.2.25
+                 IN AAAA  2001:db8::48:4558:696d:6170
+_143._tcp.imap   IN CNAME _ourca-le-tlsa.example.org.
+_4190._tcp.imap  IN CNAME _ourca-le-tlsa.example.org.
+_993._tcp.imap   IN CNAME _ourca-le-tlsa.example.org.
+imap46           IN A     192.0.2.25
+                 IN AAAA  2001:db8::48:4558:696d:6170
+_143._tcp.imap46 IN CNAME _ourca-le-tlsa.example.org.
+_993._tcp.imap46 IN CNAME _ourca-le-tlsa.example.org.
+barbican.ipv4    IN A     192.0.2.1
+finger.ipv4      IN CNAME barbican.ipv4.example.org.
+git.ipv4         IN CNAME vcs.ipv4.example.org.
+hermes.ipv4      IN A     192.0.2.25
+                 IN SSHFP 1 2 4472FF5BD0528CD49216AF4503BA6A1C48F121D0292A31D6AF193E5000AF4966
+                 IN SSHFP 3 2 EABA20C1565676A5229184CCFCF82D0EE408F91757A67D9FA51A0B6F3DB4A33B
+                 IN SSHFP 4 2 A9D89920E599D04363C8B35A4CE66C1ED257EA1D16981F060B6AED080BBB7A7C
+megalomaniac.ipv4 IN A    198.51.100.254
+                 IN SSHFP 1 2 4E9CED94D3CAF2CE915F85A63CE7279D5118A79EA03DAC59CF4859B825D2F619
+                 IN SSHFP 3 2 D3556A3DB83AB9CCEC39DC6693DD2F3E28B178C9BBA61880924821C426CC61EB
+                 IN SSHFP 4 2 C60C9D9D4728668F5F46986FF0C5B416C5E913862C4970CBFE211A6F44A111B4
+mx.ipv4          IN A     192.0.2.25
+nsauth.ipv4      IN A     192.0.2.53
+                 IN SSHFP 1 2 895804AE022FFF643B2677563CB850607C5BB564D9919896C521098C8ABC40F2
+                 IN SSHFP 3 2 28A65470BADAE611375747E1A803211C41E3D71E97741FA92CCBDF7B01F34E42
+                 IN SSHFP 4 2 6E10445C0649C03FA83E18B1873E5B89B3A20893ECB48D01E7CEDB3DD563ECF0
+people.ipv4      IN CNAME services.ipv4.example.org.
+_443._tcp.people.ipv4 IN CNAME _ourca-le-tlsa.example.org.
+security.ipv4    IN A     192.0.2.92
+_443._tcp.security.ipv4 IN CNAME _ourca-le-tlsa.example.org.
+www.security.ipv4 IN CNAME security.ipv4.example.org.
+_443._tcp.www.security.ipv4 IN CNAME _ourca-le-tlsa.example.org.
+services.ipv4    IN A     192.0.2.93
+tower.ipv4       IN A     192.0.2.42
+                 IN SSHFP 1 2 0F211D236E94768911A294F38653C4AF6FA935A5B06C975D8162F59142571451
+                 IN SSHFP 3 2 88BF7B7401C11FA2E84871EFB06CD73D8FC409154605B354DB2DDA0B82FE1160
+                 IN SSHFP 4 2 6D30900BE0FAAAE73568FC007A87B4D076CF9A351ECACC1106AEF726C34AD61D
+vcs.ipv4         IN A     192.0.2.228
+                 IN SSHFP 1 2 B518BE390BABDF43CB2D598AA6BEFA6CE6878546BF107B829D0CFC65253A97D4
+                 IN SSHFP 3 2 E92545DC0BF501F72333DDEB7A37AFC2C5B408CE39A3AD95FBC66236F0077323
+                 IN SSHFP 4 2 02289441124A487095A6CDA2E946C6A8ED9087FAF3592EC4135536C3E615521C
+www.ipv4         IN CNAME services.ipv4.example.org.
+_443._tcp.www.ipv4 IN CNAME _ourca-le-tlsa.example.org.
+barbican.ipv6    IN AAAA  2001:db8::1:1
+finger.ipv6      IN CNAME barbican.ipv6.example.org.
+git.ipv6         IN CNAME vcs.ipv6.example.org.
+hermes.ipv6      IN AAAA  2001:db8::48:4558:696d:6170
+                 IN AAAA  2001:db8::48:4558:736d:7470
+                 IN SSHFP 1 2 4472FF5BD0528CD49216AF4503BA6A1C48F121D0292A31D6AF193E5000AF4966
+                 IN SSHFP 3 2 EABA20C1565676A5229184CCFCF82D0EE408F91757A67D9FA51A0B6F3DB4A33B
+                 IN SSHFP 4 2 A9D89920E599D04363C8B35A4CE66C1ED257EA1D16981F060B6AED080BBB7A7C
+megalomaniac.ipv6 IN AAAA 2001:db8:ffef::254
+                 IN SSHFP 1 2 4E9CED94D3CAF2CE915F85A63CE7279D5118A79EA03DAC59CF4859B825D2F619
+                 IN SSHFP 3 2 D3556A3DB83AB9CCEC39DC6693DD2F3E28B178C9BBA61880924821C426CC61EB
+                 IN SSHFP 4 2 C60C9D9D4728668F5F46986FF0C5B416C5E913862C4970CBFE211A6F44A111B4
+mx.ipv6          IN AAAA  2001:db8::48:4558:736d:7470
+nsauth.ipv6      IN AAAA  2001:db8::53:1
+                 IN SSHFP 1 2 895804AE022FFF643B2677563CB850607C5BB564D9919896C521098C8ABC40F2
+                 IN SSHFP 3 2 28A65470BADAE611375747E1A803211C41E3D71E97741FA92CCBDF7B01F34E42
+                 IN SSHFP 4 2 6E10445C0649C03FA83E18B1873E5B89B3A20893ECB48D01E7CEDB3DD563ECF0
+people.ipv6      IN CNAME services.ipv6.example.org.
+_443._tcp.people.ipv6 IN CNAME _ourca-le-tlsa.example.org.
+security.ipv6    IN AAAA  2001:db8::48:4558:53:4543
+_443._tcp.security.ipv6 IN CNAME _ourca-le-tlsa.example.org.
+www.security.ipv6 IN CNAME security.ipv6.example.org.
+_443._tcp.www.security.ipv6 IN CNAME _ourca-le-tlsa.example.org.
+services.ipv6    IN AAAA  2001:db8::48:4558:5345:5256
+tower.ipv6       IN AAAA  2001:db8::1:42
+                 IN SSHFP 1 2 0F211D236E94768911A294F38653C4AF6FA935A5B06C975D8162F59142571451
+                 IN SSHFP 3 2 88BF7B7401C11FA2E84871EFB06CD73D8FC409154605B354DB2DDA0B82FE1160
+                 IN SSHFP 4 2 6D30900BE0FAAAE73568FC007A87B4D076CF9A351ECACC1106AEF726C34AD61D
+vcs.ipv6         IN AAAA  2001:db8::48:4558:4456:4353
+                 IN SSHFP 1 2 B518BE390BABDF43CB2D598AA6BEFA6CE6878546BF107B829D0CFC65253A97D4
+                 IN SSHFP 3 2 E92545DC0BF501F72333DDEB7A37AFC2C5B408CE39A3AD95FBC66236F0077323
+                 IN SSHFP 4 2 02289441124A487095A6CDA2E946C6A8ED9087FAF3592EC4135536C3E615521C
+www.ipv6         IN CNAME services.ipv6.example.org.
+_443._tcp.www.ipv6 IN CNAME _ourca-le-tlsa.example.org.
+xmpp.ipv6        IN AAAA  2001:db8::f0ab:cdef:1234:f00f
+xmpp-s2s.ipv6    IN AAAA  2001:db8::f0ab:cdef:1234:f00f
+kerb-service     IN A     192.0.2.88
+                 IN AAAA  2001:db8::48:4558:6b65:7262
+khard            IN NS    ns-cloud-d1.googledomains.com.
+                 IN NS    ns-cloud-d2.googledomains.com.
+                 IN NS    ns-cloud-d3.googledomains.com.
+                 IN NS    ns-cloud-d4.googledomains.com.
+kpeople          IN AAAA  2001:db8::48:4558:6b70:706c
+mailtest         IN MX    10 mx.example.org.
+_dmarc.mailtest  IN TXT   "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
+_adsp._domainkey.mailtest IN TXT "dkim=all"
+d201911._domainkey.mailtest IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn" "0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB"
+d201911e2._domainkey.mailtest IN TXT "v=DKIM1; k=ed25519; p=afulDDnhaTzdqKQN0jtWV04eOhAcyBk3NCyVheOf53Y="
+d202003._domainkey.mailtest IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KN" "aS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB"
+d202003e2._domainkey.mailtest IN TXT "v=DKIM1; k=ed25519; p=iqwH/hhozFdeo1xnuldr8KUi7O7g+DzmC+f0SYMKVDc="
+_report.mailtest IN TXT   "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
+_smtp-tlsrpt.mailtest IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_smtp._tls.mailtest IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+megalomaniac     IN A     198.51.100.254
+                 IN AAAA  2001:db8:ffef::254
+                 IN SSHFP 1 2 4E9CED94D3CAF2CE915F85A63CE7279D5118A79EA03DAC59CF4859B825D2F619
+                 IN SSHFP 3 2 D3556A3DB83AB9CCEC39DC6693DD2F3E28B178C9BBA61880924821C426CC61EB
+                 IN SSHFP 4 2 C60C9D9D4728668F5F46986FF0C5B416C5E913862C4970CBFE211A6F44A111B4
+mta-sts          IN A     192.0.2.93
+                 IN AAAA  2001:db8::48:4558:5345:5256
+                 IN TXT   "v=STSv1; id=20191231r1;"
+mx               IN A     192.0.2.25
+                 IN AAAA  2001:db8::48:4558:736d:7470
+                 IN TXT   "v=spf1 a include:_spflarge.example.net -all"
+_client._smtp.mx IN SRV   1 2 1 mx.example.org.
+_25._tcp.mx      IN CNAME _ourca-le-tlsa.example.org.
+_26._tcp.mx      IN CNAME _ourca-le-tlsa.example.org.
+_27._tcp.mx      IN CNAME _ourca-le-tlsa.example.org.
+news-feed        IN A     192.0.2.93
+                 IN AAAA  2001:db8::48:4558:6e6e:7470
+ns1              IN A     192.0.2.53
+                 IN AAAA  2001:db8::53:1
+ns2              IN A     203.0.113.53
+                 IN AAAA  2001:db8:113::53
+nsauth           IN A     192.0.2.53
+                 IN AAAA  2001:db8::53:1
+                 IN SSHFP 1 2 895804AE022FFF643B2677563CB850607C5BB564D9919896C521098C8ABC40F2
+                 IN SSHFP 3 2 28A65470BADAE611375747E1A803211C41E3D71E97741FA92CCBDF7B01F34E42
+                 IN SSHFP 4 2 6E10445C0649C03FA83E18B1873E5B89B3A20893ECB48D01E7CEDB3DD563ECF0
+openpgpkey       IN A     192.0.2.92
+                 IN AAAA  2001:db8::48:4558:53:4543
+opqrstuvwxyz     IN CNAME gv-abcdefghijklmn.dv.googlehosted.com.
+people           IN CNAME services.example.org.
+_443._tcp.people IN CNAME _ourca-le-tlsa.example.org.
+proxy-chatfiles  IN CNAME xmpp.example.org.
+_acme-challenge.proxy-chatfiles 15 IN CNAME _acme-challenge.proxy-chatfiles.chat-acme.d.example.net.
+realhost         IN MX    0 .
+                 IN TXT   "v=spf1 -all"
+_25._tcp.realhost IN TLSA 3 0 0 0000000000000000000000000000000000000000000000000000000000000000
+security         IN A     192.0.2.92
+                 IN AAAA  2001:db8::48:4558:53:4543
+_443._tcp.security IN CNAME _ourca-le-tlsa.example.org.
+ocsp.security    IN AAAA  2001:db8::48:4558:6f63:7370
+www.security     IN CNAME security.example.org.
+_443._tcp.www.security IN CNAME _ourca-le-tlsa.example.org.
+services         IN A     192.0.2.93
+                 IN AAAA  2001:db8::48:4558:5345:5256
+_hkp._tcp.sks    IN SRV   0 0 0 .
+_pgpkey-http._tcp.sks IN SRV 0 0 0 .
+_pgpkey-https._tcp.sks IN SRV 0 0 0 .
+_hkp._tcp.sks-peer IN SRV 0 0 0 .
+_pgpkey-http._tcp.sks-peer IN SRV 0 0 0 .
+_pgpkey-https._tcp.sks-peer IN SRV 0 0 0 .
+smtp             IN A     192.0.2.25
+                 IN AAAA  2001:db8::48:4558:736d:7470
+_1465._tcp.smtp  IN CNAME _ourca-le-tlsa.example.org.
+_1587._tcp.smtp  IN CNAME _ourca-le-tlsa.example.org.
+_465._tcp.smtp   IN CNAME _ourca-le-tlsa.example.org.
+_587._tcp.smtp   IN CNAME _ourca-le-tlsa.example.org.
+smtp46           IN A     192.0.2.25
+                 IN AAAA  2001:db8::48:4558:736d:7470
+_1465._tcp.smtp46 IN CNAME _ourca-le-tlsa.example.org.
+_1587._tcp.smtp46 IN CNAME _ourca-le-tlsa.example.org.
+_465._tcp.smtp46 IN CNAME _ourca-le-tlsa.example.org.
+_587._tcp.smtp46 IN CNAME _ourca-le-tlsa.example.org.
+svn              IN AAAA  2001:db8::48:4558:73:766e
+_443._tcp.svn    IN CNAME _ourca-le-tlsa.example.org.
+tower            IN A     192.0.2.42
+                 IN AAAA  2001:db8::1:42
+                 IN SSHFP 1 2 0F211D236E94768911A294F38653C4AF6FA935A5B06C975D8162F59142571451
+                 IN SSHFP 3 2 88BF7B7401C11FA2E84871EFB06CD73D8FC409154605B354DB2DDA0B82FE1160
+                 IN SSHFP 4 2 6D30900BE0FAAAE73568FC007A87B4D076CF9A351ECACC1106AEF726C34AD61D
+vcs              IN A     192.0.2.228
+                 IN AAAA  2001:db8::48:4558:4456:4353
+                 IN SSHFP 1 2 B518BE390BABDF43CB2D598AA6BEFA6CE6878546BF107B829D0CFC65253A97D4
+                 IN SSHFP 3 2 E92545DC0BF501F72333DDEB7A37AFC2C5B408CE39A3AD95FBC66236F0077323
+                 IN SSHFP 4 2 02289441124A487095A6CDA2E946C6A8ED9087FAF3592EC4135536C3E615521C
+webauth          IN AAAA  2001:db8::48:4558:7765:6261
+wpad             IN CNAME services.example.org.
+www              IN CNAME services.example.org.
+_443._tcp.www    IN CNAME _ourca-le-tlsa.example.org.
+xmpp             IN A     203.0.113.175
+                 IN AAAA  2001:db8::f0ab:cdef:1234:f00f
+_acme-challenge.xmpp 15 IN CNAME _acme-challenge.xmpp.chat-acme.d.example.net.
+_5222._tcp.xmpp  IN CNAME _ourca-le-tlsa.example.org.
+_5223._tcp.xmpp  IN CNAME _ourca-le-tlsa.example.org.
+fileproxy.xmpp   IN CNAME xmpp.example.org.
+pubsub.xmpp      IN CNAME xmpp-s2s.example.org.
+_acme-challenge.pubsub.xmpp 15 IN CNAME _acme-challenge.pubsub.xmpp.chat-acme.d.example.net.
+xmpp-s2s         IN A     203.0.113.175
+                 IN AAAA  2001:db8::f0ab:cdef:1234:f00f
+_5269._tcp.xmpp-s2s IN CNAME _ourca-le-tlsa.example.org.
+yoyo             IN NS    ns1.he.net.
+                 IN NS    ns2.he.net.
+                 IN NS    ns3.he.net.
+                 IN NS    ns4.he.net.
+                 IN NS    ns5.he.net.
+zyxwvutsrqpo     IN CNAME gv-nmlkjihgfedcba.dv.googlehosted.com.
+
diff --git a/commands/test_data/simple.com.zone b/commands/test_data/simple.com.zone
new file mode 100644
index 00000000..25831109
--- /dev/null
+++ b/commands/test_data/simple.com.zone
@@ -0,0 +1,21 @@
+$ORIGIN simple.com.
+$TTL 300
+@                IN SOA   ns3.serverfault.com. sysadmin.stackoverflow.com. 2020022300 3600 600 604800 1440
+           172800 IN NS   ns-1313.awsdns-36.org.
+           172800 IN NS   ns-736.awsdns-28.net.
+           172800 IN NS   ns-cloud-c1.googledomains.com.
+           172800 IN NS   ns-cloud-c2.googledomains.com.
+                 IN MX    1 aspmx.l.google.com.
+                 IN MX    5 alt1.aspmx.l.google.com.
+                 IN MX    5 alt2.aspmx.l.google.com.
+                 IN MX    10 alt3.aspmx.l.google.com.
+                 IN MX    10 alt4.aspmx.l.google.com.
+                 IN TXT   "google-site-verification=O54a_pYHGr4EB8iLoGFgX8OTZ1DkP1KWnOLpx0YCazI"
+                 IN TXT   "v=spf1 mx include:mktomail.com ~all"
+m1._domainkey    IN TXT   "v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZfEV2C82eJ4OA3Mslz4C6msjYYalg1eUcHeJQ//QM1hOZSvn4qz+hSKGi7jwNDqsZNzM8vCt2+XzdDYL3JddwUEhoDsIsZsJW0qzIVVLLWCg6TLNS3FpVyjc171o94dpoHFekfswWDoEwFQ03Woq2jchYWBrbUf7MMcdEj/EQqwIDAQAB"
+dev              IN CNAME stackoverflowsandbox2.mktoweb.com.
+dev-email        IN CNAME mkto-sj310056.com.
+m1._domainkey.dev-email IN TXT "v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIBezZ2Gc+/3PghWk+YOE6T9HdwgUTMTR0Fne2i51MNN9Qs7AqDitVdG/949iDbI2fPNZSnKtOcnlLYwvve9MhMAMI1nZ26ILhgaBJi2BMZQpGFlO4ucuo/Uj4DPZ5Ge/NZHCX0CRhAhR5sRmL2OffNcFXFrymzUuz4KzI/NyUiwIDAQAB"
+email            IN CNAME mkto-sj280138.com.
+info             IN CNAME stackoverflow.mktoweb.com.
+_sip._tcp   IN    SRV 10       60     5060 bigbox.example.com.
diff --git a/commands/test_data/simple.com.zone.js b/commands/test_data/simple.com.zone.js
new file mode 100644
index 00000000..ce6452b1
--- /dev/null
+++ b/commands/test_data/simple.com.zone.js
@@ -0,0 +1,24 @@
+var bind = NewDnsProvider("bind", "BIND");
+var REG_CHANGEME = NewRegistrar("ThirdParty", "NONE");
+D("simple.com", REG_CHANGEME,
+	DnsProvider(bind),
+	//SOA('@', 'ns3.serverfault.com.', 'sysadmin.stackoverflow.com.', 2020022300, 3600, 600, 604800, 1440),
+	NAMESERVER('ns-1313.awsdns-36.org.'),
+	NAMESERVER('ns-736.awsdns-28.net.'),
+	NAMESERVER('ns-cloud-c1.googledomains.com.'),
+	NAMESERVER('ns-cloud-c2.googledomains.com.'),
+	MX('@', 1, 'aspmx.l.google.com.'),
+	MX('@', 5, 'alt1.aspmx.l.google.com.'),
+	MX('@', 5, 'alt2.aspmx.l.google.com.'),
+	MX('@', 10, 'alt3.aspmx.l.google.com.'),
+	MX('@', 10, 'alt4.aspmx.l.google.com.'),
+	TXT('@', 'google-site-verification=O54a_pYHGr4EB8iLoGFgX8OTZ1DkP1KWnOLpx0YCazI'),
+	TXT('@', 'v=spf1 mx include:mktomail.com ~all'),
+	TXT('m1._domainkey', 'v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZfEV2C82eJ4OA3Mslz4C6msjYYalg1eUcHeJQ//QM1hOZSvn4qz+hSKGi7jwNDqsZNzM8vCt2+XzdDYL3JddwUEhoDsIsZsJW0qzIVVLLWCg6TLNS3FpVyjc171o94dpoHFekfswWDoEwFQ03Woq2jchYWBrbUf7MMcdEj/EQqwIDAQAB'),
+	CNAME('dev', 'stackoverflowsandbox2.mktoweb.com.'),
+	CNAME('dev-email', 'mkto-sj310056.com.'),
+	TXT('m1._domainkey.dev-email', 'v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIBezZ2Gc+/3PghWk+YOE6T9HdwgUTMTR0Fne2i51MNN9Qs7AqDitVdG/949iDbI2fPNZSnKtOcnlLYwvve9MhMAMI1nZ26ILhgaBJi2BMZQpGFlO4ucuo/Uj4DPZ5Ge/NZHCX0CRhAhR5sRmL2OffNcFXFrymzUuz4KzI/NyUiwIDAQAB'),
+	CNAME('email', 'mkto-sj280138.com.'),
+	CNAME('info', 'stackoverflow.mktoweb.com.'),
+	SRV('_sip._tcp', 10, 60, 5060, 'bigbox.example.com.')
+)
diff --git a/commands/test_data/simple.com.zone.tsv b/commands/test_data/simple.com.zone.tsv
new file mode 100644
index 00000000..7e478f43
--- /dev/null
+++ b/commands/test_data/simple.com.zone.tsv
@@ -0,0 +1,19 @@
+simple.com	@	300	IN	SOA	ns3.serverfault.com. sysadmin.stackoverflow.com. 2020022300 3600 600 604800 1440
+simple.com	@	172800	IN	NS	ns-1313.awsdns-36.org.
+simple.com	@	172800	IN	NS	ns-736.awsdns-28.net.
+simple.com	@	172800	IN	NS	ns-cloud-c1.googledomains.com.
+simple.com	@	172800	IN	NS	ns-cloud-c2.googledomains.com.
+simple.com	@	300	IN	MX	1 aspmx.l.google.com.
+simple.com	@	300	IN	MX	5 alt1.aspmx.l.google.com.
+simple.com	@	300	IN	MX	5 alt2.aspmx.l.google.com.
+simple.com	@	300	IN	MX	10 alt3.aspmx.l.google.com.
+simple.com	@	300	IN	MX	10 alt4.aspmx.l.google.com.
+simple.com	@	300	IN	TXT	"google-site-verification=O54a_pYHGr4EB8iLoGFgX8OTZ1DkP1KWnOLpx0YCazI"
+simple.com	@	300	IN	TXT	"v=spf1 mx include:mktomail.com ~all"
+m1._domainkey.simple.com	m1._domainkey	300	IN	TXT	"v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZfEV2C82eJ4OA3Mslz4C6msjYYalg1eUcHeJQ//QM1hOZSvn4qz+hSKGi7jwNDqsZNzM8vCt2+XzdDYL3JddwUEhoDsIsZsJW0qzIVVLLWCg6TLNS3FpVyjc171o94dpoHFekfswWDoEwFQ03Woq2jchYWBrbUf7MMcdEj/EQqwIDAQAB"
+dev.simple.com	dev	300	IN	CNAME	stackoverflowsandbox2.mktoweb.com.
+dev-email.simple.com	dev-email	300	IN	CNAME	mkto-sj310056.com.
+m1._domainkey.dev-email.simple.com	m1._domainkey.dev-email	300	IN	TXT	"v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIBezZ2Gc+/3PghWk+YOE6T9HdwgUTMTR0Fne2i51MNN9Qs7AqDitVdG/949iDbI2fPNZSnKtOcnlLYwvve9MhMAMI1nZ26ILhgaBJi2BMZQpGFlO4ucuo/Uj4DPZ5Ge/NZHCX0CRhAhR5sRmL2OffNcFXFrymzUuz4KzI/NyUiwIDAQAB"
+email.simple.com	email	300	IN	CNAME	mkto-sj280138.com.
+info.simple.com	info	300	IN	CNAME	stackoverflow.mktoweb.com.
+_sip._tcp.simple.com	_sip._tcp	300	IN	SRV	10 60 5060 bigbox.example.com.
diff --git a/commands/test_data/simple.com.zone.zone b/commands/test_data/simple.com.zone.zone
new file mode 100644
index 00000000..f3bac054
--- /dev/null
+++ b/commands/test_data/simple.com.zone.zone
@@ -0,0 +1,22 @@
+$ORIGIN simple.com.
+$TTL 300
+@                IN SOA   ns3.serverfault.com. sysadmin.stackoverflow.com. 2020022300 3600 600 604800 1440
+           172800 IN NS   ns-1313.awsdns-36.org.
+           172800 IN NS   ns-736.awsdns-28.net.
+           172800 IN NS   ns-cloud-c1.googledomains.com.
+           172800 IN NS   ns-cloud-c2.googledomains.com.
+                 IN MX    1 aspmx.l.google.com.
+                 IN MX    5 alt1.aspmx.l.google.com.
+                 IN MX    5 alt2.aspmx.l.google.com.
+                 IN MX    10 alt3.aspmx.l.google.com.
+                 IN MX    10 alt4.aspmx.l.google.com.
+                 IN TXT   "google-site-verification=O54a_pYHGr4EB8iLoGFgX8OTZ1DkP1KWnOLpx0YCazI"
+                 IN TXT   "v=spf1 mx include:mktomail.com ~all"
+m1._domainkey    IN TXT   "v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZfEV2C82eJ4OA3Mslz4C6msjYYalg1eUcHeJQ//QM1hOZSvn4qz+hSKGi7jwNDqsZNzM8vCt2+XzdDYL3JddwUEhoDsIsZsJW0qzIVVLLWCg6TLNS3FpVyjc171o94dpoHFekfswWDoEwFQ03Woq2jchYWBrbUf7MMcdEj/EQqwIDAQAB"
+_sip._tcp        IN SRV   10 60 5060 bigbox.example.com.
+dev              IN CNAME stackoverflowsandbox2.mktoweb.com.
+dev-email        IN CNAME mkto-sj310056.com.
+m1._domainkey.dev-email IN TXT "v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIBezZ2Gc+/3PghWk+YOE6T9HdwgUTMTR0Fne2i51MNN9Qs7AqDitVdG/949iDbI2fPNZSnKtOcnlLYwvve9MhMAMI1nZ26ILhgaBJi2BMZQpGFlO4ucuo/Uj4DPZ5Ge/NZHCX0CRhAhR5sRmL2OffNcFXFrymzUuz4KzI/NyUiwIDAQAB"
+email            IN CNAME mkto-sj280138.com.
+info             IN CNAME stackoverflow.mktoweb.com.
+