]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
add extra tokens to the zone file name template
authorColin Vidal <colin@isc.org>
Thu, 24 Jul 2025 12:54:07 +0000 (14:54 +0200)
committerColin Vidal <colin@isc.org>
Mon, 8 Sep 2025 10:10:09 +0000 (12:10 +0200)
Extend the `$name`, `$view` and `$type` tokens (expanding into the zone
name, zone's view name and type); the new following tokens are now also
accepted:

- $name or %s is replaced with the zone name in lower case;
- $type or %t is replaced with the zone type -- i.e., primary,
  secondary, etc);
- $view or %v is replaced with the view name;
- $char1 or %1 is replaced with the first character of the zone name;
- $char2 or %2 is replaced with the second character of the zone name
  (or a dot if there is no second character);
- $char3 or %3 is replaced with the third character of the zone name (or
  a dot if there is no third character);
- $label1 or %z is replaced with the toplevel domain of the zone (or a
  dot if it is the root zone);
- $label2 or %y is replaced with the next label under the toplevel
  domain (or a dot if there is no next label);
- $label3 or %x is replaced with the next-next label under the toplevel
  domain (or a dot if there is no next-next label).

doc/arm/reference.rst
lib/dns/zone.c
tests/dns/zonefile_test.c

index b41429d1d419281df7787edebd3cf08e9279a4b3..6f64db05e6d3b64b9ced17ff92a752d7ca3513cf 100644 (file)
@@ -7177,11 +7177,39 @@ Zone Options
    to other zone types.
 
    The filename can be generated parametrically by including special
-   tokens in the string: the first instance of ``$name`` in the string
-   is replaced with the zone name in lower case; the first instance of
-   ``$type`` is replaced with the zone type -- i.e., ``primary``,
-   ``secondary``, etc); and the first instance of ``$view`` is replaced
-   with the view name. These tokens are case-insensitive.
+   tokens in the string. The first instance of each token is replaced
+   as shown in the table below; any subsequent uses of the same token are
+   ignored.
+
+   An alternate set of tokens has been provided for compatibility with other
+   servers which use a different format for filename templates.
+
+   All tokens are case-insensitive.
+
+   +-------------+------------+-----------------------------------------------+
+   | Token       | Alternate  | Replaced with:                                |
+   +=============+============+===============================================+
+   | ``$name``   | ``%s``     |  Zone name, in lower case                     |
+   +-------------+------------+-----------------------------------------------+
+   | ``$type``   | ``%t``     |  Zone type (``primary``, ``secondary``, etc)  |
+   +-------------+------------+-----------------------------------------------+
+   | ``$view``   | ``%v``     |  View name                                    |
+   +-------------+------------+-----------------------------------------------+
+   | ``$label1`` | ``%z``     |  Top-level domain name (or ``.`` for the      |
+   |             |            |  root zone)                                   |
+   +-------------+------------+-----------------------------------------------+
+   | ``$label2`` | ``%y``     |  Second label under the TLD (or ``.`` if      |
+   |             |            |  there is no second label)                    |
+   +-------------+------------+-----------------------------------------------+
+   | ``$label3`` | ``%x``     |  Third label under the TLD (or ``.`` if       |
+   |             |            |  there is no third label)                     |
+   +-------------+------------+-----------------------------------------------+
+   | ``$char1``  | ``%1``     |  First character of the zone name             |
+   +-------------+------------+-----------------------------------------------+
+   | ``$char2``  | ``%2``     |  Second character of the zone name (or ``.``) |
+   +-------------+------------+-----------------------------------------------+
+   | ``$char3``  | ``%3``     |  Third character of the zone name (or ``.``)  |
+   +-------------+------------+-----------------------------------------------+
 
 :any:`forward`
    This option is only meaningful if the zone has a forwarders list. The ``only`` value
index a25aeb8967537b3c10722fb90000e7fd04c8e0cb..b7350b9c2a654d5d45b3ba74a869c48f4434e22c 100644 (file)
@@ -1783,10 +1783,20 @@ setstring(dns_zone_t *zone, char **field, const char *value) {
        *field = copy;
 }
 
