From 1f8c08aea82eb9f32a1551641bfa07727c0310b8 Mon Sep 17 00:00:00 2001 From: Aleksei Vasilev Date: Thu, 11 Aug 2022 21:49:14 +0300 Subject: [PATCH] initial --- .gitignore | 1 + README.md | 3 + cidr_parse.go | 116 +++++++++++++++++++++++++++ cidr_parse_test.go | 191 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 11 +++ go.sum | 15 ++++ 6 files changed, 337 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cidr_parse.go create mode 100644 cidr_parse_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4a9d73 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# CIDR Parse + +Split CIDR to the range of IP addresses. \ No newline at end of file diff --git a/cidr_parse.go b/cidr_parse.go new file mode 100644 index 0000000..a707682 --- /dev/null +++ b/cidr_parse.go @@ -0,0 +1,116 @@ +package cidr_parse + +import ( + "fmt" + "net" + "strconv" + "strings" +) + +const ( + maskFull = 0xffffffff + maskFirst = 0xff000000 + maskSecond = 0x00ff0000 + maskThird = 0x0000ff00 + maskLast = 0x000000ff + + maskShiftFirst = 24 + maskShiftSecond = 16 + maskShiftThird = 8 +) + +// CIDRParse converts CIDR to the range of IPs (v4 only in this version) +type CIDRParse struct { + firstIP uint32 + lastIP uint32 +} + +func NewCIDRParse(cidr string, includeFirstZero bool) (*CIDRParse, error) { + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, fmt.Errorf("error parsing CIDR %q: %w", cidr, err) + } + + bits, size := ipNet.Mask.Size() + if size != 32 { + return nil, fmt.Errorf("unsupported mask size: %d", size) + } + + zeroIP := ipToUint32(ipNet.IP.String()) + + firstIP := zeroIP + if !includeFirstZero && zeroIP&maskLast == 0 { + firstIP++ + } + + lastIP := zeroIP | (maskFull >> bits) + + return &CIDRParse{ + firstIP: firstIP, + lastIP: lastIP, + }, nil +} + +// FirstIP is a first IP address of the range +func (r CIDRParse) FirstIP() string { + return uint32ToIP(r.firstIP) +} + +// LastIP is a last IP address of the range +func (r CIDRParse) LastIP() string { + return uint32ToIP(r.lastIP) +} + +// Len is a length of the range +func (r CIDRParse) Len() int { + return int(r.lastIP - r.firstIP + 1) +} + +// List returns all IPs as a string slice +func (r CIDRParse) List() []string { + list := make([]string, 0, r.Len()) + next := r.NextIPFunc() + ip, ok := next() + for ok { + list = append(list, ip) + ip, ok = next() + } + return list +} + +// NextIPFunc returns a generator function to iterate IPs one by one +func (r CIDRParse) NextIPFunc() func() (string, bool) { + offset := uint32(0) + return func() (string, bool) { + ip := r.firstIP + offset + var ok bool + if ip > r.lastIP { + ip = r.lastIP + } else { + offset++ + ok = true + } + return uint32ToIP(ip), ok + } +} + +// converts IP address string to uint32 number +func ipToUint32(ip string) uint32 { + octets := make([]uint64, 4) + for i, part := range strings.Split(ip, ".") { + octets[i], _ = strconv.ParseUint(part, 10, 32) + } + + return uint32(octets[0]<>maskShiftFirst, + ip&maskSecond>>maskShiftSecond, + ip&maskThird>>maskShiftThird, + ip&maskLast, + ) +} diff --git a/cidr_parse_test.go b/cidr_parse_test.go new file mode 100644 index 0000000..ffaaf17 --- /dev/null +++ b/cidr_parse_test.go @@ -0,0 +1,191 @@ +package cidr_parse + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIpToUint32(t *testing.T) { + tests := []struct { + Name string + IP string + Result uint32 + }{ + {Name: "10.0.0.0", IP: "10.0.0.0", Result: 0xa000000}, + {Name: "127.0.0.1", IP: "127.0.0.1", Result: 0x7f000001}, + {Name: "255.255.255.255", IP: "255.255.255.255", Result: maskFull}, + {Name: "255.0.0.0", IP: "255.0.0.0", Result: maskFirst}, + {Name: "0.255.0.0", IP: "0.255.0.0", Result: maskSecond}, + {Name: "0.0.255.0", IP: "0.0.255.0", Result: maskThird}, + {Name: "0.0.0.255", IP: "0.0.0.255", Result: maskLast}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + res := ipToUint32(tt.IP) + require.Equal(t, tt.Result, res) + }) + } +} + +func TestCIDRRange_New(t *testing.T) { + tests := []struct { + Name string + CIDR string + NeedError bool + }{ + {Name: "empty_cidr", CIDR: "", NeedError: true}, + {Name: "invalid_cidr", CIDR: "10.0.0.0/33", NeedError: true}, + {Name: "invalid_string", CIDR: "test", NeedError: true}, + {Name: "valid_cidr", CIDR: "10.0.0.0/16", NeedError: false}, + {Name: "invalid_mask_size", CIDR: "2001:db8:0:160::/64", NeedError: true}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + _, err := NewCIDRParse(tt.CIDR, true) + toggleError(t, err, tt.NeedError) + }) + } +} + +func TestCIDRRange_FirstIP(t *testing.T) { + tests := []struct { + Name string + CIDR string + IncludeZero bool + FirstIP string + }{ + {Name: "10.0.0.0/16", CIDR: "10.0.0.0/16", IncludeZero: true, FirstIP: "10.0.0.0"}, + {Name: "10.0.0.0/16", CIDR: "10.0.0.0/16", FirstIP: "10.0.0.1"}, + {Name: "10.0.0.0/32", CIDR: "10.0.0.0/32", IncludeZero: true, FirstIP: "10.0.0.0"}, + {Name: "10.0.0.0/32", CIDR: "10.0.0.0/32", FirstIP: "10.0.0.1"}, + {Name: "10.0.0.127/32", CIDR: "10.0.0.127/32", FirstIP: "10.0.0.127"}, + {Name: "10.0.255.128/16", CIDR: "10.0.255.128/16", IncludeZero: true, FirstIP: "10.0.0.0"}, + {Name: "10.0.255.128/16", CIDR: "10.0.255.128/16", FirstIP: "10.0.0.1"}, + {Name: "10.0.255.128/24", CIDR: "10.0.255.128/24", IncludeZero: true, FirstIP: "10.0.255.0"}, + {Name: "10.0.255.128/24", CIDR: "10.0.255.128/24", FirstIP: "10.0.255.1"}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + r, err := NewCIDRParse(tt.CIDR, tt.IncludeZero) + require.NoError(t, err) + require.Equal(t, tt.FirstIP, r.FirstIP()) + }) + } +} + +func TestCIDRRange_LastIP(t *testing.T) { + tests := []struct { + Name string + CIDR string + LastIP string + }{ + {Name: "10.0.0.0/32", CIDR: "10.0.0.0/32", LastIP: "10.0.0.0"}, + {Name: "10.0.0.127/32", CIDR: "10.0.0.127/32", LastIP: "10.0.0.127"}, + {Name: "10.0.0.0/28", CIDR: "10.0.0.0/25", LastIP: "10.0.0.127"}, + {Name: "10.0.0.0/24", CIDR: "10.0.0.0/24", LastIP: "10.0.0.255"}, + {Name: "10.0.0.0/16", CIDR: "10.0.0.0/16", LastIP: "10.0.255.255"}, + {Name: "10.0.0.0/8", CIDR: "10.0.0.0/8", LastIP: "10.255.255.255"}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + r, err := NewCIDRParse(tt.CIDR, true) + require.NoError(t, err) + require.Equal(t, tt.LastIP, r.LastIP()) + }) + } +} + +func TestCIDRRange_Len(t *testing.T) { + tests := []struct { + Name string + CIDR string + IncludeZero bool + Len int + }{ + {Name: "10.0.0.0/32", CIDR: "10.0.0.0/32", IncludeZero: true, Len: 1}, + {Name: "10.0.0.0/32", CIDR: "10.0.0.0/32", Len: 0}, + {Name: "10.0.0.0/24", IncludeZero: true, CIDR: "10.0.0.0/24", Len: 256}, + {Name: "10.0.0.0/24", CIDR: "10.0.0.0/24", Len: 255}, + {Name: "10.0.0.0/16", CIDR: "10.0.0.0/16", IncludeZero: true, Len: 65536}, + {Name: "10.0.0.0/16", CIDR: "10.0.0.0/16", Len: 65535}, + {Name: "10.0.0.0/8", CIDR: "10.0.0.0/8", IncludeZero: true, Len: 16777216}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + r, err := NewCIDRParse(tt.CIDR, tt.IncludeZero) + require.NoError(t, err) + require.Equal(t, tt.Len, r.Len()) + }) + } +} + +func TestCIDRRange_List(t *testing.T) { + tests := []struct { + Name string + CIDR string + ListCount int + }{ + {Name: "10.0.0.0/32", CIDR: "10.0.0.0/32", ListCount: 1}, + {Name: "10.0.0.127/32", CIDR: "10.0.0.127/32", ListCount: 1}, + {Name: "10.0.0.0/28", CIDR: "10.0.0.0/25", ListCount: 128}, + {Name: "10.0.0.0/24", CIDR: "10.0.0.0/24", ListCount: 256}, + {Name: "10.0.0.0/19", CIDR: "10.0.0.0/19", ListCount: 8192}, + {Name: "10.0.0.0/16", CIDR: "10.0.0.0/16", ListCount: 65536}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + r, err := NewCIDRParse(tt.CIDR, true) + require.NoError(t, err) + require.Equal(t, tt.ListCount, len(r.List())) + }) + } +} + +func TestCIDRRange_NextIPFunc(t *testing.T) { + tests := []struct { + Name string + CIDR string + RunsCount int + IsOk bool + IP string + }{ + {Name: "10.0.0.0/32", CIDR: "10.0.0.0/32", RunsCount: 1, IsOk: true, IP: "10.0.0.0"}, + {Name: "10.0.0.0/32", CIDR: "10.0.0.0/32", RunsCount: 2, IsOk: false, IP: "10.0.0.0"}, + {Name: "10.0.0.0/24", CIDR: "10.0.0.0/24", RunsCount: 1, IsOk: true, IP: "10.0.0.0"}, + {Name: "10.0.0.0/24", CIDR: "10.0.0.0/24", RunsCount: 42, IsOk: true, IP: "10.0.0.41"}, + {Name: "10.0.0.0/24", CIDR: "10.0.0.0/24", RunsCount: 256, IsOk: true, IP: "10.0.0.255"}, + {Name: "10.0.0.0/24", CIDR: "10.0.0.0/24", RunsCount: 257, IsOk: false, IP: "10.0.0.255"}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + r, err := NewCIDRParse(tt.CIDR, true) + require.NoError(t, err) + + ip := "" + ok := false + + next := r.NextIPFunc() + for i := 0; i < tt.RunsCount; i++ { + ip, ok = next() + } + require.Equal(t, tt.IsOk, ok) + require.Equal(t, tt.IP, ip) + }) + } +} + +func toggleError(t *testing.T, err error, needError bool) { + if needError { + require.Error(t, err) + } else { + require.NoError(t, err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6fd58cd --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/goiste/cidr_parse + +go 1.18 + +require github.com/stretchr/testify v1.8.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5164829 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=