From ad0edea8ca2d3e2e91ffc4b926942ee512b75653 Mon Sep 17 00:00:00 2001 From: Joey Pereira Date: Tue, 19 Sep 2017 10:09:14 -0400 Subject: [PATCH] sql: Add INET built-in functions Adds further support for #6981. This adds several built-in functions for the INET column. See https://www.postgresql.org/docs/9.6/static/functions-net.html for reference. The added built-ins are: - abbrev(inet) text - broadcast(inet) text - family(inet) int (this is "ip_family" due to a YACC grammar conflict) - host(inet) text - hostmask(inet) inet - masklen(inet) int - netmask(inet) inet - set_masklen(inet, int) inet - text(inet) text - inet_same_family(inet, inet) boolean This does not include the built-ins - network(inet) cidr - inet_merge(inet, inet) cidr --- pkg/sql/logictest/testdata/logic_test/inet | 313 +++++++++++++++++++++ pkg/sql/parser/builtins.go | 172 +++++++++++ pkg/sql/parser/datum.go | 24 ++ pkg/util/ipaddr/ipaddr.go | 83 ++++++ pkg/util/ipaddr/ipaddr_test.go | 99 +++++++ 5 files changed, 691 insertions(+) diff --git a/pkg/sql/logictest/testdata/logic_test/inet b/pkg/sql/logictest/testdata/logic_test/inet index b5d5c4746051..90e2776bae32 100644 --- a/pkg/sql/logictest/testdata/logic_test/inet +++ b/pkg/sql/logictest/testdata/logic_test/inet @@ -280,3 +280,316 @@ SELECT * FROM arrays {} {192.168.0.1/10,::1} {192.168.0.1,192.168.0.1/10,::1,::ffff:1.2.3.4} + + +# Testing builtins + +# Test abbrev + +query T +SELECT abbrev('10.1.0.0/16'::INET) +---- +10.1.0.0/16 + +query T +SELECT abbrev('192.168.0.1/16'::INET) +---- +192.168.0.1/16 + +query T +SELECT abbrev('192.168.0.1'::INET) +---- +192.168.0.1 + +query T +SELECT abbrev('192.168.0.1/32'::INET) +---- +192.168.0.1 + +query T +SELECT abbrev('::ffff:192.168.0.1'::INET) +---- +::ffff:192.168.0.1 + +query T +SELECT abbrev('::ffff:192.168.0.1/24'::INET) +---- +::ffff:192.168.0.1/24 + +# Test broadcast + +query T +SELECT broadcast('10.1.0.0/16'::INET) +---- +10.1.255.255/16 + +query T +SELECT broadcast('192.168.0.1/16'::INET) +---- +192.168.255.255/16 + +query T +SELECT broadcast('192.168.0.1'::INET) +---- +192.168.0.1 + +query T +SELECT broadcast('192.168.0.1/32'::INET) +---- +192.168.0.1 + +query T +SELECT broadcast('::ffff:192.168.0.1'::INET) +---- +::ffff:192.168.0.1 + +query T +SELECT broadcast('::ffff:1.2.3.1/20'::INET) +---- +0:fff:ffff:ffff:ffff:ffff:ffff:ffff/20 + +query T +SELECT broadcast('2001:4f8:3:ba::/64'::INET) +---- +2001:4f8:3:ba:ffff:ffff:ffff:ffff/64 + +# Test ip_family + +query I +SELECT ip_family('10.1.0.0/16'::INET) +---- +4 + +query I +SELECT ip_family('192.168.0.1/16'::INET) +---- +4 + +query I +SELECT ip_family('192.168.0.1'::INET) +---- +4 + +query I +SELECT ip_family('::ffff:192.168.0.1'::INET) +---- +6 + +query I +SELECT ip_family('::ffff:1.2.3.1/20'::INET) +---- +6 + +query I +SELECT ip_family('2001:4f8:3:ba::/64'::INET) +---- +6 + +# Test host + +query T +SELECT host('10.1.0.0/16'::INET) +---- +10.1.0.0 + +query T +SELECT host('192.168.0.1/16'::INET) +---- +192.168.0.1 + +query T +SELECT host('192.168.0.1'::INET) +---- +192.168.0.1 + +query T +SELECT host('192.168.0.1/32'::INET) +---- +192.168.0.1 + +query T +SELECT host('::ffff:192.168.0.1'::INET) +---- +::ffff:192.168.0.1 + +query T +SELECT host('::ffff:192.168.0.1/24'::INET) +---- +::ffff:192.168.0.1 + +# Test hostmask + +query T +SELECT hostmask('192.168.1.2'::INET) +---- +0.0.0.0 + +query T +SELECT hostmask('192.168.1.2/16'::INET) +---- +0.0.255.255 + +query T +SELECT hostmask('192.168.1.2/10'::INET) +---- +0.63.255.255 + +query T +SELECT hostmask('2001:4f8:3:ba::/64'::INET) +---- +::ffff:ffff:ffff:ffff + +# Test masklen + +query I +SELECT masklen('192.168.1.2'::INET) +---- +32 + +query I +SELECT masklen('192.168.1.2/16'::INET) +---- +16 + +query I +SELECT masklen('192.168.1.2/10'::INET) +---- +10 + +query I +SELECT masklen('2001:4f8:3:ba::/64'::INET) +---- +64 + +query I +SELECT masklen('2001:4f8:3:ba::'::INET) +---- +128 + +# Test netmask + +query T +SELECT netmask('192.168.1.2'::INET) +---- +255.255.255.255 + +query T +SELECT netmask('192.168.1.2/16'::INET) +---- +255.255.0.0 + +query T +SELECT netmask('192.168.1.2/10'::INET) +---- +255.192.0.0 + +query T +SELECT netmask('192.168.1.2/0'::INET) +---- +0.0.0.0 + +query T +SELECT netmask('2001:4f8:3:ba::/64'::INET) +---- +ffff:ffff:ffff:ffff:: + +query T +SELECT netmask('2001:4f8:3:ba::/0'::INET) +---- +:: + +query T +SELECT netmask('2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128'::INET) +---- +ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + +query T +SELECT netmask('::ffff:1.2.3.1/120'::INET) +---- +ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00 + +query T +SELECT netmask('::ffff:1.2.3.1/20'::INET) +---- +ffff:f000:: + +# Test set_masklen + +query T +SELECT set_masklen('10.1.0.0/16'::INET, 10) +---- +10.1.0.0/10 + +query T +SELECT set_masklen('192.168.0.1/16'::INET, 32) +---- +192.168.0.1 + +statement error invalid mask length +SELECT set_masklen('192.168.0.1'::INET, 100) + +statement error invalid mask length +SELECT set_masklen('192.168.0.1'::INET, 33) + +statement error invalid mask length +SELECT set_masklen('192.168.0.1'::INET, -1) + +query T +SELECT set_masklen('192.168.0.1'::INET, 0) +---- +192.168.0.1/0 + +query T +SELECT set_masklen('::ffff:192.168.0.1'::INET, 100) +---- +::ffff:192.168.0.1/100 + +statement error invalid mask length +SELECT set_masklen('::ffff:192.168.0.1'::INET, -1) + +statement error invalid mask length +SELECT set_masklen('::ffff:192.168.0.1'::INET, 129) + +query T +SELECT set_masklen('::ffff:192.168.0.1/24'::INET, 0) +---- +::ffff:192.168.0.1/0 + +# Test text + +query T +SELECT text('10.1.0.0/16'::INET) +---- +10.1.0.0/16 + +query T +SELECT text('192.168.0.1/16'::INET) +---- +192.168.0.1/16 + +query T +SELECT text('192.168.0.1'::INET) +---- +192.168.0.1/32 + +query T +SELECT text('192.168.0.1/32'::INET) +---- +192.168.0.1/32 + +query T +SELECT text('::ffff:192.168.0.1'::INET) +---- +::ffff:192.168.0.1/128 + +query T +SELECT text('::ffff:192.168.0.1/24'::INET) +---- +::ffff:192.168.0.1/24 + +# Test inet_same_family + +query T +SELECT text('::ffff:192.168.0.1/24'::INET) +---- +::ffff:192.168.0.1/24 diff --git a/pkg/sql/parser/builtins.go b/pkg/sql/parser/builtins.go index 95e9b5ef373c..1208b5d6459b 100644 --- a/pkg/sql/parser/builtins.go +++ b/pkg/sql/parser/builtins.go @@ -43,6 +43,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/roachpb" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" "github.com/cockroachdb/cockroach/pkg/util/duration" + "github.com/cockroachdb/cockroach/pkg/util/ipaddr" "github.com/cockroachdb/cockroach/pkg/util/log" "github.com/cockroachdb/cockroach/pkg/util/syncutil" "github.com/cockroachdb/cockroach/pkg/util/timeutil" @@ -399,6 +400,177 @@ var Builtins = map[string][]Builtin{ }, }, + // The following functions are all part of the NET address functions. They can + // be found in the postgres reference at https://www.postgresql.org/docs/9.6/static/functions-net.html#CIDR-INET-FUNCTIONS-TABLE + // This includes: + // - abbrev + // - broadcast + // - ip_family + // - host + // - hostmask + // - masklen + // - netmask + // - set_masklen + // - text(inet) + // - inet_same_family + + "abbrev": { + Builtin{ + Types: ArgTypes{{"val", TypeINet}}, + ReturnType: fixedReturnType(TypeString), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + dIPAddr := MustBeDIPAddr(args[0]) + return NewDString(dIPAddr.IPAddr.String()), nil + }, + Info: "Converts the IP address to an abbreviated display format as text." + + "For INET types, this will omit the mask length if it's not the default (32 or IPv4, 128 for IPv6)" + + "\n\nFor example, `abbrev('192.168.1.2/24')` returns `'192.168.1.2/24'`", + }, + }, + + "broadcast": { + Builtin{ + Types: ArgTypes{{"val", TypeINet}}, + ReturnType: fixedReturnType(TypeINet), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + dIPAddr := MustBeDIPAddr(args[0]) + broadcastIPAddr := dIPAddr.IPAddr.Broadcast() + return &DIPAddr{IPAddr: broadcastIPAddr}, nil + }, + Info: "Gets the broadcast address for network represented by the value." + + "\n\nFor example, `broadcast('192.168.1.2/24')` returns `'192.168.1.255/24'`", + }, + }, + + "ip_family": { + Builtin{ + Types: ArgTypes{{"val", TypeINet}}, + ReturnType: fixedReturnType(TypeInt), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + dIPAddr := MustBeDIPAddr(args[0]) + if dIPAddr.Family == ipaddr.IPv4family { + return NewDInt(DInt(4)), nil + } + return NewDInt(DInt(6)), nil + }, + Info: "Extracts the IP family of the value; 4 for IPv4, 6 for IPv6." + + "\n\nFor example, `family('::1')` returns `6`", + }, + }, + + "host": { + Builtin{ + Types: ArgTypes{{"val", TypeINet}}, + ReturnType: fixedReturnType(TypeString), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + dIPAddr := MustBeDIPAddr(args[0]) + s := dIPAddr.IPAddr.String() + if i := strings.IndexByte(s, '/'); i != -1 { + return NewDString(s[:i]), nil + } + return NewDString(s), nil + }, + Info: "Extracts the address part of value as text, omitting the mask length." + + "\n\nFor example, `host('192.168.1.2/16')` returns `'192.168.1.2'`", + }, + }, + + "hostmask": { + Builtin{ + Types: ArgTypes{{"val", TypeINet}}, + ReturnType: fixedReturnType(TypeINet), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + dIPAddr := MustBeDIPAddr(args[0]) + ipAddr := dIPAddr.IPAddr.Hostmask() + return &DIPAddr{ipAddr}, nil + }, + Info: "Creates an IP address retaining only the host mask part of the value." + + "\n\nFor example, `hostmask('192.168.1.2/16')` returns `'0.0.255.255'`", + }, + }, + + "masklen": { + Builtin{ + Types: ArgTypes{{"val", TypeINet}}, + ReturnType: fixedReturnType(TypeInt), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + dIPAddr := MustBeDIPAddr(args[0]) + return NewDInt(DInt(dIPAddr.Mask)), nil + }, + Info: "Gets the mask length of the value." + + "\n\nFor example, `netmask('192.168.1.2/16')` returns `16`", + }, + }, + + "netmask": { + Builtin{ + Types: ArgTypes{{"val", TypeINet}}, + ReturnType: fixedReturnType(TypeINet), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + dIPAddr := MustBeDIPAddr(args[0]) + ipAddr := dIPAddr.IPAddr.Netmask() + return &DIPAddr{ipAddr}, nil + }, + Info: "Creates an IP address retaining only the network mask part of the value." + + "\n\nFor example, `netmask('192.168.1.2/16')` returns `'255.255.0.0'`", + }, + }, + + "set_masklen": { + Builtin{ + Types: ArgTypes{ + {"val", TypeINet}, + {"mask", TypeInt}, + }, + ReturnType: fixedReturnType(TypeINet), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + dIPAddr := MustBeDIPAddr(args[0]) + mask := int(MustBeDInt(args[1])) + + if !(dIPAddr.Family == ipaddr.IPv4family && mask >= 0 && mask <= 32) && !(dIPAddr.Family == ipaddr.IPv6family && mask >= 0 && mask <= 128) { + return nil, pgerror.NewErrorf( + pgerror.CodeInvalidParameterValueError, "invalid mask length: %d", mask) + } + return &DIPAddr{IPAddr: ipaddr.IPAddr{Family: dIPAddr.Family, Addr: dIPAddr.Addr, Mask: byte(mask)}}, nil + }, + Info: "Sets the mask length of `val` to `mask`.\n\n" + + "For example, `set_masklen('192.168.1.2', 16)` returns `'192.168.1.2/16'`.", + }, + }, + + "text": { + Builtin{ + Types: ArgTypes{{"val", TypeINet}}, + ReturnType: fixedReturnType(TypeString), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + dIPAddr := MustBeDIPAddr(args[0]) + s := dIPAddr.IPAddr.String() + // Ensure the string has a "/mask" suffix. + if strings.IndexByte(s, '/') == -1 { + s += "/" + strconv.Itoa(int(dIPAddr.Mask)) + } + return NewDString(s), nil + }, + Info: "Converts the IP address and mask length to text.", + }, + }, + + "inet_same_family": { + Builtin{ + Types: ArgTypes{ + {"val", TypeINet}, + {"val", TypeINet}, + }, + ReturnType: fixedReturnType(TypeBool), + fn: func(_ *EvalContext, args Datums) (Datum, error) { + first := MustBeDIPAddr(args[0]) + other := MustBeDIPAddr(args[1]) + return MakeDBool(DBool(first.Family == other.Family)), nil + }, + Info: "Checks if two IP addresses are of the same IP family.", + }, + }, + "from_ip": { Builtin{ Types: ArgTypes{{"val", TypeBytes}}, diff --git a/pkg/sql/parser/datum.go b/pkg/sql/parser/datum.go index 353dc6969f7a..9b94441c79fd 100644 --- a/pkg/sql/parser/datum.go +++ b/pkg/sql/parser/datum.go @@ -1124,6 +1124,30 @@ func NewDIPAddr(d DIPAddr) *DIPAddr { return &d } +// AsDIPAddr attempts to retrieve a *DIPAddr from an Expr, returning a *DIPAddr and +// a flag signifying whether the assertion was successful. The function should +// be used instead of direct type assertions wherever a *DIPAddr wrapped by a +// *DOidWrapper is possible. +func AsDIPAddr(e Expr) (DIPAddr, bool) { + switch t := e.(type) { + case *DIPAddr: + return *t, true + case *DOidWrapper: + return AsDIPAddr(t.Wrapped) + } + return DIPAddr{}, false +} + +// MustBeDIPAddr attempts to retrieve a DIPAddr from an Expr, panicking if the +// assertion fails. +func MustBeDIPAddr(e Expr) DIPAddr { + i, ok := AsDIPAddr(e) + if !ok { + panic(pgerror.NewErrorf(pgerror.CodeInternalError, "expected *DIPAddr, found %T", e)) + } + return i +} + // ResolvedType implements the TypedExpr interface. func (*DIPAddr) ResolvedType() Type { return TypeINet diff --git a/pkg/util/ipaddr/ipaddr.go b/pkg/util/ipaddr/ipaddr.go index fe873bbac32b..39dc615eeb28 100644 --- a/pkg/util/ipaddr/ipaddr.go +++ b/pkg/util/ipaddr/ipaddr.go @@ -246,6 +246,84 @@ func RandIPAddr(rng *rand.Rand) IPAddr { return ipAddr } +// Hostmask returns the host masked IP. This is defined as the IP address bits +// that are not masked. +func (ipAddr *IPAddr) Hostmask() IPAddr { + var newIPAddr IPAddr + newIPAddr.Family = ipAddr.Family + newIPAddr.Addr = ipAddr.Addr + + if ipAddr.Family == IPv4family { + LoMask := ^uint32(0) >> ipAddr.Mask + newIPAddr.Addr.Lo = uint64(LoMask) | IPv4mappedIPv6prefix + } else if ipAddr.Mask <= 64 { + newIPAddr.Addr.Hi = ^uint64(0) >> ipAddr.Mask + newIPAddr.Addr.Lo = ^uint64(0) + } else { + newIPAddr.Addr.Hi = uint64(0) + newIPAddr.Addr.Lo = ^uint64(0) >> (ipAddr.Mask - 64) + } + + if newIPAddr.Family == IPv4family { + newIPAddr.Mask = 32 + } else { + newIPAddr.Mask = 128 + } + + return newIPAddr +} + +// Netmask returns the network masked IP. This is defined as the IP address bits +// that are masked. +func (ipAddr *IPAddr) Netmask() IPAddr { + var newIPAddr IPAddr + newIPAddr.Family = ipAddr.Family + newIPAddr.Addr = ipAddr.Addr + + if ipAddr.Family == IPv4family { + LoMask := ^uint32(0) << (32 - ipAddr.Mask) + newIPAddr.Addr.Lo = uint64(LoMask) | IPv4mappedIPv6prefix + } else if ipAddr.Mask <= 64 { + newIPAddr.Addr.Hi = ^uint64(0) << (64 - ipAddr.Mask) + newIPAddr.Addr.Lo = uint64(0) + } else { + newIPAddr.Addr.Hi = ^uint64(0) + newIPAddr.Addr.Lo = ^uint64(0) << (128 - ipAddr.Mask) + } + + if newIPAddr.Family == IPv4family { + newIPAddr.Mask = 32 + } else { + newIPAddr.Mask = 128 + } + + return newIPAddr +} + +// Broadcast returns a new IPAddr where the host mask of the IP address is a +// full mask, i.e. 0xFF bytes. +func (ipAddr *IPAddr) Broadcast() IPAddr { + var newIPAddr IPAddr + newIPAddr.Family = ipAddr.Family + newIPAddr.Mask = ipAddr.Mask + newIPAddr.Addr = ipAddr.Addr + + if newIPAddr.Family == IPv4family { + LoMask := ^uint64(0) >> (32 + newIPAddr.Mask) + newIPAddr.Addr.Lo = newIPAddr.Addr.Lo | LoMask + } else if newIPAddr.Mask < 64 { + LoMask := ^uint64(0) + HiMask := ^uint64(0) >> newIPAddr.Mask + newIPAddr.Addr.Lo = newIPAddr.Addr.Lo | LoMask + newIPAddr.Addr.Hi = newIPAddr.Addr.Hi | HiMask + } else { + LoMask := ^uint64(0) >> (newIPAddr.Mask - 64) + newIPAddr.Addr.Lo = newIPAddr.Addr.Lo | LoMask + } + + return newIPAddr +} + // WriteIPv4Bytes writes the 4-byte IPv4 representation. If the IP is IPv6 then // the first 12-bytes are truncated. func (ip Addr) WriteIPv4Bytes(writer io.Writer) error { @@ -280,3 +358,8 @@ func (ip Addr) Sub(o uint64) Addr { func (ip Addr) Add(o uint64) Addr { return Addr(uint128.Uint128(ip).Add(o)) } + +// String wraps net.IP.String(). +func (ip Addr) String() string { + return net.IP(uint128.Uint128(ip).GetBytes()).String() +} diff --git a/pkg/util/ipaddr/ipaddr_test.go b/pkg/util/ipaddr/ipaddr_test.go index 13acc235d7c7..66adf921755d 100644 --- a/pkg/util/ipaddr/ipaddr_test.go +++ b/pkg/util/ipaddr/ipaddr_test.go @@ -267,3 +267,102 @@ func TestIPAddrString(t *testing.T) { } } } + +func TestIPAddrBroadcast(t *testing.T) { + testCases := []struct { + s string + exp string + }{ + // Basic IPv4 + {"192.168.1.2", "192.168.1.2"}, + {"192.168.1.2/16", "192.168.255.255/16"}, + {"192.168.1.2/10", "192.191.255.255/10"}, + {"192.0.0.0/10", "192.63.255.255/10"}, + // Basic IPv6 + {"2001:4f8:3:ba::/64", "2001:4f8:3:ba:ffff:ffff:ffff:ffff/64"}, + {"2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128", "2001:4f8:3:ba:2e0:81ff:fe22:d1f1"}, + {"::ffff:1.2.3.1/120", "::ffff:1.2.3.255/120"}, + {"::ffff:1.2.3.1/128", "::ffff:1.2.3.1"}, + {"::ffff:1.2.3.1/20", "0:fff:ffff:ffff:ffff:ffff:ffff:ffff/20"}, + {"::1", "::1"}, + } + for i, testCase := range testCases { + var ip IPAddr + if err := ParseINet(testCase.s, &ip); err != nil { + t.Fatalf("%d: bad test case: %s got error %s", i, testCase.s, err) + } + actual := ip.Broadcast() + if actual.String() != testCase.exp { + t.Errorf("%d: Broadcast(%s) actual:%s does not match expected:%s", i, testCase.s, actual.String(), + testCase.exp) + } + } +} + +func TestIPAddrHostmask(t *testing.T) { + testCases := []struct { + s string + exp string + }{ + // Basic IPv4 + {"192.168.1.2", "0.0.0.0"}, + {"192.168.1.2/16", "0.0.255.255"}, + {"192.168.1.2/10", "0.63.255.255"}, + {"192.168.1.2/0", "255.255.255.255"}, + // Basic IPv6 + {"2001:4f8:3:ba::/64", "::ffff:ffff:ffff:ffff"}, + {"2001:4f8:3:ba::/0", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}, + {"2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128", "::"}, + {"::ffff:1.2.3.1/120", "::ff"}, + {"::ffff:1.2.3.1/128", "::"}, + {"::ffff:1.2.3.1/20", "0:fff:ffff:ffff:ffff:ffff:ffff:ffff"}, + } + for i, testCase := range testCases { + var ip IPAddr + if err := ParseINet(testCase.s, &ip); err != nil { + t.Fatalf("%d: bad test case: %s got error %s", i, testCase.s, err) + } + + var expIP IPAddr + if err := ParseINet(testCase.exp, &expIP); err != nil { + t.Fatalf("%d: bad test case: %s got error %s", i, testCase.exp, err) + } + + actual := ip.Hostmask() + if actual.String() != testCase.exp { + t.Errorf("%d: Hostmask(%s) actual:%#v does not match expected:%#v", i, testCase.s, actual, + expIP) + } + } +} + +func TestIPAddrNetmask(t *testing.T) { + testCases := []struct { + s string + exp string + }{ + // Basic IPv4 + {"192.168.1.2", "255.255.255.255"}, + {"192.168.1.2/16", "255.255.0.0"}, + {"192.168.1.2/10", "255.192.0.0"}, + {"192.168.1.2/0", "0.0.0.0"}, + // Basic IPv6 + {"2001:4f8:3:ba::/64", "ffff:ffff:ffff:ffff::"}, + {"2001:4f8:3:ba::/0", "::"}, + {"2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}, + {"::ffff:1.2.3.1/120", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00"}, + {"::ffff:1.2.3.1/20", "ffff:f000::"}, + } + for i, testCase := range testCases { + var ip IPAddr + if err := ParseINet(testCase.s, &ip); err != nil { + t.Fatalf("%d: bad test case: %s got error %s", i, testCase.s, err) + } + + actual := ip.Netmask() + if actual.String() != testCase.exp { + t.Errorf("%d: Netmask(%s) actual:%s does not match expected:%s", i, testCase.s, actual, + testCase.exp) + } + } +}