diff --git a/ips.go b/ips.go new file mode 100644 index 0000000..6d24464 --- /dev/null +++ b/ips.go @@ -0,0 +1,141 @@ +package utils + +import "net" + +// IsIPv4 works the same way as net.ParseIP, +// but without check for IPv6 case and without returning net.IP slice, whereby IsIPv4 makes no allocations. +func IsIPv4(s string) bool { + for i := 0; i < net.IPv4len; i++ { + if len(s) == 0 { + return false + } + + if i > 0 { + if s[0] != '.' { + return false + } + s = s[1:] + } + + n, ci := 0, 0 + + for ci = 0; ci < len(s) && '0' <= s[ci] && s[ci] <= '9'; ci++ { + n = n*10 + int(s[ci]-'0') + if n >= 0xFF { + return false + } + } + + if ci == 0 || n > 0xFF || (ci > 1 && s[0] == '0') { + return false + } + + s = s[ci:] + } + + return len(s) == 0 +} + +// IsIPv6 works the same way as net.ParseIP, +// but without check for IPv4 case and without returning net.IP slice, whereby IsIPv6 makes no allocations. +func IsIPv6(s string) bool { + ellipsis := -1 // position of ellipsis in ip + + // Might have leading ellipsis + if len(s) >= 2 && s[0] == ':' && s[1] == ':' { + ellipsis = 0 + s = s[2:] + // Might be only ellipsis + if len(s) == 0 { + return true + } + } + + // Loop, parsing hex numbers followed by colon. + i := 0 + for i < net.IPv6len { + // Hex number. + n, ci := 0, 0 + + for ci = 0; ci < len(s); ci++ { + if '0' <= s[ci] && s[ci] <= '9' { + n *= 16 + n += int(s[ci] - '0') + } else if 'a' <= s[ci] && s[ci] <= 'f' { + n *= 16 + n += int(s[ci]-'a') + 10 + } else if 'A' <= s[ci] && s[ci] <= 'F' { + n *= 16 + n += int(s[ci]-'A') + 10 + } else { + break + } + if n > 0xFFFF { + return false + } + } + if ci == 0 || n > 0xFFFF { + return false + } + + if ci < len(s) && s[ci] == '.' { + if ellipsis < 0 && i != net.IPv6len-net.IPv4len { + return false + } + if i+net.IPv4len > net.IPv6len { + return false + } + + if !IsIPv4(s) { + return false + } + + s = "" + i += net.IPv4len + break + } + + // Save this 16-bit chunk. + i += 2 + + // Stop at end of string. + s = s[ci:] + if len(s) == 0 { + break + } + + // Otherwise must be followed by colon and more. + if s[0] != ':' || len(s) == 1 { + return false + } + s = s[1:] + + // Look for ellipsis. + if s[0] == ':' { + if ellipsis >= 0 { // already have one + return false + } + ellipsis = i + s = s[1:] + if len(s) == 0 { // can be at end + break + } + } + } + + // Must have used entire string. + if len(s) != 0 { + return false + } + + // If didn't parse enough, expand ellipsis. + if i < net.IPv6len { + if ellipsis < 0 { + return false + } + } else if ellipsis >= 0 { + // Ellipsis must represent at least one 0 group. + return false + } + return true +} diff --git a/ips_test.go b/ips_test.go new file mode 100644 index 0000000..ce9109c --- /dev/null +++ b/ips_test.go @@ -0,0 +1,87 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_IsIPv4(t *testing.T) { + t.Parallel() + + require.Equal(t, true, IsIPv4("174.23.33.100")) + require.Equal(t, true, IsIPv4("127.0.0.1")) + require.Equal(t, true, IsIPv4("0.0.0.0")) + + require.Equal(t, false, IsIPv4(".0.0.0")) + require.Equal(t, false, IsIPv4("0.0.0.")) + require.Equal(t, false, IsIPv4("0.0.0")) + require.Equal(t, false, IsIPv4(".0.0.0.")) + require.Equal(t, false, IsIPv4("0.0.0.0.0")) + require.Equal(t, false, IsIPv4("0")) + require.Equal(t, false, IsIPv4("")) + require.Equal(t, false, IsIPv4("2345:0425:2CA1::0567:5673:23b5")) + require.Equal(t, false, IsIPv4("invalid")) +} + +// go test -v -run=^$ -bench=UnsafeString -benchmem -count=2 + +func Benchmark_IsIPv4(b *testing.B) { + ip := "174.23.33.100" + var res bool + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = IsIPv4(ip) + } + require.Equal(b, true, res) + }) + + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = net.ParseIP(ip) != nil + } + require.Equal(b, true, res) + }) +} + +func Test_IsIPv6(t *testing.T) { + t.Parallel() + + require.Equal(t, true, IsIPv6("9396:9549:b4f7:8ed0:4791:1330:8c06:e62d")) + require.Equal(t, true, IsIPv6("2345:0425:2CA1::0567:5673:23b5")) + require.Equal(t, true, IsIPv6("2001:1:2:3:4:5:6:7")) + + require.Equal(t, false, IsIPv6("1.1.1.1")) + require.Equal(t, false, IsIPv6("2001:1:2:3:4:5:6:")) + require.Equal(t, false, IsIPv6(":1:2:3:4:5:6:")) + require.Equal(t, false, IsIPv6("1:2:3:4:5:6:")) + require.Equal(t, false, IsIPv6("")) + require.Equal(t, false, IsIPv6("invalid")) +} + +// go test -v -run=^$ -bench=UnsafeString -benchmem -count=2 + +func Benchmark_IsIPv6(b *testing.B) { + ip := "9396:9549:b4f7:8ed0:4791:1330:8c06:e62d" + var res bool + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = IsIPv6(ip) + } + require.Equal(b, true, res) + }) + + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = net.ParseIP(ip) != nil + } + require.Equal(b, true, res) + }) +}