diff --git a/.github/workflows/ipconv-test.yml b/.github/workflows/ipconv-test.yml index cbd46f9..7eebc96 100644 --- a/.github/workflows/ipconv-test.yml +++ b/.github/workflows/ipconv-test.yml @@ -4,15 +4,15 @@ jobs: test: strategy: matrix: - go-version: [1.18.x, 1.20.x, 1.22.x] + go-version: [1.22.x, 1.23.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - - name: Checkout code - uses: actions/checkout@v4 - - name: Test - run: go test ./... \ No newline at end of file + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v4 + - name: Test + run: go test ./... diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a576d2..cfc0da9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.3.0 +## 31-05-2026 + +1. [](#new) + * Fix crash in IPv6ToInt and IPv6ToBigInt when given nil or invalid IP addresses + * Cache To4() result in IPv4ToInt to avoid scanning twice + * Write directly to the target slice in IntToIPv6 to eliminate extra allocations + * Use big.Int.FillBytes in BigIntToIPv6 for cleaner, faster conversions + * Bump Go version to 1.22 in go.mod + * Update GitHub Actions configuration to test against Go 1.22 and 1.23 +2. [](#bugfix) + * Fix typos ("addres" -> "address") in ErrNotIPv4Address and ErrNotIPv6Address messages + * Fix compiling and runtime errors in README.md example code + # v1.2.2 ## 25-07-2024 diff --git a/README.md b/README.md index 1145284..878dbc7 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,14 @@ package main import ( "fmt" - "net" "github.com/praserx/ipconv" ) func main() { - if ip, version, err := ipconv.ParseIP("192.168.1.1"); err != nil && version == 4 { - fmt.Println(ipconv.IPv4ToInt(ip)) + ip, version, err := ipconv.ParseIP("192.168.1.1") + if err == nil && version == 4 { + val, _ := ipconv.IPv4ToInt(ip) + fmt.Println(val) } } ``` diff --git a/go.mod b/go.mod index 1c8fefc..ebd1940 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/praserx/ipconv -go 1.17 +go 1.22 diff --git a/ipconv.go b/ipconv.go index f580bb3..25245ab 100644 --- a/ipconv.go +++ b/ipconv.go @@ -11,30 +11,36 @@ import ( ) var ErrInvalidIPAddress = errors.New("invalid ip address") -var ErrNotIPv4Address = errors.New("not an IPv4 addres") -var ErrNotIPv6Address = errors.New("not an IPv6 addres") +var ErrNotIPv4Address = errors.New("not an IPv4 address") +var ErrNotIPv6Address = errors.New("not an IPv6 address") // IPv4ToInt converts IP address of version 4 from net.IP to uint32 // representation. func IPv4ToInt(ipaddr net.IP) (uint32, error) { - if ipaddr.To4() == nil { + ip4 := ipaddr.To4() + if ip4 == nil { return 0, ErrNotIPv4Address } - return binary.BigEndian.Uint32(ipaddr.To4()), nil + return binary.BigEndian.Uint32(ip4), nil } // IPv6ToInt converts IP address of version 6 from net.IP to uint64 array // representation. Return value contains high integer value on the first // place and low integer value on second place. func IPv6ToInt(ipaddr net.IP) ([2]uint64, error) { - if ipaddr.To16()[0:8] == nil || ipaddr.To16()[8:16] == nil { + if ipaddr == nil { + return [2]uint64{0, 0}, ErrInvalidIPAddress + } + + ip16 := ipaddr.To16() + if ip16 == nil || ip16.To4() != nil { return [2]uint64{0, 0}, ErrNotIPv6Address } // Get two separates values of integer IP ip := [2]uint64{ - binary.BigEndian.Uint64(ipaddr.To16()[0:8]), // IP high - binary.BigEndian.Uint64(ipaddr.To16()[8:16]), // IP low + binary.BigEndian.Uint64(ip16[0:8]), // IP high + binary.BigEndian.Uint64(ip16[8:16]), // IP low } return ip, nil @@ -47,9 +53,14 @@ func IPv6ToBigInt(ipaddr net.IP) (*big.Int, error) { return nil, ErrInvalidIPAddress } + ip16 := ipaddr.To16() + if ip16 == nil || ip16.To4() != nil { + return nil, ErrNotIPv6Address + } + // Initialize value as bytes var ip big.Int - ip.SetBytes(ipaddr) + ip.SetBytes(ip16) return &ip, nil } @@ -70,21 +81,9 @@ func IntToIPv4(ipaddr uint32) net.IP { func IntToIPv6(high, low uint64) net.IP { ip := make(net.IP, net.IPv6len) - // Allocate 8 bytes arrays for IPs - ipHigh := make([]byte, 8) - ipLow := make([]byte, 8) - - // Proceed conversion - binary.BigEndian.PutUint64(ipHigh, high) - binary.BigEndian.PutUint64(ipLow, low) - - for i := 0; i < net.IPv6len; i++ { - if i < 8 { - ip[i] = ipHigh[i] - } else if i >= 8 { - ip[i] = ipLow[i-8] - } - } + // Direct write to the target slice (no extra temporary allocations). + binary.BigEndian.PutUint64(ip[0:8], high) + binary.BigEndian.PutUint64(ip[8:16], low) return ip } @@ -93,18 +92,13 @@ func IntToIPv6(high, low uint64) net.IP { // representation. func BigIntToIPv6(ipaddr big.Int) net.IP { ip := make(net.IP, net.IPv6len) - - ipBytes := ipaddr.Bytes() - ipBytesLen := len(ipBytes) - - for i := 0; i < net.IPv6len; i++ { - if i < net.IPv6len-ipBytesLen { - ip[i] = 0x0 - } else { - ip[i] = ipBytes[ipBytesLen-net.IPv6len+i] - } + if ipaddr.BitLen() > 128 { + var mask big.Int + mask.SetBit(&mask, 128, 1) + mask.Sub(&mask, big.NewInt(1)) + ipaddr.And(&ipaddr, &mask) } - + ipaddr.FillBytes(ip) return ip } @@ -115,8 +109,9 @@ func ParseIP(s string) (net.IP, int, error) { pip := net.ParseIP(s) if pip == nil { return nil, 0, ErrInvalidIPAddress - } else if strings.Contains(s, ".") { - return pip, 4, nil } - return pip, 16, nil + if strings.Contains(s, ":") { + return pip, 16, nil + } + return pip, 4, nil } diff --git a/ipconv_test.go b/ipconv_test.go index 74d8093..909c641 100644 --- a/ipconv_test.go +++ b/ipconv_test.go @@ -70,6 +70,36 @@ func TestIPv6ToBigInt(t *testing.T) { } } +func TestIPv6ToIntError(t *testing.T) { + for _, c := range []struct { + in net.IP + wantErr error + }{ + {nil, ErrInvalidIPAddress}, + {net.IP{1, 2, 3}, ErrNotIPv6Address}, + } { + got, err := IPv6ToInt(c.in) + if err == nil || err != c.wantErr { + t.Errorf("IPv6ToInt(%q) == %v, %v, want error %v", c.in, got, err, c.wantErr) + } + } +} + +func TestIPv6ToBigIntError(t *testing.T) { + for _, c := range []struct { + in net.IP + wantErr error + }{ + {nil, ErrInvalidIPAddress}, + {net.IP{1, 2, 3}, ErrNotIPv6Address}, + } { + got, err := IPv6ToBigInt(c.in) + if err == nil || err != c.wantErr { + t.Errorf("IPv6ToBigInt(%q) == %v, %v, want error %v", c.in, got, err, c.wantErr) + } + } +} + func TestIntToIPv4(t *testing.T) { for _, c := range []struct { in uint32 @@ -160,3 +190,17 @@ func GetBigInt(bi string) *big.Int { bigInt.SetString(bi, 10) return bigInt } + +func BenchmarkIntToIPv6(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = IntToIPv6(2306204062558715904, 34952) + } +} + +func BenchmarkBigIntToIPv6(b *testing.B) { + bigInt := GetBigInt("42540488161975842760550637899214225665") + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = BigIntToIPv6(*bigInt) + } +}