+typedef struct foundtoken foundtoken_t;
+typedef isc_result_t (*tokenparse_t)(const dns_zone_t *zone,
+                                    const foundtoken_t *token,
+                                    isc_buffer_t *b);
+struct foundtoken {
+       const char *pos;
+       size_t len;
+       tokenparse_t parse;
+};
+
 static int
-position_order(const void *a, const void *b) {
+foundtoken_order(const void *a, const void *b) {
        /* sort char pointers in order of which occurs first in memory */
-       return (char *)*(char **)a - (char *)*(char **)b;
+       return ((foundtoken_t *)a)->pos - ((foundtoken_t *)b)->pos;
 }
 
 static isc_result_t
@@ -1801,16 +1811,157 @@ putmem(isc_buffer_t *b, const char *base, size_t length) {
        return ISC_R_SUCCESS;
 }
 
+static isc_result_t
+tokenparse_type(const dns_zone_t *zone, const foundtoken_t *token,
+               isc_buffer_t *b) {
+       const char *typename = dns_zonetype_name(zone->type);
+
+       UNUSED(token);
+
+       return putmem(b, typename, strlen(typename));
+}
+
+static isc_result_t
+tokenparse_name(const dns_zone_t *zone, const foundtoken_t *token,
+               isc_buffer_t *b) {
+       isc_result_t result;
+       char buf[DNS_NAME_FORMATSIZE];
+       dns_fixedname_t fn;
+       dns_name_t *name = dns_fixedname_initname(&fn);
+
+       UNUSED(token);
+
+       result = dns_name_downcase(&zone->origin, name);
+       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+       dns_name_format(name, buf, sizeof(buf));
+       result = putmem(b, buf, strlen(buf));
+
+       return result;
+}
+
+static isc_result_t
+tokenparse_view(const dns_zone_t *zone, const foundtoken_t *token,
+               isc_buffer_t *b) {
+       UNUSED(token);
+
+       return putmem(b, zone->view->name, strlen(zone->view->name));
+}
+
+static isc_result_t
+tokenparse_char(const dns_zone_t *zone, const foundtoken_t *token,
+               isc_buffer_t *b) {
+       isc_result_t result;
+       char buf[DNS_NAME_FORMATSIZE];
+       dns_fixedname_t fn;
+       dns_name_t *name = dns_fixedname_initname(&fn);
+       size_t chartokidx;
+       char c;
+
+       result = dns_name_downcase(&zone->origin, name);
+       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+       dns_name_format(name, buf, sizeof(buf));
+
+       chartokidx = token->pos[token->len - 1] - '1';
+       INSIST(chartokidx <= 2);
+       if (chartokidx < strlen(buf)) {
+               c = buf[chartokidx];
+       } else {
+               c = '.';
+       }
+       result = putmem(b, &c, 1);
+
+       return result;
+}
+
+static isc_result_t
+tokenparse_label(const dns_zone_t *zone, const foundtoken_t *token,
+                isc_buffer_t *b) {
+       isc_result_t result;
+       char buf[DNS_NAME_FORMATSIZE];
+       dns_fixedname_t fn;
+       dns_name_t *name = dns_fixedname_initname(&fn);
+       unsigned int labels = dns_name_countlabels(&zone->origin);
+       char labeltokidx;
+       int ilabel = -1;
+
+       result = dns_name_fromstring(name, ".", NULL, 0, NULL);
+       INSIST(result == ISC_R_SUCCESS);
+
+       labeltokidx = token->pos[token->len - 1];
+       if (token->len == 2) {
+               /*
+                * %z, %y, %x pattern
+                */
+               INSIST(labeltokidx >= 'x' && labeltokidx <= 'z');
+       } else {
+               /*
+                * $label1, $label2, $label3 pattern
+                */
+               INSIST(labeltokidx >= '1' && labeltokidx <= '3');
+       }
+
+       /*
+        * Even if implicit, the root label counts as one label.
+        */
+       if (labeltokidx == '1' || labeltokidx == 'z') {
+               ilabel = labels - 2;
+       } else if (labeltokidx == '2' || labeltokidx == 'y') {
+               ilabel = labels - 3;
+       } else if (labeltokidx == '3' || labeltokidx == 'x') {
+               ilabel = labels - 4;
+       }
+
+       if (ilabel >= 0) {
+               dns_name_getlabelsequence(&zone->origin, ilabel, 1, name);
+               result = dns_name_downcase(name, name);
+               RUNTIME_CHECK(result == ISC_R_SUCCESS);
+       }
+       dns_name_format(name, buf, sizeof(buf));
+       result = putmem(b, buf, strlen(buf));
+
+       return result;
+}
+
+typedef struct {
+       const char *name;
+       tokenparse_t parse;
+} token_t;
+static const token_t tokens[] = {
+       { "$type", tokenparse_type },    { "$name", tokenparse_name },
+       { "$view", tokenparse_view },    { "$char1", tokenparse_char },
+       { "$char2", tokenparse_char },   { "$char3", tokenparse_char },
+       { "$label1", tokenparse_label }, { "$label2", tokenparse_label },
+       { "$label3", tokenparse_label }, { "%t", tokenparse_type },
+       { "%s", tokenparse_name },       { "%v", tokenparse_view },
+       { "%1", tokenparse_char },       { "%2", tokenparse_char },
+       { "%3", tokenparse_char },       { "%z", tokenparse_label },
+       { "%y", tokenparse_label },      { "%x", tokenparse_label }
+};
+
 /*
- * Set the masterfile field, expanding $name to the zone name,
- * $type to the zone type, and $view to the view name. Cap the
- * length at PATH_MAX.
+ * Set the masterfile field, expanding:
+ *
+ *    - $name or "%s" to the zone name
+ *    - $type or "%t" to the zone type
+ *    - $view or "%v" to the view name.
+ *    - $char1 or "%1" to the first character of the zone name
+ *    - $char2 or "%2" to the second character of the zone name (or a dot if
+ *      there is no second character)
+ *    - $char3 or "%3" to the third character of the zone name (or a dot if
+ *      there is no third character)
+ *    - $label1 or "%z" to the toplevel domain of the zone (or a dot if it is
+ *      the TLD)
+ *    - $label2 or "%y" to the next label under the toplevel domain (or a dot if
+ *      there is no next label)
+ *    - $label2 or "%x" to the next-next label under the toplevel domain (or a
+ *      dot if there is no next-next label)
+ *
+ * Cap the length at PATH_MAX.
  */
 static void
 setfilename(dns_zone_t *zone, char **field, const char *value) {
        isc_result_t result;
-       char *t = NULL, *n = NULL, *v = NULL;
-       char *positions[3];
+       foundtoken_t founds[ARRAY_SIZE(tokens)];
        char filename[PATH_MAX];
        isc_buffer_t b;
        size_t tags = 0;
@@ -1820,19 +1971,16 @@ setfilename(dns_zone_t *zone, char **field, const char *value) {
                return;
        }
 
-       t = strcasestr(value, "$type");
-       if (t != NULL) {
-               positions[tags++] = t;
-       }
-
-       n = strcasestr(value, "$name");
-       if (n != NULL) {
-               positions[tags++] = n;
-       }
+       for (size_t i = 0; i < ARRAY_SIZE(tokens); i++) {
+               const token_t *token = &tokens[i];
+               const char *p = strcasestr(value, token->name);
 
-       v = strcasestr(value, "$view");
-       if (v != NULL) {
-               positions[tags++] = v;
+               if (p != NULL) {
+                       founds[tags++] =
+                               (foundtoken_t){ .pos = p,
+                                               .len = strlen(token->name),
+                                               .parse = token->parse };
+               }
        }
 
        if (tags == 0) {
@@ -1843,38 +1991,20 @@ setfilename(dns_zone_t *zone, char **field, const char *value) {
        isc_buffer_init(&b, filename, sizeof(filename));
 
        /* sort the tag offsets in order of occurrence */
-       qsort(positions, tags, sizeof(char *), position_order);
+       qsort(founds, tags, sizeof(foundtoken_t), foundtoken_order);
 
        const char *p = value;
        for (size_t i = 0; i < tags; i++) {
-               size_t tokenlen = 0;
+               foundtoken_t *token = &founds[i];
 
-               CHECK(putmem(&b, p, positions[i] - p));
+               CHECK(putmem(&b, p, token->pos - p));
 
-               p = positions[i];
+               p = token->pos;
                INSIST(p != NULL);
-               if (p == n) {
-                       dns_fixedname_t fn;
-                       dns_name_t *name = dns_fixedname_initname(&fn);
-                       char namebuf[DNS_NAME_FORMATSIZE];
-
-                       result = dns_name_downcase(&zone->origin, name);
-                       RUNTIME_CHECK(result == ISC_R_SUCCESS);
-                       dns_name_format(name, namebuf, sizeof(namebuf));
-                       CHECK(putmem(&b, namebuf, strlen(namebuf)));
-                       tokenlen = 5; /* "$name" */
-               } else if (p == t) {
-                       const char *typename = dns_zonetype_name(zone->type);
-                       CHECK(putmem(&b, typename, strlen(typename)));
-                       tokenlen = 5; /* "$type" */
-               } else if (p == v) {
-                       CHECK(putmem(&b, zone->view->name,
-                                    strlen(zone->view->name)));
-                       tokenlen = 5; /* "$view" */
-               }
+               CHECK(token->parse(zone, token, &b));
 
                /* Advance the input pointer past the token */
-               p += tokenlen;
+               p += founds[i].len;
        }
 
        const char *end = value + strlen(value);
index 1525798f919c125be190515272c206abd0cb9064..58fad0a1aa8d18f9894e6f560a03a70d7ac2db36 100644 (file)
@@ -33,7 +33,7 @@
 #include <tests/dns.h>
 
 typedef struct {
-       const char *input, *expected;
+       const char *name, *input, *expected;
 } zonefile_test_params_t;
 
 static int
@@ -52,45 +52,94 @@ ISC_LOOP_TEST_IMPL(filename) {
        isc_result_t result;
        dns_zone_t *zone = NULL;
        const zonefile_test_params_t tests[] = {
-               { "$name", "example.com" },
-               { "$name.db", "example.com.db" },
-               { "./dir/$name.db", "./dir/example.com.db" },
-               { "$type", "primary" },
-               { "$type-file", "primary-file" },
-               { "./dir/$type", "./dir/primary" },
-               { "./$type/$name.db", "./primary/example.com.db" },
-               { "./$TyPe/$NAmE.db", "./primary/example.com.db" },
-               { "./$name/$type", "./example.com/primary" },
-               { "$name.$type", "example.com.primary" },
-               { "$type$name", "primaryexample.com" },
-               { "$type$type", "primary$type" },
-               { "$name$name", "example.com$name" },
-               { "typename", "typename" },
-               { "$view", "local" },
-               { "./$type/$view-$name.db", "./primary/local-example.com.db" },
-               { "./$view/$type-$name.db", "./local/primary-example.com.db" },
-               { "./$name/$view-$type.db", "./example.com/local-primary.db" },
-               { "", "" },
+               { "example.COM", "$name", "example.com" },
+               { "example.COM", "$name.db", "example.com.db" },
+               { "example.COM", "./dir/$name.db", "./dir/example.com.db" },
+               { "example.COM", "%s", "example.com" },
+               { "example.COM", "%s.db", "example.com.db" },
+               { "example.COM", "./dir/%s.db", "./dir/example.com.db" },
+               { "example.COM", "$type", "primary" },
+               { "example.COM", "$type-file", "primary-file" },
+               { "example.COM", "./dir/$type", "./dir/primary" },
+               { "example.COM", "./$type/$name.db",
+                 "./primary/example.com.db" },
+               { "example.COM", "%t", "primary" },
+               { "example.COM", "%t-file", "primary-file" },
+               { "example.COM", "./dir/%t", "./dir/primary" },
+               { "example.COM", "./%t/%s.db", "./primary/example.com.db" },
+               { "example.COM", "./$TyPe/$NAmE.db",
+                 "./primary/example.com.db" },
+               { "example.COM", "./$name/$type", "./example.com/primary" },
+               { "example.COM", "$name.$type", "example.com.primary" },
+               { "example.COM", "$type$name", "primaryexample.com" },
+               { "example.COM", "$type$type", "primary$type" },
+               { "example.COM", "$name$name", "example.com$name" },
+               { "example.COM", "typename", "typename" },
+               { "example.COM", "$view", "local" },
+               { "example.COM", "%v", "local" },
+               { "example.COM", "./$type/$view-$name.db",
+                 "./primary/local-example.com.db" },
+               { "example.COM", "./$view/$type-$name.db",
+                 "./local/primary-example.com.db" },
+               { "example.COM", "./$name/$view-$type.db",
+                 "./example.com/local-primary.db" },
+               { "example.COM", "./%s/%v-%t.db",
+                 "./example.com/local-primary.db" },
+               { "example.COM", "", "" },
+               { "example.COM", "$char1", "e" },
+               { "example.COM", "$char2", "x" },
+               { "example.COM", "$char3", "a" },
+               { "example.COM", "%1", "e" },
+               { "example.COM", "%2", "x" },
+               { "example.COM", "%3", "a" },
+               { "example.COM", "$label1", "com" },
+               { "example.COM", "$label2", "example" },
+               { "example.COM", "$label3", "." },
+               { "example.COM", "%z", "com" },
+               { "example.COM", "%y", "example" },
+               { "example.COM", "%x", "." },
+               { "example", "$label1", "example" },
+               { "example", "$label2", "." },
+               { "example", "$label3", "." },
+               { "a.b.c.d.e", "$label1", "e" },
+               { "a.b.c.d.e", "$label2", "d" },
+               { "a.b.c.d.e", "$label3", "c" },
+               { "a.b.c", "$char1", "a" },
+               { "a.b.c", "$char2", "." },
+               { "a.b.c", "$char3", "b" },
+               { "a.b.c", "%1", "a" },
+               { "a.b.c", "%2", "." },
+               { "a.b.c", "%3", "b" },
+               { "a", "%1", "a" },
+               { "a", "%2", "." },
+               { "a", "%3", "." },
+               { "a.b.c.d", "%1$char2%3$label1%x", "a.bdb" }
        };
 
        dns_view_t *view = NULL;
        result = dns_test_makeview("local", false, false, &view);
        assert_int_equal(result, ISC_R_SUCCESS);
 
-       /* use .COM here to test that the name is correctly downcased */
-       result = dns_test_makezone("example.COM", &zone, view, false);
-       assert_int_equal(result, ISC_R_SUCCESS);
-
-       dns_zone_setview(zone, view);
-       dns_view_detach(&view);
-
        for (size_t i = 0; i < ARRAY_SIZE(tests); i++) {
+               result = dns_test_makezone(tests[i].name, &zone, view, false);
+               assert_int_equal(result, ISC_R_SUCCESS);
+
+               dns_zone_setview(zone, view);
                dns_zone_setfile(zone, tests[i].input, NULL,
                                 dns_masterformat_text,
                                 &dns_master_style_default);
                assert_string_equal(dns_zone_getfile(zone), tests[i].expected);
+
+               dns_zone_detach(&zone);
        }
 
+       /* use .COM here to test that the name is correctly downcased */
+       result = dns_test_makezone("example.COM", &zone, view, false);
+       assert_int_equal(result, ISC_R_SUCCESS);
+
+       dns_zone_setview(zone, view);
+       dns_view_detach(&view);
+
        /* test PATH_MAX overrun */
        char longname[PATH_MAX] = { 0 };
        memset(longname, 'x', sizeof(longname) - 1);