diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65487b7..38bfa54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ env: REPO_NAME: ${{ github.event.repository.name }} OUT_FOLDER: ${{ github.event.repository.name }} EXECUTABLE_NAME: ${{ github.event.repository.name }} - GO_VERSION: '1.23' + GO_VERSION: '1.24' permissions: contents: write @@ -47,7 +47,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Build ${{ env.OS_GOOS }}/${{ env.OS_GOARCH }} - run: GOOS=${{ env.OS_GOOS }} GOARCH=${{ env.OS_GOARCH }} go build -o ${{ env.EXECUTABLE_NAME }}.exe + run: GOOS=${{ env.OS_GOOS }} GOARCH=${{ env.OS_GOARCH }} go build -o ${{ env.EXECUTABLE_NAME }}.exe -ldflags "-X main.version=${{ env.VERSION }}" shell: bash - name: Create archive diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..24ff8d2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Test + +on: + pull_request: + branches: [ "main" ] + +env: + GO_VERSION: '1.24' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... \ No newline at end of file diff --git a/go.mod b/go.mod index 741671c..fc2f4b8 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,4 @@ module cider go 1.23.2 -require github.com/urfave/cli/v2 v2.27.5 - -require ( - github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect -) +require github.com/urfave/cli/v3 v3.0.0-beta1 diff --git a/go.sum b/go.sum index c8b6c7e..3c92cd0 100644 --- a/go.sum +++ b/go.sum @@ -4,5 +4,7 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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= diff --git a/internal/cidr/cidr_block.go b/internal/cidr/cidr_block.go new file mode 100644 index 0000000..32d13f3 --- /dev/null +++ b/internal/cidr/cidr_block.go @@ -0,0 +1,172 @@ +package cidr + +import ( + "cider/internal/list" + "cider/internal/utils" + "fmt" + "math" + "slices" + "strconv" + "strings" +) + +const ( + INT_SIZE = 32 +) + +type CIDRBlock struct { + Network string + HostPortion int +} + +func NewBlock(network string) *CIDRBlock { + networkAndHostPortion := strings.Split(network, "/") + + networkPortion := networkAndHostPortion[0] + hostPortion := networkAndHostPortion[1] + + return &CIDRBlock{ + Network: networkPortion, + HostPortion: must(strconv.Atoi(hostPortion)), + } +} + +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)) + + 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.HostPortion) + } + + subnets = append(subnets, fmt.Sprintf("%s/%v", subnetBlock.Network, subnetBlock.HostPortion)) + + next = NewBlock(fmt.Sprintf("%s/%v", 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 (b *CIDRBlock) Contains(ip string) bool { + IP_addr := ipToDecimal(ip) + CIDR_addr := ipToDecimal(b.Network) + CIDR_mask := -1 << (INT_SIZE - b.HostPortion) + + 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.ParseInt(parts[0], base, INT_SIZE)) + octet2 := must(strconv.ParseInt(parts[1], base, INT_SIZE)) + octet3 := must(strconv.ParseInt(parts[2], base, INT_SIZE)) + octet4 := must(strconv.ParseInt(parts[3], base, INT_SIZE)) + + return int((octet1 * 16777216) + (octet2 * 65536) + (octet3 * 256) + octet4) +} + +func (b *CIDRBlock) NetworkPortionBinary() string { + octets := strings.Split(b.Network, ".") + 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.ParseInt(s, 10, INT_SIZE)) + asBinaryString := strconv.FormatInt(asInt, 2) + paddedBynaryString := utils.PadLeft(asBinaryString, '0', 8) + return paddedBynaryString +} + +func (b *CIDRBlock) SubnetMask() string { + ones := strings.Repeat("1", b.HostPortion) + zeroes := strings.Repeat("0", INT_SIZE-b.HostPortion) + + mask := ones + zeroes + + octets := stringToOctets(mask) + + return fmt.Sprintf("%v.%v.%v.%v", octets[0], octets[1], octets[2], octets[3]) +} + +func (b *CIDRBlock) AvailableHosts() uint { + numAddresses := math.Pow(2, float64(INT_SIZE)-float64(b.HostPortion)) + + return uint(numAddresses) +} + +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 { + return fmt.Sprintf("%v", b.AvailableHosts()-5) + } + + return "N/A" +} + +func (b *CIDRBlock) StartAddressOfNextBlock() string { + octets := strings.Split(b.BroadcastAddress(), ".") + octets = list.Map(octets, toBin) + binStr := strings.Join(octets, "") + + next := must(strconv.ParseInt(binStr, 2, INT_SIZE)) + 1 + + asBinaryString := strconv.FormatInt(next, 2) + asBinaryString = utils.PadLeft(asBinaryString, '0', 32) + + octetsInt := stringToOctets(asBinaryString) + + return fmt.Sprintf("%v.%v.%v.%v", octetsInt[0], octetsInt[1], octetsInt[2], octetsInt[3]) +} + +func (b *CIDRBlock) NetworkAddress() string { + ipBin := strings.ReplaceAll(b.NetworkPortionBinary(), ".", "")[0:b.HostPortion] + + broadcast := ipBin + strings.Repeat("0", INT_SIZE-b.HostPortion) + + octets := stringToOctets(broadcast) + + return fmt.Sprintf("%v.%v.%v.%v", octets[0], octets[1], octets[2], octets[3]) +} + +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.HostPortion] + + broadcast := ipBin + strings.Repeat("1", INT_SIZE-b.HostPortion) + + octets := stringToOctets(broadcast) + + return fmt.Sprintf("%v.%v.%v.%v", octets[0], octets[1], octets[2], octets[3]) +} + +func stringToOctets(ipString string) []int64 { + octets := make([]int64, 4) + + base := 2 + octets[0] = must(strconv.ParseInt(ipString[0:8], base, INT_SIZE)) + octets[1] = must(strconv.ParseInt(ipString[8:16], base, INT_SIZE)) + octets[2] = must(strconv.ParseInt(ipString[16:24], base, INT_SIZE)) + octets[3] = must(strconv.ParseInt(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 new file mode 100644 index 0000000..b34cfc6 --- /dev/null +++ b/internal/cidr/cidr_block_test.go @@ -0,0 +1,265 @@ +package cidr_test + +import ( + "cider/internal/cidr" + "reflect" + "testing" +) + +func Test_SubnetSuccess(t *testing.T) { + tests := map[string]struct { + 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"}}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual, _ := test.input.Subnet(test.sizes) + + if !reflect.DeepEqual(actual, test.expected) { + t.Fatalf("%s returns correct subnet: got %v expected %v", name, actual, test.expected) + } + }) + } +} + +func Test_SubnetFailure(t *testing.T) { + tests := map[string]struct { + 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}}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual, err := test.input.Subnet(test.sizes) + + if err == nil { + t.Fatalf("%s expected error: got %v", name, actual) + } + }) + } +} + +func Test_Contains(t *testing.T) { + tests := map[string]struct { + ip string + ipRange *cidr.CIDRBlock + expected bool + }{ + "ip inside range": {ip: "10.50.30.7", ipRange: cidr.NewBlock("10.0.0.0/8"), expected: true}, + "ip outside range": {ip: "10.50.30.7", ipRange: cidr.NewBlock("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) + + if actual != test.expected { + t.Fatalf("%s: got %v expected %v", name, actual, test.expected) + } + }) + } +} + +func Test_NetworkPortionBinary(t *testing.T) { + tests := map[string]struct { + input *cidr.CIDRBlock + expected string + }{ + "/0": {input: cidr.NewBlock("10.0.0.0/0"), expected: "00001010.00000000.00000000.00000000"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := test.input.NetworkPortionBinary() + + if actual != test.expected { + t.Fatalf("%s returns correct binary representation: got %v expected %v", name, actual, test.expected) + } + }) + } +} + +func Test_SubnetMask(t *testing.T) { + tests := map[string]struct { + 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"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := test.input.SubnetMask() + + if actual != test.expected { + t.Fatalf("%s returns correct subnet mask: got %v expected %v", name, actual, test.expected) + } + }) + } +} + +func Test_AvailableHosts(t *testing.T) { + tests := map[string]struct { + 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}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := test.input.AvailableHosts() + + if actual != test.expected { + t.Fatalf("%s returns correct number of addresses: got %v expected %v", name, actual, test.expected) + } + }) + } +} + +func Test_StartAddressOfNextBlock(t *testing.T) { + tests := map[string]struct { + input *cidr.CIDRBlock + expected string + }{ + "/17": {input: cidr.NewBlock("10.0.0.0/17"), expected: "10.0.128.0"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := test.input.StartAddressOfNextBlock() + + if actual != test.expected { + t.Fatalf("%s returns next block address: got %v expected %v", name, actual, test.expected) + } + }) + } +} + +func Test_NetworkAddress(t *testing.T) { + tests := map[string]struct { + input *cidr.CIDRBlock + expected string + }{ + "/26": {input: cidr.NewBlock("192.168.33.64/26"), expected: "192.168.33.64"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := test.input.NetworkAddress() + + if actual != test.expected { + t.Fatalf("%s returns correct network address: got %v expected %v", name, actual, test.expected) + } + }) + } +} + +func Test_BroadcastAddress(t *testing.T) { + tests := map[string]struct { + input *cidr.CIDRBlock + expected string + }{ + "/26": {input: cidr.NewBlock("192.168.33.64/26"), expected: "192.168.33.127"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := test.input.BroadcastAddress() + + if actual != test.expected { + t.Fatalf("%s returns correct broadcast address: got %v expected %v", name, actual, test.expected) + } + }) + } +} diff --git a/internal/commands/in/command.go b/internal/commands/in/command.go index aac8f9f..357d9d5 100644 --- a/internal/commands/in/command.go +++ b/internal/commands/in/command.go @@ -1,13 +1,9 @@ package in import ( + "cider/internal/cidr" + "cider/internal/list" "fmt" - "strconv" - "strings" -) - -const ( - INT_SIZE = 32 ) type handler struct{} @@ -21,54 +17,23 @@ func (h *handler) Handle(args []string) error { return fmt.Errorf("command expects at least 2 arguments") } - found := false ip := args[0] - for _, cidr := range args[1:] { - inRange := isInRange(ip, cidr) + ranges := list.Map(args[1:], func(i string) *cidr.CIDRBlock { + return cidr.NewBlock(i) + }) - found = found || inRange + inRange := list.Filter(ranges, func(cidr *cidr.CIDRBlock) bool { + return cidr.Contains(ip) + }) - if inRange { - fmt.Println(cidr) - } - } - - if !found { + if len(inRange) == 0 { fmt.Printf("%s is not in any of the provided ranges\n", ip) + return nil } - return nil -} - -// https://stackoverflow.com/questions/9622967/how-to-see-if-an-ip-address-belongs-inside-of-a-range-of-ips-using-cidr-notation -func isInRange(ipAddress, CIDRmask string) bool { - - parts := strings.Split(CIDRmask, "/") - - IP_addr := ipToDecimal(ipAddress) - CIDR_addr := ipToDecimal(parts[0]) - CIDR_mask := -1 << (INT_SIZE - must(strconv.ParseInt(parts[1], 10, INT_SIZE))) - - 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.ParseInt(parts[0], base, INT_SIZE)) - octet2 := must(strconv.ParseInt(parts[1], base, INT_SIZE)) - octet3 := must(strconv.ParseInt(parts[2], base, INT_SIZE)) - octet4 := must(strconv.ParseInt(parts[3], base, INT_SIZE)) - - return int((octet1 * 16777216) + (octet2 * 65536) + (octet3 * 256) + octet4) -} - -func must[T any](x T, e error) T { - if e != nil { - panic(e) + for _, ip := range inRange { + fmt.Println(ip) } - return x + return nil } diff --git a/internal/commands/info/command.go b/internal/commands/info/command.go new file mode 100644 index 0000000..c34f016 --- /dev/null +++ b/internal/commands/info/command.go @@ -0,0 +1,57 @@ +package info + +import ( + "cider/internal/cidr" + "cider/internal/list" + "cider/internal/utils" + "fmt" + "math" +) + +type handler struct{} + +func New() *handler { + return &handler{} +} + +type pair struct { + a, b string +} + +func (h *handler) Handle(args []string) error { + if len(args) != 1 { + return fmt.Errorf("command expects exactly one argument") + } + + ip := args[0] + + block := cidr.NewBlock(ip) + + entries := []pair{ + {a: "Address range", b: fmt.Sprintf("%s - %s", block.NetworkAddress(), block.BroadcastAddress())}, + {a: "Start of next block", b: block.StartAddressOfNextBlock()}, + {a: "Cidr", b: fmt.Sprintf("/%v", block.HostPortion)}, + {a: "Subnet mask", b: block.SubnetMask()}, + {a: "Addresses", b: fmt.Sprintf("%v", block.AvailableHosts())}, + {a: "Azure addresses", b: block.AvailableAzureHosts()}, + } + + printOutput(entries) + + return nil +} + +func printOutput(entries []pair) { + keys := []string{} + for _, pair := range entries { + keys = append(keys, pair.a) + } + + longestTitle := list.Fold(keys, 0, func(title string, acc int) int { + return int(math.Max(float64(acc), float64(len(title)))) + }) + + for _, pair := range entries { + fmt.Printf("%s : %v\n", utils.PadRight(pair.a, ' ', longestTitle), pair.b) + } +} diff --git a/internal/commands/ranges/cidr_block.go b/internal/commands/ranges/cidr_block.go deleted file mode 100644 index f4af900..0000000 --- a/internal/commands/ranges/cidr_block.go +++ /dev/null @@ -1,7 +0,0 @@ -package ranges - -type CIDRBlock struct { - NetworkPortion uint // 0 - SubnetMask string // 0.0.0.0 - AvailableHosts uint // 4294967296 -} diff --git a/internal/commands/ranges/command.go b/internal/commands/ranges/command.go index 66d6b95..45637b0 100644 --- a/internal/commands/ranges/command.go +++ b/internal/commands/ranges/command.go @@ -1,11 +1,10 @@ package ranges import ( + "cider/internal/cidr" "fmt" - "math" "os" "strconv" - "strings" "text/tabwriter" ) @@ -28,27 +27,27 @@ func (*handler) Handle(arg string) error { } // argument was given - try to parse it - cidr, err := strconv.ParseInt(arg, 10, INT_SIZE) + hostPortion, err := strconv.ParseInt(arg, 10, INT_SIZE) if err != nil { return fmt.Errorf("%s is not a valid integer", arg) } - if cidr < 0 || cidr > INT_SIZE { - return fmt.Errorf("%v is not a valid size - must be between 0 and 32", cidr) + if hostPortion < 0 || hostPortion > INT_SIZE { + return fmt.Errorf("%v is not a valid size - must be between 0 and 32", hostPortion) } - block := calculateCIDRBlock(int(cidr)) + block := calculateCIDRBlock(int(hostPortion)) - table := []*CIDRBlock{block} + table := []*cidr.CIDRBlock{block} return printCIDRBlocks(table) } -func calculateAllCIDRBlocks() []*CIDRBlock { - blocks := []*CIDRBlock{} - for cidr := 0; cidr < INT_SIZE+1; cidr++ { - block := calculateCIDRBlock(cidr) +func calculateAllCIDRBlocks() []*cidr.CIDRBlock { + blocks := []*cidr.CIDRBlock{} + for i := 0; i < INT_SIZE+1; i++ { + block := calculateCIDRBlock(i) blocks = append(blocks, block) } @@ -56,64 +55,17 @@ func calculateAllCIDRBlocks() []*CIDRBlock { return blocks } -func calculateCIDRBlock(cidr int) *CIDRBlock { - numAddresses := calculateNumAddresses(INT_SIZE, cidr) - mask := calculateSubnetMask(cidr) - - block := &CIDRBlock{ - NetworkPortion: uint(cidr), - SubnetMask: mask, - AvailableHosts: numAddresses, - } - - return block -} - -func calculateSubnetMask(cidr int) string { - ones := strings.Repeat("1", cidr) - zeroes := strings.Repeat("0", INT_SIZE-cidr) - - mask := ones + zeroes - - base := 2 - octet1 := must(strconv.ParseInt(mask[0:8], base, INT_SIZE)) - octet2 := must(strconv.ParseInt(mask[8:16], base, INT_SIZE)) - octet3 := must(strconv.ParseInt(mask[16:24], base, INT_SIZE)) - octet4 := must(strconv.ParseInt(mask[24:32], base, INT_SIZE)) - - return fmt.Sprintf("%v.%v.%v.%v", octet1, octet2, octet3, octet4) -} - -func calculateNumAddresses(addressLength, prefixLength int) uint { - numAddresses := math.Pow(2, float64(addressLength)-float64(prefixLength)) - - return uint(numAddresses) +func calculateCIDRBlock(hostPortion int) *cidr.CIDRBlock { + return cidr.NewBlock(fmt.Sprintf("10.0.0.0/%v", hostPortion)) } -func printCIDRBlocks(blocks []*CIDRBlock) error { +func printCIDRBlocks(blocks []*cidr.CIDRBlock) error { w := tabwriter.NewWriter(os.Stdout, 2, 4, 1, ' ', 0) - fmt.Fprint(w, "CIDR\tSubnet Mask\tAddresses\tAzure Addresses\n") + fmt.Fprint(w, "Cidr\tSubnet mask\tAddresses\tAzure addresses\n") for _, block := range blocks { - - availableAzureAddresses := "N/A" - - // 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 block.AvailableHosts >= 5 { - availableAzureAddresses = fmt.Sprintf("%v", block.AvailableHosts-5) - } - - fmt.Fprintf(w, "/%v\t%s\t%v\t%s\n", block.NetworkPortion, block.SubnetMask, block.AvailableHosts, availableAzureAddresses) + fmt.Fprintf(w, "/%v\t%s\t%v\t%s\n", block.HostPortion, block.SubnetMask(), block.AvailableHosts(), block.AvailableAzureHosts()) } return w.Flush() } - -func must[T any](x T, e error) T { - if e != nil { - panic(e) - } - - return x -} diff --git a/internal/commands/subnet/command.go b/internal/commands/subnet/command.go new file mode 100644 index 0000000..4f3b126 --- /dev/null +++ b/internal/commands/subnet/command.go @@ -0,0 +1,37 @@ +package subnet + +import ( + "cider/internal/cidr" + "cider/internal/list" + "fmt" + "strconv" +) + +type handler struct{} + +func New() *handler { + return &handler{} +} + +func (*handler) Handle(args []string) error { + rangeToSplit := args[0] + sizes := list.Map(args[1:], func(s string) int { + n, _ := strconv.Atoi(s) + + return n + }) + + block := cidr.NewBlock(rangeToSplit) + + subnets, err := block.Subnet(sizes) + + if err != nil { + return err + } + + for _, subnet := range subnets { + fmt.Println(subnet) + } + + return nil +} diff --git a/internal/list/list.go b/internal/list/list.go new file mode 100644 index 0000000..fe7bd74 --- /dev/null +++ b/internal/list/list.go @@ -0,0 +1,65 @@ +package list + +func Filter[T any](v []T, f func(T) bool) []T { + var res []T + for _, i := range v { + if !f(i) { + continue + } + + res = append(res, i) + } + + return res +} + +func Map[T, K any](v []T, f func(T) K) []K { + var res []K + for _, i := range v { + res = append(res, f(i)) + } + return res +} + +func FlatMap[T, K any](v []T, f func(T) []K) []K { + var res []K + for _, i := range v { + res = append(res, f(i)...) + } + return res +} + +func Contains[T any](v []T, f func(T) bool) bool { + return len(Filter(v, f)) > 0 +} + +func All[T any](v []T, f func(T) bool) bool { + return len(Filter(v, f)) == len(v) +} + +func First[T any](v []T, f func(T) bool) T { + for _, i := range v { + if f(i) { + return i + } + } + + panic("no element satisifies predicate") +} + +func FirstOrDefault[T any](v []T, def T, f func(T) bool) T { + for _, i := range v { + if f(i) { + return i + } + } + + return def +} + +func Fold[T, K any](v []T, acc K, f func(T, K) K) K { + for _, i := range v { + acc = f(i, acc) + } + return acc +} diff --git a/internal/utils/strings.go b/internal/utils/strings.go new file mode 100644 index 0000000..bae2766 --- /dev/null +++ b/internal/utils/strings.go @@ -0,0 +1,23 @@ +package utils + +import "strings" + +func PadLeft(s string, paddingChar rune, totalWidth int) string { + if len(s) >= totalWidth { + return s + } + + padding := totalWidth - len(s) + + return strings.Repeat(string(paddingChar), padding) + s +} + +func PadRight(s string, paddingChar rune, totalWidth int) string { + if len(s) >= totalWidth { + return s + } + + padding := totalWidth - len(s) + + return s + strings.Repeat(string(paddingChar), padding) +} diff --git a/main.go b/main.go index 907fa5f..c117122 100644 --- a/main.go +++ b/main.go @@ -2,25 +2,31 @@ package main import ( "cider/internal/commands/in" + "cider/internal/commands/info" "cider/internal/commands/ranges" + "cider/internal/commands/subnet" + "context" + "fmt" "log" "os" "strconv" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) +var version string + func main() { - app := &cli.App{ + cmd := &cli.Command{ Name: "cider", Usage: "CIDR cli tool", Commands: []*cli.Command{ { Name: "ranges", - Usage: "Displays all CIDR ranges", + Usage: "display all CIDR ranges", Aliases: []string{"r"}, - Action: func(c *cli.Context) error { - arg := c.Args().First() + Action: func(_ context.Context, command *cli.Command) error { + arg := command.Args().First() handler := ranges.New() @@ -30,10 +36,10 @@ func main() { }, { Name: "in", - Usage: "Determines if an ip falls within a range", + Usage: "determine if an ip falls within a range", Aliases: []string{"i"}, - Action: func(c *cli.Context) error { - args := c.Args().Slice() + Action: func(_ context.Context, command *cli.Command) error { + args := command.Args().Slice() handler := in.New() @@ -41,6 +47,42 @@ func main() { }, UsageText: "in [ip] [range1] [optional range2] [optional rangeN]", }, + { + Name: "subnet", + Usage: "split a range into multiple smaller ranges", + Aliases: []string{"s"}, + Action: func(_ context.Context, command *cli.Command) error { + args := command.Args().Slice() + + handler := subnet.New() + + return handler.Handle(args) + }, + UsageText: "subnet [range] [size1] [optional size2] [optional sizeN]", + }, + { + Name: "info", + Usage: "display information about a range", + Aliases: []string{"f"}, + Action: func(_ context.Context, command *cli.Command) error { + args := command.Args().Slice() + + handler := info.New() + + return handler.Handle(args) + }, + UsageText: "info [range]", + }, + { + Name: "version", + Aliases: []string{"v"}, + Description: "show version", + Action: func(_ context.Context, command *cli.Command) error { + fmt.Println(version) + + return nil + }, + }, }, } @@ -55,7 +97,7 @@ func main() { } } - if err := app.Run(args); err != nil { + if err := cmd.Run(context.Background(), args); err != nil { log.Fatal(err) } }