Skip to content

Commit 09a6408

Browse files
committed
Support zone identifiers in IPv6 addresses
These are supported by Ruby's socket library if the operating system supports zone indentifiers, so they should be supported by ipaddr. See RFCs 4007 and 6874 for additional information. Implements Ruby Feature #10911
1 parent 6c907a4 commit 09a6408

File tree

2 files changed

+67
-5
lines changed

2 files changed

+67
-5
lines changed

lib/ipaddr.rb

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,13 @@ def to_s
213213
# Returns a string containing the IP address representation in
214214
# canonical form.
215215
def to_string
216-
return _to_string(@addr)
216+
str = _to_string(@addr)
217+
218+
if @family == Socket::AF_INET6
219+
str << zone_id.to_s
220+
end
221+
222+
return str
217223
end
218224

219225
# Returns a network byte ordered string form of the IP address.
@@ -385,7 +391,7 @@ def eql?(other)
385391

386392
# Returns a hash value used by Hash, Set, and Array classes
387393
def hash
388-
return ([@addr, @mask_addr].hash << 1) | (ipv4? ? 0 : 1)
394+
return ([@addr, @mask_addr, @zone_id].hash << 1) | (ipv4? ? 0 : 1)
389395
end
390396

391397
# Creates a Range object for the network address.
@@ -441,18 +447,44 @@ def inspect
441447
af = "IPv4"
442448
when Socket::AF_INET6
443449
af = "IPv6"
450+
zone_id = @zone_id.to_s
444451
else
445452
raise AddressFamilyError, "unsupported address family"
446453
end
447-
return sprintf("#<%s: %s:%s/%s>", self.class.name,
448-
af, _to_string(@addr), _to_string(@mask_addr))
454+
return sprintf("#<%s: %s:%s%s/%s>", self.class.name,
455+
af, _to_string(@addr), zone_id, _to_string(@mask_addr))
449456
end
450457

451458
# Returns the netmask in string format e.g. 255.255.0.0
452459
def netmask
453460
_to_string(@mask_addr)
454461
end
455462

463+
# Returns the IPv6 zone identifier, if present.
464+
# Raises InvalidAddressError if not an IPv6 address.
465+
def zone_id
466+
if @family == Socket::AF_INET6
467+
@zone_id
468+
else
469+
raise InvalidAddressError, "not an IPv6 address"
470+
end
471+
end
472+
473+
# Returns the IPv6 zone identifier, if present.
474+
# Raises InvalidAddressError if not an IPv6 address.
475+
def zone_id=(zid)
476+
if @family == Socket::AF_INET6
477+
case zid
478+
when nil, /\A%(\w+)\z/
479+
@zone_id = zid
480+
else
481+
raise InvalidAddressError, "invalid zone identifier for address"
482+
end
483+
else
484+
raise InvalidAddressError, "not an IPv6 address"
485+
end
486+
end
487+
456488
protected
457489

458490
# Set +@addr+, the internal stored ip address, to given +addr+. The
@@ -561,6 +593,11 @@ def initialize(addr = '::', family = Socket::AF_UNSPEC)
561593
prefix = $1
562594
family = Socket::AF_INET6
563595
end
596+
if prefix =~ /\A(.*)(%\w+)\z/
597+
prefix = $1
598+
zone_id = $2
599+
family = Socket::AF_INET6
600+
end
564601
# It seems AI_NUMERICHOST doesn't do the job.
565602
#Socket.getaddrinfo(left, nil, Socket::AF_INET6, Socket::SOCK_STREAM, nil,
566603
# Socket::AI_NUMERICHOST)
@@ -575,6 +612,7 @@ def initialize(addr = '::', family = Socket::AF_UNSPEC)
575612
@addr = in6_addr(prefix)
576613
@family = Socket::AF_INET6
577614
end
615+
@zone_id = zone_id
578616
if family != Socket::AF_UNSPEC && @family != family
579617
raise AddressFamilyError, "address family mismatch"
580618
end

test/test_ipaddr.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ def test_s_new
4343
assert_equal("3ffe:0505:0002:0000:0000:0000:0000:0000", a.to_string)
4444
assert_equal(Socket::AF_INET6, a.family)
4545
assert_equal(48, a.prefix)
46+
assert_nil(a.zone_id)
47+
48+
a = IPAddr.new("fe80::1%ab0")
49+
assert_equal("fe80::1%ab0", a.to_s)
50+
assert_equal("fe80:0000:0000:0000:0000:0000:0000:0001%ab0", a.to_string)
51+
assert_equal(Socket::AF_INET6, a.family)
52+
assert_equal(false, a.ipv4?)
53+
assert_equal(true, a.ipv6?)
54+
assert_equal("#<IPAddr: IPv6:fe80:0000:0000:0000:0000:0000:0000:0001%ab0/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>", a.inspect)
55+
assert_equal(128, a.prefix)
56+
assert_equal('%ab0', a.zone_id)
4657

4758
a = IPAddr.new("0.0.0.0")
4859
assert_equal("0.0.0.0", a.to_s)
@@ -87,7 +98,8 @@ def test_s_new
8798

8899
assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("192.168.0.256") }
89100
assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("192.168.0.011") }
90-
assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("fe80::1%fxp0") }
101+
assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("fe80::1%") }
102+
assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("fe80::1%]") }
91103
assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("[192.168.1.2]/120") }
92104
assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("[2001:200:300::]\nINVALID") }
93105
assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("192.168.0.1/32\nINVALID") }
@@ -208,6 +220,18 @@ def test_netmask
208220
a = IPAddr.new("192.168.1.2/24")
209221
assert_equal(a.netmask, "255.255.255.0")
210222
end
223+
224+
def test_zone_id
225+
a = IPAddr.new("192.168.1.2")
226+
assert_raise(IPAddr::InvalidAddressError) { a.zone_id = '%ab0' }
227+
assert_raise(IPAddr::InvalidAddressError) { a.zone_id }
228+
229+
a = IPAddr.new("1:2:3:4:5:6:7:8")
230+
a.zone_id = '%ab0'
231+
assert_equal('%ab0', a.zone_id)
232+
assert_equal("1:2:3:4:5:6:7:8%ab0", a.to_s)
233+
assert_raise(IPAddr::InvalidAddressError) { a.zone_id = '%' }
234+
end
211235
end
212236

213237
class TC_Operator < Test::Unit::TestCase

0 commit comments

Comments
 (0)