github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/pkg/prettyzone/sorting.go (about)

     1  package prettyzone
     2  
     3  // Generate zonefiles.
     4  // This generates a zonefile that prioritizes beauty over efficiency.
     5  
     6  import (
     7  	"bytes"
     8  	"log"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/StackExchange/dnscontrol/v2/models"
    13  )
    14  
    15  type zoneGenData struct {
    16  	Origin     string
    17  	DefaultTTL uint32
    18  	Records    models.Records
    19  	Comments   []string
    20  }
    21  
    22  func (z *zoneGenData) Len() int      { return len(z.Records) }
    23  func (z *zoneGenData) Swap(i, j int) { z.Records[i], z.Records[j] = z.Records[j], z.Records[i] }
    24  func (z *zoneGenData) Less(i, j int) bool {
    25  	a, b := z.Records[i], z.Records[j]
    26  
    27  	// Sort by name.
    28  	compA, compB := a.NameFQDN, b.NameFQDN
    29  	if compA != compB {
    30  		if a.Name == "@" {
    31  			compA = "@"
    32  		}
    33  		if b.Name == "@" {
    34  			compB = "@"
    35  		}
    36  		return zoneLabelLess(compA, compB)
    37  	}
    38  
    39  	// sub-sort by type
    40  	if a.Type != b.Type {
    41  		return zoneRrtypeLess(a.Type, b.Type)
    42  	}
    43  
    44  	// sub-sort within type:
    45  	switch a.Type { // #rtype_variations
    46  	case "A":
    47  		ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
    48  		ipa, ipb := ta2.To4(), tb2.To4()
    49  		if ipa == nil || ipb == nil {
    50  			log.Fatalf("should not happen: IPs are not 4 bytes: %#v %#v", ta2, tb2)
    51  		}
    52  		return bytes.Compare(ipa, ipb) == -1
    53  	case "AAAA":
    54  		ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
    55  		ipa, ipb := ta2.To16(), tb2.To16()
    56  		if ipa == nil || ipb == nil {
    57  			log.Fatalf("should not happen: IPs are not 16 bytes: %#v %#v", ta2, tb2)
    58  		}
    59  		return bytes.Compare(ipa, ipb) == -1
    60  	case "MX":
    61  		// sort by priority. If they are equal, sort by Mx.
    62  		if a.MxPreference == b.MxPreference {
    63  			return a.GetTargetField() < b.GetTargetField()
    64  		}
    65  		return a.MxPreference < b.MxPreference
    66  	case "SRV":
    67  		//ta2, tb2 := a.(*dns.SRV), b.(*dns.SRV)
    68  		pa, pb := a.SrvPort, b.SrvPort
    69  		if pa != pb {
    70  			return pa < pb
    71  		}
    72  		pa, pb = a.SrvPriority, b.SrvPriority
    73  		if pa != pb {
    74  			return pa < pb
    75  		}
    76  		pa, pb = a.SrvWeight, b.SrvWeight
    77  		if pa != pb {
    78  			return pa < pb
    79  		}
    80  	case "PTR":
    81  		//ta2, tb2 := a.(*dns.PTR), b.(*dns.PTR)
    82  		pa, pb := a.GetTargetField(), b.GetTargetField()
    83  		if pa != pb {
    84  			return pa < pb
    85  		}
    86  	case "CAA":
    87  		//ta2, tb2 := a.(*dns.CAA), b.(*dns.CAA)
    88  		// sort by tag
    89  		pa, pb := a.CaaTag, b.CaaTag
    90  		if pa != pb {
    91  			return pa < pb
    92  		}
    93  		// then flag
    94  		fa, fb := a.CaaFlag, b.CaaFlag
    95  		if fa != fb {
    96  			// flag set goes before ones without flag set
    97  			return fa > fb
    98  		}
    99  	default:
   100  		// pass through. String comparison is sufficient.
   101  	}
   102  	return a.String() < b.String()
   103  }
   104  
   105  func zoneLabelLess(a, b string) bool {
   106  	// Compare two zone labels for the purpose of sorting the RRs in a Zone.
   107  
   108  	// If they are equal, we are done. All other code is simplified
   109  	// because we can assume a!=b.
   110  	if a == b {
   111  		return false
   112  	}
   113  
   114  	// Sort @ at the top, then *, then everything else lexigraphically.
   115  	// i.e. @ always is less. * is is less than everything but @.
   116  	if a == "@" {
   117  		return true
   118  	}
   119  	if b == "@" {
   120  		return false
   121  	}
   122  	if a == "*" {
   123  		return true
   124  	}
   125  	if b == "*" {
   126  		return false
   127  	}
   128  
   129  	// Split into elements and match up last elements to first. Compare the
   130  	// first non-equal elements.
   131  
   132  	as := strings.Split(a, ".")
   133  	bs := strings.Split(b, ".")
   134  	ia := len(as) - 1
   135  	ib := len(bs) - 1
   136  
   137  	var min int
   138  	if ia < ib {
   139  		min = len(as) - 1
   140  	} else {
   141  		min = len(bs) - 1
   142  	}
   143  
   144  	// Skip the matching highest elements, then compare the next item.
   145  	for i, j := ia, ib; min >= 0; i, j, min = i-1, j-1, min-1 {
   146  		// Compare as[i] < bs[j]
   147  		// Sort @ at the top, then *, then everything else.
   148  		// i.e. @ always is less. * is is less than everything but @.
   149  		// If both are numeric, compare as integers, otherwise as strings.
   150  
   151  		if as[i] != bs[j] {
   152  
   153  			// If the first element is *, it is always less.
   154  			if i == 0 && as[i] == "*" {
   155  				return true
   156  			}
   157  			if j == 0 && bs[j] == "*" {
   158  				return false
   159  			}
   160  
   161  			// If the elements are both numeric, compare as integers:
   162  			au, aerr := strconv.ParseUint(as[i], 10, 64)
   163  			bu, berr := strconv.ParseUint(bs[j], 10, 64)
   164  			if aerr == nil && berr == nil {
   165  				return au < bu
   166  			}
   167  			// otherwise, compare as strings:
   168  			return as[i] < bs[j]
   169  		}
   170  	}
   171  	// The min top elements were equal, so the shorter name is less.
   172  	return ia < ib
   173  }
   174  
   175  func zoneRrtypeLess(a, b string) bool {
   176  	// Compare two RR types for the purpose of sorting the RRs in a Zone.
   177  
   178  	if a == b {
   179  		return false
   180  	}
   181  
   182  	// List SOAs, NSs, etc. then all others alphabetically.
   183  
   184  	for _, t := range []string{"SOA", "NS", "CNAME",
   185  		"A", "AAAA", "MX", "SRV", "TXT",
   186  	} {
   187  		if a == t {
   188  			return true
   189  		}
   190  		if b == t {
   191  			return false
   192  		}
   193  	}
   194  	return a < b
   195  }