diff --git a/documentation/1-Guides/Arbitraries.md b/documentation/1-Guides/Arbitraries.md index b15bf8eecd1..8ad34d90ca1 100644 --- a/documentation/1-Guides/Arbitraries.md +++ b/documentation/1-Guides/Arbitraries.md @@ -69,6 +69,7 @@ More specific strings: - `fc.unicodeJson()` or `fc.unicodeJson(maxDepth: number)` json strings having keys generated using `fc.unicodeString()`. String values are also produced by `fc.unicodeString()` - `fc.lorem()`, `fc.lorem(maxWordsCount: number)` or `fc.lorem(maxWordsCount: number, sentencesMode: boolean)` lorem ipsum strings. Generator can be configured by giving it a maximum number of characters by using `maxWordsCount` or switching the mode to sentences by setting `sentencesMode` to `true` in which case `maxWordsCount` is used to cap the number of sentences allowed - `fc.ipV4()` IP v4 strings +- `fc.ipV4Extended()` IP v4 strings including all the formats supported by WhatWG standard (for instance: 0x6f.9) - `fc.ipV6()` IP v6 strings - `fc.domain()` Domain name with extension following RFC 1034, RFC 1123 and WHATWG URL Standard - `fc.webAuthority()` Web authority following RFC 3986 diff --git a/src/check/arbitrary/IpArbitrary.ts b/src/check/arbitrary/IpArbitrary.ts index bd0fbcf2451..5af58227534 100644 --- a/src/check/arbitrary/IpArbitrary.ts +++ b/src/check/arbitrary/IpArbitrary.ts @@ -1,4 +1,5 @@ import { array } from './ArrayArbitrary'; +import { constantFrom } from './ConstantArbitrary'; import { Arbitrary } from './definition/Arbitrary'; import { nat } from './IntegerArbitrary'; import { oneof } from './OneOfArbitrary'; @@ -16,6 +17,36 @@ function ipV4(): Arbitrary { return tuple(nat(255), nat(255), nat(255), nat(255)).map(([a, b, c, d]) => `${a}.${b}.${c}.${d}`); } +/** + * For valid IP v4 according to WhatWG + * + * Following WhatWG, the specification for web-browsers + * https://url.spec.whatwg.org/ + * + * There is no equivalent for IP v6 according to the IP v6 parser + * https://url.spec.whatwg.org/#concept-ipv6-parser + */ +function ipV4Extended(): Arbitrary { + const natRepr = (maxValue: number) => + tuple(constantFrom('dec', 'oct', 'hex'), nat(maxValue)).map(([style, v]) => { + switch (style) { + case 'oct': + return `0${Number(v).toString(8)}`; + case 'hex': + return `0x${Number(v).toString(16)}`; + case 'dec': + default: + return `${v}`; + } + }); + return oneof( + tuple(natRepr(255), natRepr(255), natRepr(255), natRepr(255)).map(([a, b, c, d]) => `${a}.${b}.${c}.${d}`), + tuple(natRepr(255), natRepr(255), natRepr(65535)).map(([a, b, c]) => `${a}.${b}.${c}`), + tuple(natRepr(255), natRepr(16777215)).map(([a, b]) => `${a}.${b}`), + natRepr(4294967295) + ); +} + /** * For valid IP v6 * @@ -55,4 +86,4 @@ function ipV6(): Arbitrary { ); } -export { ipV4, ipV6 }; +export { ipV4, ipV4Extended, ipV6 }; diff --git a/src/check/arbitrary/WebArbitrary.ts b/src/check/arbitrary/WebArbitrary.ts index b2685f0f443..bc9f3d02317 100644 --- a/src/check/arbitrary/WebArbitrary.ts +++ b/src/check/arbitrary/WebArbitrary.ts @@ -1,10 +1,10 @@ import { array } from './ArrayArbitrary'; -import { constant } from './ConstantArbitrary'; import { constantFrom } from './ConstantArbitrary'; +import { constant } from './ConstantArbitrary'; import { buildAlphaNumericPercentArb } from './helpers/SpecificCharacterRange'; import { domain, hostUserInfo } from './HostArbitrary'; import { nat } from './IntegerArbitrary'; -import { ipV4, ipV6 } from './IpArbitrary'; +import { ipV4, ipV4Extended, ipV6 } from './IpArbitrary'; import { oneof } from './OneOfArbitrary'; import { option } from './OptionArbitrary'; import { stringOf } from './StringArbitrary'; @@ -15,6 +15,8 @@ export interface WebAuthorityConstraints { withIPv4?: boolean; /** Enable IPv6 in host */ withIPv6?: boolean; + /** Enable extended IPv4 format */ + withIPv4Extended?: boolean; /** Enable user information prefix */ withUserInfo?: boolean; /** Enable port suffix */ @@ -32,7 +34,8 @@ export function webAuthority(constraints?: WebAuthorityConstraints) { const c = constraints || {}; const hostnameArbs = [domain()] .concat(c.withIPv4 === true ? [ipV4()] : []) - .concat(c.withIPv6 === true ? [ipV6().map(ip => `[${ip}]`)] : []); + .concat(c.withIPv6 === true ? [ipV6().map(ip => `[${ip}]`)] : []) + .concat(c.withIPv4Extended === true ? [ipV4Extended()] : []); return tuple( c.withUserInfo === true ? option(hostUserInfo()) : constant(null), oneof(...hostnameArbs), diff --git a/src/fast-check-default.ts b/src/fast-check-default.ts index bb516546766..7227b081933 100644 --- a/src/fast-check-default.ts +++ b/src/fast-check-default.ts @@ -23,7 +23,7 @@ import { frequency } from './check/arbitrary/FrequencyArbitrary'; import { compareBooleanFunc, compareFunc, func } from './check/arbitrary/FunctionArbitrary'; import { domain } from './check/arbitrary/HostArbitrary'; import { integer, maxSafeInteger, maxSafeNat, nat } from './check/arbitrary/IntegerArbitrary'; -import { ipV4, ipV6 } from './check/arbitrary/IpArbitrary'; +import { ipV4, ipV4Extended, ipV6 } from './check/arbitrary/IpArbitrary'; import { letrec } from './check/arbitrary/LetRecArbitrary'; import { lorem } from './check/arbitrary/LoremArbitrary'; import { mapToConstant } from './check/arbitrary/MapToConstantArbitrary'; @@ -160,6 +160,7 @@ export { date, // web ipV4, + ipV4Extended, ipV6, domain, webAuthority, diff --git a/test/e2e/NoRegression.spec.ts b/test/e2e/NoRegression.spec.ts index 2d89751a2ad..8091713ed8f 100644 --- a/test/e2e/NoRegression.spec.ts +++ b/test/e2e/NoRegression.spec.ts @@ -194,6 +194,9 @@ describe(`NoRegression`, () => { it('ipV4', () => { expect(() => fc.assert(fc.property(fc.ipV4(), v => testFunc(v)), settings)).toThrowErrorMatchingSnapshot(); }); + it('ipV4Extended', () => { + expect(() => fc.assert(fc.property(fc.ipV4Extended(), v => testFunc(v)), settings)).toThrowErrorMatchingSnapshot(); + }); it('ipV6', () => { expect(() => fc.assert(fc.property(fc.ipV6(), v => testFunc(v)), settings)).toThrowErrorMatchingSnapshot(); }); diff --git a/test/e2e/__snapshots__/NoRegression.spec.ts.snap b/test/e2e/__snapshots__/NoRegression.spec.ts.snap index 1305c0b7ca7..843ef07e005 100644 --- a/test/e2e/__snapshots__/NoRegression.spec.ts.snap +++ b/test/e2e/__snapshots__/NoRegression.spec.ts.snap @@ -1352,6 +1352,22 @@ Execution summary: . . . . . . . √ [\\"0.54.0.0\\"]" `; +exports[`NoRegression ipV4Extended 1`] = ` +"Property failed after 1 tests +{ seed: 42, path: \\"0:1:1\\", endOnFailure: true } +Counterexample: [\\"00.0\\"] +Shrunk 2 time(s) +Got error: Property failed by returning false + +Execution summary: +× [\\"0112.0x0\\"] +. √ [\\"74.0x0\\"] +. × [\\"00.0x0\\"] +. . √ [\\"0.0x0\\"] +. . × [\\"00.0\\"] +. . . √ [\\"0.0\\"]" +`; + exports[`NoRegression ipV6 1`] = ` "Property failed after 1 tests { seed: 42, path: \\"0:0:0:0:0:0\\", endOnFailure: true } diff --git a/test/e2e/arbitraries/WebArbitrary.spec.ts b/test/e2e/arbitraries/WebArbitrary.spec.ts index 24f486a4bfc..e4424e2b46c 100644 --- a/test/e2e/arbitraries/WebArbitrary.spec.ts +++ b/test/e2e/arbitraries/WebArbitrary.spec.ts @@ -35,7 +35,7 @@ describe(`WebArbitrary (seed: ${seed})`, () => { it('Should produce valid URL parts', () => { fc.assert( fc.property( - fc.webAuthority({ withIPv4: true, withIPv6: true, withUserInfo: true, withPort: true }), + fc.webAuthority({ withIPv4: true, withIPv6: true, withIPv4Extended: true, withUserInfo: true, withPort: true }), fc.array(fc.webSegment()).map(p => p.map(v => `/${v}`).join('')), fc.webQueryParameters(), fc.webFragments(), diff --git a/test/unit/check/arbitrary/IpArbitrary.spec.ts b/test/unit/check/arbitrary/IpArbitrary.spec.ts index 9b293f36b7e..83ae8b74754 100644 --- a/test/unit/check/arbitrary/IpArbitrary.spec.ts +++ b/test/unit/check/arbitrary/IpArbitrary.spec.ts @@ -1,15 +1,28 @@ -import { ipV4, ipV6 } from '../../../../src/check/arbitrary/IpArbitrary'; +import { ipV4, ipV6, ipV4Extended } from '../../../../src/check/arbitrary/IpArbitrary'; import * as genericHelper from './generic/GenericArbitraryHelper'; const isValidIpV4 = (i: string) => { if (typeof i !== 'string') return false; - const m = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(i); - if (m === null) return false; - return [m[1], m[2], m[3], m[4]].every(g => { - const n = +g; - return n >= 0 && n <= 255 && String(n) === g; + const chunks = i.split('.').map(v => { + if (v[0] === '0') { + if (v[1] === 'x' || v[1] === 'X') return parseInt(v, 16); + return parseInt(v, 8); + } + return parseInt(v, 10); }); + + // one invalid chunk + if (chunks.find(v => Number.isNaN(v)) !== undefined) return false; + + // maximal amount of 4 chunks + if (chunks.length > 4) return false; + + // all chunks, except the last one are inferior or equal to 255 + if (chunks.slice(0, -1).find(v => v < 0 && v > 255) !== undefined) return false; + + // last chunk must be below 256^(5 − number of chunks) + return chunks[chunks.length - 1] < 256 ** (5 - chunks.length); }; const isValidIpV6 = (i: string) => { if (typeof i !== 'string') return false; @@ -19,7 +32,10 @@ const isValidIpV6 = (i: string) => { if (i.substr(firstElision + 1).includes('::')) return false; } const chunks = i.split(':'); - const endByIpV4 = isValidIpV4(chunks[chunks.length - 1]); + const last = chunks[chunks.length - 1]; + // The ipv4 can only be composed of 4 decimal chunks separated by dots + // 1.1000 is not a valid IP v4 in the context of IP v6 + const endByIpV4 = last.includes('.') && isValidIpV4(last); const nonEmptyChunks = chunks.filter(c => c !== ''); const hexaChunks = endByIpV4 ? nonEmptyChunks.slice(0, nonEmptyChunks.length - 1) : nonEmptyChunks; @@ -35,6 +51,11 @@ describe('IpArbitrary', () => { isValidValue: (g: string) => isValidIpV4(g) }); }); + describe('ipV4Extended', () => { + genericHelper.isValidArbitrary(() => ipV4Extended(), { + isValidValue: (g: string) => isValidIpV4(g) + }); + }); describe('ipV6', () => { genericHelper.isValidArbitrary(() => ipV6(), { isValidValue: (g: string) => isValidIpV6(g) diff --git a/test/unit/check/arbitrary/WebArbitrary.spec.ts b/test/unit/check/arbitrary/WebArbitrary.spec.ts index b7f32a5673a..7e8eeff6ad9 100644 --- a/test/unit/check/arbitrary/WebArbitrary.spec.ts +++ b/test/unit/check/arbitrary/WebArbitrary.spec.ts @@ -28,6 +28,7 @@ describe('WebArbitrary', () => { { withIPv4: fc.boolean(), withIPv6: fc.boolean(), + withIPv4Extended: fc.boolean(), withUserInfo: fc.boolean(), withPort: fc.boolean() },