Index: trunk/DNSDB.pm
===================================================================
--- trunk/DNSDB.pm	(revision 679)
+++ trunk/DNSDB.pm	(revision 685)
@@ -1603,8 +1603,26 @@
 # really have a sane way to handle this type of expansion at the moment
 # due to the size of the address space.
-# Takes a reference to a template string to be expanded, and an IP to use in the replacement.
+# Takes a reference to a template string to be expanded, an IP to use in the replacement,
+# an optional netblock for the %ngb (net, gw, bcast) expansion, and an optional index
+# number for %c "n'th usable IP in block/range" patterns.
+# Alters the template string referred.
 sub _template4_expand {
+  # ugh pthui
+  my $self;
+  $self = shift if ref($_[0]) eq 'DNSDB';
+
   my $tmpl = shift;
   my $ip = shift;
+  my $subnet = shift;   # for %ngb and %c
+  my $ipindex = shift;  # for %c
+
+  # blank $tmpl on config template_skip_0 or template_skip_255, unless we have a %ngb
+  if ($$tmpl !~ /\%-?n-?g-?b\%/) {
+    if ( ($ip =~ /\.0$/ && $self->{template_skip_0}) ||
+         ($ip =~ /\.255$/ && $self->{template_skip_255}) ) {
+      $$tmpl = '';
+      return;
+    }
+  }
 
   my @ipparts = split /\./, $ip;
@@ -1615,4 +1633,101 @@
     push @ippad, sprintf("%0.3u", $_);
   }
+
+  # Two or three consecutive separator characters (_ or -) should be rare - users that use them
+  # anywhere other than punycoded internationalized domains get to keep the pieces when it breaks.
+  # We clean up the ones that we may inadvertently generate after replacing %c and %ngb%
+  my ($thrsep) = ($$tmpl =~ /[_-]{3}/);
+  my ($twosep) = ($$tmpl =~ /[_-]{2}/);
+
+  # Take the simplest path to pattern substitution;  replace only exactly the %c or %ngb%
+  # patterns as-is.  Then check after to see if we've caused doubled separator characters (- or _)
+  # and eliminate them, but only if the original template didn't have them already.  Also
+  # unconditionally drop separator characters immediately before a dot;  these do not always
+  # strictly make the label invalid but almost always, and any exceptions should never show up
+  # in a template record that expands to "many" real records anyway.
+
+  # %ngb and %c require a netblock
+  if ($subnet) {
+    # extract the fragments
+    my ($ngb,$n,$g,$b) = ($$tmpl =~ /(\%(-?n)(-?g)(-?b)\%)/);
+    my ($c) = ($$tmpl =~ /(\%-?c)/);  my $nld = '';  my $cld = '';
+    $c = '' if !$c;
+    my $skipgw = ($c =~ /\%-c/ ? 0 : 1);
+    my $ipkill = 0;
+
+##fixme: still have one edge case not handled well:
+# %c%n-gb%
+# do we drop the record as per -g, or publish the record with an index of 1 as per %c?
+# arguably this is a "that's a STUPID question!" case
+
+    if ($c) {
+      # "n'th usable IP in the block" pattern.  We need the caller to provide an index
+      # number otherwise we see exponential time growth because we have to iterate over
+      # the whole block to map the IP back to an index.  :/
+      # NetAddr::IP does not have a method for asking "what index is IP <foo> at?"
+
+      # no index, or index == 0, (AKA network address), or IP == broadcast, blank the index fragment
+      if (!$ipindex || ($$subnet->broadcast->addr eq $ip)) {
+        $$tmpl =~ s/$c//;
+      } else {
+        # if we have %c, AKA "skip the gateway", and we're on the nominal gateway IP, blank the index fragment
+        if ($skipgw && $$subnet->first->addr eq $ip) {
+          $$tmpl =~ s/$c//;
+        }
+        # else replace the index fragment with the passed index minus $skipgw, so that we can start the
+        # resulting index at 1 on net+2
+        else {
+          $$tmpl =~ s/$c/($ipindex-$skipgw)/e;
+        }
+      }
+    } # if ($c)
+
+    if ($ngb) {
+      # individually check the network, standard gateway (net+1) IP, and broadcast IP
+      # blank $$tmpl if n, g, or b was prefixed with - (this allows "hiding" net/gw/bcast entries)
+
+      if ($$subnet->network->addr eq $ip) {
+        if ($n eq '-n') {
+          $$tmpl = '';
+        } else {
+          $$tmpl =~ s/$ngb/net/;
+          $ipkill = 1;
+        }
+      } elsif ($$subnet->first->addr eq $ip) {
+        if ($g eq '-g') {
+          $$tmpl = '';
+        } else {
+          $$tmpl =~ s/$ngb/gw/;
+          $ipkill = 1;
+        }
+      } elsif ($$subnet->broadcast->addr eq $ip) {
+        if ($b eq '-b') {
+          $$tmpl = '';
+        } else {
+          $$tmpl =~ s/$ngb/bcast/;
+          $ipkill = 1;
+        }
+      } else {
+        $$tmpl =~ s/$ngb//;
+      }
+    }
+
+    # We don't (usually) want to expand the IP-related patterns on the -net, -gw, or -bcast IPs.
+    # Arguably this is another place for another config knob, or possibly further extension of
+    # the template pattern to control it on a per-subnet basis.
+    if ($ipkill) {
+      # kill common IP patterns
+      $$tmpl =~ s/\%[_.-]?[irdh]//;
+      # kill IP octet patterns
+      $$tmpl =~ s/\%[1234][dh0](?:[_.-]\%[1234][dh0]){0,3}//;
+    }
+
+    # and now clean up to make sure we leave a valid DNS label... mostly.  Should arguably
+    # split on /\./ and process each label separately.
+    $$tmpl =~ s/([_-]){3}/$1/ if !$thrsep;
+    $$tmpl =~ s/([_-]){2}/$1/ if !$twosep;
+    $$tmpl =~ s/[_-]\././;
+
+  } # if ($subnet)
 
   # IP substitutions in template records:
