X-Git-Url: https://git.distorted.org.uk/~mdw/zone/blobdiff_plain/80b5c2ffcd36eb88b6b8f87dc711cf93ebfc27d9..146571dae4dcaa1d2c543140d55cbd14c286aaf3:/zone.lisp diff --git a/zone.lisp b/zone.lisp index b9c8fa1..32f1aed 100644 --- a/zone.lisp +++ b/zone.lisp @@ -111,6 +111,94 @@ (when timep (format s "~2,'0D:~2,'0D:~2,'0D" hr min sec))))) +(defun natural-string< (string1 string2 + &key (start1 0) (end1 nil) + (start2 0) (end2 nil)) + "Answer whether STRING1 precedes STRING2 in a vaguely natural ordering. + + In particular, digit sequences are handled in a moderately sensible way. + Split the strings into maximally long alternating sequences of non-numeric + and numeric characters, such that the non-numeric sequences are + non-empty. Compare these lexicographically; numeric sequences order + according to their integer values, non-numeric sequences in the usual + lexicographic ordering. + + Returns two values: whether STRING1 strictly precedes STRING2, and whether + STRING1 strictly follows STRING2." + + (let ((end1 (or end1 (length string1))) + (end2 (or end2 (length string2)))) + (loop + (cond ((>= start1 end1) + (let ((eqp (>= start2 end2))) + (return (values (not eqp) nil)))) + ((>= start2 end2) + (return (values nil t))) + ((and (digit-char-p (char string1 start1)) + (digit-char-p (char string2 start2))) + (let* ((lim1 (or (position-if-not #'digit-char-p string1 + :start start1 :end end1) + end1)) + (n1 (parse-integer string1 :start start1 :end lim1)) + (lim2 (or (position-if-not #'digit-char-p string2 + :start start2 :end end2) + end2)) + (n2 (parse-integer string2 :start start2 :end lim2))) + (cond ((< n1 n2) (return (values t nil))) + ((> n1 n2) (return (values nil t)))) + (setf start1 lim1 + start2 lim2))) + (t + (let ((lim1 (or (position-if #'digit-char-p string1 + :start start1 :end end1) + end1)) + (lim2 (or (position-if #'digit-char-p string2 + :start start2 :end end2) + end2))) + (cond ((string< string1 string2 + :start1 start1 :end1 lim1 + :start2 start2 :end2 lim2) + (return (values t nil))) + ((string> string1 string2 + :start1 start1 :end1 lim1 + :start2 start2 :end2 lim2) + (return (values nil t)))) + (setf start1 lim1 + start2 lim2))))))) + +(defun domain-name< (name-a name-b) + "Answer whether NAME-A precedes NAME-B in an ordering of domain names. + + Split the names into labels at the dots, and then lexicographically + compare the sequences of labels, right to left, using `natural-string<'. + + Returns two values: whether NAME-A strictly precedes NAME-B, and whether + NAME-A strictly follows NAME-B." + (let ((pos-a (length name-a)) + (pos-b (length name-b))) + (loop (let ((dot-a (or (position #\. name-a + :from-end t :end pos-a) + -1)) + (dot-b (or (position #\. name-b + :from-end t :end pos-b) + -1))) + (multiple-value-bind (precp follp) + (natural-string< name-a name-b + :start1 (1+ dot-a) :end1 pos-a + :start2 (1+ dot-b) :end2 pos-b) + (cond (precp + (return (values t nil))) + (follp + (return (values nil t))) + ((= dot-a -1) + (let ((eqp (= dot-b -1))) + (return (values (not eqp) nil)))) + ((= dot-b -1) + (return (values nil t))) + (t + (setf pos-a dot-a + pos-b dot-b)))))))) + ;;;-------------------------------------------------------------------------- ;;; Zone types. @@ -291,31 +379,11 @@ "Return the ZONE's records, in a pleasant sorted order." (sort (copy-seq (zone-records zone)) (lambda (zr-a zr-b) - (let* ((name-a (zr-name zr-a)) (pos-a (length name-a)) - (name-b (zr-name zr-b)) (pos-b (length name-b))) - (loop (let ((dot-a (or (position #\. name-a - :from-end t :end pos-a) - -1)) - (dot-b (or (position #\. name-b - :from-end t :end pos-b) - -1))) - (cond ((string< name-a name-b - :start1 (1+ dot-a) :end1 pos-a - :start2 (1+ dot-b) :end2 pos-b) - (return t)) - ((string> name-a name-b - :start1 (1+ dot-a) :end1 pos-a - :start2 (1+ dot-b) :end2 pos-b) - (return nil)) - ((= dot-a dot-b -1) - (return (string< (zr-type zr-a) (zr-type zr-b)))) - ((= dot-a -1) - (return t)) - ((= dot-b -1) - (return nil)) - (t - (setf pos-a dot-a) - (setf pos-b dot-b))))))))) + (multiple-value-bind (precp follp) + (domain-name< (zr-name zr-a) (zr-name zr-b)) + (cond (precp t) + (follp nil) + (t (string< (zr-type zr-a) (zr-type zr-b)))))))) ;;;-------------------------------------------------------------------------- ;;; Serial numbering. @@ -993,11 +1061,53 @@ (defvar *zone-output-stream* nil "Stream to write zone data on.") -(defmethod zone-write :around (format zone stream) - (declare (ignore format)) +(defgeneric zone-write-raw-rrdata (format zr type data) + (:documentation "Write an otherwise unsupported record in a given FORMAT. + + ZR gives the record object, which carries the name and TTL; the TYPE is + the numeric RRTYPE code; and DATA is an octet vector giving the RRDATA. + This is used by the default `zone-write-record' method to handle record + types which aren't directly supported by the format driver.")) + +(export 'zone-write-header) +(defgeneric zone-write-header (format zone) + (:documentation "Emit the header for a ZONE, in a given FORMAT. + + The header includes any kind of initial comment, the SOA record, and any + other necessary preamble. There is no default implementation. + + This is part of the protocol used by the default method on `zone-write'; + if you override that method.")) + +(export 'zone-write-trailer) +(defgeneric zone-write-trailer (format zone) + (:documentation "Emit the header for a ZONE, in a given FORMAT. + + The footer may be empty, and is so by default. + + This is part of the protocol used by the default method on `zone-write'; + if you override that method.") + (:method (format zone) + (declare (ignore format zone)) + nil)) + +(export 'zone-write-record) +(defgeneric zone-write-record (format type zr) + (:documentation "Emit a record of the given TYPE (a keyword). + + There is no default implementation.")) + +(defmethod zone-write (format zone stream) + "This default method calls `zone-write-header', then `zone-write-record' + for each record in the zone, and finally `zone-write-trailer'. While it's + running, `*writing-zone*' is bound to the zone object, and + `*zone-output-stream*' to the output stream." (let ((*writing-zone* zone) (*zone-output-stream* stream)) - (call-next-method))) + (zone-write-header format zone) + (dolist (zr (zone-records-sorted zone)) + (zone-write-record format (zr-type zr) zr)) + (zone-write-trailer format zone))) (export 'zone-save) (defun zone-save (zones &key (format :bind)) @@ -1045,11 +1155,12 @@ (setf *bind-last-record-name* name) name)))) -(export 'bind-record) -(defgeneric bind-record (type zr)) +(defmethod zone-write :around ((format (eql :bind)) zone stream) + (let ((*bind-last-record-name* nil)) + (call-next-method))) -(defmethod zone-write ((format (eql :bind)) zone stream) - (format stream "~ +(defmethod zone-write-header ((format (eql :bind)) zone) + (format *zone-output-stream* "~ ;;; Zone file `~(~A~)' ;;; (generated ~A) @@ -1058,15 +1169,14 @@ $TTL ~2@*~D~2%" (zone-name zone) (iso-date :now :datep t :timep t) (zone-default-ttl zone)) - (let* ((*bind-last-record-name* nil) - (soa (zone-soa zone)) + (let* ((soa (zone-soa zone)) (admin (let* ((name (soa-admin soa)) (at (position #\@ name)) (copy (format nil "~(~A~)." name))) (when at (setf (char copy at) #\.)) copy))) - (format stream "~ + (format *zone-output-stream* "~ ~A~30TIN SOA~40T~A ( ~55@A~60T ;administrator ~45T~10D~60T ;serial @@ -1081,49 +1191,49 @@ $TTL ~2@*~D~2%" (soa-refresh soa) (soa-retry soa) (soa-expire soa) - (soa-min-ttl soa)) - (dolist (zr (zone-records-sorted zone)) - (bind-record (zr-type zr) zr)))) + (soa-min-ttl soa)))) (export 'bind-format-record) -(defun bind-format-record (name ttl type format args) +(defun bind-format-record (zr format &rest args) (format *zone-output-stream* "~A~20T~@[~8D~]~30TIN ~A~40T~?~%" - (bind-output-hostname name) - (and (/= ttl (zone-default-ttl *writing-zone*)) - ttl) - (string-upcase (symbol-name type)) + (bind-output-hostname (zr-name zr)) + (let ((ttl (zr-ttl zr))) + (and (/= ttl (zone-default-ttl *writing-zone*)) + ttl)) + (string-upcase (symbol-name (zr-type zr))) format args)) -(export 'bind-record-type) -(defgeneric bind-record-type (type) - (:method (type) type)) - -(export 'bind-record-format-args) -(defgeneric bind-record-format-args (type data) - (:method ((type (eql :a)) data) (list "~A" (ipaddr-string data))) - (:method ((type (eql :aaaa)) data) (list "~A" (ipaddr-string data))) - (:method ((type (eql :ptr)) data) (list "~A" (bind-hostname data))) - (:method ((type (eql :cname)) data) (list "~A" (bind-hostname data))) - (:method ((type (eql :ns)) data) (list "~A" (bind-hostname data))) - (:method ((type (eql :mx)) data) - (list "~2D ~A" (cdr data) (bind-hostname (car data)))) - (:method ((type (eql :srv)) data) - (destructuring-bind (prio weight port host) data - (list "~2D ~5D ~5D ~A" prio weight port (bind-hostname host)))) - (:method ((type (eql :sshfp)) data) - (cons "~2D ~2D ~A" data)) - (:method ((type (eql :txt)) data) - (cons "~#[\"\"~;~S~:;(~@{~%~8T~S~} )~]" - (mapcar #'stringify data)))) - -(defmethod bind-record (type zr) - (destructuring-bind (format &rest args) - (bind-record-format-args type (zr-data zr)) - (bind-format-record (zr-name zr) - (zr-ttl zr) - (bind-record-type type) - format args))) +(defmethod zone-write-record ((format (eql :bind)) (type (eql :a)) zr) + (bind-format-record zr "~A" (ipaddr-string (zr-data zr)))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :aaaa)) zr) + (bind-format-record zr "~A" (ipaddr-string (zr-data zr)))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :ptr)) zr) + (bind-format-record zr "~A" (bind-hostname (zr-data zr)))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :cname)) zr) + (bind-format-record zr "~A" (bind-hostname (zr-data zr)))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :ns)) zr) + (bind-format-record zr "~A" (bind-hostname (zr-data zr)))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :mx)) zr) + (bind-format-record zr "~2D ~A" + (cdr (zr-data zr)) + (bind-hostname (car (zr-data zr))))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :srv)) zr) + (destructuring-bind (prio weight port host) (zr-data zr) + (bind-format-record zr "~2D ~5D ~5D ~A" + prio weight port (bind-hostname host)))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :sshfp)) zr) + (bind-format-record zr "~{~2D ~2D ~A~}" (zr-data zr))) + +(defmethod zone-write-record ((format (eql :bind)) (type (eql :txt)) zr) + (bind-format-record zr "~{~#[\"\"~;~S~:;(~@{~%~8T~S~} )~]~}" (zr-data zr))) ;;;-------------------------------------------------------------------------- ;;; tinydns-data output format. @@ -1143,53 +1253,60 @@ $TTL ~2@*~D~2%" (write-char (code-char byte) out))))) (zr-ttl zr))) -(defgeneric tinydns-record (type zr) - (:method ((type (eql :a)) zr) - (tinydns-output #\+ (zr-name zr) - (ipaddr-string (zr-data zr)) (zr-ttl zr))) - (:method ((type (eql :aaaa)) zr) - (tinydns-output #\3 (zr-name zr) - (format nil "~(~32,'0X~)" (ipaddr-addr (zr-data zr))) - (zr-ttl zr))) - (:method ((type (eql :ptr)) zr) - (tinydns-output #\^ (zr-name zr) (zr-data zr) (zr-ttl zr))) - (:method ((type (eql :cname)) zr) - (tinydns-output #\C (zr-name zr) (zr-data zr) (zr-ttl zr))) - (:method ((type (eql :ns)) zr) - (tinydns-output #\& (zr-name zr) nil (zr-data zr) (zr-ttl zr))) - (:method ((type (eql :mx)) zr) - (let ((name (car (zr-data zr))) - (prio (cdr (zr-data zr)))) - (tinydns-output #\@ (zr-name zr) nil name prio (zr-ttl zr)))) - (:method ((type (eql :txt)) zr) - (tinydns-raw-record 16 zr +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :a)) zr) + (tinydns-output #\+ (zr-name zr) + (ipaddr-string (zr-data zr)) (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :aaaa)) zr) + (tinydns-output #\3 (zr-name zr) + (format nil "~(~32,'0X~)" (ipaddr-addr (zr-data zr))) + (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :ptr)) zr) + (tinydns-output #\^ (zr-name zr) (zr-data zr) (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :cname)) zr) + (tinydns-output #\C (zr-name zr) (zr-data zr) (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :ns)) zr) + (tinydns-output #\& (zr-name zr) nil (zr-data zr) (zr-ttl zr))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :mx)) zr) + (let ((name (car (zr-data zr))) + (prio (cdr (zr-data zr)))) + (tinydns-output #\@ (zr-name zr) nil name prio (zr-ttl zr)))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :txt)) zr) + (tinydns-raw-record 16 zr + (build-record + (dolist (s (zr-data zr)) + (rec-u8 (length s)) + (rec-raw-string s))))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :srv)) zr) + (destructuring-bind (prio weight port host) (zr-data zr) + (tinydns-raw-record 33 zr + (build-record + (rec-u16 prio) + (rec-u16 weight) + (rec-u16 port) + (rec-name host))))) + +(defmethod zone-write-record ((format (eql :tinydns)) (type (eql :sshfp)) zr) + (destructuring-bind (alg type fpr) (zr-data zr) + (tinydns-raw-record 44 zr (build-record - (dolist (s (zr-data zr)) - (rec-u8 (length s)) - (rec-raw-string s))))) - (:method ((type (eql :srv)) zr) - (destructuring-bind (prio weight port host) (zr-data zr) - (tinydns-raw-record 33 zr - (build-record - (rec-u16 prio) - (rec-u16 weight) - (rec-u16 port) - (rec-name host))))) - (:method ((type (eql :sshfp)) zr) - (destructuring-bind (alg type fpr) (zr-data zr) - (tinydns-raw-record 44 zr - (build-record - (rec-u8 alg) - (rec-u8 type) - (do ((i 0 (+ i 2)) - (n (length fpr))) - ((>= i n)) - (rec-u8 (parse-integer fpr - :start i :end (+ i 2) - :radix 16)))))))) - -(defmethod zone-write ((format (eql :tinydns)) zone stream) - (format stream "~ + (rec-u8 alg) + (rec-u8 type) + (do ((i 0 (+ i 2)) + (n (length fpr))) + ((>= i n)) + (rec-u8 (parse-integer fpr + :start i :end (+ i 2) + :radix 16)))))))) + +(defmethod zone-write-header ((format (eql :tinydns)) zone) + (format *zone-output-stream* "~ ### Zone file `~(~A~)' ### (generated ~A) ~%" @@ -1207,8 +1324,6 @@ $TTL ~2@*~D~2%" (soa-refresh soa) (soa-expire soa) (soa-min-ttl soa) - (zone-default-ttl zone))) - (dolist (zr (zone-records-sorted zone)) - (tinydns-record (zr-type zr) zr))) + (zone-default-ttl zone)))) ;;;----- That's all, folks --------------------------------------------------