From 99d2ae5aa60936d41f1f9180223417188c3ad866 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 12 Aug 2017 18:10:02 +0200 Subject: [PATCH] Add IPv6 zone index support Fixes https://github.com/whitequark/ipaddr.js/issues/60 --- README.md | 11 ++++-- ipaddr.min.js | 2 +- lib/ipaddr.js | 78 ++++++++++++++++++++++++++++++----------- src/ipaddr.coffee | 61 ++++++++++++++++++++++++-------- test/ipaddr.test.coffee | 29 +++++++++++++++ 5 files changed, 144 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e2a56a0..938790e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ or throws an `Error` if the passed string is not a valid representation of an IP address. The `ipaddr.process` method works just like the `ipaddr.parse` one, but it -automatically converts IPv4-mapped IPv6 addresses to their IPv4 couterparts +automatically converts IPv4-mapped IPv6 addresses to their IPv4 counterparts before returning. It is useful when you have a Node.js instance listening on an IPv6 socket, and the `net.ivp6.bindv6only` sysctl parameter (or its equivalent on non-Linux OS) is set to 0. In this case, you can accept IPv4 @@ -154,6 +154,13 @@ var addr = ipaddr.parse("2001:db8:10::1234:DEAD"); addr.parts // => [0x2001, 0xdb8, 0x10, 0, 0, 0, 0x1234, 0xdead] ``` +A IPv6 zone index can be accessed via `addr.zoneId`: + +```js +var addr = ipaddr.parse("2001:db8::%eth0"); +addr.zoneId // => 'eth0' +``` + #### IPv4 properties `toIPv4MappedAddress()` will return a corresponding IPv4-mapped IPv6 address. @@ -180,7 +187,7 @@ ipaddr.IPv4.subnetMaskFromPrefixLength("24") == "255.255.255.0" ipaddr.IPv4.subnetMaskFromPrefixLength("29") == "255.255.255.248" ``` -`broadcastAddressFromCIDR()` will return the broadcast address for a given IPv4 interface and netmask in CIDR notation. +`broadcastAddressFromCIDR()` will return the broadcast address for a given IPv4 interface and netmask in CIDR notation. ```js ipaddr.IPv4.broadcastAddressFromCIDR("172.0.0.1/24") == "172.0.0.255" ``` diff --git a/ipaddr.min.js b/ipaddr.min.js index 9472242..a52373a 100644 --- a/ipaddr.min.js +++ b/ipaddr.min.js @@ -1 +1 @@ -(function(){var r,t,n,e,i,o,a,s;t={},s=this,"undefined"!=typeof module&&null!==module&&module.exports?module.exports=t:s.ipaddr=t,a=function(r,t,n,e){var i,o;if(r.length!==t.length)throw new Error("ipaddr: cannot match CIDR for objects with different lengths");for(i=0;e>0;){if((o=n-e)<0&&(o=0),r[i]>>o!=t[i]>>o)return!1;e-=n,i+=1}return!0},t.subnetMatch=function(r,t,n){var e,i,o,a,s;null==n&&(n="unicast");for(o in t)for(!(a=t[o])[0]||a[0]instanceof Array||(a=[a]),e=0,i=a.length;e=0;t=n+=-1){if(!((e=this.octets[t])in a))return null;if(o=a[e],i&&0!==o)return null;8!==o&&(i=!0),r+=o}return 32-r},r}(),n="(0?\\d+|0x[a-f0-9]+)",e={fourOctet:new RegExp("^"+n+"\\."+n+"\\."+n+"\\."+n+"$","i"),longValue:new RegExp("^"+n+"$","i")},t.IPv4.parser=function(r){var t,n,i,o,a;if(n=function(r){return"0"===r[0]&&"x"!==r[1]?parseInt(r,8):parseInt(r)},t=r.match(e.fourOctet))return function(){var r,e,o,a;for(a=[],r=0,e=(o=t.slice(1,6)).length;r4294967295||a<0)throw new Error("ipaddr: address outside defined range");return function(){var r,t;for(t=[],o=r=0;r<=24;o=r+=8)t.push(a>>o&255);return t}().reverse()}return null},t.IPv6=function(){function r(r){var t,n,e,i,o,a;if(16===r.length)for(this.parts=[],t=n=0;n<=14;t=n+=2)this.parts.push(r[t]<<8|r[t+1]);else{if(8!==r.length)throw new Error("ipaddr: ipv6 part count should be 8 or 16");this.parts=r}for(e=0,i=(a=this.parts).length;e>8),r.push(255&e);return r},r.prototype.toNormalizedString=function(){var r;return function(){var t,n,e,i;for(i=[],t=0,n=(e=this.parts).length;t>8,255&r,n>>8,255&n])},r.prototype.prefixLengthFromSubnetMask=function(){var r,t,n,e,i,o,a;for(a={0:16,32768:15,49152:14,57344:13,61440:12,63488:11,64512:10,65024:9,65280:8,65408:7,65472:6,65504:5,65520:4,65528:3,65532:2,65534:1,65535:0},r=0,i=!1,t=n=7;n>=0;t=n+=-1){if(!((e=this.parts[t])in a))return null;if(o=a[e],i&&0!==o)return null;16!==o&&(i=!0),r+=o}return 128-r},r}(),i="(?:[0-9a-f]+::?)+",o={native:new RegExp("^(::)?("+i+")?([0-9a-f]+)?(::)?$","i"),transitional:new RegExp("^((?:"+i+")|(?:::)(?:"+i+")?)"+n+"\\."+n+"\\."+n+"\\."+n+"$","i")},r=function(r,t){var n,e,i,o,a;if(r.indexOf("::")!==r.lastIndexOf("::"))return null;for(n=0,e=-1;(e=r.indexOf(":",e+1))>=0;)n++;if("::"===r.substr(0,2)&&n--,"::"===r.substr(-2,2)&&n--,n>t)return null;for(a=t-n,o=":";a--;)o+="0:";return":"===(r=r.replace("::",o))[0]&&(r=r.slice(1)),":"===r[r.length-1]&&(r=r.slice(0,-1)),function(){var t,n,e,o;for(o=[],t=0,n=(e=r.split(":")).length;t=0&&t<=32)return[this.parse(n[1]),t];throw new Error("ipaddr: string is not formatted like an IPv4 CIDR range")},t.IPv4.subnetMaskFromPrefixLength=function(r){var n,e;if(r<0||r>32)throw new Error("ipaddr: invalid prefix length");for(e=Array(4).fill(0),n=0;n=0&&t<=128)return[this.parse(n[1]),t];throw new Error("ipaddr: string is not formatted like an IPv6 CIDR range")},t.isValid=function(r){return t.IPv6.isValid(r)||t.IPv4.isValid(r)},t.parse=function(r){if(t.IPv6.isValid(r))return t.IPv6.parse(r);if(t.IPv4.isValid(r))return t.IPv4.parse(r);throw new Error("ipaddr: the address has neither IPv6 nor IPv4 format")},t.parseCIDR=function(r){try{return t.IPv6.parseCIDR(r)}catch(n){n;try{return t.IPv4.parseCIDR(r)}catch(r){throw r,new Error("ipaddr: the address has neither IPv6 nor IPv4 CIDR format")}}},t.fromByteArray=function(r){var n;if(4===(n=r.length))return new t.IPv4(r);if(16===n)return new t.IPv6(r);throw new Error("ipaddr: the binary input is neither an IPv6 nor IPv4 address")},t.process=function(r){var t;return"ipv6"===(t=this.parse(r)).kind()&&t.isIPv4MappedAddress()?t.toIPv4Address():t}}).call(this); \ No newline at end of file +(function(){var r,t,n,e,i,o,a,s,p;t={},s=this,"undefined"!=typeof module&&null!==module&&module.exports?module.exports=t:s.ipaddr=t,a=function(r,t,n,e){var i,o;if(r.length!==t.length)throw new Error("ipaddr: cannot match CIDR for objects with different lengths");for(i=0;e>0;){if((o=n-e)<0&&(o=0),r[i]>>o!=t[i]>>o)return!1;e-=n,i+=1}return!0},t.subnetMatch=function(r,t,n){var e,i,o,a,s;null==n&&(n="unicast");for(o in t)for(!(a=t[o])[0]||a[0]instanceof Array||(a=[a]),e=0,i=a.length;e=0;t=n+=-1){if(!((e=this.octets[t])in a))return null;if(o=a[e],i&&0!==o)return null;8!==o&&(i=!0),r+=o}return 32-r},r}(),n="(0?\\d+|0x[a-f0-9]+)",e={fourOctet:new RegExp("^"+n+"\\."+n+"\\."+n+"\\."+n+"$","i"),longValue:new RegExp("^"+n+"$","i")},t.IPv4.parser=function(r){var t,n,i,o,a;if(n=function(r){return"0"===r[0]&&"x"!==r[1]?parseInt(r,8):parseInt(r)},t=r.match(e.fourOctet))return function(){var r,e,o,a;for(a=[],r=0,e=(o=t.slice(1,6)).length;r4294967295||a<0)throw new Error("ipaddr: address outside defined range");return function(){var r,t;for(t=[],o=r=0;r<=24;o=r+=8)t.push(a>>o&255);return t}().reverse()}return null},t.IPv6=function(){function r(r,t){var n,e,i,o,a,s;if(16===r.length)for(this.parts=[],n=e=0;e<=14;n=e+=2)this.parts.push(r[n]<<8|r[n+1]);else{if(8!==r.length)throw new Error("ipaddr: ipv6 part count should be 8 or 16");this.parts=r}for(i=0,o=(s=this.parts).length;i>8),r.push(255&e);return r},r.prototype.toNormalizedString=function(){var r,t,n;return r=function(){var r,n,e,i;for(i=[],r=0,n=(e=this.parts).length;r>8,255&r,n>>8,255&n])},r.prototype.prefixLengthFromSubnetMask=function(){var r,t,n,e,i,o,a;for(a={0:16,32768:15,49152:14,57344:13,61440:12,63488:11,64512:10,65024:9,65280:8,65408:7,65472:6,65504:5,65520:4,65528:3,65532:2,65534:1,65535:0},r=0,i=!1,t=n=7;n>=0;t=n+=-1){if(!((e=this.parts[t])in a))return null;if(o=a[e],i&&0!==o)return null;16!==o&&(i=!0),r+=o}return 128-r},r}(),i="(?:[0-9a-f]+::?)+",p="%[0-9a-z]{1,}",o={zoneIndex:new RegExp(p,"i"),native:new RegExp("^(::)?("+i+")?([0-9a-f]+)?(::)?("+p+")?$","i"),transitional:new RegExp("^((?:"+i+")|(?:::)(?:"+i+")?)"+n+"\\."+n+"\\."+n+"\\."+n+"(%[0-9a-z]{1,})?$","i")},r=function(r,t){var n,e,i,o,a,s;if(r.indexOf("::")!==r.lastIndexOf("::"))return null;for((s=(r.match(p)||[])[0])&&(s=s.substring(1),r=r.replace(/%.+$/,"")),n=0,e=-1;(e=r.indexOf(":",e+1))>=0;)n++;if("::"===r.substr(0,2)&&n--,"::"===r.substr(-2,2)&&n--,n>t)return null;for(a=t-n,o=":";a--;)o+="0:";return":"===(r=r.replace("::",o))[0]&&(r=r.slice(1)),":"===r[r.length-1]&&(r=r.slice(0,-1)),t=function(){var t,n,e,o;for(o=[],t=0,n=(e=r.split(":")).length;t=0&&t<=32)return[this.parse(n[1]),t];throw new Error("ipaddr: string is not formatted like an IPv4 CIDR range")},t.IPv4.subnetMaskFromPrefixLength=function(r){var n,e;if(r<0||r>32)throw new Error("ipaddr: invalid prefix length");for(e=Array(4).fill(0),n=0;n=0&&t<=128)return[this.parse(n[1]),t];throw new Error("ipaddr: string is not formatted like an IPv6 CIDR range")},t.isValid=function(r){return t.IPv6.isValid(r)||t.IPv4.isValid(r)},t.parse=function(r){if(t.IPv6.isValid(r))return t.IPv6.parse(r);if(t.IPv4.isValid(r))return t.IPv4.parse(r);throw new Error("ipaddr: the address has neither IPv6 nor IPv4 format")},t.parseCIDR=function(r){try{return t.IPv6.parseCIDR(r)}catch(n){n;try{return t.IPv4.parseCIDR(r)}catch(r){throw r,new Error("ipaddr: the address has neither IPv6 nor IPv4 CIDR format")}}},t.fromByteArray=function(r){var n;if(4===(n=r.length))return new t.IPv4(r);if(16===n)return new t.IPv6(r);throw new Error("ipaddr: the binary input is neither an IPv6 nor IPv4 address")},t.process=function(r){var t;return"ipv6"===(t=this.parse(r)).kind()&&t.isIPv4MappedAddress()?t.toIPv4Address():t}}).call(this); \ No newline at end of file diff --git a/lib/ipaddr.js b/lib/ipaddr.js index 841df0e..a1107da 100644 --- a/lib/ipaddr.js +++ b/lib/ipaddr.js @@ -1,5 +1,5 @@ (function() { - var expandIPv6, ipaddr, ipv4Part, ipv4Regexes, ipv6Part, ipv6Regexes, matchCIDR, root; + var expandIPv6, ipaddr, ipv4Part, ipv4Regexes, ipv6Part, ipv6Regexes, matchCIDR, root, zoneIndex; ipaddr = {}; @@ -193,7 +193,7 @@ }; ipaddr.IPv6 = (function() { - function IPv6(parts) { + function IPv6(parts, zoneId) { var i, k, l, len, part, ref; if (parts.length === 16) { this.parts = []; @@ -212,6 +212,9 @@ throw new Error("ipaddr: ipv6 part should fit in 16 bits"); } } + if (zoneId) { + this.zoneId = zoneId; + } } IPv6.prototype.kind = function() { @@ -219,7 +222,7 @@ }; IPv6.prototype.toString = function() { - var compactStringParts, k, len, part, pushPart, state, stringParts; + var addr, compactStringParts, k, len, part, pushPart, state, stringParts, suffix; stringParts = (function() { var k, len, ref, results; ref = this.parts; @@ -268,7 +271,12 @@ pushPart(''); pushPart(''); } - return compactStringParts.join(":"); + addr = compactStringParts.join(":"); + suffix = ''; + if (this.zoneId) { + suffix = '%' + this.zoneId; + } + return addr + suffix; }; IPv6.prototype.toByteArray = function() { @@ -284,8 +292,8 @@ }; IPv6.prototype.toNormalizedString = function() { - var part; - return ((function() { + var addr, part, suffix; + addr = ((function() { var k, len, ref, results; ref = this.parts; results = []; @@ -295,6 +303,11 @@ } return results; }).call(this)).join(":"); + suffix = ''; + if (this.zoneId) { + suffix = '%' + this.zoneId; + } + return addr + suffix; }; IPv6.prototype.match = function(other, cidrRange) { @@ -386,16 +399,24 @@ ipv6Part = "(?:[0-9a-f]+::?)+"; + zoneIndex = "%[0-9a-z]{1,}"; + ipv6Regexes = { - "native": new RegExp("^(::)?(" + ipv6Part + ")?([0-9a-f]+)?(::)?$", 'i'), - transitional: new RegExp(("^((?:" + ipv6Part + ")|(?:::)(?:" + ipv6Part + ")?)") + (ipv4Part + "\\." + ipv4Part + "\\." + ipv4Part + "\\." + ipv4Part + "$"), 'i') + zoneIndex: new RegExp(zoneIndex, 'i'), + "native": new RegExp("^(::)?(" + ipv6Part + ")?([0-9a-f]+)?(::)?(" + zoneIndex + ")?$", 'i'), + transitional: new RegExp(("^((?:" + ipv6Part + ")|(?:::)(?:" + ipv6Part + ")?)") + (ipv4Part + "\\." + ipv4Part + "\\." + ipv4Part + "\\." + ipv4Part) + ("(" + zoneIndex + ")?$"), 'i') }; expandIPv6 = function(string, parts) { - var colonCount, lastColon, part, replacement, replacementCount; + var colonCount, lastColon, part, replacement, replacementCount, zoneId; if (string.indexOf('::') !== string.lastIndexOf('::')) { return null; } + zoneId = (string.match(zoneIndex) || [])[0]; + if (zoneId) { + zoneId = zoneId.substring(1); + string = string.replace(/%.+$/, ''); + } colonCount = 0; lastColon = -1; while ((lastColon = string.indexOf(':', lastColon + 1)) >= 0) { @@ -422,7 +443,7 @@ if (string[string.length - 1] === ':') { string = string.slice(0, -1); } - return (function() { + parts = (function() { var k, len, ref, results; ref = string.split(":"); results = []; @@ -432,15 +453,19 @@ } return results; })(); + return { + parts: parts, + zoneId: zoneId + }; }; ipaddr.IPv6.parser = function(string) { - var k, len, match, octet, octets, parts; - if (string.match(ipv6Regexes['native'])) { + var addr, k, len, match, octet, octets; + if (ipv6Regexes['native'].test(string)) { return expandIPv6(string, 8); } else if (match = string.match(ipv6Regexes['transitional'])) { - parts = expandIPv6(match[1].slice(0, -1), 6); - if (parts) { + addr = expandIPv6(match[1].slice(0, -1), 6); + if (addr.parts) { octets = [parseInt(match[2]), parseInt(match[3]), parseInt(match[4]), parseInt(match[5])]; for (k = 0, len = octets.length; k < len; k++) { octet = octets[k]; @@ -448,9 +473,12 @@ return null; } } - parts.push(octets[0] << 8 | octets[1]); - parts.push(octets[2] << 8 | octets[3]); - return parts; + addr.parts.push(octets[0] << 8 | octets[1]); + addr.parts.push(octets[2] << 8 | octets[3]); + return { + parts: addr.parts, + zoneId: addr.zoneId + }; } } return null; @@ -480,12 +508,13 @@ }; ipaddr.IPv6.isValid = function(string) { - var e; + var addr, e; if (typeof string === "string" && string.indexOf(":") === -1) { return false; } try { - new this(this.parser(string)); + addr = this.parser(string); + new this(addr.parts, addr.zoneId); return true; } catch (error1) { e = error1; @@ -493,7 +522,7 @@ } }; - ipaddr.IPv4.parse = ipaddr.IPv6.parse = function(string) { + ipaddr.IPv4.parse = function(string) { var parts; parts = this.parser(string); if (parts === null) { @@ -502,6 +531,15 @@ return new this(parts); }; + ipaddr.IPv6.parse = function(string) { + var addr; + addr = this.parser(string); + if (addr.parts === null) { + throw new Error("ipaddr: string is not formatted like ip address"); + } + return new this(addr.parts, addr.zoneId); + }; + ipaddr.IPv4.parseCIDR = function(string) { var maskLength, match; if (match = string.match(/^(.+)\/(\d+)$/)) { diff --git a/src/ipaddr.coffee b/src/ipaddr.coffee index c07d14d..955f50b 100644 --- a/src/ipaddr.coffee +++ b/src/ipaddr.coffee @@ -189,7 +189,7 @@ class ipaddr.IPv6 # Constructs an IPv6 address from an array of eight 16-bit parts # or sixteen 8-bit parts in network order (MSB first). # Throws an error if the input is invalid. - constructor: (parts) -> + constructor: (parts, zoneId) -> if parts.length == 16 @parts = [] for i in [0..14] by 2 @@ -203,6 +203,9 @@ class ipaddr.IPv6 if !(0 <= part <= 0xffff) throw new Error "ipaddr: ipv6 part should fit in 16 bits" + if zoneId + @zoneId = zoneId + # The 'kind' method exists on both IPv4 and IPv6 classes. kind: -> return 'ipv6' @@ -242,7 +245,13 @@ class ipaddr.IPv6 pushPart('') pushPart('') - return compactStringParts.join ":" + addr = compactStringParts.join ":" + + suffix = '' + if @zoneId + suffix = '%' + @zoneId + + return addr + suffix # Returns an array of byte-sized values in network order (MSB first) toByteArray: -> @@ -256,7 +265,13 @@ class ipaddr.IPv6 # Returns the address in expanded format with all zeroes included, like # 2001:db8:8:66:0:0:0:1 toNormalizedString: -> - return (part.toString(16) for part in @parts).join ":" + addr = (part.toString(16) for part in @parts).join ":" + + suffix = '' + if @zoneId + suffix = '%' + @zoneId + + return addr + suffix # Checks if this address matches other one within given CIDR range. match: (other, cidrRange) -> @@ -347,10 +362,13 @@ class ipaddr.IPv6 # hexadecimal IPv6 and a transitional variant with dotted-decimal IPv4 at # the end. ipv6Part = "(?:[0-9a-f]+::?)+" +zoneIndex = "%[0-9a-z]{1,}" ipv6Regexes = - native: new RegExp "^(::)?(#{ipv6Part})?([0-9a-f]+)?(::)?$", 'i' + zoneIndex: new RegExp zoneIndex, 'i' + native: new RegExp "^(::)?(#{ipv6Part})?([0-9a-f]+)?(::)?(#{zoneIndex})?$", 'i' transitional: new RegExp "^((?:#{ipv6Part})|(?:::)(?:#{ipv6Part})?)" + - "#{ipv4Part}\\.#{ipv4Part}\\.#{ipv4Part}\\.#{ipv4Part}$", 'i' + "#{ipv4Part}\\.#{ipv4Part}\\.#{ipv4Part}\\.#{ipv4Part}" + + "(#{zoneIndex})?$", 'i' # Expand :: in an IPv6 address or address part consisting of `parts` groups. expandIPv6 = (string, parts) -> @@ -358,6 +376,12 @@ expandIPv6 = (string, parts) -> if string.indexOf('::') != string.lastIndexOf('::') return null + # Remove zone index and save it for later + zoneId = (string.match(zoneIndex) || [])[0]; + if zoneId + zoneId = zoneId.substring(1); + string = string.replace(/%.+$/, ''); + # How many parts do we already have? colonCount = 0 lastColon = -1 @@ -386,25 +410,26 @@ expandIPv6 = (string, parts) -> string = string[1..-1] if string[0] == ':' string = string[0..-2] if string[string.length-1] == ':' - return (parseInt(part, 16) for part in string.split(":")) + parts = (parseInt(part, 16) for part in string.split(":")) + return {parts: parts, zoneId: zoneId} # Parse an IPv6 address. ipaddr.IPv6.parser = (string) -> - if string.match(ipv6Regexes['native']) + if ipv6Regexes['native'].test(string) return expandIPv6(string, 8) else if match = string.match(ipv6Regexes['transitional']) - parts = expandIPv6(match[1][0..-2], 6) - if parts + addr = expandIPv6(match[1][0..-2], 6) + if addr.parts octets = [parseInt(match[2]), parseInt(match[3]), parseInt(match[4]), parseInt(match[5])] for octet in octets if !(0 <= octet <= 255) return null - parts.push(octets[0] << 8 | octets[1]) - parts.push(octets[2] << 8 | octets[3]) - return parts + addr.parts.push(octets[0] << 8 | octets[1]) + addr.parts.push(octets[2] << 8 | octets[3]) + return {parts: addr.parts, zoneId: addr.zoneId} return null @@ -433,20 +458,28 @@ ipaddr.IPv6.isValid = (string) -> return false try - new this(@parser(string)) + addr = @parser(string) + new this(addr.parts, addr.zoneId) return true catch e return false # Tries to parse and validate a string with IPv4/IPv6 address. # Throws an error if it fails. -ipaddr.IPv4.parse = ipaddr.IPv6.parse = (string) -> +ipaddr.IPv4.parse= (string) -> parts = @parser(string) if parts == null throw new Error "ipaddr: string is not formatted like ip address" return new this(parts) +ipaddr.IPv6.parse = (string) -> + addr = @parser(string) + if addr.parts == null + throw new Error "ipaddr: string is not formatted like ip address" + + return new this(addr.parts, addr.zoneId) + ipaddr.IPv4.parseCIDR = (string) -> if match = string.match(/^(.+)\/(\d+)$/) maskLength = parseInt(match[2]) diff --git a/test/ipaddr.test.coffee b/test/ipaddr.test.coffee index 3e7724d..f3d4af7 100644 --- a/test/ipaddr.test.coffee +++ b/test/ipaddr.test.coffee @@ -142,6 +142,12 @@ module.exports = test.equal(new ipaddr.IPv6([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0]).toString(), '2001:db8::') test.done() + 'returns IPv6 zoneIndex': (test) -> + addr = new ipaddr.IPv6([0x2001, 0xdb8, 0xf53a, 0, 0, 0, 0, 1], 'utun0') + test.equal(addr.toNormalizedString(), '2001:db8:f53a:0:0:0:0:1%utun0') + test.equal(addr.toString(), '2001:db8:f53a::1%utun0') + test.done() + 'returns correct kind for IPv6': (test) -> addr = new ipaddr.IPv6([0x2001, 0xdb8, 0xf53a, 0, 0, 0, 0, 1]) test.equal(addr.kind(), 'ipv6') @@ -156,21 +162,27 @@ module.exports = test.equal(ipaddr.IPv6.isIPv6('2001:db8:F53A::1'), true) test.equal(ipaddr.IPv6.isIPv6('200001::1'), true) test.equal(ipaddr.IPv6.isIPv6('::ffff:192.168.1.1'), true) + test.equal(ipaddr.IPv6.isIPv6('::ffff:192.168.1.1%z'), true) test.equal(ipaddr.IPv6.isIPv6('::ffff:300.168.1.1'), false) test.equal(ipaddr.IPv6.isIPv6('::ffff:300.168.1.1:0'), false) test.equal(ipaddr.IPv6.isIPv6('fe80::wtf'), false) + test.equal(ipaddr.IPv6.isIPv6('fe80::%'), false) test.done() 'validates IPv6 addresses': (test) -> test.equal(ipaddr.IPv6.isValid('2001:db8:F53A::1'), true) test.equal(ipaddr.IPv6.isValid('200001::1'), false) test.equal(ipaddr.IPv6.isValid('::ffff:192.168.1.1'), true) + test.equal(ipaddr.IPv6.isValid('::ffff:192.168.1.1%z'), true) test.equal(ipaddr.IPv6.isValid('::ffff:300.168.1.1'), false) test.equal(ipaddr.IPv6.isValid('::ffff:300.168.1.1:0'), false) test.equal(ipaddr.IPv6.isValid('::ffff:222.1.41.9000'), false) test.equal(ipaddr.IPv6.isValid('2001:db8::F53A::1'), false) test.equal(ipaddr.IPv6.isValid('fe80::wtf'), false) + test.equal(ipaddr.IPv6.isValid('fe80::%'), false) test.equal(ipaddr.IPv6.isValid('2002::2:'), false) + test.equal(ipaddr.IPv6.isValid('::%z'), true) + test.equal(ipaddr.IPv6.isValid(undefined), false) test.done() @@ -180,6 +192,8 @@ module.exports = test.deepEqual(ipaddr.IPv6.parse('2001:db8:F53A::').parts, [0x2001, 0xdb8, 0xf53a, 0, 0, 0, 0, 0]) test.deepEqual(ipaddr.IPv6.parse('::1').parts, [0, 0, 0, 0, 0, 0, 0, 1]) test.deepEqual(ipaddr.IPv6.parse('::').parts, [0, 0, 0, 0, 0, 0, 0, 0]) + test.deepEqual(ipaddr.IPv6.parse('::%z').parts, [0, 0, 0, 0, 0, 0, 0, 0]) + test.deepEqual(ipaddr.IPv6.parse('::%z').zoneId, 'z') test.done() 'barfs at invalid IPv6': (test) -> @@ -194,7 +208,10 @@ module.exports = test.equal(addr.match(ipaddr.IPv6.parse('2001:db8:f53b::1:1'), 48), false) test.equal(addr.match(ipaddr.IPv6.parse('2001:db8:f531::1:1'), 44), true) test.equal(addr.match(ipaddr.IPv6.parse('2001:db8:f500::1'), 40), true) + test.equal(addr.match(ipaddr.IPv6.parse('2001:db8:f500::1%z'), 40), true) test.equal(addr.match(ipaddr.IPv6.parse('2001:db9:f500::1'), 40), false) + test.equal(addr.match(ipaddr.IPv6.parse('2001:db9:f500::1'), 40), false) + test.equal(addr.match(ipaddr.IPv6.parse('2001:db9:f500::1%z'), 40), false) test.equal(addr.match(addr, 128), true) test.done() @@ -205,7 +222,9 @@ module.exports = test.equal(addr.match(ipaddr.IPv6.parseCIDR('2001:db8:f53b::1:1/48')), false) test.equal(addr.match(ipaddr.IPv6.parseCIDR('2001:db8:f531::1:1/44')), true) test.equal(addr.match(ipaddr.IPv6.parseCIDR('2001:db8:f500::1/40')), true) + test.equal(addr.match(ipaddr.IPv6.parseCIDR('2001:db8:f500::1%z/40')), true) test.equal(addr.match(ipaddr.IPv6.parseCIDR('2001:db9:f500::1/40')), false) + test.equal(addr.match(ipaddr.IPv6.parseCIDR('2001:db9:f500::1%z/40')), false) test.equal(addr.match(ipaddr.IPv6.parseCIDR('2001:db8:f53a::1/128')), true) test.throws -> ipaddr.IPv6.parseCIDR('2001:db8:f53a::1') @@ -240,11 +259,13 @@ module.exports = test.equal(ipaddr.IPv6.parse('2001::4242').range(), 'teredo') test.equal(ipaddr.IPv6.parse('2001:db8::3210').range(), 'reserved') test.equal(ipaddr.IPv6.parse('2001:470:8:66::1').range(), 'unicast') + test.equal(ipaddr.IPv6.parse('2001:470:8:66::1%z').range(), 'unicast') test.done() 'is able to determine IP address type': (test) -> test.equal(ipaddr.parse('8.8.8.8').kind(), 'ipv4') test.equal(ipaddr.parse('2001:db8:3312::1').kind(), 'ipv6') + test.equal(ipaddr.parse('2001:db8:3312::1%z').kind(), 'ipv6') test.done() 'throws an error if tried to parse an invalid address': (test) -> @@ -256,6 +277,7 @@ module.exports = test.equal(ipaddr.process('8.8.8.8').kind(), 'ipv4') test.equal(ipaddr.process('2001:db8:3312::1').kind(), 'ipv6') test.equal(ipaddr.process('::ffff:192.168.1.1').kind(), 'ipv4') + test.equal(ipaddr.process('::ffff:192.168.1.1%z').kind(), 'ipv4') test.done() 'correctly converts IPv6 and IPv4 addresses to byte arrays': (test) -> @@ -264,6 +286,9 @@ module.exports = # Fuck yeah. The first byte of Google's IPv6 address is 42. 42! test.deepEqual(ipaddr.parse('2a00:1450:8007::68').toByteArray(), [42, 0x00, 0x14, 0x50, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68 ]) + test.deepEqual(ipaddr.parse('2a00:1450:8007::68%z').toByteArray(), + [42, 0x00, 0x14, 0x50, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68 ]) + test.done() 'correctly parses 1 as an IPv4 address': (test) -> @@ -286,6 +311,7 @@ module.exports = 'does not hang on ::8:8:8:8:8:8:8:8:8': (test) -> test.equal(ipaddr.IPv6.isValid('::8:8:8:8:8:8:8:8:8'), false) + test.equal(ipaddr.IPv6.isValid('::8:8:8:8:8:8:8:8:8%z'), false) test.done() 'subnetMatch does not fail on empty range': (test) -> @@ -365,10 +391,13 @@ module.exports = test.equal(ipaddr.IPv6.parse('ffff:ffff:ffff:ffff::').prefixLengthFromSubnetMask(), 64) test.equal(ipaddr.IPv6.parse('ffff:ffff:ffff:ff80::').prefixLengthFromSubnetMask(), 57) test.equal(ipaddr.IPv6.parse('ffff:ffff:ffff::').prefixLengthFromSubnetMask(), 48) + test.equal(ipaddr.IPv6.parse('ffff:ffff:ffff::%z').prefixLengthFromSubnetMask(), 48) test.equal(ipaddr.IPv6.parse('::').prefixLengthFromSubnetMask(), 0) + test.equal(ipaddr.IPv6.parse('::%z').prefixLengthFromSubnetMask(), 0) # negative cases test.equal(ipaddr.IPv6.parse('2001:db8::').prefixLengthFromSubnetMask(), null) test.equal(ipaddr.IPv6.parse('ffff:0:0:ffff::').prefixLengthFromSubnetMask(), null) + test.equal(ipaddr.IPv6.parse('ffff:0:0:ffff::%z').prefixLengthFromSubnetMask(), null) test.done() 'subnetMaskFromPrefixLength returns correct subnet mask given prefix length': (test) ->