diff --git a/app/forms/ip-pool-range-add.tsx b/app/forms/ip-pool-range-add.tsx index f3ec199c2e..a32671fbfd 100644 --- a/app/forms/ip-pool-range-add.tsx +++ b/app/forms/ip-pool-range-add.tsx @@ -9,7 +9,7 @@ import { useNavigate } from 'react-router-dom' import { useApiMutation, useApiQueryClient, type IpRange } from '@oxide/api' import { Message } from '@oxide/ui' -import { IPV4_REGEX, IPV6_REGEX } from '@oxide/util' +import { validateIp } from '@oxide/util' import { SideModalForm, TextField } from 'app/components/form' import { useForm, useIpPoolSelector } from 'app/hooks' @@ -21,12 +21,6 @@ const defaultValues: IpRange = { last: '', } -function validateIp(s: string) { - const isv4 = IPV4_REGEX.test(s) - const isv6 = !isv4 && IPV6_REGEX.test(s) - return { isv4, isv6, valid: isv4 || isv6 } -} - const invalidAddressError = { type: 'pattern', message: 'Not a valid IP address' } const diffVersionError = { diff --git a/libs/util/str.spec.ts b/libs/util/str.spec.ts index 629d2be1fd..37115bd02e 100644 --- a/libs/util/str.spec.ts +++ b/libs/util/str.spec.ts @@ -7,15 +7,7 @@ */ import { describe, expect, it, test } from 'vitest' -import { - camelCase, - capitalize, - commaSeries, - IPV4_REGEX, - IPV6_REGEX, - kebabCase, - titleCase, -} from './str' +import { camelCase, capitalize, commaSeries, kebabCase, titleCase, validateIp } from './str' describe('capitalize', () => { it('capitalizes the first letter', () => { @@ -88,29 +80,12 @@ describe('titleCase', () => { // Rust playground comparing results with std::net::{Ipv4Addr, Ipv6Addr} // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=493b3345b9f6c0b1c8ee91834e99ef7b -test.each(['123.4.56.7', '1.2.3.4'])('ipv4Regex passes: %s', (s) => { - expect(IPV4_REGEX.test(s)).toBe(true) -}) - -test.each([ - '', - '1', - 'abc', - 'a.b.c.d', - // some implementations (I think incorrectly) allow leading zeros but nexus does not - '01.102.103.104', - '::ffff:192.0.2.128', - '127.0.0', - '127.0.0.1.', - '127.0.0.1 ', - ' 127.0.0.1', - '10002.3.4', - '1.2.3.4.5', - '256.0.0.0', - '260.0.0.0', -])('ipv4Regex fails: %s', (s) => { - expect(IPV4_REGEX.test(s)).toBe(false) -}) +test.each(['123.4.56.7', '1.2.3.4'])( + 'validateIp catches valid IPV4 / invalid IPV6: %s', + (s) => { + expect(validateIp(s)).toStrictEqual({ isv4: true, isv6: false, valid: true }) + } +) test.each([ '2001:db8:3333:4444:5555:6666:7777:8888', @@ -129,15 +104,26 @@ test.each([ '::ffff:255.255.255.255', 'fe08::7:8', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', -])('ipv6Regex passes: %s', (s) => { - expect(IPV6_REGEX.test(s)).toBe(true) +])('validateIp catches invalid IPV4 / valid IPV6: %s', (s) => { + expect(validateIp(s)).toStrictEqual({ isv4: false, isv6: true, valid: true }) }) test.each([ '', '1', 'abc', - '123.4.56.7', + 'a.b.c.d', + // some implementations (I think incorrectly) allow leading zeros but nexus does not + '01.102.103.104', + '127.0.0', + '127.0.0.1.', + '127.0.0.1 ', + ' 127.0.0.1', + '10002.3.4', + '1.2.3.4.5', + '256.0.0.0', + '260.0.0.0', + '256.1.1.1', '2001:0db8:85a3:0000:0000:8a2e:0370:7334 ', ' 2001:db8::', '1:2:3:4:5:6:7:8:9', @@ -151,6 +137,6 @@ test.each([ 'fe08::7:8%', 'fe08::7:8i', 'fe08::7:8interface', -])('ipv6Regex fails: %s', (s) => { - expect(IPV6_REGEX.test(s)).toBe(false) +])('validateIp catches invalid IP: %s', (s) => { + expect(validateIp(s)).toStrictEqual({ isv4: false, isv6: false, valid: false }) }) diff --git a/libs/util/str.ts b/libs/util/str.ts index 4c6fd9d95a..55cde1f7bb 100644 --- a/libs/util/str.ts +++ b/libs/util/str.ts @@ -54,8 +54,14 @@ export const titleCase = (text: string): string => { // but they didn't match results with std::new on simple test cases // https://github.com/fabian-hiller/valibot/blob/2554aea5/library/src/regex.ts#L43-L54 -export const IPV4_REGEX = +const IPV4_REGEX = /^(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])(?:\.(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])){3}$/u -export const IPV6_REGEX = +const IPV6_REGEX = /^(?:(?:[\da-f]{1,4}:){7}[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,7}:|(?:[\da-f]{1,4}:){1,6}:[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,5}(?::[\da-f]{1,4}){1,2}|(?:[\da-f]{1,4}:){1,4}(?::[\da-f]{1,4}){1,3}|(?:[\da-f]{1,4}:){1,3}(?::[\da-f]{1,4}){1,4}|(?:[\da-f]{1,4}:){1,2}(?::[\da-f]{1,4}){1,5}|[\da-f]{1,4}:(?::[\da-f]{1,4}){1,6}|:(?:(?::[\da-f]{1,4}){1,7}|:)|fe80:(?::[\da-f]{0,4}){0,4}%[\da-z]+|::(?:f{4}(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)|(?:[\da-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d))$/iu + +export const validateIp = (ip: string) => { + const isv4 = IPV4_REGEX.test(ip) + const isv6 = !isv4 && IPV6_REGEX.test(ip) + return { isv4, isv6, valid: isv4 || isv6 } +}