@@ -1650,4 +1765,5 @@
   $$tmpl =~ s/\%([1234])h/$iphex[$1-1]/g;
   $$tmpl =~ s/\%([1234])0/$ippad[$1-1]/g;
+
 } # _template4_expand()
 
@@ -1661,5 +1777,5 @@
     my $tmphost = $hname;
     # we don't actually need to test with the real IP passed;  that saves a bit of fiddling.
-    _template4_expand(\$tmphost, '10.10.10.10');
+    DNSDB::_template4_expand(\$tmphost, '10.10.10.10');
     if ($tmphost =~ /\%/ || lc($tmphost) !~ /^(?:\*\.)?(?:[0-9a-z_.-]+)$/) {
       $errstr = "Invalid template $hname";
@@ -5965,5 +6081,5 @@
 ##  forked process
   sub __publish_subnet {
-    my $obj = shift;	# *sigh*  need to pass in the DNSDB object so we can read a couple of options
+    my $self = shift;	# *sigh*  need to pass in the DNSDB object so we can read a couple of options
     my $sub = shift;
     my $recflags = shift;
@@ -5980,10 +6096,10 @@
 
     my $iplist = $sub->splitref(32);
+    my $ipindex = -1;
     foreach (@$iplist) {
       my $ip = $_->addr;
+      $ipindex++;
       # make as if we split the non-octet-aligned block into octet-aligned blocks as with SOA
       my $lastoct = (split /\./, $ip)[3];
-      next if $ip =~ /\.0$/ && $obj->{template_skip_0};
-      next if $ip =~ /\.255$/ && $obj->{template_skip_255};
       next if $$recflags{$ip}; # && $self->{skip_bcast_255}
       $$recflags{$ip}++;
@@ -5991,6 +6107,8 @@
       my $rec = $hpat;	# start fresh with the template for each IP
 ##fixme:  there really isn't a good way to handle sub-/24 zones here.  This way at least
-# seems less bad than some alternatives.  
-      _template4_expand(\$rec, $ip);
+# seems less bad than some alternatives.
+      $self->_template4_expand(\$rec, $ip, \$sub, $ipindex);
+      # _template4_expand may blank $rec;  if so, don't publish a record
+      next if !$rec;
       if ($ptronly || $zone->masklen > 24) {
         print $fh "^$lastoct.$arpabase:$rec:$ttl:$stamp:$loc\n" or die $!;
