Subject: bug#26820: 26.0.50; DNS mode and IPv6 reverse zones

From: [email-masked] (Peder O. Klingenberg)
Date: Sun, 07 May 2017 20:58:59 +0200

[email-masked] (Peder O. Klingenberg) writes:

> I'll pass along the patch as soon as I get a bug# to attach to it. :)

As promised, here's the patch.  I even wrote tests, which is a first for
me.  I couldn't find dns-mode documented in any manual, so I haven't
written any info docs, but the docstrings should hopefully give enough
information to use the feature.

>From 8fe87aafd6aa77f5d4c1ac126e2d2e7e69ce236c Mon Sep 17 00:00:00 2001
From: "Peder O. Klingenberg" <[email-masked]>
Date: Sun, 7 May 2017 17:58:41 +0200
Subject: [PATCH] Command to convert IPv6 addresses in zone files

* lisp/textmodes/dns-mode.el (dns-mode-ipv6-to-nibbles):
New command to convert IPv6 addresses to a format suitable for
reverse lookup zone files.
(dns-mode-reverse-and-expand-ipv6): The algorithm central to
the command above.
(dns-mode-map, dns-mode-menu): Key and menu item for the new
command.
* test/lisp/textmodes/dns-mode-tests.el (dns-mode-ipv6-conversion)
(dns-mode-ipv6-text-replacement): Tests for new functionality
in dns-mode.el

(bug#26820)
---
 etc/NEWS                              |   8 +++
 lisp/textmodes/dns-mode.el            | 102 +++++++++++++++++++++++++++++++++-
 test/lisp/textmodes/dns-mode-tests.el |  58 +++++++++++++++++++
 3 files changed, 167 insertions(+), 1 deletion(-)
 create mode 100644 test/lisp/textmodes/dns-mode-tests.el

diff --git a/etc/NEWS b/etc/NEWS
index 4599efd7da..293ef4f960 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -784,6 +784,14 @@ file.
 *** Emacs does no longer prompt the user before killing Flymake
 processes on exit.
 
+** DNS mode
+
++++
+*** DNS mode has a tool to convert IPv6 addresses
+Reverse zones for IPv6 needs IPv6 addresses spelled out in a
+cumbersome format.  A new command 'dns-mode-ipv6-to-nibbles' helps
+with the conversion.
+
 
 * New Modes and Packages in Emacs 26.1
 
diff --git a/lisp/textmodes/dns-mode.el b/lisp/textmodes/dns-mode.el
index 01f509d913..6f2f40cd2c 100644
--- a/lisp/textmodes/dns-mode.el
+++ b/lisp/textmodes/dns-mode.el
@@ -113,6 +113,7 @@ dns-mode-syntax-table
 (defvar dns-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map "\C-c\C-s" 'dns-mode-soa-increment-serial)
+    (define-key map "\C-c\C-e" 'dns-mode-ipv6-to-nibbles)
     map)
   "Keymap for DNS master file mode.")
 
@@ -124,7 +125,8 @@ dns-mode-menu
 (easy-menu-define dns-mode-menu dns-mode-map
   "DNS Menu."
   '("DNS"
-    ["Increment SOA serial" dns-mode-soa-increment-serial t]))
+    ["Increment SOA serial" dns-mode-soa-increment-serial t]
+    ["Convert IPv6 address to nibbles" dns-mode-ipv6-to-nibbles t]))
 
 ;; Mode.
 
@@ -220,6 +222,104 @@ dns-mode-soa-maybe-increment-serial
 	   ;; We return nil in case this is used in write-contents-functions.
 	   nil)))
 
