Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions app/forms/ip-pool-range-add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 = {
Expand Down
60 changes: 23 additions & 37 deletions libs/util/str.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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 })
})
10 changes: 8 additions & 2 deletions libs/util/str.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}