(join-strings #\. (list prefix zone-name))
prefix))))
+(export 'zone-records-sorted)
+(defun zone-records-sorted (zone)
+ "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)))))))))
+
;;;--------------------------------------------------------------------------
;;; Serial numbering.
:make-ptr-p (zr-make-ptr-p zr)))))))))))
;;;--------------------------------------------------------------------------
+;;; Building raw record vectors.
+
+(defvar *record-vector* nil
+ "The record vector under construction.")
+
+(defun rec-ensure (n)
+ "Ensure that at least N octets are spare in the current record."
+ (let ((want (+ n (fill-pointer *record-vector*)))
+ (have (array-dimension *record-vector* 0)))
+ (unless (<= want have)
+ (adjust-array *record-vector*
+ (do ((new (* 2 have) (* 2 new)))
+ ((<= want new) new))))))
+
+(defun rec-byte (octets value)
+ "Append an unsigned byte, OCTETS octets wide, with VALUE, to the record."
+ (rec-ensure octets)
+ (do ((i (1- octets) (1- i)))
+ ((minusp i))
+ (vector-push (ldb (byte 8 (* 8 i)) value) *record-vector*)))
+
+(defun rec-u8 (value)
+ "Append an 8-bit VALUE to the current record."
+ (rec-byte 1 value))
+(defun rec-u16 (value)
+ "Append a 16-bit VALUE to the current record."
+ (rec-byte 2 value))
+(defun rec-u32 (value)
+ "Append a 32-bit VALUE to the current record."
+ (rec-byte 4 value))
+
+(defun rec-raw-string (s &key (start 0) end)
+ "Append (a (substring of) a raw string S to the current record.
+
+ No arrangement is made for reporting the length of the string. That must
+ be done by the caller, if necessary."
+ (setf-default end (length s))
+ (rec-ensure (- end start))
+ (do ((i start (1+ i)))
+ ((>= i end))
+ (vector-push (char-code (char s i)) *record-vector*)))
+
+(defun rec-name (s)
+ "Append a domain name S.
+
+ No attempt is made to perform compression of the name."
+ (let ((i 0) (n (length s)))
+ (loop (let* ((dot (position #\. s :start i))
+ (lim (or dot n)))
+ (rec-u8 (- lim i))
+ (rec-raw-string s :start i :end lim)
+ (if dot
+ (setf i (1+ dot))
+ (return))))
+ (when (< i n)
+ (rec-u8 0))))
+
+(defmacro build-record (&body body)
+ "Build a raw record, and return it as a vector of octets."
+ `(let ((*record-vector* (make-array 256
+ :element-type '(unsigned-byte 8)
+ :fill-pointer 0
+ :adjustable t)))
+ ,@body
+ (copy-seq *record-vector*)))
+
+;;;--------------------------------------------------------------------------
;;; Zone file output.
(export 'zone-write)
;;;--------------------------------------------------------------------------
;;; Bind format output.
+(defvar *bind-last-record-name* nil
+ "The previously emitted record name.
+
+ Used for eliding record names on output.")
+
(export 'bind-hostname)
(defun bind-hostname (hostname)
- (if (not hostname)
- "@"
- (let* ((h (string-downcase (stringify hostname)))
- (hl (length h))
- (r (string-downcase (zone-name *writing-zone*)))
- (rl (length r)))
- (cond ((string= r h) "@")
- ((and (> hl rl)
- (char= (char h (- hl rl 1)) #\.)
- (string= h r :start1 (- hl rl)))
- (subseq h 0 (- hl rl 1)))
- (t (concatenate 'string h "."))))))
+ (let* ((h (string-downcase (stringify hostname)))
+ (hl (length h))
+ (r (string-downcase (zone-name *writing-zone*)))
+ (rl (length r)))
+ (cond ((string= r h) "@")
+ ((and (> hl rl)
+ (char= (char h (- hl rl 1)) #\.)
+ (string= h r :start1 (- hl rl)))
+ (subseq h 0 (- hl rl 1)))
+ (t (concatenate 'string h ".")))))
+
+(export 'bind-output-hostname)
+(defun bind-output-hostname (hostname)
+ (let ((name (bind-hostname hostname)))
+ (cond ((and *bind-last-record-name*
+ (string= name *bind-last-record-name*))
+ "")
+ (t
+ (setf *bind-last-record-name* name)
+ name))))
(export 'bind-record)
(defgeneric bind-record (type zr))
(zone-name zone)
(iso-date :now :datep t :timep t)
(zone-default-ttl zone))
- (let* ((soa (zone-soa zone))
+ (let* ((*bind-last-record-name* nil)
+ (soa (zone-soa zone))
(admin (let* ((name (soa-admin soa))
(at (position #\@ name))
(copy (format nil "~(~A~)." name)))
~45T~10D~60T ;retry
~45T~10D~60T ;expire
~45T~10D )~60T ;min-ttl~2%"
- (bind-hostname (zone-name zone))
+ (bind-output-hostname (zone-name zone))
(bind-hostname (soa-source soa))
admin
(soa-serial soa)
(soa-refresh soa)
(soa-retry soa)
(soa-expire soa)
- (soa-min-ttl soa)))
- (dolist (zr (zone-records zone))
- (bind-record (zr-type zr) zr)))
+ (soa-min-ttl soa))
+ (dolist (zr (zone-records-sorted zone))
+ (bind-record (zr-type zr) zr))))
(export 'bind-format-record)
(defun bind-format-record (name ttl type format args)
(format *zone-output-stream*
"~A~20T~@[~8D~]~30TIN ~A~40T~?~%"
- (bind-hostname name)
+ (bind-output-hostname name)
(and (/= ttl (zone-default-ttl *writing-zone*))
ttl)
(string-upcase (symbol-name type))
(bind-record-type type)
format args)))
+;;;--------------------------------------------------------------------------
+;;; tinydns-data output format.
+
+(defun tinydns-output (code &rest fields)
+ (format *zone-output-stream* "~C~{~@[~A~]~^:~}~%" code fields))
+
+(defun tinydns-raw-record (type zr data)
+ (tinydns-output #\: (zr-name zr) type
+ (with-output-to-string (out)
+ (dotimes (i (length data))
+ (let ((byte (aref data i)))
+ (if (or (<= byte 32)
+ (>= byte 128)
+ (member byte '(#\: #\\) :key #'char-code))
+ (format out "\\~3,'0O" byte)
+ (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
+ (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 "~
+### Zone file `~(~A~)'
+### (generated ~A)
+~%"
+ (zone-name zone)
+ (iso-date :now :datep t :timep t))
+ (let ((soa (zone-soa zone)))
+ (tinydns-output #\Z
+ (zone-name zone)
+ (soa-source soa)
+ (let* ((name (copy-seq (soa-admin soa)))
+ (at (position #\@ name)))
+ (when at (setf (char name at) #\.))
+ name)
+ (soa-serial soa)
+ (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)))
+
;;;----- That's all, folks --------------------------------------------------