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:
1 parent
fa8feb6bd9
commit
d18fe18ccd
8 files changed
+96
-15
No files matched your search
@@ -42,6 +42,14 @@ All notable changes to Jarl are documented here. The format follows
|
||||
|
||||
### Changed
|
||||
|
||||
- Records, zones and validation findings sort numeric labels by value, so
|
||||
`2.10.0.10.in-addr.arpa` comes before `101.10.0.10.in-addr.arpa` in the
|
||||
web UI, the CLI and the rendered zone files. Names still group by label
|
||||
from the right (subdomains stay under their parent); other labels keep
|
||||
the old byte order (`host10` before `host2`). Zone files are rewritten
|
||||
in the new order the next time they change; named doesn't care about
|
||||
record order.
|
||||
|
||||
- `cname_chain` no longer warns about one CNAME pointing at another (a
|
||||
service name at a host alias is common, and named answers the chain at
|
||||
once). A chain of three or more CNAMEs gets an info note that shows
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -637,7 +637,7 @@ func (s *Server) search(w http.ResponseWriter, r *http.Request, p *auth.Principa
|
||||
}
|
||||
sort.Slice(recs, func(i, j int) bool {
|
||||
if recs[i].Name != recs[j].Name {
|
||||
return model.CanonicalLess(recs[i].Name, recs[j].Name)
|
||||
return model.NameLess(recs[i].Name, recs[j].Name)
|
||||
}
|
||||
return recs[i].ID < recs[j].ID
|
||||
})
|
||||
|
||||
@@ -215,7 +215,7 @@ func clientNames(st *model.State, want map[netip.Addr]bool, readable func(*model
|
||||
}
|
||||
slices.SortFunc(recs, func(a, b *model.Record) int {
|
||||
if a.Name != b.Name {
|
||||
if model.CanonicalLess(a.Name, b.Name) {
|
||||
if model.NameLess(a.Name, b.Name) {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
|
||||
@@ -186,7 +186,7 @@ func (c *checker) checkZoneRecords(z *model.Zone) {
|
||||
for n := range ix.types {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Slice(names, func(i, j int) bool { return model.CanonicalLess(names[i], names[j]) })
|
||||
sort.Slice(names, func(i, j int) bool { return model.NameLess(names[i], names[j]) })
|
||||
|
||||
for _, name := range names {
|
||||
types := ix.types[name]
|
||||
|
||||
@@ -189,7 +189,7 @@ func sortProblems(ps []Problem) {
|
||||
return a.Zone < b.Zone
|
||||
}
|
||||
if a.Name != b.Name {
|
||||
return model.CanonicalLess(a.Name, b.Name)
|
||||
return model.NameLess(a.Name, b.Name)
|
||||
}
|
||||
return a.Message < b.Message
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user