dnscontrol/pkg/prettyzone/sorting.go
Florian Ritterhoff 3a9b413175
NEW RECORD TYPE: HTTPS & SVCB (#2919)
Thanks so much for this contribution!  I have a feeling that a lot of people are going to need these records soon!
2024-05-01 11:37:15 -04:00

221 lines
5.1 KiB
Go

package prettyzone
// Generate zonefiles.
// This generates a zonefile that prioritizes beauty over efficiency.
import (
"bytes"
"log"
"strconv"
"strings"
"github.com/StackExchange/dnscontrol/v4/models"
)
// ZoneGenData is the configuration description for the zone generator.
type ZoneGenData struct {
Origin string
DefaultTTL uint32
Records models.Records
Comments []string
}
func (z *ZoneGenData) Len() int { return len(z.Records) }
func (z *ZoneGenData) Swap(i, j int) { z.Records[i], z.Records[j] = z.Records[j], z.Records[i] }
func (z *ZoneGenData) Less(i, j int) bool {
a, b := z.Records[i], z.Records[j]
// Sort by name.
// If we are at the apex, use "@" in the sorting.
compA, compB := a.NameFQDN, b.NameFQDN
if compA != compB {
if a.Name == "@" {
compA = "@"
}
if b.Name == "@" {
compB = "@"
}
return LabelLess(compA, compB)
}
// sub-sort by type
if a.Type != b.Type {
return zoneRrtypeLess(a.Type, b.Type)
}
// sub-sort within type:
switch a.Type { // #rtype_variations
case "A":
ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
ipa, ipb := ta2.To4(), tb2.To4()
if ipa == nil || ipb == nil {
log.Fatalf("should not happen: IPs are not 4 bytes: %#v %#v", ta2, tb2)
}
return bytes.Compare(ipa, ipb) == -1
case "AAAA":
ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
ipa, ipb := ta2.To16(), tb2.To16()
if ipa == nil || ipb == nil {
log.Fatalf("should not happen: IPs are not 16 bytes: %#v %#v", ta2, tb2)
}
return bytes.Compare(ipa, ipb) == -1
case "MX":
// sort by priority. If they are equal, sort by Mx.
if a.MxPreference == b.MxPreference {
return a.GetTargetField() < b.GetTargetField()
}
return a.MxPreference < b.MxPreference
case "SRV":
//ta2, tb2 := a.(*dns.SRV), b.(*dns.SRV)
pa, pb := a.SrvPort, b.SrvPort
if pa != pb {
return pa < pb
}
pa, pb = a.SrvPriority, b.SrvPriority
if pa != pb {
return pa < pb
}
pa, pb = a.SrvWeight, b.SrvWeight
if pa != pb {
return pa < pb
}
case "SVCB", "HTTPS":
// sort by priority. If they are equal, sort by record.
if a.SvcPriority == b.SvcPriority {
return a.GetTargetField() < b.GetTargetField()
}
return a.SvcPriority < b.SvcPriority
case "PTR":
//ta2, tb2 := a.(*dns.PTR), b.(*dns.PTR)
pa, pb := a.GetTargetField(), b.GetTargetField()
if pa != pb {
return pa < pb
}
case "CAA":
//ta2, tb2 := a.(*dns.CAA), b.(*dns.CAA)
// sort by tag
pa, pb := a.CaaTag, b.CaaTag
if pa != pb {
return pa < pb
}
// then flag
fa, fb := a.CaaFlag, b.CaaFlag
if fa != fb {
// flag set goes before ones without flag set
return fa > fb
}
case "DS":
pa, pb := a.DsKeyTag, b.DsKeyTag
if pa != pb {
return pa < pb
}
case "DNSKEY":
pa, pb := a.DnskeyFlags, b.DnskeyFlags
if pa != pb {
return pa < pb
}
fa, fb := a.DnskeyProtocol, b.DnskeyProtocol
if fa != fb {
return fa < fb
}
default:
// pass through. String comparison is sufficient.
}
return a.String() < b.String()
}
// LabelLess provides a "Less" function for two labels as needed for sorting. It
// sorts labels in prefix order, to make output pretty.
func LabelLess(a, b string) bool {
// Compare two zone labels for the purpose of sorting the RRs in a Zone.
// If they are equal, we are done. All other code is simplified
// because we can assume a!=b.
if a == b {
return false
}
// Sort @ at the top, then *, then everything else lexigraphically.
// i.e. @ always is less. * is is less than everything but @.
if a == "@" {
return true
}
if b == "@" {
return false
}
if a == "*" {
return true
}
if b == "*" {
return false
}
// Split into elements and match up last elements to first. Compare the
// first non-equal elements.
as := strings.Split(a, ".")
bs := strings.Split(b, ".")
ia := len(as) - 1
ib := len(bs) - 1
var min int
if ia < ib {
min = len(as) - 1
} else {
min = len(bs) - 1
}
// Skip the matching highest elements, then compare the next item.
for i, j := ia, ib; min >= 0; i, j, min = i-1, j-1, min-1 {
// Compare as[i] < bs[j]
// Sort @ at the top, then *, then everything else.
// i.e. @ always is less. * is is less than everything but @.
// If both are numeric, compare as integers, otherwise as strings.
if as[i] != bs[j] {
// If the first element is *, it is always less.
if i == 0 && as[i] == "*" {
return true
}
if j == 0 && bs[j] == "*" {
return false
}
// If the elements are both numeric, compare as integers:
au, aerr := strconv.ParseUint(as[i], 10, 64)
bu, berr := strconv.ParseUint(bs[j], 10, 64)
if aerr == nil && berr == nil {
return au < bu
}
// otherwise, compare as strings:
return as[i] < bs[j]
}
}
// The min top elements were equal, so the shorter name is less.
return ia < ib
}
func zoneRrtypeLess(a, b string) bool {
// Compare two RR types for the purpose of sorting the RRs in a Zone.
if a == b {
return false
}
// List SOAs, NSs, etc. then all others alphabetically.
for _, t := range []string{"SOA", "NS", "CNAME",
"A", "AAAA", "MX", "SRV", "TXT",
} {
if a == t {
return true
}
if b == t {
return false
}
}
return a < b
}