diff --git a/cmd/cmd.go b/cmd/cmd.go index b90c18f..9825497 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -2,28 +2,26 @@ package cmd import ( "context" - "log" - "os" + "fmt" + "io" "strconv" "github.com/urfave/cli/v3" ) -func Execute() { +func Execute(stdout, stderr io.Writer, args []string) { cmd := &cli.Command{ Name: "cider", Usage: "cli tool to help with common IP related tasks", Commands: []*cli.Command{ - newRanges(), - newIn(), - newSubnet(), - newInfo(), - newVersion(), + newRanges(stdout), + newIn(stdout), + newSubnet(stdout), + newInfo(stdout), + newVersion(stdout), }, } - args := os.Args - // if the first arg is a number, treat it as the "ranges" command if len(args) == 2 { maybeNumber := args[1] @@ -34,6 +32,6 @@ func Execute() { } if err := cmd.Run(context.Background(), args); err != nil { - log.Fatal(err) + fmt.Fprint(stderr, err) } } diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 0000000..d821ccd --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,44 @@ +package cmd_test + +import ( + "bytes" + "cider/cmd" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testCase struct { + name string + input []string + stdOutput string + stdErr string +} + +func executeTestCases(t *testing.T, testCases []testCase) { + executeTestCasesWithCustomAssertion( + t, + testCases, + func(t *testing.T, tc testCase, stdout, stderr string) { + assert.Equal(t, tc.stdOutput, stdout, "std output not as expected") + assert.Equal(t, tc.stdErr, stderr, "err output not as expected") + }, + ) +} + +func executeTestCasesWithCustomAssertion( + t *testing.T, + testCases []testCase, + assertion func(t *testing.T, tc testCase, stdout, stderr string), +) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) + + cmd.Execute(stdOut, stdErr, tc.input) + + assertion(t, tc, stdOut.String(), stdErr.String()) + }) + } +} diff --git a/cmd/in.go b/cmd/in.go index e615be0..49ab90e 100644 --- a/cmd/in.go +++ b/cmd/in.go @@ -3,11 +3,12 @@ package cmd import ( "cider/internal/commands/in" "context" + "io" "github.com/urfave/cli/v3" ) -func newIn() *cli.Command { +func newIn(stdout io.Writer) *cli.Command { return &cli.Command{ Name: "in", Usage: "Determine if an ip or range falls within one or more ranges", @@ -15,7 +16,7 @@ func newIn() *cli.Command { Action: func(_ context.Context, command *cli.Command) error { args := command.Args().Slice() - handler := in.New() + handler := in.New(stdout) return handler.Handle(args) }, diff --git a/cmd/in_test.go b/cmd/in_test.go new file mode 100644 index 0000000..ac55418 --- /dev/null +++ b/cmd/in_test.go @@ -0,0 +1,27 @@ +package cmd_test + +import "testing" + +func TestIn(t *testing.T) { + testCases := []testCase{ + { + name: "missing args", + input: []string{"cider", "in"}, + stdOutput: "", + stdErr: "command expects at least 2 arguments", + }, + { + name: "ip inside range", + input: []string{"cider", "in", "10.164.214.32", "10.164.214.0/26"}, + stdOutput: "10.164.214.0/26\n", + stdErr: "", + }, + { + name: "ip outside range", + input: []string{"cider", "in", "10.164.215.0", "10.164.214.0/26"}, + stdOutput: "10.164.215.0 is not in any of the provided ranges\n", + stdErr: "", + }, + } + executeTestCases(t, testCases) +} diff --git a/cmd/info.go b/cmd/info.go index 182b6ae..bcc9595 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -3,11 +3,12 @@ package cmd import ( "cider/internal/commands/info" "context" + "io" "github.com/urfave/cli/v3" ) -func newInfo() *cli.Command { +func newInfo(stdout io.Writer) *cli.Command { return &cli.Command{ Name: "info", Usage: "Display information about a range", @@ -15,7 +16,7 @@ func newInfo() *cli.Command { Action: func(_ context.Context, command *cli.Command) error { args := command.Args().Slice() - handler := info.New() + handler := info.New(stdout) return handler.Handle(args) }, diff --git a/cmd/info_test.go b/cmd/info_test.go new file mode 100644 index 0000000..392a45c --- /dev/null +++ b/cmd/info_test.go @@ -0,0 +1,21 @@ +package cmd_test + +import "testing" + +func TestInfo(t *testing.T) { + testCases := []testCase{ + { + name: "missing args", + input: []string{"cider", "info"}, + stdOutput: "", + stdErr: "command expects exactly one argument", + }, + { + name: "prints info", + input: []string{"cider", "info", "10.0.64.0/18"}, + stdOutput: "Address range : 10.0.64.0 - 10.0.127.255\nStart of next block : 10.0.128.0\nMask : /18 (255.255.192.0)\nAddresses : 16384\nAzure addresses : 16379\nBinary : 00001010.00000000.01000000.00000000\nDecimal : 167788544\n", + stdErr: "", + }, + } + executeTestCases(t, testCases) +} diff --git a/cmd/ranges.go b/cmd/ranges.go index 7305c84..2032a02 100644 --- a/cmd/ranges.go +++ b/cmd/ranges.go @@ -3,11 +3,12 @@ package cmd import ( "cider/internal/commands/ranges" "context" + "io" "github.com/urfave/cli/v3" ) -func newRanges() *cli.Command { +func newRanges(stdout io.Writer) *cli.Command { return &cli.Command{ Name: "ranges", Usage: "Display all CIDR ranges", @@ -15,7 +16,7 @@ func newRanges() *cli.Command { Action: func(_ context.Context, command *cli.Command) error { arg := command.Args().First() - handler := ranges.New() + handler := ranges.New(stdout) return handler.Handle(arg) }, diff --git a/cmd/ranges_test.go b/cmd/ranges_test.go new file mode 100644 index 0000000..d9cd6aa --- /dev/null +++ b/cmd/ranges_test.go @@ -0,0 +1,41 @@ +package cmd_test + +import ( + "testing" +) + +func TestRanges(t *testing.T) { + testCases := []testCase{ + { + name: "single range", + input: []string{"cider", "ranges", "21"}, + stdOutput: "Cidr Mask Addresses Azure addresses\n/21 255.255.248.0 2048 2043\n", + stdErr: "", + }, + { + name: "single range shorthand", + input: []string{"cider", "r", "21"}, + stdOutput: "Cidr Mask Addresses Azure addresses\n/21 255.255.248.0 2048 2043\n", + stdErr: "", + }, + { + name: "single range no command", + input: []string{"cider", "21"}, + stdOutput: "Cidr Mask Addresses Azure addresses\n/21 255.255.248.0 2048 2043\n", + stdErr: "", + }, + { + name: "all ranges", + input: []string{"cider", "ranges"}, + stdOutput: "Cidr Mask Addresses Azure addresses\n/0 0.0.0.0 4294967296 4294967291\n/1 128.0.0.0 2147483648 2147483643\n/2 192.0.0.0 1073741824 1073741819\n/3 224.0.0.0 536870912 536870907\n/4 240.0.0.0 268435456 268435451\n/5 248.0.0.0 134217728 134217723\n/6 252.0.0.0 67108864 67108859\n/7 254.0.0.0 33554432 33554427\n/8 255.0.0.0 16777216 16777211\n/9 255.128.0.0 8388608 8388603\n/10 255.192.0.0 4194304 4194299\n/11 255.224.0.0 2097152 2097147\n/12 255.240.0.0 1048576 1048571\n/13 255.248.0.0 524288 524283\n/14 255.252.0.0 262144 262139\n/15 255.254.0.0 131072 131067\n/16 255.255.0.0 65536 65531\n/17 255.255.128.0 32768 32763\n/18 255.255.192.0 16384 16379\n/19 255.255.224.0 8192 8187\n/20 255.255.240.0 4096 4091\n/21 255.255.248.0 2048 2043\n/22 255.255.252.0 1024 1019\n/23 255.255.254.0 512 507\n/24 255.255.255.0 256 251\n/25 255.255.255.128 128 123\n/26 255.255.255.192 64 59\n/27 255.255.255.224 32 27\n/28 255.255.255.240 16 11\n/29 255.255.255.248 8 3\n/30 255.255.255.252 4 N/A\n/31 255.255.255.254 2 N/A\n/32 255.255.255.255 1 N/A\n", + stdErr: "", + }, + { + name: "string as range", + input: []string{"cider", "ranges", "not a range"}, + stdOutput: "", + stdErr: "not a range is not a valid integer", + }, + } + executeTestCases(t, testCases) +} diff --git a/cmd/subnet.go b/cmd/subnet.go index c17252f..c4b59b3 100644 --- a/cmd/subnet.go +++ b/cmd/subnet.go @@ -3,11 +3,12 @@ package cmd import ( "cider/internal/commands/subnet" "context" + "io" "github.com/urfave/cli/v3" ) -func newSubnet() *cli.Command { +func newSubnet(stdout io.Writer) *cli.Command { return &cli.Command{ Name: "subnet", Usage: "Split a range into multiple smaller ranges", @@ -15,7 +16,7 @@ func newSubnet() *cli.Command { Action: func(_ context.Context, command *cli.Command) error { args := command.Args().Slice() - handler := subnet.New() + handler := subnet.New(stdout) return handler.Handle(args) }, diff --git a/cmd/subnet_test.go b/cmd/subnet_test.go new file mode 100644 index 0000000..281af34 --- /dev/null +++ b/cmd/subnet_test.go @@ -0,0 +1,27 @@ +package cmd_test + +import "testing" + +func TestSubnet(t *testing.T) { + testCases := []testCase{ + { + name: "missing args", + input: []string{"cider", "subnet"}, + stdOutput: "", + stdErr: "command expects at least 2 arguments", + }, + { + name: "single arg", + input: []string{"cider", "subnet", "10.163.0.0/16"}, + stdOutput: "", + stdErr: "command expects at least 2 arguments", + }, + { + name: "valid range", + input: []string{"cider", "subnet", "10.163.0.0/16", "19", "19", "19", "19"}, + stdOutput: "10.163.0.0/19\n10.163.32.0/19\n10.163.64.0/19\n10.163.96.0/19\n", + stdErr: "", + }, + } + executeTestCases(t, testCases) +} diff --git a/cmd/version.go b/cmd/version.go index ff27d91..1e79352 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,19 +3,20 @@ package cmd import ( "context" "fmt" + "io" "github.com/urfave/cli/v3" ) -var version string +var version string = "version" -func newVersion() *cli.Command { +func newVersion(stdout io.Writer) *cli.Command { return &cli.Command{ Name: "version", Aliases: []string{"v"}, Usage: "Show version", Action: func(_ context.Context, command *cli.Command) error { - fmt.Println(version) + fmt.Fprintln(stdout, version) return nil }, diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 0000000..4c78d5c --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,17 @@ +package cmd_test + +import ( + "testing" +) + +func TestVersion(t *testing.T) { + testCases := []testCase{ + { + name: "returns version", + input: []string{"cider", "version"}, + stdOutput: "version\n", + stdErr: "", + }, + } + executeTestCases(t, testCases) +} diff --git a/go.mod b/go.mod index 7a6c392..a5a66f6 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,13 @@ module cider go 1.24 -require github.com/urfave/cli/v3 v3.0.0-beta1 +require ( + github.com/stretchr/testify v1.10.0 + github.com/urfave/cli/v3 v3.0.0-beta1 +) + +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 index 3c92cd0..0c8075b 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,19 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cidr/cidr_block.go b/internal/cidr/cidr_block.go index 3c80ed1..d72fa0a 100644 --- a/internal/cidr/cidr_block.go +++ b/internal/cidr/cidr_block.go @@ -1,7 +1,9 @@ package cidr import ( + "cider/internal/ip" "cider/internal/list" + "cider/internal/must" "cider/internal/utils" "fmt" "math" @@ -14,83 +16,71 @@ const ( INT_SIZE = 32 ) -type CIDRBlock struct { - Network string - Host int +type CidrBlock struct { + Ip *ip.Ip + Host int } -func NewBlock(network string) *CIDRBlock { - networkAndHostPortion := strings.Split(network, "/") - - networkPortion := networkAndHostPortion[0] - hostPortion := networkAndHostPortion[1] - - return &CIDRBlock{ - Network: networkPortion, - Host: must(strconv.Atoi(hostPortion)), +func NewBlock(ip *ip.Ip, host int) *CidrBlock { + return &CidrBlock{ + Ip: ip, + Host: host, } } -func (b *CIDRBlock) Subnet(sizes []int) ([]string, error) { +func (b *CidrBlock) Subnet(sizes []int) ([]string, error) { // sort subnets largest to smallest to prevent fragmentation slices.Sort(sizes) next := b subnets := []string{} for _, size := range sizes { - subnetBlock := NewBlock(fmt.Sprintf("%s/%v", next.Network, size)) + subnetBlock := NewBlock(next.Ip, size) - if !b.Contains(subnetBlock.Network) { - return nil, fmt.Errorf("invalid configuration: subnet %s/%v is outside provided network range %s/%v", next.Network, size, b.Network, b.Host) + if !b.ContainsIp(subnetBlock.Ip) { + return nil, fmt.Errorf("invalid configuration: subnet %s/%v is outside provided network range %s/%v", next.Ip, size, b.Ip, b.Host) } - subnets = append(subnets, fmt.Sprintf("%s/%v", subnetBlock.Network, subnetBlock.Host)) + subnets = append(subnets, fmt.Sprintf("%s/%v", subnetBlock.Ip.Ip(), subnetBlock.Host)) - next = NewBlock(fmt.Sprintf("%s/%v", subnetBlock.StartAddressOfNextBlock(), size)) + next = NewBlock(ip.NewIp(subnetBlock.StartAddressOfNextBlock()), size) } return subnets, nil } // https://stackoverflow.com/questions/9622967/how-to-see-if-an-ip-address-belongs-inside-of-a-range-of-ips-using-cidr-notation -func (outer *CIDRBlock) Contains(inner string) bool { - innerNetwork := strings.Split(inner, "/")[0] - - IP_addr := ipToDecimal(innerNetwork) - CIDR_addr := ipToDecimal(outer.Network) - CIDR_mask := -1 << (INT_SIZE - outer.Host) +func (b *CidrBlock) ContainsIp(inner *ip.Ip) bool { + IP_addr := inner.ToDecimal() + CIDR_addr := b.Ip.ToDecimal() + CIDR_mask := -1 << (INT_SIZE - b.Host) return (IP_addr & CIDR_mask) == (CIDR_addr & CIDR_mask) } -// http://www.aboutmyip.com/AboutMyXApp/IP2Integer.jsp -func ipToDecimal(ip string) int { - parts := strings.Split(ip, ".") - - base := 10 - octet1 := must(strconv.ParseUint(parts[0], base, INT_SIZE)) - octet2 := must(strconv.ParseUint(parts[1], base, INT_SIZE)) - octet3 := must(strconv.ParseUint(parts[2], base, INT_SIZE)) - octet4 := must(strconv.ParseUint(parts[3], base, INT_SIZE)) - - return int((octet1 * 16777216) + (octet2 * 65536) + (octet3 * 256) + octet4) +func (b *CidrBlock) ContainsRange(inner *CidrBlock) bool { + return b.ContainsIp(inner.Ip) } -func (b *CIDRBlock) NetworkPortionBinary() string { - octets := strings.Split(b.Network, ".") +func (b *CidrBlock) NetworkPortionBinary() string { + octets := strings.Split(b.Ip.Ip(), ".") octets = list.Map(octets, toBin) return fmt.Sprintf("%s.%s.%s.%s", octets[0], octets[1], octets[2], octets[3]) } func toBin(s string) string { - asInt := must(strconv.ParseUint(s, 10, INT_SIZE)) + asInt := must.Must(strconv.ParseUint(s, 10, INT_SIZE)) asBinaryString := strconv.FormatUint(asInt, 2) - paddedBynaryString := utils.PadLeft(asBinaryString, '0', 8) - return paddedBynaryString + paddedBinaryString := utils.PadLeft(asBinaryString, '0', 8) + return paddedBinaryString } -func (b *CIDRBlock) Mask() string { +func (b *CidrBlock) ToDecimal() int { + return b.Ip.ToDecimal() +} + +func (b *CidrBlock) Mask() string { ones := strings.Repeat("1", b.Host) zeroes := strings.Repeat("0", INT_SIZE-b.Host) @@ -101,13 +91,13 @@ func (b *CIDRBlock) Mask() string { return fmt.Sprintf("%v.%v.%v.%v", octets[0], octets[1], octets[2], octets[3]) } -func (b *CIDRBlock) AvailableHosts() uint { +func (b *CidrBlock) AvailableHosts() uint { numAddresses := math.Pow(2, float64(INT_SIZE)-float64(b.Host)) return uint(numAddresses) } -func (b *CIDRBlock) AvailableAzureHosts() string { +func (b *CidrBlock) AvailableAzureHosts() string { // Azure reserves the first four addresses and the last address, for a total of five IP addresses within each subnet // https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets if b.AvailableHosts() >= 5 { @@ -117,12 +107,12 @@ func (b *CIDRBlock) AvailableAzureHosts() string { return "N/A" } -func (b *CIDRBlock) StartAddressOfNextBlock() string { +func (b *CidrBlock) StartAddressOfNextBlock() string { octets := strings.Split(b.BroadcastAddress(), ".") octets = list.Map(octets, toBin) binStr := strings.Join(octets, "") - next := must(strconv.ParseUint(binStr, 2, INT_SIZE)) + 1 + next := must.Must(strconv.ParseUint(binStr, 2, INT_SIZE)) + 1 asBinaryString := strconv.FormatUint(next, 2) asBinaryString = utils.PadLeft(asBinaryString, '0', 32) @@ -132,7 +122,7 @@ func (b *CIDRBlock) StartAddressOfNextBlock() string { return fmt.Sprintf("%v.%v.%v.%v", octetsInt[0], octetsInt[1], octetsInt[2], octetsInt[3]) } -func (b *CIDRBlock) NetworkAddress() string { +func (b *CidrBlock) NetworkAddress() string { ipBin := strings.ReplaceAll(b.NetworkPortionBinary(), ".", "")[0:b.Host] broadcast := ipBin + strings.Repeat("0", INT_SIZE-b.Host) @@ -142,7 +132,7 @@ func (b *CIDRBlock) NetworkAddress() string { return fmt.Sprintf("%v.%v.%v.%v", octets[0], octets[1], octets[2], octets[3]) } -func (b *CIDRBlock) BroadcastAddress() string { +func (b *CidrBlock) BroadcastAddress() string { // https://stackoverflow.com/questions/1470792/how-to-calculate-the-ip-range-when-the-ip-address-and-the-netmask-is-given ipBin := strings.ReplaceAll(b.NetworkPortionBinary(), ".", "")[0:b.Host] @@ -157,18 +147,10 @@ func stringToOctets(ipString string) []uint64 { octets := make([]uint64, 4) base := 2 - octets[0] = must(strconv.ParseUint(ipString[0:8], base, INT_SIZE)) - octets[1] = must(strconv.ParseUint(ipString[8:16], base, INT_SIZE)) - octets[2] = must(strconv.ParseUint(ipString[16:24], base, INT_SIZE)) - octets[3] = must(strconv.ParseUint(ipString[24:32], base, INT_SIZE)) + octets[0] = must.Must(strconv.ParseUint(ipString[0:8], base, INT_SIZE)) + octets[1] = must.Must(strconv.ParseUint(ipString[8:16], base, INT_SIZE)) + octets[2] = must.Must(strconv.ParseUint(ipString[16:24], base, INT_SIZE)) + octets[3] = must.Must(strconv.ParseUint(ipString[24:32], base, INT_SIZE)) return octets } - -func must[T any](x T, e error) T { - if e != nil { - panic(e) - } - - return x -} diff --git a/internal/cidr/cidr_block_test.go b/internal/cidr/cidr_block_test.go index 299d8ea..2510dbb 100644 --- a/internal/cidr/cidr_block_test.go +++ b/internal/cidr/cidr_block_test.go @@ -2,18 +2,19 @@ package cidr_test import ( "cider/internal/cidr" + "cider/internal/ip" "reflect" "testing" ) func Test_SubnetSuccess(t *testing.T) { tests := map[string]struct { - input *cidr.CIDRBlock + input *cidr.CidrBlock sizes []int expected []string }{ - "two even subnets": {input: cidr.NewBlock("10.0.0.0/16"), sizes: []int{17, 17}, expected: []string{"10.0.0.0/17", "10.0.128.0/17"}}, - "subnets of different sizes": {input: cidr.NewBlock("10.0.0.0/16"), sizes: []int{18, 17, 20}, expected: []string{"10.0.0.0/17", "10.0.128.0/18", "10.0.192.0/20"}}, + "two even subnets": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 16), sizes: []int{17, 17}, expected: []string{"10.0.0.0/17", "10.0.128.0/17"}}, + "subnets of different sizes": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 16), sizes: []int{18, 17, 20}, expected: []string{"10.0.0.0/17", "10.0.128.0/18", "10.0.192.0/20"}}, } for name, test := range tests { @@ -31,11 +32,11 @@ func Test_SubnetSuccess(t *testing.T) { func Test_SubnetFailure(t *testing.T) { tests := map[string]struct { - input *cidr.CIDRBlock + input *cidr.CidrBlock sizes []int }{ - "invalid configuration": {input: cidr.NewBlock("10.0.0.0/16"), sizes: []int{16, 16}}, - "insufficient space": {input: cidr.NewBlock("10.0.0.0/30"), sizes: []int{32, 32, 32, 32, 32}}, + "invalid configuration": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 16), sizes: []int{16, 16}}, + "insufficient space": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 30), sizes: []int{32, 32, 32, 32, 32}}, } for name, test := range tests { @@ -51,23 +52,65 @@ func Test_SubnetFailure(t *testing.T) { } } -func Test_Contains(t *testing.T) { +func Test_ContainsIp(t *testing.T) { tests := map[string]struct { - ip string - ipRange *cidr.CIDRBlock + ip *ip.Ip + ipRange *cidr.CidrBlock expected bool }{ - "ip inside range": {ip: "10.0.0.5", ipRange: cidr.NewBlock("10.0.0.0/28"), expected: true}, - "ip outside range": {ip: "10.0.0.17", ipRange: cidr.NewBlock("10.0.0.0/28"), expected: false}, - "range inside range": {ip: "10.0.0.4/30", ipRange: cidr.NewBlock("10.0.0.0/28"), expected: true}, - "range outside range": {ip: "10.0.0.16/29", ipRange: cidr.NewBlock("10.0.0.0/28"), expected: false}, + "ip inside range": {ip: ip.NewIp("10.0.0.5"), ipRange: cidr.NewBlock(ip.NewIp("10.0.0.0"), 28), expected: true}, + "ip outside range": {ip: ip.NewIp("10.0.0.17"), ipRange: cidr.NewBlock(ip.NewIp("10.0.0.0"), 28), expected: false}, } for name, test := range tests { t.Run(name, func(t *testing.T) { t.Parallel() - actual := test.ipRange.Contains(test.ip) + actual := test.ipRange.ContainsIp(test.ip) + + if actual != test.expected { + t.Fatalf("%s: got %v expected %v", name, actual, test.expected) + } + }) + } +} + +func Test_ContainsRange(t *testing.T) { + tests := map[string]struct { + inner *cidr.CidrBlock + ipRange *cidr.CidrBlock + expected bool + }{ + "range inside range": {inner: cidr.NewBlock(ip.NewIp("10.0.0.4"), 30), ipRange: cidr.NewBlock(ip.NewIp("10.0.0.0"), 28), expected: true}, + "range outside range": {inner: cidr.NewBlock(ip.NewIp("10.0.0.16"), 29), ipRange: cidr.NewBlock(ip.NewIp("10.0.0.0"), 28), expected: false}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := test.ipRange.ContainsRange(test.inner) + + if actual != test.expected { + t.Fatalf("%s: got %v expected %v", name, actual, test.expected) + } + }) + } +} + +func Test_ToDecimal(t *testing.T) { + tests := map[string]struct { + block *cidr.CidrBlock + expected int + }{ + "decimal": {block: cidr.NewBlock(ip.NewIp("10.0.0.5"), 10), expected: 167772165}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := test.block.ToDecimal() if actual != test.expected { t.Fatalf("%s: got %v expected %v", name, actual, test.expected) @@ -78,12 +121,12 @@ func Test_Contains(t *testing.T) { func Test_NetworkPortionBinary(t *testing.T) { tests := map[string]struct { - input *cidr.CIDRBlock + input *cidr.CidrBlock expected string }{ - "10.0.0.0/8": {input: cidr.NewBlock("10.0.0.0/8"), expected: "00001010.00000000.00000000.00000000"}, - "172.16.0.0/12": {input: cidr.NewBlock("172.16.0.0/12"), expected: "10101100.00010000.00000000.00000000"}, - "192.168.0.0/16": {input: cidr.NewBlock("192.168.0.0/16"), expected: "11000000.10101000.00000000.00000000"}, + "10.0.0.0/8": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 8), expected: "00001010.00000000.00000000.00000000"}, + "172.16.0.0/12": {input: cidr.NewBlock(ip.NewIp("172.16.0.0"), 12), expected: "10101100.00010000.00000000.00000000"}, + "192.168.0.0/16": {input: cidr.NewBlock(ip.NewIp("192.168.0.0"), 16), expected: "11000000.10101000.00000000.00000000"}, } for name, test := range tests { @@ -101,42 +144,42 @@ func Test_NetworkPortionBinary(t *testing.T) { func Test_Mask(t *testing.T) { tests := map[string]struct { - input *cidr.CIDRBlock + input *cidr.CidrBlock expected string }{ - "/0": {input: cidr.NewBlock("10.0.0.0/0"), expected: "0.0.0.0"}, - "/1": {input: cidr.NewBlock("10.0.0.0/1"), expected: "128.0.0.0"}, - "/2": {input: cidr.NewBlock("10.0.0.0/2"), expected: "192.0.0.0"}, - "/3": {input: cidr.NewBlock("10.0.0.0/3"), expected: "224.0.0.0"}, - "/4": {input: cidr.NewBlock("10.0.0.0/4"), expected: "240.0.0.0"}, - "/5": {input: cidr.NewBlock("10.0.0.0/5"), expected: "248.0.0.0"}, - "/6": {input: cidr.NewBlock("10.0.0.0/6"), expected: "252.0.0.0"}, - "/7": {input: cidr.NewBlock("10.0.0.0/7"), expected: "254.0.0.0"}, - "/8": {input: cidr.NewBlock("10.0.0.0/8"), expected: "255.0.0.0"}, - "/9": {input: cidr.NewBlock("10.0.0.0/9"), expected: "255.128.0.0"}, - "/10": {input: cidr.NewBlock("10.0.0.0/10"), expected: "255.192.0.0"}, - "/11": {input: cidr.NewBlock("10.0.0.0/11"), expected: "255.224.0.0"}, - "/12": {input: cidr.NewBlock("10.0.0.0/12"), expected: "255.240.0.0"}, - "/13": {input: cidr.NewBlock("10.0.0.0/13"), expected: "255.248.0.0"}, - "/14": {input: cidr.NewBlock("10.0.0.0/14"), expected: "255.252.0.0"}, - "/15": {input: cidr.NewBlock("10.0.0.0/15"), expected: "255.254.0.0"}, - "/16": {input: cidr.NewBlock("10.0.0.0/16"), expected: "255.255.0.0"}, - "/17": {input: cidr.NewBlock("10.0.0.0/17"), expected: "255.255.128.0"}, - "/18": {input: cidr.NewBlock("10.0.0.0/18"), expected: "255.255.192.0"}, - "/19": {input: cidr.NewBlock("10.0.0.0/19"), expected: "255.255.224.0"}, - "/20": {input: cidr.NewBlock("10.0.0.0/20"), expected: "255.255.240.0"}, - "/21": {input: cidr.NewBlock("10.0.0.0/21"), expected: "255.255.248.0"}, - "/22": {input: cidr.NewBlock("10.0.0.0/22"), expected: "255.255.252.0"}, - "/23": {input: cidr.NewBlock("10.0.0.0/23"), expected: "255.255.254.0"}, - "/24": {input: cidr.NewBlock("10.0.0.0/24"), expected: "255.255.255.0"}, - "/25": {input: cidr.NewBlock("10.0.0.0/25"), expected: "255.255.255.128"}, - "/26": {input: cidr.NewBlock("10.0.0.0/26"), expected: "255.255.255.192"}, - "/27": {input: cidr.NewBlock("10.0.0.0/27"), expected: "255.255.255.224"}, - "/28": {input: cidr.NewBlock("10.0.0.0/28"), expected: "255.255.255.240"}, - "/29": {input: cidr.NewBlock("10.0.0.0/29"), expected: "255.255.255.248"}, - "/30": {input: cidr.NewBlock("10.0.0.0/30"), expected: "255.255.255.252"}, - "/31": {input: cidr.NewBlock("10.0.0.0/31"), expected: "255.255.255.254"}, - "/32": {input: cidr.NewBlock("10.0.0.0/32"), expected: "255.255.255.255"}, + "/0": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 0), expected: "0.0.0.0"}, + "/1": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 1), expected: "128.0.0.0"}, + "/2": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 2), expected: "192.0.0.0"}, + "/3": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 3), expected: "224.0.0.0"}, + "/4": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 4), expected: "240.0.0.0"}, + "/5": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 5), expected: "248.0.0.0"}, + "/6": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 6), expected: "252.0.0.0"}, + "/7": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 7), expected: "254.0.0.0"}, + "/8": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 8), expected: "255.0.0.0"}, + "/9": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 9), expected: "255.128.0.0"}, + "/10": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 10), expected: "255.192.0.0"}, + "/11": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 11), expected: "255.224.0.0"}, + "/12": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 12), expected: "255.240.0.0"}, + "/13": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 13), expected: "255.248.0.0"}, + "/14": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 14), expected: "255.252.0.0"}, + "/15": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 15), expected: "255.254.0.0"}, + "/16": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 16), expected: "255.255.0.0"}, + "/17": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 17), expected: "255.255.128.0"}, + "/18": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 18), expected: "255.255.192.0"}, + "/19": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 19), expected: "255.255.224.0"}, + "/20": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 20), expected: "255.255.240.0"}, + "/21": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 21), expected: "255.255.248.0"}, + "/22": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 22), expected: "255.255.252.0"}, + "/23": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 23), expected: "255.255.254.0"}, + "/24": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 24), expected: "255.255.255.0"}, + "/25": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 25), expected: "255.255.255.128"}, + "/26": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 26), expected: "255.255.255.192"}, + "/27": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 27), expected: "255.255.255.224"}, + "/28": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 28), expected: "255.255.255.240"}, + "/29": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 29), expected: "255.255.255.248"}, + "/30": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 30), expected: "255.255.255.252"}, + "/31": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 31), expected: "255.255.255.254"}, + "/32": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 32), expected: "255.255.255.255"}, } for name, test := range tests { @@ -154,42 +197,42 @@ func Test_Mask(t *testing.T) { func Test_AvailableHosts(t *testing.T) { tests := map[string]struct { - input *cidr.CIDRBlock + input *cidr.CidrBlock expected uint }{ - "/0": {input: cidr.NewBlock("10.0.0.0/0"), expected: 4294967296}, - "/1": {input: cidr.NewBlock("10.0.0.0/1"), expected: 2147483648}, - "/2": {input: cidr.NewBlock("10.0.0.0/2"), expected: 1073741824}, - "/3": {input: cidr.NewBlock("10.0.0.0/3"), expected: 536870912}, - "/4": {input: cidr.NewBlock("10.0.0.0/4"), expected: 268435456}, - "/5": {input: cidr.NewBlock("10.0.0.0/5"), expected: 134217728}, - "/6": {input: cidr.NewBlock("10.0.0.0/6"), expected: 67108864}, - "/7": {input: cidr.NewBlock("10.0.0.0/7"), expected: 33554432}, - "/8": {input: cidr.NewBlock("10.0.0.0/8"), expected: 16777216}, - "/9": {input: cidr.NewBlock("10.0.0.0/9"), expected: 8388608}, - "/10": {input: cidr.NewBlock("10.0.0.0/10"), expected: 4194304}, - "/11": {input: cidr.NewBlock("10.0.0.0/11"), expected: 2097152}, - "/12": {input: cidr.NewBlock("10.0.0.0/12"), expected: 1048576}, - "/13": {input: cidr.NewBlock("10.0.0.0/13"), expected: 524288}, - "/14": {input: cidr.NewBlock("10.0.0.0/14"), expected: 262144}, - "/15": {input: cidr.NewBlock("10.0.0.0/15"), expected: 131072}, - "/16": {input: cidr.NewBlock("10.0.0.0/16"), expected: 65536}, - "/17": {input: cidr.NewBlock("10.0.0.0/17"), expected: 32768}, - "/18": {input: cidr.NewBlock("10.0.0.0/18"), expected: 16384}, - "/19": {input: cidr.NewBlock("10.0.0.0/19"), expected: 8192}, - "/20": {input: cidr.NewBlock("10.0.0.0/20"), expected: 4096}, - "/21": {input: cidr.NewBlock("10.0.0.0/21"), expected: 2048}, - "/22": {input: cidr.NewBlock("10.0.0.0/22"), expected: 1024}, - "/23": {input: cidr.NewBlock("10.0.0.0/23"), expected: 512}, - "/24": {input: cidr.NewBlock("10.0.0.0/24"), expected: 256}, - "/25": {input: cidr.NewBlock("10.0.0.0/25"), expected: 128}, - "/26": {input: cidr.NewBlock("10.0.0.0/26"), expected: 64}, - "/27": {input: cidr.NewBlock("10.0.0.0/27"), expected: 32}, - "/28": {input: cidr.NewBlock("10.0.0.0/28"), expected: 16}, - "/29": {input: cidr.NewBlock("10.0.0.0/29"), expected: 8}, - "/30": {input: cidr.NewBlock("10.0.0.0/30"), expected: 4}, - "/31": {input: cidr.NewBlock("10.0.0.0/31"), expected: 2}, - "/32": {input: cidr.NewBlock("10.0.0.0/32"), expected: 1}, + "/0": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 0), expected: 4294967296}, + "/1": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 1), expected: 2147483648}, + "/2": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 2), expected: 1073741824}, + "/3": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 3), expected: 536870912}, + "/4": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 4), expected: 268435456}, + "/5": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 5), expected: 134217728}, + "/6": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 6), expected: 67108864}, + "/7": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 7), expected: 33554432}, + "/8": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 8), expected: 16777216}, + "/9": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 9), expected: 8388608}, + "/10": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 10), expected: 4194304}, + "/11": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 11), expected: 2097152}, + "/12": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 12), expected: 1048576}, + "/13": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 13), expected: 524288}, + "/14": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 14), expected: 262144}, + "/15": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 15), expected: 131072}, + "/16": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 16), expected: 65536}, + "/17": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 17), expected: 32768}, + "/18": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 18), expected: 16384}, + "/19": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 19), expected: 8192}, + "/20": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 20), expected: 4096}, + "/21": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 21), expected: 2048}, + "/22": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 22), expected: 1024}, + "/23": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 23), expected: 512}, + "/24": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 24), expected: 256}, + "/25": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 25), expected: 128}, + "/26": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 26), expected: 64}, + "/27": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 27), expected: 32}, + "/28": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 28), expected: 16}, + "/29": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 29), expected: 8}, + "/30": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 30), expected: 4}, + "/31": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 31), expected: 2}, + "/32": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 32), expected: 1}, } for name, test := range tests { @@ -207,15 +250,15 @@ func Test_AvailableHosts(t *testing.T) { func Test_StartAddressOfNextBlock(t *testing.T) { tests := map[string]struct { - input *cidr.CIDRBlock + input *cidr.CidrBlock expected string }{ - "10.0.0.0/8": {input: cidr.NewBlock("10.0.0.0/8"), expected: "11.0.0.0"}, - "127.0.0.0/8": {input: cidr.NewBlock("127.0.0.0/8"), expected: "128.0.0.0"}, - "169.254.0.0/16": {input: cidr.NewBlock("169.254.0.0/16"), expected: "169.255.0.0"}, - "172.16.0.0/12": {input: cidr.NewBlock("172.16.0.0/12"), expected: "172.32.0.0"}, - "192.0.2.0/24": {input: cidr.NewBlock("192.0.2.0/24"), expected: "192.0.3.0"}, - "192.168.0.0/16": {input: cidr.NewBlock("192.168.0.0/16"), expected: "192.169.0.0"}, + "10.0.0.0/8": {input: cidr.NewBlock(ip.NewIp("10.0.0.0"), 8), expected: "11.0.0.0"}, + "127.0.0.0/8": {input: cidr.NewBlock(ip.NewIp("127.0.0.0"), 8), expected: "128.0.0.0"}, + "169.254.0.0/16": {input: cidr.NewBlock(ip.NewIp("169.254.0.0"), 16), expected: "169.255.0.0"}, + "172.16.0.0/12": {input: cidr.NewBlock(ip.NewIp("172.16.0.0"), 12), expected: "172.32.0.0"}, + "192.0.2.0/24": {input: cidr.NewBlock(ip.NewIp("192.0.2.0"), 24), expected: "192.0.3.0"}, + "192.168.0.0/16": {input: cidr.NewBlock(ip.NewIp("192.168.0.0"), 16), expected: "192.169.0.0"}, } for name, test := range tests { @@ -233,10 +276,10 @@ func Test_StartAddressOfNextBlock(t *testing.T) { func Test_NetworkAddress(t *testing.T) { tests := map[string]struct { - input *cidr.CIDRBlock + input *cidr.CidrBlock expected string }{ - "/26": {input: cidr.NewBlock("192.168.33.64/26"), expected: "192.168.33.64"}, + "/26": {input: cidr.NewBlock(ip.NewIp("192.168.33.64"), 26), expected: "192.168.33.64"}, } for name, test := range tests { @@ -254,10 +297,10 @@ func Test_NetworkAddress(t *testing.T) { func Test_BroadcastAddress(t *testing.T) { tests := map[string]struct { - input *cidr.CIDRBlock + input *cidr.CidrBlock expected string }{ - "/26": {input: cidr.NewBlock("192.168.33.64/26"), expected: "192.168.33.127"}, + "/26": {input: cidr.NewBlock(ip.NewIp("192.168.33.64"), 26), expected: "192.168.33.127"}, } for name, test := range tests { diff --git a/internal/commands/in/command.go b/internal/commands/in/command.go index 22abb6a..ac8af84 100644 --- a/internal/commands/in/command.go +++ b/internal/commands/in/command.go @@ -2,14 +2,23 @@ package in import ( "cider/internal/cidr" + "cider/internal/ip" "cider/internal/list" + "cider/internal/must" "fmt" + "io" + "strconv" + "strings" ) -type handler struct{} +type handler struct { + stdout io.Writer +} -func New() *handler { - return &handler{} +func New(stdout io.Writer) *handler { + return &handler{ + stdout: stdout, + } } func (h *handler) Handle(args []string) error { @@ -17,23 +26,45 @@ func (h *handler) Handle(args []string) error { return fmt.Errorf("command expects at least 2 arguments") } - ranges := list.Map(args[1:], func(i string) *cidr.CIDRBlock { - return cidr.NewBlock(i) + ranges := list.Map(args[1:], func(i string) *cidr.CidrBlock { + s := strings.Split(i, "/") + + ip := ip.NewIp(s[0]) + + host := int(must.Must(strconv.ParseInt(s[1], 10, 32))) + + return cidr.NewBlock(ip, host) }) ipOrRange := args[0] - blocksInRange := list.Filter(ranges, func(cidr *cidr.CIDRBlock) bool { - return cidr.Contains(ipOrRange) - }) + var blocksInRange []*cidr.CidrBlock + if strings.Contains(ipOrRange, "/") { + s := strings.Split(ipOrRange, "/") + + ip := ip.NewIp(s[0]) + + host := int(must.Must(strconv.ParseInt(s[1], 10, 32))) + + block := cidr.NewBlock(ip, host) + + blocksInRange = list.Filter(ranges, func(cidr *cidr.CidrBlock) bool { + return cidr.ContainsRange(block) + }) + } else { + ip := ip.NewIp(ipOrRange) + blocksInRange = list.Filter(ranges, func(cidr *cidr.CidrBlock) bool { + return cidr.ContainsIp(ip) + }) + } if len(blocksInRange) == 0 { - fmt.Printf("%s is not in any of the provided ranges\n", ipOrRange) + fmt.Fprintf(h.stdout, "%s is not in any of the provided ranges\n", ipOrRange) return nil } for _, block := range blocksInRange { - fmt.Printf("%s/%d\n", block.Network, block.Host) + fmt.Fprintf(h.stdout, "%s/%d\n", block.Ip.Ip(), block.Host) } return nil diff --git a/internal/commands/info/command.go b/internal/commands/info/command.go index fb211a1..66ccefe 100644 --- a/internal/commands/info/command.go +++ b/internal/commands/info/command.go @@ -2,16 +2,25 @@ package info import ( "cider/internal/cidr" + "cider/internal/ip" "cider/internal/list" + "cider/internal/must" "cider/internal/utils" "fmt" + "io" "math" + "strconv" + "strings" ) -type handler struct{} +type handler struct { + stdout io.Writer +} -func New() *handler { - return &handler{} +func New(stdout io.Writer) *handler { + return &handler{ + stdout: stdout, + } } type pair struct { @@ -23,9 +32,15 @@ func (h *handler) Handle(args []string) error { return fmt.Errorf("command expects exactly one argument") } - ip := args[0] + ipString := args[0] + + s := strings.Split(ipString, "/") + + ip := ip.NewIp(s[0]) + + host := int(must.Must(strconv.ParseInt(s[1], 10, 32))) - block := cidr.NewBlock(ip) + block := cidr.NewBlock(ip, host) entries := []pair{ {item1: "Address range", item2: fmt.Sprintf("%s - %s", block.NetworkAddress(), block.BroadcastAddress())}, @@ -33,14 +48,16 @@ func (h *handler) Handle(args []string) error { {item1: "Mask", item2: fmt.Sprintf("%s (%s)", fmt.Sprintf("/%v", block.Host), block.Mask())}, {item1: "Addresses", item2: fmt.Sprintf("%v", block.AvailableHosts())}, {item1: "Azure addresses", item2: block.AvailableAzureHosts()}, + {item1: "Binary", item2: block.NetworkPortionBinary()}, + {item1: "Decimal", item2: fmt.Sprintf("%v", block.ToDecimal())}, } - printOutput(entries) + h.printOutput(entries) return nil } -func printOutput(entries []pair) { +func (h *handler) printOutput(entries []pair) { keys := []string{} for _, pair := range entries { keys = append(keys, pair.item1) @@ -51,6 +68,6 @@ func printOutput(entries []pair) { }) for _, pair := range entries { - fmt.Printf("%s : %v\n", utils.PadRight(pair.item1, ' ', longestTitle), pair.item2) + fmt.Fprintf(h.stdout, "%s : %v\n", utils.PadRight(pair.item1, ' ', longestTitle), pair.item2) } } diff --git a/internal/commands/ranges/command.go b/internal/commands/ranges/command.go index 4541134..434f23d 100644 --- a/internal/commands/ranges/command.go +++ b/internal/commands/ranges/command.go @@ -2,8 +2,9 @@ package ranges import ( "cider/internal/cidr" + "cider/internal/ip" "fmt" - "os" + "io" "strconv" "text/tabwriter" ) @@ -12,18 +13,22 @@ const ( INT_SIZE = 32 ) -type handler struct{} +type handler struct { + stdout io.Writer +} -func New() *handler { - return &handler{} +func New(stdout io.Writer) *handler { + return &handler{ + stdout: stdout, + } } -func (*handler) Handle(arg string) error { +func (h *handler) Handle(arg string) error { // no args if arg == "" { table := calculateAllCidrBlocks() - return printCidrBlocks(table) + return h.printCidrBlocks(table) } // argument was given. Try to parse it @@ -39,13 +44,13 @@ func (*handler) Handle(arg string) error { block := defaultCidrBlockFromHostPortion(int(hostPortion)) - blocks := []*cidr.CIDRBlock{block} + blocks := []*cidr.CidrBlock{block} - return printCidrBlocks(blocks) + return h.printCidrBlocks(blocks) } -func calculateAllCidrBlocks() []*cidr.CIDRBlock { - blocks := []*cidr.CIDRBlock{} +func calculateAllCidrBlocks() []*cidr.CidrBlock { + blocks := []*cidr.CidrBlock{} for i := range INT_SIZE + 1 { block := defaultCidrBlockFromHostPortion(i) @@ -55,12 +60,12 @@ func calculateAllCidrBlocks() []*cidr.CIDRBlock { return blocks } -func defaultCidrBlockFromHostPortion(hostPortion int) *cidr.CIDRBlock { - return cidr.NewBlock(fmt.Sprintf("10.0.0.0/%v", hostPortion)) +func defaultCidrBlockFromHostPortion(hostPortion int) *cidr.CidrBlock { + return cidr.NewBlock(ip.NewIp("10.0.0.0"), hostPortion) } -func printCidrBlocks(blocks []*cidr.CIDRBlock) error { - w := tabwriter.NewWriter(os.Stdout, 2, 4, 1, ' ', 0) +func (h *handler) printCidrBlocks(blocks []*cidr.CidrBlock) error { + w := tabwriter.NewWriter(h.stdout, 2, 4, 1, ' ', 0) fmt.Fprint(w, "Cidr\tMask\tAddresses\tAzure addresses\n") for _, block := range blocks { diff --git a/internal/commands/subnet/command.go b/internal/commands/subnet/command.go index 4f3b126..af0f8ba 100644 --- a/internal/commands/subnet/command.go +++ b/internal/commands/subnet/command.go @@ -2,18 +2,30 @@ package subnet import ( "cider/internal/cidr" + "cider/internal/ip" "cider/internal/list" + "cider/internal/must" "fmt" + "io" "strconv" + "strings" ) -type handler struct{} +type handler struct { + stdout io.Writer +} -func New() *handler { - return &handler{} +func New(stdout io.Writer) *handler { + return &handler{ + stdout: stdout, + } } -func (*handler) Handle(args []string) error { +func (h *handler) Handle(args []string) error { + if len(args) < 2 { + return fmt.Errorf("command expects at least 2 arguments") + } + rangeToSplit := args[0] sizes := list.Map(args[1:], func(s string) int { n, _ := strconv.Atoi(s) @@ -21,7 +33,13 @@ func (*handler) Handle(args []string) error { return n }) - block := cidr.NewBlock(rangeToSplit) + s := strings.Split(rangeToSplit, "/") + + ip := ip.NewIp(s[0]) + + host := int(must.Must(strconv.ParseInt(s[1], 10, 32))) + + block := cidr.NewBlock(ip, host) subnets, err := block.Subnet(sizes) @@ -30,7 +48,7 @@ func (*handler) Handle(args []string) error { } for _, subnet := range subnets { - fmt.Println(subnet) + fmt.Fprintln(h.stdout, subnet) } return nil diff --git a/internal/ip/ip.go b/internal/ip/ip.go new file mode 100644 index 0000000..c29f7ff --- /dev/null +++ b/internal/ip/ip.go @@ -0,0 +1,46 @@ +package ip + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + INT_SIZE = 32 +) + +type Ip struct { + decimalParts []string +} + +func NewIp(address string) *Ip { + parts := strings.Split(address, ".") + + return &Ip{ + decimalParts: parts, + } +} + +func (ip *Ip) ToDecimal() int { + base := 10 + octet1 := must(strconv.ParseUint(ip.decimalParts[0], base, INT_SIZE)) + octet2 := must(strconv.ParseUint(ip.decimalParts[1], base, INT_SIZE)) + octet3 := must(strconv.ParseUint(ip.decimalParts[2], base, INT_SIZE)) + octet4 := must(strconv.ParseUint(ip.decimalParts[3], base, INT_SIZE)) + + // http://www.aboutmyip.com/AboutMyXApp/IP2Integer.jsp + return int((octet1 * 16777216) + (octet2 * 65536) + (octet3 * 256) + octet4) +} + +func (ip *Ip) Ip() string { + return fmt.Sprintf("%v.%v.%v.%v", ip.decimalParts[0], ip.decimalParts[1], ip.decimalParts[2], ip.decimalParts[3]) +} + +func must[T any](x T, e error) T { + if e != nil { + panic(e) + } + + return x +} diff --git a/internal/ip/ip_test.go b/internal/ip/ip_test.go new file mode 100644 index 0000000..8426859 --- /dev/null +++ b/internal/ip/ip_test.go @@ -0,0 +1,27 @@ +package ip_test + +import ( + "cider/internal/ip" + "testing" +) + +func Test_ToDecimal(t *testing.T) { + tests := map[string]struct { + ip *ip.Ip + expected int + }{ + "decimal": {ip: ip.NewIp("10.0.0.5"), expected: 167772165}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := test.ip.ToDecimal() + + if actual != test.expected { + t.Fatalf("%s: got %v expected %v", name, actual, test.expected) + } + }) + } +} diff --git a/internal/must/must.go b/internal/must/must.go new file mode 100644 index 0000000..8b6c421 --- /dev/null +++ b/internal/must/must.go @@ -0,0 +1,9 @@ +package must + +func Must[T any](x T, e error) T { + if e != nil { + panic(e) + } + + return x +} diff --git a/main.go b/main.go index 4c8912f..6a56754 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,9 @@ package main import ( "cider/cmd" + "os" ) func main() { - cmd.Execute() + cmd.Execute(os.Stdout, os.Stderr, os.Args) }