Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
goiste committed Aug 11, 2022
0 parents commit 1f8c08a
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CIDR Parse

Split CIDR to the range of IP addresses.
116 changes: 116 additions & 0 deletions cidr_parse.go
Original file line number Diff line number Diff line change
@@ -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 | octets[1]<<maskShiftSecond | octets[2]<<maskShiftThird | octets[3])
}

// converts uint32 number to IP string
func uint32ToIP(ip uint32) string {
return fmt.Sprintf(
"%d.%d.%d.%d",
ip&maskFirst>>maskShiftFirst,
ip&maskSecond>>maskShiftSecond,
ip&maskThird>>maskShiftThird,
ip&maskLast,
)
}
191 changes: 191 additions & 0 deletions cidr_parse_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=

0 comments on commit 1f8c08a

Please sign in to comment.