+;;;###autoload
+(defun dns-mode-ipv6-to-nibbles (negate-prefix-p)
+  "Replace an IPv6 address around or preceeding point by its
+ip6.arpa-representation for use in reverse zone files.  The
+replaced address is placed in the kill ring.
+
+The address can be a complete address (no prefix designator), can
+have a normal prefix designator (e.g. /48), in which case only
+the required number of nibbles are output, or can have a negative
+prefix designator (e.g. /-112), in which case only the parts of
+the address *not* covered by the absolute value of the prefix
+length is output, as a relative address (without .ip6.arpa. at
+the end).  This is useful when $ORIGIN is specified in the zone
+file.
+
+If called with a prefix argument (C-u), the value of the detected
+prefix length is negated.
+
+Examples:
+
+2001:db8::12  =>
+2.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
+
+2001:db8::12/32  =>
+8.b.d.0.1.0.0.2.ip6.arpa.
+
+2001:db8::12/-32  =>
+2.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0
+
+::42/112 (with C-u prefix) =>
+2.4.0.0"
+  (interactive "P")
+  (skip-syntax-backward " ")
+  (skip-syntax-backward "w_.")
+  (re-search-forward "\\([[:xdigit:]:]+\\)\\(/-?[0-9]\\{2,3\\}\\)?")
+  (kill-new (match-string 0))
+  (let ((address (match-string 1))
+        (prefix-length (match-string 2)))
+    (if prefix-length
+        (progn
+          (setq prefix-length (string-to-number (substring prefix-length 1)))
+          (if negate-prefix-p
+              (setq prefix-length (* -1 prefix-length)))))
+    (replace-match (save-match-data
+                     (dns-mode-reverse-and-expand-ipv6 address prefix-length)))))
+
+(defun dns-mode-reverse-and-expand-ipv6 (address &optional prefix-length)
+  "Convert an IPv6 address to (parts of) an ip6.arpa nibble format.
+
+ADDRESS must be an IPv6 address in the usual colon-separaated
+format, without a prefix designator at the end.  PREFIX-LENGTH,
+if given, must be a number whose absolute value is the length in
+bits of the network part of the address.
+
+If PREFIX-LENGTH is `nil', an absolute address is returned
+representing the full IPv6 address.
+
+If PREFIX-LENGTH is positive, an absolute address is returned
+representing the network prefix indicated.
+
+If PREFIX-LENGTH is negative, a relative address is returned
+representing the host parts of the address with respect to the
+indicated network prefix.
+
+See `dns-mode-ipv6-to-nibbles' for examples."
+  (let* ((chunks (split-string address ":"))
+         (prefix-length-nibbles (if prefix-length
+                                    (ceiling (abs prefix-length) 4)
+                                  32))
+         (filler-chunks (- 8 (length (remove "" chunks))))
+         (expanded-address (apply #'concat
+                                  (loop with filler-done = nil
+                                        for chunk in chunks
+                                        if (and (not filler-done)
+                                                (string= "" chunk))
+                                        append (prog1
+                                                   (loop repeat filler-chunks
+                                                         collect "0000")
+                                                 (setq filler-done t))
+                                        else
+                                        if (not (string= "" chunk))
+                                        collect (format "%04x" (string-to-number chunk 16)))))
+         (rev-address-nibbles (nreverse (if (and prefix-length
+                                                 (minusp prefix-length))
+                                            (substring expanded-address prefix-length-nibbles)
+                                          (substring expanded-address 0 prefix-length-nibbles)))))
+    (with-temp-buffer
+      (loop for char across rev-address-nibbles
+            do
+            (insert char)
+            (insert "."))
+      (if (and prefix-length
+               (minusp prefix-length))
+          (delete-char -1)
+        (insert "ip6.arpa."))
+      (insert " ")
+      (buffer-string))))
+
 (provide 'dns-mode)
 
 ;;; dns-mode.el ends here
diff --git a/test/lisp/textmodes/dns-mode-tests.el b/test/lisp/textmodes/dns-mode-tests.el
new file mode 100644
index 0000000000..34e86201d8
--- /dev/null
+++ b/test/lisp/textmodes/dns-mode-tests.el
@@ -0,0 +1,58 @@
+;;; dns-mode-tests.el --- Test suite for dns-mode  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2017 Free Software Foundation, Inc.
+
+;; Author: Peder O. Klingenberg <[email-masked]>
+;; Keywords: dns zone
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'dns-mode)
+
+;;; IPv6 reverse zones
+(ert-deftest dns-mode-ipv6-conversion ()
+  (let ((address "2001:db8::42"))
+    (should (equal (dns-mode-reverse-and-expand-ipv6 address)
+                   "2.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. "))
+    (should (equal (dns-mode-reverse-and-expand-ipv6 address 32)
+                   "8.b.d.0.1.0.0.2.ip6.arpa. "))
+    (should (equal (dns-mode-reverse-and-expand-ipv6 address -112)
+                   "2.4.0.0 "))))
+
+(ert-deftest dns-mode-ipv6-text-replacement ()
+  (let ((address "2001:db8::42/32"))
+    (with-temp-buffer
+      ;; Conversion with point directly after address
+      (insert address)
+      (dns-mode-ipv6-to-nibbles nil)
+      (should (equal (buffer-string) "8.b.d.0.1.0.0.2.ip6.arpa. "))
+      ;; Kill ring contains the expected
+      (erase-buffer)
+      (yank)
+      (should (equal (buffer-string) address))
+      ;; Point at beginning of address (and prefix arg to command)
+      (goto-char (point-min))
+      (dns-mode-ipv6-to-nibbles t)
+      (should (equal (buffer-string) "2.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 "))
+      ;; Point separated from address by whitespace
+      (erase-buffer)
+      (insert address)
+      (insert " ")
+      (dns-mode-ipv6-to-nibbles nil)
+      (should (equal (buffer-string) "8.b.d.0.1.0.0.2.ip6.arpa.  ")))))
-- 
2.11.0