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
|
### 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
|
- `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
|
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
|
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 {
|
sort.Slice(out, func(i, j int) bool {
|
||||||
a, b := out[i], out[j]
|
a, b := out[i], out[j]
|
||||||
if a.Name != b.Name {
|
if a.Name != b.Name {
|
||||||
return CanonicalLess(a.Name+".", b.Name+".")
|
return NameLess(a.Name+".", b.Name+".")
|
||||||
}
|
}
|
||||||
if a.Exact != b.Exact {
|
if a.Exact != b.Exact {
|
||||||
return !a.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 {
|
if pi, pj := pos[out[i].ViewID], pos[out[j].ViewID]; pi != pj {
|
||||||
return pi < pj
|
return pi < pj
|
||||||
}
|
}
|
||||||
return CanonicalLess(out[i].Name, out[j].Name)
|
return NameLess(out[i].Name, out[j].Name)
|
||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortZones sorts zones by name in DNS canonical order.
|
// SortZones sorts zones by name (see NameLess).
|
||||||
func SortZones(zs []*Zone) {
|
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).
|
// 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
|
// 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.
|
// data.
|
||||||
func SortRecords(rs []*Record, z *Zone) {
|
func SortRecords(rs []*Record, z *Zone) {
|
||||||
apex := ""
|
apex := ""
|
||||||
@@ -662,7 +662,7 @@ func SortRecords(rs []*Record, z *Zone) {
|
|||||||
if b.Name == apex {
|
if b.Name == apex {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return CanonicalLess(a.Name, b.Name)
|
return NameLess(a.Name, b.Name)
|
||||||
}
|
}
|
||||||
if a.Type != b.Type {
|
if a.Type != b.Type {
|
||||||
return typeOrder(a.Type) < typeOrder(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
|
// NameLess compares names label by label from the right, like DNS
|
||||||
// 6.1): by label from the right.
|
// canonical order (RFC 4034 section 6.1), except that two all-digit
|
||||||
func CanonicalLess(a, b string) bool {
|
// 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, "."), ".")
|
la := strings.Split(strings.TrimSuffix(a, "."), ".")
|
||||||
lb := strings.Split(strings.TrimSuffix(b, "."), ".")
|
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 {
|
for i, j := len(la)-1, len(lb)-1; i >= 0 && j >= 0; i, j = i-1, j-1 {
|
||||||
if la[i] != lb[j] {
|
if c := compareLabel(la[i], lb[j]); c != 0 {
|
||||||
return la[i] < lb[j]
|
return c < 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return len(la) < len(lb)
|
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.
|
// RRSetKey identifies an RRset.
|
||||||
type RRSetKey struct {
|
type RRSetKey struct {
|
||||||
Name string
|
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 {
|
sort.Slice(recs, func(i, j int) bool {
|
||||||
if recs[i].Name != recs[j].Name {
|
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
|
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 {
|
slices.SortFunc(recs, func(a, b *model.Record) int {
|
||||||
if a.Name != b.Name {
|
if a.Name != b.Name {
|
||||||
if model.CanonicalLess(a.Name, b.Name) {
|
if model.NameLess(a.Name, b.Name) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ func (c *checker) checkZoneRecords(z *model.Zone) {
|
|||||||
for n := range ix.types {
|
for n := range ix.types {
|
||||||
names = append(names, n)
|
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 {
|
for _, name := range names {
|
||||||
types := ix.types[name]
|
types := ix.types[name]
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ func sortProblems(ps []Problem) {
|
|||||||
return a.Zone < b.Zone
|
return a.Zone < b.Zone
|
||||||
}
|
}
|
||||||
if a.Name != b.Name {
|
if a.Name != b.Name {
|
||||||
return model.CanonicalLess(a.Name, b.Name)
|
return model.NameLess(a.Name, b.Name)
|
||||||
}
|
}
|
||||||
return a.Message < b.Message
|
return a.Message < b.Message
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in new issue
Block a user