From 98fe336493575c6f9c63b367c8892a74f8389522 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Sun, 3 Dec 2023 00:47:31 +0100 Subject: [PATCH 01/17] feat: init `explain` subcommand --- cmd/explain.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 cmd/explain.go diff --git a/cmd/explain.go b/cmd/explain.go new file mode 100644 index 0000000..2c43805 --- /dev/null +++ b/cmd/explain.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "fmt" + "net" + "os" + + "github.com/bschaatsbergen/cidr/pkg/core" + "github.com/spf13/cobra" +) + +var ( + explainCmd = &cobra.Command{ + Use: "explain", + Short: "Provides information about a CIDR range", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + fmt.Println("error: provide a CIDR range and an IP address") + fmt.Println("See 'cidr contains -h' for help and examples") + os.Exit(1) + } + network, err := core.ParseCIDR(args[0]) + if err != nil { + fmt.Printf("error: %s\n", err) + fmt.Println("See 'cidr contains -h' for help and examples") + os.Exit(1) + } + foo, bar := explain(network) + fmt.Println(foo) + fmt.Println(bar) + }, + } +) + +func init() { + rootCmd.AddCommand(explainCmd) +} + +func explain(network *net.IPNet) (string, string) { + return "", "" +} From f4eaa78633f1854b9885186fe1ca7e307219b5bd Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 5 Dec 2023 01:41:10 +0100 Subject: [PATCH 02/17] feat: implement GetNetMask, GetBaseAddress and GetBroadcastAddress --- pkg/core/const.go | 5 +++ pkg/core/core.go | 27 ++++++++++++++++ pkg/core/core_test.go | 74 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 pkg/core/const.go diff --git a/pkg/core/const.go b/pkg/core/const.go new file mode 100644 index 0000000..329e8d8 --- /dev/null +++ b/pkg/core/const.go @@ -0,0 +1,5 @@ +package core + +const ( + IPv6HasNoBroadcastAddressError = string("IPv6 has no broadcast addresses") +) diff --git a/pkg/core/core.go b/pkg/core/core.go index fc3562c..862be57 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -1,6 +1,7 @@ package core import ( + "errors" "net" ) @@ -44,3 +45,29 @@ func ContainsAddress(network *net.IPNet, ip net.IP) bool { func Overlaps(network1, network2 *net.IPNet) bool { return network1.Contains(network2.IP) || network2.Contains(network1.IP) } + +// GetNetMask returns the netmask of the given IP network. +func GetNetMask(network *net.IPNet) net.IP { + netMask := net.IP(network.Mask) + return netMask +} + +// GetBaseAddress returns the base address of the given IP network. +func GetBaseAddress(network *net.IPNet) net.IP { + return network.IP +} + +// GetBroadcastAddress returns the broadcast address of the given IPv4 network, or an error if the IP network is IPv6. +func GetBroadcastAddress(network *net.IPNet) (net.IP, error) { + if network.IP.To4() == nil { + // IPv6 networks do not have broadcast addresses. + return nil, errors.New(IPv6HasNoBroadcastAddressError) + } + + ip := make(net.IP, len(network.IP)) + for i := range ip { + ip[i] = network.IP[i] | ^network.Mask[i] + } + + return ip, nil +} diff --git a/pkg/core/core_test.go b/pkg/core/core_test.go index 54bf22c..f512992 100644 --- a/pkg/core/core_test.go +++ b/pkg/core/core_test.go @@ -182,6 +182,80 @@ func TestContainsAddress(t *testing.T) { } } +func TestGetNetMask(t *testing.T) { + IPv4CIDR, err := ParseCIDR("10.0.0.0/16") + if err != nil { + t.Log(err) + t.Fail() + } + + IPv6CIDR, err := ParseCIDR("2001:db8:1234:1a00::/106") + if err != nil { + t.Log(err) + t.Fail() + } + + tests := []struct { + name string + cidr *net.IPNet + expectedNetMask net.IP + }{ + { + name: "Get the netmask of an IPv4 CIDR", + cidr: IPv4CIDR, + expectedNetMask: net.IP(net.IP{0xff, 0xff, 0x0, 0x0}), + }, + { + name: "Get the netmask of an IPv6 CIDR", + cidr: IPv6CIDR, + expectedNetMask: net.IP(net.IP{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0}), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + netMask := GetNetMask(tt.cidr) + assert.Equal(t, tt.expectedNetMask, netMask, "Netmask is not correct") + }) + } +} + +func TestGetBaseAddress(t *testing.T) { + IPv4CIDR, err := ParseCIDR("192.168.90.4/30") + if err != nil { + t.Log(err) + t.Fail() + } + + IPv6CIDR, err := ParseCIDR("4a00:db8:1234:1a00::/127") + if err != nil { + t.Log(err) + t.Fail() + } + + tests := []struct { + name string + cidr *net.IPNet + expectedBaseAddress net.IP + }{ + { + name: "Get the base address of an IPv4 CIDR", + cidr: IPv4CIDR, + expectedBaseAddress: IPv4CIDR.IP, + }, + { + name: "Get the base address of an IPv6 CIDR", + cidr: IPv6CIDR, + expectedBaseAddress: IPv6CIDR.IP, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + baseAddress := GetBaseAddress(tt.cidr) + assert.Equal(t, tt.expectedBaseAddress, baseAddress, "Base address is not correct") + }) + } +} + func TestParseCIDR(t *testing.T) { tests := []struct { name string From 086a911cec0f0f43dd121349e864ec7ac7eb5d37 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 5 Dec 2023 01:46:14 +0100 Subject: [PATCH 03/17] chore: add read permissions and set cache to false --- .github/workflows/ci.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dd05925..ed51da1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,9 @@ name: CI on: pull_request +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest @@ -11,7 +14,7 @@ jobs: uses: actions/setup-go@v4 with: go-version-file: go.mod - cache: true + cache: false - name: Build run: go build -v ./... - name: Run linters From 1e88c1199e0de93cefdf11961316c7a4eba89398 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 5 Dec 2023 01:51:11 +0100 Subject: [PATCH 04/17] chore: bump checkout to v4 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ed51da1..ff0395a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v4 with: From 6e0614e23b3468563657b9d57fc7b0b6145ff688 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 5 Dec 2023 01:54:00 +0100 Subject: [PATCH 05/17] chore: skip-pkg-cache: true and skip-build-cache: true --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ff0395a..d2b7dc2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,5 +21,7 @@ jobs: uses: golangci/golangci-lint-action@v3 with: version: latest + skip-pkg-cache: true + skip-build-cache: true - name: Run tests run: go test -v ./... From 37fc787c65b25a0413a8ffeb178c74a5e70be8ee Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 5 Dec 2023 01:56:04 +0100 Subject: [PATCH 06/17] chore: fix unnecessary conversion --- pkg/core/core_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/core/core_test.go b/pkg/core/core_test.go index f512992..d4c1251 100644 --- a/pkg/core/core_test.go +++ b/pkg/core/core_test.go @@ -203,12 +203,12 @@ func TestGetNetMask(t *testing.T) { { name: "Get the netmask of an IPv4 CIDR", cidr: IPv4CIDR, - expectedNetMask: net.IP(net.IP{0xff, 0xff, 0x0, 0x0}), + expectedNetMask: net.IP{0xff, 0xff, 0x0, 0x0}, }, { name: "Get the netmask of an IPv6 CIDR", cidr: IPv6CIDR, - expectedNetMask: net.IP(net.IP{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0}), + expectedNetMask: net.IP{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0}, }, } for _, tt := range tests { From f647fa6313e891018df0216a7d38f1170156ac7d Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 5 Dec 2023 13:46:18 +0100 Subject: [PATCH 07/17] chore: handle IPv4 CIDR ranges with large prefixes having no broadcast addresses --- pkg/core/const.go | 3 ++- pkg/core/core.go | 7 +++++++ pkg/helper/help.go | 12 ++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 pkg/helper/help.go diff --git a/pkg/core/const.go b/pkg/core/const.go index 329e8d8..e194fcd 100644 --- a/pkg/core/const.go +++ b/pkg/core/const.go @@ -1,5 +1,6 @@ package core const ( - IPv6HasNoBroadcastAddressError = string("IPv6 has no broadcast addresses") + IPv6HasNoBroadcastAddressError = string("IPv6 has no broadcast addresses") + IPv4PrefixHasNoBroadcastAddressError = string("this IPv4 CIDR range has no broadcast address") ) diff --git a/pkg/core/core.go b/pkg/core/core.go index 862be57..b7e3f22 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -3,6 +3,8 @@ package core import ( "errors" "net" + + "github.com/bschaatsbergen/cidr/pkg/helper" ) // GetAddressCount returns the number of usable addresses in the given IP network. @@ -64,6 +66,11 @@ func GetBroadcastAddress(network *net.IPNet) (net.IP, error) { return nil, errors.New(IPv6HasNoBroadcastAddressError) } + if prefixLen, _ := network.Mask.Size(); helper.ContainsInt([]int{31, 32}, prefixLen) { + // Handle edge case for /31 and /32 networks as they have no broadcast address. + return nil, errors.New(IPv4PrefixHasNoBroadcastAddressError) + } + ip := make(net.IP, len(network.IP)) for i := range ip { ip[i] = network.IP[i] | ^network.Mask[i] diff --git a/pkg/helper/help.go b/pkg/helper/help.go new file mode 100644 index 0000000..250d211 --- /dev/null +++ b/pkg/helper/help.go @@ -0,0 +1,12 @@ +package helper + +// ContainsInt checks if the given slice of integers contains the specified integer. +// It returns true if the integer is within the slice, otherwise false. +func ContainsInt(s []int, e int) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} From d937b94d9a2284a99641f907314f7576b70c5772 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 5 Dec 2023 13:48:12 +0100 Subject: [PATCH 08/17] chore: rename file and change variable names to more transparent and clear in readability --- pkg/helper/{help.go => contains.go} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename pkg/helper/{help.go => contains.go} (66%) diff --git a/pkg/helper/help.go b/pkg/helper/contains.go similarity index 66% rename from pkg/helper/help.go rename to pkg/helper/contains.go index 250d211..8906138 100644 --- a/pkg/helper/help.go +++ b/pkg/helper/contains.go @@ -2,9 +2,9 @@ package helper // ContainsInt checks if the given slice of integers contains the specified integer. // It returns true if the integer is within the slice, otherwise false. -func ContainsInt(s []int, e int) bool { - for _, a := range s { - if a == e { +func ContainsInt(ints []int, specifiedInt int) bool { + for _, i := range ints { + if i == specifiedInt { return true } } From 5bfb6a783fca961e6b18b4cdbe33ba5eaa2b15e5 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 5 Dec 2023 13:48:53 +0100 Subject: [PATCH 09/17] chore: move comment above if logic --- pkg/core/core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/core.go b/pkg/core/core.go index b7e3f22..52046cc 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -66,8 +66,8 @@ func GetBroadcastAddress(network *net.IPNet) (net.IP, error) { return nil, errors.New(IPv6HasNoBroadcastAddressError) } + // Handle edge case for /31 and /32 networks as they have no broadcast address. if prefixLen, _ := network.Mask.Size(); helper.ContainsInt([]int{31, 32}, prefixLen) { - // Handle edge case for /31 and /32 networks as they have no broadcast address. return nil, errors.New(IPv4PrefixHasNoBroadcastAddressError) } From 44b65d38d20ffb6392440215f3b8150964b32e28 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 5 Dec 2023 15:56:34 +0100 Subject: [PATCH 10/17] chore: add GetLastUsableIPAddress --- pkg/core/core.go | 59 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/pkg/core/core.go b/pkg/core/core.go index 52046cc..7b732ef 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -7,6 +7,15 @@ import ( "github.com/bschaatsbergen/cidr/pkg/helper" ) +// ParseCIDR parses the given CIDR notation string and returns the corresponding IP network. +func ParseCIDR(network string) (*net.IPNet, error) { + _, ip, err := net.ParseCIDR(network) + if err != nil { + return nil, err + } + return ip, err +} + // GetAddressCount returns the number of usable addresses in the given IP network. // It considers the network type (IPv4 or IPv6) and handles edge cases for specific prefix lengths. // The result excludes the network address and broadcast address. @@ -27,15 +36,6 @@ func GetAddressCount(network *net.IPNet) uint64 { return 1<<(uint64(bits)-uint64(prefixLen)) - 2 } -// ParseCIDR parses the given CIDR notation string and returns the corresponding IP network. -func ParseCIDR(network string) (*net.IPNet, error) { - _, ip, err := net.ParseCIDR(network) - if err != nil { - return nil, err - } - return ip, err -} - // ContainsAddress checks if the given IP network contains the specified IP address. // It returns true if the address is within the network, otherwise false. func ContainsAddress(network *net.IPNet, ip net.IP) bool { @@ -59,6 +59,47 @@ func GetBaseAddress(network *net.IPNet) net.IP { return network.IP } +// GetLastUsableIPAddress returns the last usable IP address in the given IP network. +func GetLastUsableIPAddress(network *net.IPNet) (net.IP, error) { + // If it's an IPv6 network + if network.IP.To4() == nil { + ones, bits := network.Mask.Size() + if ones == bits { + return nil, errors.New("IPv6 network has no last address") + } + + // The last address is the last usable address + lastIP := make(net.IP, len(network.IP)) + copy(lastIP, network.IP) + for i := range lastIP { + lastIP[i] |= ^network.Mask[i] + } + + return lastIP, nil + } + + // If it's an IPv4 network, first handle edge cases + switch ones, _ := network.Mask.Size(); ones { + case 32: + return nil, errors.New("IPv4 network has no last address") + case 31: + // For /31 network, the other address is the last usable address + lastIP := make(net.IP, len(network.IP)) + copy(lastIP, network.IP) + lastIP[3] |= 1 // Flip the last bit to get the other address + return lastIP, nil + default: + // Subtract 1 from the broadcast address to get the last usable address + ip := make(net.IP, len(network.IP)) + for i := range ip { + ip[i] = network.IP[i] | ^network.Mask[i] + } + ip[3]-- // Subtract 1 from the last octet + + return ip, nil + } +} + // GetBroadcastAddress returns the broadcast address of the given IPv4 network, or an error if the IP network is IPv6. func GetBroadcastAddress(network *net.IPNet) (net.IP, error) { if network.IP.To4() == nil { From 3d2a05adf02f6eacf030a13774922ae7dcd558b5 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Sat, 9 Dec 2023 14:26:23 +0100 Subject: [PATCH 11/17] chore: add GetFirstUsableIPAddress, GetPrefixLength and errors --- pkg/core/const.go | 8 ++++++-- pkg/core/core.go | 45 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/pkg/core/const.go b/pkg/core/const.go index e194fcd..a57130b 100644 --- a/pkg/core/const.go +++ b/pkg/core/const.go @@ -1,6 +1,10 @@ package core const ( - IPv6HasNoBroadcastAddressError = string("IPv6 has no broadcast addresses") - IPv4PrefixHasNoBroadcastAddressError = string("this IPv4 CIDR range has no broadcast address") + IPv6HasNoBroadcastAddressError = "IPv6 has no broadcast addresses" + IPv4PrefixHasNoBroadcastAddressError = "this IPv4 CIDR range has no broadcast address" + IPv4NetworkHasNoFirstUsableAddressError = "IPv4 network has no first usable address" + IPv6NetworkHasNoFirstUsableAddressError = "IPv6 network has no first usable address" + IPv4NetworkHasNoLastUsableAddressError = "IPv4 network has no last usable address" + IPv6NetworkHasNoLastUsableAddressError = "IPv6 network has no last usable address" ) diff --git a/pkg/core/core.go b/pkg/core/core.go index 7b732ef..f501894 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -54,18 +54,59 @@ func GetNetMask(network *net.IPNet) net.IP { return netMask } +// GetPrefixLength returns the prefix length from the given netmask. +func GetPrefixLength(netmask net.IP) int { + ones, _ := net.IPMask(netmask).Size() + return ones +} + // GetBaseAddress returns the base address of the given IP network. func GetBaseAddress(network *net.IPNet) net.IP { return network.IP } +// GetFirstUsableIPAddress returns the first usable IP address in the given IP network. +func GetFirstUsableIPAddress(network *net.IPNet) (net.IP, error) { + // If it's an IPv6 network + if network.IP.To4() == nil { + ones, bits := network.Mask.Size() + if ones == bits { + return nil, errors.New(IPv6NetworkHasNoFirstUsableAddressError) + } + + // The first address is the first usable address + firstIP := make(net.IP, len(network.IP)) + copy(firstIP, network.IP) + + return firstIP, nil + } + + // If it's an IPv4 network, first handle edge cases + switch ones, _ := network.Mask.Size(); ones { + case 32: + return nil, errors.New(IPv4NetworkHasNoFirstUsableAddressError) + case 31: + // For /31 network, the current address is the only usable address + firstIP := make(net.IP, len(network.IP)) + copy(firstIP, network.IP) + return firstIP, nil + default: + // Add 1 to the network address to get the first usable address + ip := make(net.IP, len(network.IP)) + copy(ip, network.IP) + ip[3]++ // Add 1 to the last octet + + return ip, nil + } +} + // GetLastUsableIPAddress returns the last usable IP address in the given IP network. func GetLastUsableIPAddress(network *net.IPNet) (net.IP, error) { // If it's an IPv6 network if network.IP.To4() == nil { ones, bits := network.Mask.Size() if ones == bits { - return nil, errors.New("IPv6 network has no last address") + return nil, errors.New(IPv6NetworkHasNoLastUsableAddressError) } // The last address is the last usable address @@ -81,7 +122,7 @@ func GetLastUsableIPAddress(network *net.IPNet) (net.IP, error) { // If it's an IPv4 network, first handle edge cases switch ones, _ := network.Mask.Size(); ones { case 32: - return nil, errors.New("IPv4 network has no last address") + return nil, errors.New(IPv4NetworkHasNoLastUsableAddressError) case 31: // For /31 network, the other address is the last usable address lastIP := make(net.IP, len(network.IP)) From b100164e80b58016d03c8667d7734d61b2d52d1b Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Sat, 9 Dec 2023 15:05:39 +0100 Subject: [PATCH 12/17] chore: produce a human readable number and print coloured network details --- cmd/explain.go | 61 +++++++++++++++++++++++++++++++++++++++++++---- go.mod | 1 + go.sum | 2 ++ pkg/core/const.go | 10 ++++---- pkg/core/core.go | 2 +- 5 files changed, 66 insertions(+), 10 deletions(-) diff --git a/cmd/explain.go b/cmd/explain.go index 2c43805..d6b28ed 100644 --- a/cmd/explain.go +++ b/cmd/explain.go @@ -6,7 +6,10 @@ import ( "os" "github.com/bschaatsbergen/cidr/pkg/core" + "github.com/fatih/color" "github.com/spf13/cobra" + "golang.org/x/text/language" + "golang.org/x/text/message" ) var ( @@ -25,9 +28,8 @@ var ( fmt.Println("See 'cidr contains -h' for help and examples") os.Exit(1) } - foo, bar := explain(network) - fmt.Println(foo) - fmt.Println(bar) + broadcastAddress, netmask, prefixLength, baseAdress, count, firstUsableIPAddress, lastUsableIPAddress := getNetworkDetails(network) + explain(broadcastAddress, netmask, prefixLength, baseAdress, count, firstUsableIPAddress, lastUsableIPAddress) }, } ) @@ -36,6 +38,55 @@ func init() { rootCmd.AddCommand(explainCmd) } -func explain(network *net.IPNet) (string, string) { - return "", "" +func getNetworkDetails(network *net.IPNet) (string, string, string, string, string, string, string) { + // Broadcast address + var broadcastAddress string + + ipBroadcast, err := core.GetBroadcastAddress(network) + if err != nil { + broadcastAddress = err.Error() + } else { + broadcastAddress = ipBroadcast.String() + } + + // Netmask + netmask := core.GetNetMask(network) + prefixLength := core.GetPrefixLength(netmask) + + // Base address + baseAddress := core.GetBaseAddress(network) + + // Address count + var count string + addressCount := core.GetAddressCount(network) + // Produce a human readable number + count = message.NewPrinter(language.English).Sprintf("%d", addressCount) + + // First usable IP address + var firstUsableIPAddress string + firstUsableIP, err := core.GetFirstUsableIPAddress(network) + if err != nil { + firstUsableIPAddress = err.Error() + } else { + firstUsableIPAddress = firstUsableIP.String() + } + + // Last usable IP address + var lastUsableIPAddress string + lastUsableIP, err := core.GetLastUsableIPAddress(network) + if err != nil { + lastUsableIPAddress = err.Error() + } else { + lastUsableIPAddress = lastUsableIP.String() + } + + return broadcastAddress, netmask.String(), fmt.Sprint(prefixLength), baseAddress.String(), count, firstUsableIPAddress, lastUsableIPAddress +} + +func explain(broadcastAddress, netmask, prefixLength, baseAddress, count, firstUsableIPAddress, lastUsableIPAddress string) { + fmt.Printf(color.BlueString("Base Address:\t\t ")+"%s\n", baseAddress) + fmt.Printf(color.BlueString("Usable IP Address range: ")+"%s to %s\n", firstUsableIPAddress, lastUsableIPAddress) + fmt.Printf(color.BlueString("Broadcast Address:\t ")+"%s\n", broadcastAddress) + fmt.Printf(color.BlueString("Address Count:\t\t ")+"%s\n", count) + fmt.Printf(color.BlueString("Netmask:\t\t ")+"%s (/%s bits)\n", netmask, prefixLength) } diff --git a/go.mod b/go.mod index 5b75a48..343a58f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21.4 require ( github.com/fatih/color v1.16.0 github.com/spf13/cobra v1.8.0 + golang.org/x/text v0.14.0 ) require ( diff --git a/go.sum b/go.sum index d9501b8..fcf720e 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/core/const.go b/pkg/core/const.go index a57130b..bd525e3 100644 --- a/pkg/core/const.go +++ b/pkg/core/const.go @@ -1,10 +1,12 @@ package core const ( - IPv6HasNoBroadcastAddressError = "IPv6 has no broadcast addresses" - IPv4PrefixHasNoBroadcastAddressError = "this IPv4 CIDR range has no broadcast address" + IPv6HasNoBroadcastAddressError = "IPv6 network has no broadcast addresses" + IPv4HasNoBroadcastAddressError = "IPv4 network has no broadcast address" + IPv4NetworkHasNoFirstUsableAddressError = "IPv4 network has no first usable address" IPv6NetworkHasNoFirstUsableAddressError = "IPv6 network has no first usable address" - IPv4NetworkHasNoLastUsableAddressError = "IPv4 network has no last usable address" - IPv6NetworkHasNoLastUsableAddressError = "IPv6 network has no last usable address" + + IPv4NetworkHasNoLastUsableAddressError = "IPv4 network has no last usable address" + IPv6NetworkHasNoLastUsableAddressError = "IPv6 network has no last usable address" ) diff --git a/pkg/core/core.go b/pkg/core/core.go index f501894..9afab2b 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -150,7 +150,7 @@ func GetBroadcastAddress(network *net.IPNet) (net.IP, error) { // Handle edge case for /31 and /32 networks as they have no broadcast address. if prefixLen, _ := network.Mask.Size(); helper.ContainsInt([]int{31, 32}, prefixLen) { - return nil, errors.New(IPv4PrefixHasNoBroadcastAddressError) + return nil, errors.New(IPv4HasNoBroadcastAddressError) } ip := make(net.IP, len(network.IP)) From c1647fc0810b3f2b52fb45d1849870a4eaedb9cf Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Sat, 9 Dec 2023 15:07:35 +0100 Subject: [PATCH 13/17] chore: ignore goconst on explain func --- cmd/explain.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/explain.go b/cmd/explain.go index d6b28ed..237cb64 100644 --- a/cmd/explain.go +++ b/cmd/explain.go @@ -83,6 +83,7 @@ func getNetworkDetails(network *net.IPNet) (string, string, string, string, stri return broadcastAddress, netmask.String(), fmt.Sprint(prefixLength), baseAddress.String(), count, firstUsableIPAddress, lastUsableIPAddress } +//nolint:goconst func explain(broadcastAddress, netmask, prefixLength, baseAddress, count, firstUsableIPAddress, lastUsableIPAddress string) { fmt.Printf(color.BlueString("Base Address:\t\t ")+"%s\n", baseAddress) fmt.Printf(color.BlueString("Usable IP Address range: ")+"%s to %s\n", firstUsableIPAddress, lastUsableIPAddress) From 1252e937d15d0110691051cbdaf65ad61dd4e143 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Sat, 9 Dec 2023 16:29:04 +0100 Subject: [PATCH 14/17] chore: add 2 helper functions to determine if a network is an IPv4 or IPv6 network --- pkg/helper/network.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pkg/helper/network.go diff --git a/pkg/helper/network.go b/pkg/helper/network.go new file mode 100644 index 0000000..6d2cc2b --- /dev/null +++ b/pkg/helper/network.go @@ -0,0 +1,15 @@ +package helper + +import "net" + +// isIPv4Network checks if the given network is an IPv4 network. +// It returns true if the network is an IPv4 network, otherwise false. +func IsIPv4Network(network *net.IPNet) bool { + return network.IP.To4() != nil +} + +// isIPv6Network checks if the given network is an IPv6 network. +// It returns true if the network is an IPv6 network, otherwise false. +func IsIPv6Network(network *net.IPNet) bool { + return network.IP.To16() != nil +} From ae631a3730b6d73b717bb3ea5f5b11e7ad38da5e Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Sat, 9 Dec 2023 16:29:26 +0100 Subject: [PATCH 15/17] chore: improve the explainCmd by using a networkDetails struct --- cmd/explain.go | 95 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/cmd/explain.go b/cmd/explain.go index 237cb64..4e10d28 100644 --- a/cmd/explain.go +++ b/cmd/explain.go @@ -6,6 +6,7 @@ import ( "os" "github.com/bschaatsbergen/cidr/pkg/core" + "github.com/bschaatsbergen/cidr/pkg/helper" "github.com/fatih/color" "github.com/spf13/cobra" "golang.org/x/text/language" @@ -28,8 +29,8 @@ var ( fmt.Println("See 'cidr contains -h' for help and examples") os.Exit(1) } - broadcastAddress, netmask, prefixLength, baseAdress, count, firstUsableIPAddress, lastUsableIPAddress := getNetworkDetails(network) - explain(broadcastAddress, netmask, prefixLength, baseAdress, count, firstUsableIPAddress, lastUsableIPAddress) + details := getNetworkDetails(network) + explain(details) }, } ) @@ -38,56 +39,86 @@ func init() { rootCmd.AddCommand(explainCmd) } -func getNetworkDetails(network *net.IPNet) (string, string, string, string, string, string, string) { - // Broadcast address - var broadcastAddress string +type networkDetailsToDisplay struct { + IsIPV4Network bool + IsIPV6Network bool + BroadcastAddress string + BroadcastAddressHasError bool + Netmask net.IP + PrefixLength int + BaseAddress net.IP + Count string + UsableAddressRangeHasError bool + FirstUsableIPAddress string + LastUsableIPAddress string +} + +func getNetworkDetails(network *net.IPNet) *networkDetailsToDisplay { + details := &networkDetailsToDisplay{} + + // Determine whether the network is an IPv4 or IPv6 network. + if helper.IsIPv4Network(network) { + details.IsIPV4Network = true + } else if helper.IsIPv6Network(network) { + details.IsIPV6Network = true + } + // Obtain the broadcast address, handling errors if they occur. ipBroadcast, err := core.GetBroadcastAddress(network) if err != nil { - broadcastAddress = err.Error() + // Set error flags and store the error message so that it can be displayed later. + details.BroadcastAddressHasError = true + details.BroadcastAddress = err.Error() } else { - broadcastAddress = ipBroadcast.String() + details.BroadcastAddress = ipBroadcast.String() } - // Netmask - netmask := core.GetNetMask(network) - prefixLength := core.GetPrefixLength(netmask) + // Obtain the netmask and prefix length. + details.Netmask = core.GetNetMask(network) + details.PrefixLength = core.GetPrefixLength(details.Netmask) - // Base address - baseAddress := core.GetBaseAddress(network) + // Obtain the base address of the network. + details.BaseAddress = core.GetBaseAddress(network) - // Address count - var count string - addressCount := core.GetAddressCount(network) - // Produce a human readable number - count = message.NewPrinter(language.English).Sprintf("%d", addressCount) + // Obtain the total count of addresses in the network. + count := core.GetAddressCount(network) + // Format the count as a human-readable string and store it in the details struct. + details.Count = message.NewPrinter(language.English).Sprintf("%d", count) - // First usable IP address - var firstUsableIPAddress string + // Obtain the first and last usable IP addresses, handling errors if they occur. firstUsableIP, err := core.GetFirstUsableIPAddress(network) if err != nil { - firstUsableIPAddress = err.Error() + // Set error flags if an error occurs during the retrieval of the first usable IP address. + details.UsableAddressRangeHasError = true } else { - firstUsableIPAddress = firstUsableIP.String() + details.FirstUsableIPAddress = firstUsableIP.String() } - // Last usable IP address - var lastUsableIPAddress string lastUsableIP, err := core.GetLastUsableIPAddress(network) if err != nil { - lastUsableIPAddress = err.Error() + // Set error flags if an error occurs during the retrieval of the last usable IP address. + details.UsableAddressRangeHasError = true } else { - lastUsableIPAddress = lastUsableIP.String() + details.LastUsableIPAddress = lastUsableIP.String() } - return broadcastAddress, netmask.String(), fmt.Sprint(prefixLength), baseAddress.String(), count, firstUsableIPAddress, lastUsableIPAddress + // Return the populated 'networkDetailsToDisplay' struct. + return details } //nolint:goconst -func explain(broadcastAddress, netmask, prefixLength, baseAddress, count, firstUsableIPAddress, lastUsableIPAddress string) { - fmt.Printf(color.BlueString("Base Address:\t\t ")+"%s\n", baseAddress) - fmt.Printf(color.BlueString("Usable IP Address range: ")+"%s to %s\n", firstUsableIPAddress, lastUsableIPAddress) - fmt.Printf(color.BlueString("Broadcast Address:\t ")+"%s\n", broadcastAddress) - fmt.Printf(color.BlueString("Address Count:\t\t ")+"%s\n", count) - fmt.Printf(color.BlueString("Netmask:\t\t ")+"%s (/%s bits)\n", netmask, prefixLength) +func explain(details *networkDetailsToDisplay) { + fmt.Printf(color.BlueString("Base Address:\t\t ")+"%s\n", details.BaseAddress) + if !details.UsableAddressRangeHasError { + fmt.Printf(color.BlueString("Usable Address Range:\t ")+"%s to %s\n", details.FirstUsableIPAddress, details.LastUsableIPAddress) + } else { + fmt.Printf(color.RedString("Usable Address Range:\t ")+"%s\n", "unable to calculate usable address range") + } + if !details.BroadcastAddressHasError && details.IsIPV4Network { + fmt.Printf(color.BlueString("Broadcast Address:\t ")+"%s\n", details.BroadcastAddress) + } else if details.BroadcastAddressHasError && details.IsIPV4Network { + fmt.Printf(color.RedString("Broadcast Address:\t ")+"%s\n", details.BroadcastAddress) + } + fmt.Printf(color.BlueString("Address Count:\t\t ")+"%s\n", details.Count) + fmt.Printf(color.BlueString("Netmask:\t\t ")+"%s (/%d bits)\n", details.Netmask, details.PrefixLength) } From cb58113616d041fecde2e36befacbafffc3ee493 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Sat, 9 Dec 2023 17:25:58 +0100 Subject: [PATCH 16/17] chore: add tests --- pkg/core/core_test.go | 209 ++++++++++++++++++++++++++++++------ pkg/helper/contains_test.go | 43 ++++++++ 2 files changed, 219 insertions(+), 33 deletions(-) create mode 100644 pkg/helper/contains_test.go diff --git a/pkg/core/core_test.go b/pkg/core/core_test.go index d4c1251..6f46791 100644 --- a/pkg/core/core_test.go +++ b/pkg/core/core_test.go @@ -1,32 +1,33 @@ -package core +package core_test import ( "net" "testing" + "github.com/bschaatsbergen/cidr/pkg/core" "github.com/stretchr/testify/assert" ) func TestGetAddressCount(t *testing.T) { - IPv4CIDR, err := ParseCIDR("10.0.0.0/16") + IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } - IPv6CIDR, err := ParseCIDR("2001:db8:1234:1a00::/106") + IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() } - largeIPv4PrefixCIDR, err := ParseCIDR("172.16.18.0/31") + largeIPv4PrefixCIDR, err := core.ParseCIDR("172.16.18.0/31") if err != nil { t.Log(err) t.Fail() } - largestIPv4PrefixCIDR, err := ParseCIDR("172.16.18.0/32") + largestIPv4PrefixCIDR, err := core.ParseCIDR("172.16.18.0/32") if err != nil { t.Log(err) t.Fail() @@ -60,38 +61,38 @@ func TestGetAddressCount(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - count := GetAddressCount(tt.cidr) + count := core.GetAddressCount(tt.cidr) assert.Equal(t, int(tt.expectedCount), int(count), "Both address counts should be equal") }) } } func TestOverlaps(t *testing.T) { - firstIPv4CIDR, err := ParseCIDR("10.0.0.0/16") + firstIPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } - secondIPv4CIDR, err := ParseCIDR("10.0.14.0/22") + secondIPv4CIDR, err := core.ParseCIDR("10.0.14.0/22") if err != nil { t.Log(err) t.Fail() } - thirdIPv4CIDR, err := ParseCIDR("10.1.0.0/28") + thirdIPv4CIDR, err := core.ParseCIDR("10.1.0.0/28") if err != nil { t.Log(err) t.Fail() } - firstIPv6CIDR, err := ParseCIDR("2001:db8:1111:2222:1::/80") + firstIPv6CIDR, err := core.ParseCIDR("2001:db8:1111:2222:1::/80") if err != nil { t.Log(err) t.Fail() } - secondIPv6CIDR, err := ParseCIDR("2001:db8:1111:2222:1:1::/96") + secondIPv6CIDR, err := core.ParseCIDR("2001:db8:1111:2222:1:1::/96") if err != nil { t.Log(err) t.Fail() @@ -124,20 +125,20 @@ func TestOverlaps(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - overlaps := Overlaps(tt.cidrA, tt.cidrB) + overlaps := core.Overlaps(tt.cidrA, tt.cidrB) assert.Equal(t, tt.overlaps, overlaps, "Given CIDRs should overlap") }) } } func TestContainsAddress(t *testing.T) { - IPv4CIDR, err := ParseCIDR("10.0.0.0/16") + IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } - IPv6CIDR, err := ParseCIDR("2001:db8:1234:1a00::/106") + IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() @@ -176,57 +177,199 @@ func TestContainsAddress(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - overlaps := ContainsAddress(tt.cidr, tt.ip) + overlaps := core.ContainsAddress(tt.cidr, tt.ip) assert.Equal(t, tt.contains, overlaps, "Given IP address should be part of the given CIDR") }) } } -func TestGetNetMask(t *testing.T) { - IPv4CIDR, err := ParseCIDR("10.0.0.0/16") +func TestGetPrefixLength(t *testing.T) { + IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } - IPv6CIDR, err := ParseCIDR("2001:db8:1234:1a00::/106") + IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() } tests := []struct { - name string - cidr *net.IPNet - expectedNetMask net.IP + name string + netMask net.IP + expectedPrefixLength int }{ { - name: "Get the netmask of an IPv4 CIDR", - cidr: IPv4CIDR, - expectedNetMask: net.IP{0xff, 0xff, 0x0, 0x0}, + name: "Get the prefix length of an IPv4 netmask", + netMask: net.IP(IPv4CIDR.Mask), + expectedPrefixLength: 16, }, { - name: "Get the netmask of an IPv6 CIDR", - cidr: IPv6CIDR, - expectedNetMask: net.IP{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0}, + name: "Get the prefix length of an IPv6 netmask", + netMask: net.IP(IPv6CIDR.Mask), + expectedPrefixLength: 106, }, } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prefixLength := core.GetPrefixLength(tt.netMask) + assert.Equal(t, tt.expectedPrefixLength, prefixLength, "Prefix length is not correct") + }) + } +} + +func TestGetFirstUsableIPAddress(t *testing.T) { + IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") + if err != nil { + t.Log(err) + t.Fail() + } + + IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") + if err != nil { + t.Log(err) + t.Fail() + } + + tests := []struct { + name string + CIDR *net.IPNet + expectedFirstUsableIPAddress net.IP + }{ + { + name: "Get the first usable IP address of an IPv4 CIDR range", + CIDR: IPv4CIDR, + expectedFirstUsableIPAddress: net.ParseIP("10.0.0.1").To4(), + }, + { + name: "Get the first usable IP address of an IPv6 CIDR range", + CIDR: IPv6CIDR, + expectedFirstUsableIPAddress: net.ParseIP("2001:db8:1234:1a00::").To16(), + }, + } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - netMask := GetNetMask(tt.cidr) - assert.Equal(t, tt.expectedNetMask, netMask, "Netmask is not correct") + firstUsableIPAddress, err := core.GetFirstUsableIPAddress(tt.CIDR) + if err != nil { + t.Log(err) + t.Fail() + } + assert.Equal(t, tt.expectedFirstUsableIPAddress, firstUsableIPAddress, "First usable IP address is not correct") + }) + } +} + +func TestGetLastUsableIPAddress(t *testing.T) { + IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") + if err != nil { + t.Log(err) + t.Fail() + } + + IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") + if err != nil { + t.Log(err) + t.Fail() + } + + tests := []struct { + name string + CIDR *net.IPNet + expectedLastUsableIPAddress net.IP + }{ + { + name: "Get the last usable IP address of an IPv4 CIDR range", + CIDR: IPv4CIDR, + expectedLastUsableIPAddress: net.ParseIP("10.0.255.254").To4(), + }, + { + name: "Get the last usable IP address of an IPv6 CIDR range", + CIDR: IPv6CIDR, + expectedLastUsableIPAddress: net.ParseIP("2001:db8:1234:1a00::3f:ffff").To16(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + lastUsableIPAddress, err := core.GetLastUsableIPAddress(tt.CIDR) + if err != nil { + t.Log(err) + t.Fail() + } + assert.Equal(t, tt.expectedLastUsableIPAddress, lastUsableIPAddress, "Last usable IP address is not correct") + }) + } +} + +func TestGetBroadcastAddress(t *testing.T) { + IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") + if err != nil { + t.Log(err) + t.Fail() + } + + IPv4CIDRWithNoBroadcastAddress, err := core.ParseCIDR("10.0.0.0/31") + if err != nil { + t.Log(err) + t.Fail() + } + + IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") + if err != nil { + t.Log(err) + t.Fail() + } + + tests := []struct { + name string + CIDR *net.IPNet + expectedBroadcastAddress net.IP + wantErr bool + }{ + { + name: "Get the broadcast IP address of an IPv4 CIDR range", + CIDR: IPv4CIDR, + expectedBroadcastAddress: net.ParseIP("10.0.255.255").To4(), + wantErr: false, + }, + { + name: "Get the broadcast IP address of an IPv4 CIDR range that has no broadcast address", + CIDR: IPv4CIDRWithNoBroadcastAddress, + expectedBroadcastAddress: nil, + wantErr: true, + }, + { + name: "Get the broadcast IP address of an IPv6 CIDR range", + CIDR: IPv6CIDR, + expectedBroadcastAddress: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + broadcastAddress, err := core.GetBroadcastAddress(tt.CIDR) + if err != nil { + assert.Equal(t, tt.wantErr, true, "Expected error when getting broadcast address, but got none") + } else { + assert.Equal(t, tt.expectedBroadcastAddress, broadcastAddress, "Broadcast IP address is not correct") + } }) } } func TestGetBaseAddress(t *testing.T) { - IPv4CIDR, err := ParseCIDR("192.168.90.4/30") + IPv4CIDR, err := core.ParseCIDR("192.168.90.4/30") if err != nil { t.Log(err) t.Fail() } - IPv6CIDR, err := ParseCIDR("4a00:db8:1234:1a00::/127") + IPv6CIDR, err := core.ParseCIDR("4a00:db8:1234:1a00::/127") if err != nil { t.Log(err) t.Fail() @@ -250,7 +393,7 @@ func TestGetBaseAddress(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - baseAddress := GetBaseAddress(tt.cidr) + baseAddress := core.GetBaseAddress(tt.cidr) assert.Equal(t, tt.expectedBaseAddress, baseAddress, "Base address is not correct") }) } @@ -290,7 +433,7 @@ func TestParseCIDR(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := ParseCIDR(tt.cidrStr) + _, err := core.ParseCIDR(tt.cidrStr) if (err != nil) != tt.wantErr { t.Errorf("ParseCIDR() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/helper/contains_test.go b/pkg/helper/contains_test.go new file mode 100644 index 0000000..e0ff9e4 --- /dev/null +++ b/pkg/helper/contains_test.go @@ -0,0 +1,43 @@ +package helper_test + +import ( + "testing" + + "github.com/bschaatsbergen/cidr/pkg/helper" +) + +func TestContainsInt(t *testing.T) { + type args struct { + ints []int + specifiedInt int + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "ContainsInt() should return true", + args: args{ + ints: []int{1, 2, 3, 4, 5}, + specifiedInt: 3, + }, + want: true, + }, + { + name: "ContainsInt() should return false", + args: args{ + ints: []int{1, 2, 3, 4, 5}, + specifiedInt: 6, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := helper.ContainsInt(tt.args.ints, tt.args.specifiedInt); got != tt.want { + t.Errorf("ContainsInt() = %v, want %v", got, tt.want) + } + }) + } +} From f9dce3a644e5698a630a3f5ebc0b5820938276ea Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Sat, 9 Dec 2023 17:41:54 +0100 Subject: [PATCH 17/17] chore: add a length indicator to the prefix length --- cmd/explain.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/explain.go b/cmd/explain.go index 4e10d28..191e9f1 100644 --- a/cmd/explain.go +++ b/cmd/explain.go @@ -108,6 +108,8 @@ func getNetworkDetails(network *net.IPNet) *networkDetailsToDisplay { //nolint:goconst func explain(details *networkDetailsToDisplay) { + var lengthIndicator string + fmt.Printf(color.BlueString("Base Address:\t\t ")+"%s\n", details.BaseAddress) if !details.UsableAddressRangeHasError { fmt.Printf(color.BlueString("Usable Address Range:\t ")+"%s to %s\n", details.FirstUsableIPAddress, details.LastUsableIPAddress) @@ -120,5 +122,12 @@ func explain(details *networkDetailsToDisplay) { fmt.Printf(color.RedString("Broadcast Address:\t ")+"%s\n", details.BroadcastAddress) } fmt.Printf(color.BlueString("Address Count:\t\t ")+"%s\n", details.Count) - fmt.Printf(color.BlueString("Netmask:\t\t ")+"%s (/%d bits)\n", details.Netmask, details.PrefixLength) + + if details.PrefixLength > 1 { + lengthIndicator = "bits" + } else { + lengthIndicator = "bit" + } + + fmt.Printf(color.BlueString("Netmask:\t\t ")+"%s (/%d %s)\n", details.Netmask, details.PrefixLength, lengthIndicator) }