diff --git a/msg.go b/msg.go index 59a6ede9..3d13135c 100644 --- a/msg.go +++ b/msg.go @@ -36,7 +36,7 @@ var ( // ErrFqdn indicates that a domain name does not have a closing dot. ErrFqdn error = &Error{err: "domain must be fully qualified"} // ErrId indicates there is a mismatch with the message's ID. - ErrId error = &Error{err: "id mismatch"} + ErrId error = &Error{err: "id mismatch"} // ErrKeyAlg indicates that the algorithm in the key is not valid. ErrKeyAlg error = &Error{err: "bad key algorithm"} ErrKey error = &Error{err: "bad key"} diff --git a/parse_test.go b/parse_test.go index c2579bcc..1bf4d871 100644 --- a/parse_test.go +++ b/parse_test.go @@ -907,11 +907,11 @@ func TestILNP(t *testing.T) { func TestGposEidNimloc(t *testing.T) { dt := map[string]string{ - "444433332222111199990123000000ff. NSAP-PTR foo.bar.com.": "444433332222111199990123000000ff.\t3600\tIN\tNSAP-PTR\tfoo.bar.com.", - "lillee. IN GPOS -32.6882 116.8652 10.0": "lillee.\t3600\tIN\tGPOS\t-32.6882 116.8652 10.0", - "hinault. IN GPOS -22.6882 116.8652 250.0": "hinault.\t3600\tIN\tGPOS\t-22.6882 116.8652 250.0", - "VENERA. IN NIMLOC 75234159EAC457800920": "VENERA.\t3600\tIN\tNIMLOC\t75234159EAC457800920", - "VAXA. IN EID 3141592653589793": "VAXA.\t3600\tIN\tEID\t3141592653589793", + "444433332222111199990123000000ff. NSAP-PTR foo.bar.com.": "444433332222111199990123000000ff.\t3600\tIN\tNSAP-PTR\tfoo.bar.com.", + "lillee. IN GPOS -32.6882 116.8652 10.0": "lillee.\t3600\tIN\tGPOS\t-32.6882 116.8652 10.0", + "hinault. IN GPOS -22.6882 116.8652 250.0": "hinault.\t3600\tIN\tGPOS\t-22.6882 116.8652 250.0", + "VENERA. IN NIMLOC 75234159EAC457800920": "VENERA.\t3600\tIN\tNIMLOC\t75234159EAC457800920", + "VAXA. IN EID 3141592653589793": "VAXA.\t3600\tIN\tEID\t3141592653589793", } for i, o := range dt { rr, err := NewRR(i) @@ -1507,7 +1507,7 @@ func TestPackCAA(t *testing.T) { func TestParseURI(t *testing.T) { lt := map[string]string{ "_http._tcp. IN URI 10 1 \"http://www.example.com/path\"": "_http._tcp.\t3600\tIN\tURI\t10 1 \"http://www.example.com/path\"", - "_http._tcp. IN URI 10 1 \"\"": "_http._tcp.\t3600\tIN\tURI\t10 1 \"\"", + "_http._tcp. IN URI 10 1 \"\"": "_http._tcp.\t3600\tIN\tURI\t10 1 \"\"", } for i, o := range lt { rr, err := NewRR(i) diff --git a/privaterr.go b/privaterr.go index 4b9c325b..7290791b 100644 --- a/privaterr.go +++ b/privaterr.go @@ -49,7 +49,7 @@ func mkPrivateRR(rrtype uint16) *PrivateRR { // Header return the RR header of r. func (r *PrivateRR) Header() *RR_Header { return &r.Hdr } -func (r *PrivateRR) String() string { return r.Hdr.String() + r.Data.String() } +func (r *PrivateRR) String() string { return r.Hdr.String() + r.Data.String() } // Private len and copy parts to satisfy RR interface. func (r *PrivateRR) len() int { return r.Hdr.len() + r.Data.Len() } diff --git a/sanitize.go b/sanitize.go new file mode 100644 index 00000000..b489f3f0 --- /dev/null +++ b/sanitize.go @@ -0,0 +1,84 @@ +package dns + +// Dedup removes identical RRs from rrs. It preserves the original ordering. +// The lowest TTL of any duplicates is used in the remaining one. Dedup modifies +// rrs. +// m is used to store the RRs temporay. If it is nil a new map will be allocated. +func Dedup(rrs []RR, m map[string]RR) []RR { + if m == nil { + m = make(map[string]RR) + } + // Save the keys, so we don't have to call normalizedString twice. + keys := make([]*string, 0, len(rrs)) + + for _, r := range rrs { + key := normalizedString(r) + keys = append(keys, &key) + if _, ok := m[key]; ok { + // Shortest TTL wins. + if m[key].Header().Ttl > r.Header().Ttl { + m[key].Header().Ttl = r.Header().Ttl + } + continue + } + + m[key] = r + } + // If the length of the result map equals the amount of RRs we got, + // it means they were all different. We can then just return the original rrset. + if len(m) == len(rrs) { + return rrs + } + + j := 0 + for i, r := range rrs { + // If keys[i] lives in the map, we should copy and remove it. + if _, ok := m[*keys[i]]; ok { + delete(m, *keys[i]) + rrs[j] = r + j++ + } + + if len(m) == 0 { + break + } + } + + return rrs[:j] +} + +// normalizedString returns a normalized string from r. The TTL +// is removed and the domain name is lowercased. We go from this: +// DomainNameTTLCLASSTYPERDATA to: +// lowercasenameCLASSTYPE... +func normalizedString(r RR) string { + // A string Go DNS makes has: domainnameTTL... + b := []byte(r.String()) + + // find the first non-escaped tab, then another, so we capture where the TTL lives. + esc := false + ttlStart, ttlEnd := 0, 0 + for i := 0; i < len(b) && ttlEnd == 0; i++ { + switch { + case b[i] == '\\': + esc = !esc + case b[i] == '\t' && !esc: + if ttlStart == 0 { + ttlStart = i + continue + } + if ttlEnd == 0 { + ttlEnd = i + } + case b[i] >= 'A' && b[i] <= 'Z' && !esc: + b[i] += 32 + default: + esc = false + } + } + + // remove TTL. + copy(b[ttlStart:], b[ttlEnd:]) + cut := ttlEnd - ttlStart + return string(b[:len(b)-cut]) +} diff --git a/sanitize_test.go b/sanitize_test.go new file mode 100644 index 00000000..79335727 --- /dev/null +++ b/sanitize_test.go @@ -0,0 +1,85 @@ +package dns + +import "testing" + +func TestDedup(t *testing.T) { + // make it []string + testcases := map[[3]RR][]string{ + [...]RR{ + newRR(t, "mIek.nl. IN A 127.0.0.1"), + newRR(t, "mieK.nl. IN A 127.0.0.1"), + newRR(t, "miek.Nl. IN A 127.0.0.1"), + }: []string{"mIek.nl.\t3600\tIN\tA\t127.0.0.1"}, + [...]RR{ + newRR(t, "miEk.nl. 2000 IN A 127.0.0.1"), + newRR(t, "mieK.Nl. 1000 IN A 127.0.0.1"), + newRR(t, "Miek.nL. 500 IN A 127.0.0.1"), + }: []string{"miEk.nl.\t500\tIN\tA\t127.0.0.1"}, + [...]RR{ + newRR(t, "miek.nl. IN A 127.0.0.1"), + newRR(t, "miek.nl. CH A 127.0.0.1"), + newRR(t, "miek.nl. IN A 127.0.0.1"), + }: []string{"miek.nl.\t3600\tIN\tA\t127.0.0.1", + "miek.nl.\t3600\tCH\tA\t127.0.0.1", + }, + [...]RR{ + newRR(t, "miek.nl. CH A 127.0.0.1"), + newRR(t, "miek.nl. IN A 127.0.0.1"), + newRR(t, "miek.de. IN A 127.0.0.1"), + }: []string{"miek.nl.\t3600\tCH\tA\t127.0.0.1", + "miek.nl.\t3600\tIN\tA\t127.0.0.1", + "miek.de.\t3600\tIN\tA\t127.0.0.1", + }, + [...]RR{ + newRR(t, "miek.de. IN A 127.0.0.1"), + newRR(t, "miek.nl. 200 IN A 127.0.0.1"), + newRR(t, "miek.nl. 300 IN A 127.0.0.1"), + }: []string{"miek.de.\t3600\tIN\tA\t127.0.0.1", + "miek.nl.\t200\tIN\tA\t127.0.0.1", + }, + } + + for rr, expected := range testcases { + out := Dedup([]RR{rr[0], rr[1], rr[2]}, nil) + for i, o := range out { + if o.String() != expected[i] { + t.Fatalf("expected %v, got %v", expected[i], o.String()) + } + } + } +} + +func BenchmarkDedup(b *testing.B) { + rrs := []RR{ + newRR(nil, "miEk.nl. 2000 IN A 127.0.0.1"), + newRR(nil, "mieK.Nl. 1000 IN A 127.0.0.1"), + newRR(nil, "Miek.nL. 500 IN A 127.0.0.1"), + } + m := make(map[string]RR) + for i := 0; i < b.N; i++ { + Dedup(rrs,m ) + } +} + +func TestNormalizedString(t *testing.T) { + tests := map[RR]string{ + newRR(t, "mIEk.Nl. 3600 IN A 127.0.0.1"): "miek.nl.\tIN\tA\t127.0.0.1", + newRR(t, "m\\ iek.nL. 3600 IN A 127.0.0.1"): "m\\ iek.nl.\tIN\tA\t127.0.0.1", + newRR(t, "m\\\tIeK.nl. 3600 in A 127.0.0.1"): "m\\tiek.nl.\tIN\tA\t127.0.0.1", + } + for tc, expected := range tests { + n := normalizedString(tc) + if n != expected { + t.Logf("expected %s, got %s", expected, n) + t.Fail() + } + } +} + +func newRR(t *testing.T, s string) RR { + r, e := NewRR(s) + if e != nil { + t.Logf("newRR: %s", e) + } + return r +}