IPv6 Addresses

aspect 2015-01: Having noticed that tcllib_ip's advertised support for IPv6 is flaky, and the module itself is rather messy for my taste to clean up, I did what any reasonable person would and implemented my own parser.

In modern Tcl we have bignums, so the easiest way to deal with IPv6 addresses is just convert them to (128-bit) integers, do arithmetic, then format again as IPv6.

To complete this module, it should probably have some knowledge of address types, parse Base85 and take some flags to format. Any kind of tcllib_ip functionality ought to be built on (ip, mask) pairs, and seriously consider a more consistent API. These are good exercises for the reader.

The Perl IPv6 module at http://cpansearch.perl.org/src/TMONROE/Net-IPv6Addr-0.2/ is a decent place to read more about IPv6 addresses, and has the relevant RFCs included.

if 0 {
    Native representation of an IP address (4 or 6) is as an integer.  Use [expr] to work with it.

    This parser is pretty permissive, but assertions should catch actual errors.

    Useful procs:

      set addr_as_int [ipv6 parse 2002::127.0.0.1]
      incr addr_as_int 0x01000000
      ipv6 format $addr_as_int

    Base85 isn't fully supported yet, but is a trivial addition.

    To make computation useful needs some simple operations on {ip mask} pairs.
    netaddr-tcl is a better model than tcllib_ip.
}



namespace eval ipv6 {

    namespace ensemble create -map {parse Parse format Format}

    # some helpers:
    #interp alias {} assert {} debug assert
    if {[info commands assert] eq ""} {
        proc assert {expr} {
            if {![uplevel 1 [list expr $expr]]} {
                throw ASSERT "Assertion failed! $expr"
            }
        }
        proc all {args} {
            set ls [lindex $args end]
            set cmd [lrange $args 0 end-1]
            if {$cmd eq ""} {
                set cmd K
            }
            foreach x $ls {
                if {![uplevel 1 {*}$cmd [list $x]]} {return false}
            }
            return true
        }
    }

    proc Parse {str} {
        try {
            set x [ParseAddr $str]
            assert {[string length $x] == 32}
        } on error {e o} {
            dict incr o -level
            dict set o -errorcode {IPv6 PARSE ERROR}
            return {*}$o "IPv6 parse error: $e"
        }
        scan $x %llx x
        return $x
    }

    # Affix::Affix
    proc ParseAddr {str} {
        if {[regexp {^(.*)::(.*)$} $str -> pre suf]} {
            set pre [ParseAffix $pre]
            set suf [ParseAffix $suf]
            set nz [expr {32 - [string length $pre] - [string length $suf]}]
            string cat $pre [string repeat 0 $nz] $suf
        } else {
            ParseAffix $str
        }
    }

    # a Word(:Word)*
    proc ParseAffix {str} {
        set parts [split $str :]
        join [lmap word $parts {ParseWord $word}] {}
    }

    # a single part of an address:  [:xdigit:]{1,3}  or  dotted-quad
    proc ParseWord {str} {
        if {[regexp {^(\d+)\.(\d+)\.(\d+)\.(\d+)$} $str -> o1 o2 o3 o4]} {
            assert {[all {expr 256 >} [list $o1 $o2 $o3 $o4]]}
            format {%02X%02X%02X%02X} $o1 $o2 $o3 $o4
        } else {
            assert {[string is xdigit -strict $str]}
            assert {[string length $str] <= 4}
            format %04s $str
        }
    }

    # none of the variants are exposed, for want of a nice interface
    proc Format {n} {
        Compress [FormatLong $n]
    }
    proc Fourmat {n} {
        Compress [Fourify [FormatLong $n]]
    }
    proc FourmatLong {n} {
        Fourify [FormatLong $n]
    }
    proc Compress {s} {
        regsub {(^|:)(0($|:))+} $s :: s
        return $s
    }
    proc FormatLong {n} {
        set s [format %032llX $n]
        set parts [lmap {a b c d} [split $s {}] {
            format %X [string cat 0x $a $b $c $d]
        }]
        set s [join $parts :]
    }
    proc Fourify {s} {
        regexp {(.*):([^:]*):([^:]*)$} $s -> pre x y
        scan $x %X x
        scan $y %X y
        set a [expr {$x >> 8}]
        set b [expr {$x & 0xff}]
        set c [expr {$y >> 8}]
        set d [expr {$y & 0xff}]
        set v4 [format {%d.%d.%d.%d} $a $b $c $d]
        string cat $pre : $v4
    }

    proc Base85 {n} {
        set base85 [split [string cat {*}{
             0123456789
             ABCDEFGHIJKLMNOPQRSTUVWXYZ
             abcdefghijklmnopqrstuvwxyz
             {!#$%&()*+-;<=>?@^_`{|}~}
        }] {}]
        loop i 0 20 {
            lappend res [lindex $base85 [expr {$n % 85}]]
            set n [expr {$n / 85}]
        }
        join [lreverse $res] {}
    }

    proc test {} {
        foreach {short long} {
                1080::8:800:200C:417A           1080:0:0:0:8:800:200C:417A
                FF01::43                        FF01:0:0:0:0:0:0:43
                ::1                             0:0:0:0:0:0:0:1
                ::                              0:0:0:0:0:0:0:0
                ::13.1.68.3                     0:0:0:0:0:0:13.1.68.3
                ::FFFF:129.144.52.38            0:0:0:0:0:FFFF:129.144.52.38
        } {
            assert {[set a [ipv6 parse $short]] eq [ipv6 parse $long]}
            assert {$short eq [ipv6 format $a] || $short eq [ipv6::Fourmat $a]}
            assert {$long eq [ipv6::FormatLong $a] || $long eq [ipv6::FourmatLong $a]}
        }
    }

}

ipv6::test