Sort numeric labels by value

Records, zones and findings now order all-digit labels numerically, so
reverse zones list 2 before 101. model.CanonicalLess is replaced by
model.NameLess; other labels keep byte order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-20 08:49:58 +01:00
1 parent fa8feb6bd9
commit d18fe18ccd
8 files changed
+96 -15

No files matched your search

+1 -1
View File
@@ -204,7 +204,7 @@ func (s *State) SortedBlockRules() []*BlockRule {
sort.Slice(out, func(i, j int) bool {
a, b := out[i], out[j]
if a.Name != b.Name {
return CanonicalLess(a.Name+".", b.Name+".")
return NameLess(a.Name+".", b.Name+".")
}
if a.Exact != b.Exact {
return !a.Exact
+41 -10
View File
@@ -524,14 +524,14 @@ func (s *State) SortedZones() []*Zone {
if pi, pj := pos[out[i].ViewID], pos[out[j].ViewID]; pi != pj {
return pi < pj
}
return CanonicalLess(out[i].Name, out[j].Name)
return NameLess(out[i].Name, out[j].Name)
})
return out
}
// SortZones sorts zones by name in DNS canonical order.
// SortZones sorts zones by name (see NameLess).
func SortZones(zs []*Zone) {
sort.Slice(zs, func(i, j int) bool { return CanonicalLess(zs[i].Name, zs[j].Name) })
sort.Slice(zs, func(i, j int) bool { return NameLess(zs[i].Name, zs[j].Name) })
}
// FindZone finds a zone by name within a view (viewID 0 for no view).
@@ -646,7 +646,7 @@ func typeOrder(t string) string {
}
// SortRecords sorts records in zone-file order: apex first, then names in
// DNS canonical order (so subdomains group together), then by type and
// NameLess order (so subdomains group together), then by type and
// data.
func SortRecords(rs []*Record, z *Zone) {
apex := ""
@@ -662,7 +662,7 @@ func SortRecords(rs []*Record, z *Zone) {
if b.Name == apex {
return false
}
return CanonicalLess(a.Name, b.Name)
return NameLess(a.Name, b.Name)
}
if a.Type != b.Type {
return typeOrder(a.Type) < typeOrder(b.Type)
@@ -674,19 +674,50 @@ func SortRecords(rs []*Record, z *Zone) {
})
}
// CanonicalLess compares names in DNS canonical order (RFC 4034 section
// 6.1): by label from the right.
func CanonicalLess(a, b string) bool {
// NameLess compares names label by label from the right, like DNS
// canonical order (RFC 4034 section 6.1), except that two all-digit
// labels compare as numbers, so 2.10.0.10.in-addr.arpa comes before
// 101.10.0.10.in-addr.arpa. It orders names for people and zone files;
// BIND does its own canonical ordering where DNSSEC needs it.
func NameLess(a, b string) bool {
la := strings.Split(strings.TrimSuffix(a, "."), ".")
lb := strings.Split(strings.TrimSuffix(b, "."), ".")
for i, j := len(la)-1, len(lb)-1; i >= 0 && j >= 0; i, j = i-1, j-1 {
if la[i] != lb[j] {
return la[i] < lb[j]
if c := compareLabel(la[i], lb[j]); c != 0 {
return c < 0
}
}
return len(la) < len(lb)
}
func compareLabel(a, b string) int {
if a == b {
return 0
}
if allDigits(a) && allDigits(b) {
na, nb := strings.TrimLeft(a, "0"), strings.TrimLeft(b, "0")
if len(na) != len(nb) {
return len(na) - len(nb)
}
if c := strings.Compare(na, nb); c != 0 {
return c
}
}
return strings.Compare(a, b)
}
func allDigits(s string) bool {
if s == "" {
return false
}
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return true
}
// RRSetKey identifies an RRset.
type RRSetKey struct {
Name string
+42
View File
@@ -0,0 +1,42 @@
package model
import (
"slices"
"testing"
)
func TestNameLess(t *testing.T) {
want := []string{
"10.0.10.in-addr.arpa.",
"1.10.0.10.in-addr.arpa.",
"02.10.0.10.in-addr.arpa.",
"2.10.0.10.in-addr.arpa.",
"10.10.0.10.in-addr.arpa.",
"101.10.0.10.in-addr.arpa.",
"x.101.10.0.10.in-addr.arpa.",
"254.10.0.10.in-addr.arpa.",
"a.10.0.10.in-addr.arpa.",
}
got := slices.Clone(want)
slices.Reverse(got)
slices.SortFunc(got, func(a, b string) int {
if NameLess(a, b) {
return -1
}
if NameLess(b, a) {
return 1
}
return 0
})
if !slices.Equal(got, want) {
t.Fatalf("got %v\nwant %v", got, want)
}
// Non-numeric labels keep canonical (byte) order.
if !NameLess("host10.example.", "host2.example.") {
t.Error("host10 should sort before host2")
}
if !NameLess("example.", "www.example.") {
t.Error("parent should sort before child")
}
}