From 868ded6a5fd7ea20b93d49e70f2ba632ef73e816 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Fri, 5 Jul 2024 16:24:37 +0200 Subject: [PATCH 1/5] feat(internet): improve ipv4 method --- src/index.ts | 3 +- src/modules/internet/index.ts | 124 +++++++++++++++++- .../__snapshots__/internet.spec.ts.snap | 22 +++- test/modules/internet.spec.ts | 50 ++++++- 4 files changed, 188 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0b5903408da..25ff62863bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -69,7 +69,8 @@ export type { GitModule } from './modules/git'; export type { HackerModule } from './modules/hacker'; export type { HelpersModule, SimpleHelpersModule } from './modules/helpers'; export type { ImageModule } from './modules/image'; -export type { InternetModule } from './modules/internet'; +export { IPv4Network } from './modules/internet'; +export type { IPv4NetworkType, InternetModule } from './modules/internet'; export type { LocationModule } from './modules/location'; export type { LoremModule } from './modules/lorem'; export type { MusicModule } from './modules/music'; diff --git a/src/modules/internet/index.ts b/src/modules/internet/index.ts index f6884d880aa..1dc46d59cd6 100644 --- a/src/modules/internet/index.ts +++ b/src/modules/internet/index.ts @@ -23,6 +23,82 @@ export type HTTPStatusCodeType = export type HTTPProtocolType = 'http' | 'https'; +export enum IPv4Network { + /** + * Equivalent to: `0.0.0.0/0` + */ + Any = 'any', + /** + * Equivalent to: `127.0.0.0/8` + * + * @see [RFC1122](https://www.rfc-editor.org/rfc/rfc1122) + */ + Loopback = 'loopback', + /** + * Equivalent to: `10.0.0.0/8` + * + * @see [RFC1918](https://www.rfc-editor.org/rfc/rfc1918) + */ + PrivateA = 'private-a', + /** + * Equivalent to: `172.16.0.0/12` + * + * @see [RFC1918](https://www.rfc-editor.org/rfc/rfc1918) + */ + PrivateB = 'private-b', + /** + * Equivalent to: `192.168.0.0/16` + * + * @see [RFC1918](https://www.rfc-editor.org/rfc/rfc1918) + */ + PrivateC = 'private-c', + /** + * Equivalent to: `192.0.2.0/24` + * + * @see [RFC5737](https://www.rfc-editor.org/rfc/rfc5737) + */ + TestNet1 = 'test-net-1', + /** + * Equivalent to: `198.51.100.0/24` + * + * @see [RFC5737](https://www.rfc-editor.org/rfc/rfc5737) + */ + TestNet2 = 'test-net-2', + /** + * Equivalent to: `203.0.113.0/24` + * + * @see [RFC5737](https://www.rfc-editor.org/rfc/rfc5737) + */ + TestNet3 = 'test-net-3', + /** + * Equivalent to: `169.254.0.0/16` + * + * @see [RFC3927](https://www.rfc-editor.org/rfc/rfc3927) + */ + LinkLocal = 'link-local', + /** + * Equivalent to: `224.0.0.0/4` + * + * @see [RFC5771](https://www.rfc-editor.org/rfc/rfc5771) + */ + Multicast = 'multicast', +} + +export type IPv4NetworkType = `${IPv4Network}`; + +const ipv4Networks: Record = { + [IPv4Network.Any]: '0.0.0.0/0', + [IPv4Network.Loopback]: '127.0.0.0/8', + [IPv4Network.PrivateA]: '10.0.0.0/8', + [IPv4Network.PrivateB]: '172.16.0.0/12', + [IPv4Network.PrivateC]: '192.168.0.0/16', + [IPv4Network.TestNet1]: '192.0.2.0/24', + [IPv4Network.TestNet2]: '198.51.100.0/24', + [IPv4Network.TestNet3]: '203.0.113.0/24', + [IPv4Network.LinkLocal]: '169.254.0.0/16', + [IPv4Network.Multicast]: '224.0.0.0/4', +}; + /** * Module to generate internet related entries. * @@ -485,15 +561,55 @@ export class InternetModule extends ModuleBase { /** * Generates a random IPv4 address. * + * @param options The optional options object. + * @param options.cidrBlock The optional CIDR block to use. Defaults to `'0.0.0.0/0'`. + * @param options.network The optional network to use. This is intended as an alias for well-known `cidrBlock`s. Defaults to `'any'`. + * * @example * faker.internet.ipv4() // '245.108.222.0' + * faker.internet.ipv4({ cidrBlock: '192.168.0.0/16' }) // '192.168.215.224' + * faker.internet.ipv4({ network: 'private-a' }) // '10.199.154.205' * * @since 6.1.1 */ - ipv4(): string { - return Array.from({ length: 4 }, () => this.faker.number.int(255)).join( - '.' - ); + ipv4( + options?: + | { + /** + * The optional CIDR block to use. + * + * @default '0.0.0.0/0' + */ + cidrBlock?: `${number}.${number}.${number}.${number}/${number}`; + } + | { + /** + * The optional network to use. This is intended as an alias for well-known `cidrBlock`s. + * + * @default 'any' + */ + network?: IPv4NetworkType; + } + ): string; + ipv4( + options: { cidrBlock?: string; network?: IPv4NetworkType } = {} + ): string { + const { network = 'any', cidrBlock = ipv4Networks[network] } = options; + + const [ipText, subnet] = cidrBlock.split('/'); + const subnetMask = 0xffffffff >>> Number.parseInt(subnet); + const networkIp = + ipText + .split('.') + .map(Number) + .map((octet, index) => octet << (24 - 8 * index)) + .reduce((acc, octet) => acc | octet, 0) & ~subnetMask; + const hostOffset = this.faker.number.int(subnetMask); + const ip = networkIp | hostOffset; + + return Array.from({ length: 4 }, (_, index) => + ((ip >> (24 - 8 * index)) & 0xff).toString() + ).join('.'); } /** diff --git a/test/modules/__snapshots__/internet.spec.ts.snap b/test/modules/__snapshots__/internet.spec.ts.snap index bb330fbdbb1..9ec2d26525f 100644 --- a/test/modules/__snapshots__/internet.spec.ts.snap +++ b/test/modules/__snapshots__/internet.spec.ts.snap @@ -64,9 +64,13 @@ exports[`internet > 42 > httpStatusCode > noArgs 1`] = `226`; exports[`internet > 42 > httpStatusCode > with options 1`] = `410`; -exports[`internet > 42 > ip 1`] = `"243.187.153.39"`; +exports[`internet > 42 > ip 1`] = `"243.98.3.69"`; -exports[`internet > 42 > ipv4 1`] = `"95.243.187.153"`; +exports[`internet > 42 > ipv4 > noArgs 1`] = `"95.225.220.121"`; + +exports[`internet > 42 > ipv4 > with cidrBlock 1`] = `"192.168.13.95"`; + +exports[`internet > 42 > ipv4 > with network 1`] = `"229.254.29.199"`; exports[`internet > 42 > ipv6 1`] = `"8ead:331d:df0f:c444:6b96:d368:ab4b:d1d3"`; @@ -182,7 +186,11 @@ exports[`internet > 1211 > httpStatusCode > with options 1`] = `429`; exports[`internet > 1211 > ip 1`] = `"d4fe:fa7f:baec:9dc4:c48f:a8eb:f46f:b7c8"`; -exports[`internet > 1211 > ipv4 1`] = `"237.228.57.255"`; +exports[`internet > 1211 > ipv4 > noArgs 1`] = `"237.179.127.46"`; + +exports[`internet > 1211 > ipv4 > with cidrBlock 1`] = `"192.168.13.237"`; + +exports[`internet > 1211 > ipv4 > with network 1`] = `"238.219.55.242"`; exports[`internet > 1211 > ipv6 1`] = `"ed4f:efa7:fbae:c9dc:4c48:fa8e:bf46:fb7c"`; @@ -296,9 +304,13 @@ exports[`internet > 1337 > httpStatusCode > noArgs 1`] = `201`; exports[`internet > 1337 > httpStatusCode > with options 1`] = `407`; -exports[`internet > 1337 > ip 1`] = `"40.71.117.82"`; +exports[`internet > 1337 > ip 1`] = `"40.159.131.70"`; + +exports[`internet > 1337 > ipv4 > noArgs 1`] = `"67.20.12.145"`; + +exports[`internet > 1337 > ipv4 > with cidrBlock 1`] = `"192.168.13.67"`; -exports[`internet > 1337 > ipv4 1`] = `"67.40.71.117"`; +exports[`internet > 1337 > ipv4 > with network 1`] = `"228.49.64.201"`; exports[`internet > 1337 > ipv6 1`] = `"536a:7b5f:a28d:2f9b:b79c:a46e:a394:bc4f"`; diff --git a/test/modules/internet.spec.ts b/test/modules/internet.spec.ts index 25c5150fe3f..485c4abdf1b 100644 --- a/test/modules/internet.spec.ts +++ b/test/modules/internet.spec.ts @@ -1,6 +1,7 @@ import validator from 'validator'; import { describe, expect, it } from 'vitest'; import { allFakers, faker } from '../../src'; +import { IPv4Network } from '../../src/modules/internet'; import { seededTests } from '../support/seeded-runs'; import { times } from './../support/times'; @@ -15,7 +16,6 @@ describe('internet', () => { 'domainSuffix', 'domainWord', 'ip', - 'ipv4', 'ipv6', 'port', 'userAgent' @@ -133,6 +133,12 @@ describe('internet', () => { protocol: 'http', }); }); + + t.describe('ipv4', (t) => { + t.it('noArgs') + .it('with cidrBlock', { cidrBlock: '192.168.13.37/24' }) + .it('with network', { network: IPv4Network.Multicast }); + }); }); describe.each(times(NON_SEEDED_BASED_RUN).map(() => faker.seed()))( @@ -595,6 +601,48 @@ describe('internet', () => { expect(+part).toBeLessThanOrEqual(255); } }); + + it('should return a random IPv4 for a given CIDR block', () => { + const actual = faker.internet.ipv4({ + cidrBlock: '192.168.42.255/24', + }); + + expect(actual).toBeTruthy(); + expect(actual).toBeTypeOf('string'); + expect(actual).toSatisfy((value: string) => validator.isIP(value, 4)); + expect(actual).toMatch(/^192\.168\.42\.\d{1,3}$/); + }); + + it.each([ + [IPv4Network.Any, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/], + [IPv4Network.Loopback, /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/], + [IPv4Network.PrivateA, /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/], + [ + IPv4Network.PrivateB, + /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/, + ], + [IPv4Network.PrivateC, /^192\.168\.\d{1,3}\.\d{1,3}$/], + [IPv4Network.TestNet1, /^192\.0\.2\.\d{1,3}$/], + [IPv4Network.TestNet2, /^198\.51\.100\.\d{1,3}$/], + [IPv4Network.TestNet3, /^203\.0\.113\.\d{1,3}$/], + [IPv4Network.LinkLocal, /^169\.254\.\d{1,3}\.\d{1,3}$/], + [ + IPv4Network.Multicast, + /^2(2[4-9]|3[0-9])\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, + ], + ] as const)( + 'should return a random IPv4 for %s network', + (network, regex) => { + const actual = faker.internet.ipv4({ network }); + + expect(actual).toBeTruthy(); + expect(actual).toBeTypeOf('string'); + expect(actual).toSatisfy((value: string) => + validator.isIP(value, 4) + ); + expect(actual).toMatch(regex); + } + ); }); describe('ipv6()', () => { From c9226465523f8e6afc6df34e70688ea4342971ee Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 14 Jul 2024 16:59:07 +0200 Subject: [PATCH 2/5] chore: apply suggestions --- src/modules/internet/index.ts | 40 +++++++++++++++++++++++++++++++++++ test/modules/internet.spec.ts | 18 ++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/modules/internet/index.ts b/src/modules/internet/index.ts index 1dc46d59cd6..da410216c4d 100644 --- a/src/modules/internet/index.ts +++ b/src/modules/internet/index.ts @@ -558,6 +558,46 @@ export class InternetModule extends ModuleBase { return this.faker.datatype.boolean() ? this.ipv4() : this.ipv6(); } + /** + * Generates a random IPv4 address. + * + * @param options The optional options object. + * @param options.cidrBlock The optional CIDR block to use. Defaults to `'0.0.0.0/0'`. + * + * @example + * faker.internet.ipv4() // '245.108.222.0' + * faker.internet.ipv4({ cidrBlock: '192.168.0.0/16' }) // '192.168.215.224' + * + * @since 6.1.1 + */ + ipv4(options?: { + /** + * The optional CIDR block to use. + * + * @default '0.0.0.0/0' + */ + cidrBlock?: `${number}.${number}.${number}.${number}/${number}`; + }): string; + /** + * Generates a random IPv4 address. + * + * @param options The optional options object. + * @param options.network The optional network to use. This is intended as an alias for well-known `cidrBlock`s. Defaults to `'any'`. + * + * @example + * faker.internet.ipv4() // '245.108.222.0' + * faker.internet.ipv4({ network: 'private-a' }) // '10.199.154.205' + * + * @since 6.1.1 + */ + ipv4(options?: { + /** + * The optional network to use. This is intended as an alias for well-known `cidrBlock`s. + * + * @default 'any' + */ + network?: IPv4NetworkType; + }): string; /** * Generates a random IPv4 address. * diff --git a/test/modules/internet.spec.ts b/test/modules/internet.spec.ts index 485c4abdf1b..d6121a9c635 100644 --- a/test/modules/internet.spec.ts +++ b/test/modules/internet.spec.ts @@ -613,6 +613,24 @@ describe('internet', () => { expect(actual).toMatch(/^192\.168\.42\.\d{1,3}$/); }); + it('should return a random IPv4 for a given CIDR block non-8ish network mask', () => { + const actual = faker.internet.ipv4({ + cidrBlock: '192.168.0.255/20', + }); + + expect(actual).toBeTruthy(); + expect(actual).toBeTypeOf('string'); + expect(actual).toSatisfy((value: string) => validator.isIP(value, 4)); + + const [first, second, third, fourth] = actual.split('.').map(Number); + expect(first).toBe(192); + expect(second).toBe(168); + expect(third).toBeGreaterThanOrEqual(0); + expect(third).toBeLessThanOrEqual(15); + expect(fourth).toBeGreaterThanOrEqual(0); + expect(fourth).toBeLessThanOrEqual(255); + }); + it.each([ [IPv4Network.Any, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/], [IPv4Network.Loopback, /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/], From 8294134e5dc3e2e0dd01a00ed409985de1fc21da Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Thu, 1 Aug 2024 23:03:03 +0200 Subject: [PATCH 3/5] refactor: linearize code --- src/modules/internet/index.ts | 30 ++++++++++++++++++------------ test/modules/internet.spec.ts | 31 ++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/modules/internet/index.ts b/src/modules/internet/index.ts index da410216c4d..cc3d124e156 100644 --- a/src/modules/internet/index.ts +++ b/src/modules/internet/index.ts @@ -1,3 +1,4 @@ +import { FakerError } from '../../errors/faker-error'; import { ModuleBase } from '../../internal/module-base'; import { charMapping } from './char-mappings'; import * as random_ua from './user-agent'; @@ -572,11 +573,11 @@ export class InternetModule extends ModuleBase { */ ipv4(options?: { /** - * The optional CIDR block to use. + * The optional CIDR block to use. Should follow the format `x.x.x.x/y`. * * @default '0.0.0.0/0' */ - cidrBlock?: `${number}.${number}.${number}.${number}/${number}`; + cidrBlock?: string; }): string; /** * Generates a random IPv4 address. @@ -636,20 +637,25 @@ export class InternetModule extends ModuleBase { ): string { const { network = 'any', cidrBlock = ipv4Networks[network] } = options; + if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/.test(cidrBlock)) { + throw new FakerError( + `Invalid CIDR block provided: ${cidrBlock}. Must be in the format x.x.x.x/y.` + ); + } + const [ipText, subnet] = cidrBlock.split('/'); const subnetMask = 0xffffffff >>> Number.parseInt(subnet); - const networkIp = - ipText - .split('.') - .map(Number) - .map((octet, index) => octet << (24 - 8 * index)) - .reduce((acc, octet) => acc | octet, 0) & ~subnetMask; + const [rawIp1, rawIp2, rawIp3, rawIp4] = ipText.split('.').map(Number); + const rawIp = (rawIp1 << 24) | (rawIp2 << 16) | (rawIp3 << 8) | rawIp4; + const networkIp = rawIp & ~subnetMask; const hostOffset = this.faker.number.int(subnetMask); const ip = networkIp | hostOffset; - - return Array.from({ length: 4 }, (_, index) => - ((ip >> (24 - 8 * index)) & 0xff).toString() - ).join('.'); + return [ + (ip >>> 24) & 0xff, + (ip >>> 16) & 0xff, + (ip >>> 8) & 0xff, + ip & 0xff, + ].join('.'); } /** diff --git a/test/modules/internet.spec.ts b/test/modules/internet.spec.ts index d6121a9c635..bf30777d55b 100644 --- a/test/modules/internet.spec.ts +++ b/test/modules/internet.spec.ts @@ -1,6 +1,6 @@ import validator from 'validator'; import { describe, expect, it } from 'vitest'; -import { allFakers, faker } from '../../src'; +import { allFakers, faker, FakerError } from '../../src'; import { IPv4Network } from '../../src/modules/internet'; import { seededTests } from '../support/seeded-runs'; import { times } from './../support/times'; @@ -631,6 +631,35 @@ describe('internet', () => { expect(fourth).toBeLessThanOrEqual(255); }); + it.each([ + '', + '...', + '.../', + '.0.0.0/0', + '0..0.0/0', + '0.0..0/0', + '0.0.0./0', + '0.0.0.0/', + 'a.0.0.0/0', + '0.b.0.0/0', + '0.0.c.0/0', + '0.0.0.d/0', + '0.0.0.0/e', + ])( + 'should throw an error if not following the x.x.x.x/y format', + (cidrBlock) => { + expect(() => + faker.internet.ipv4({ + cidrBlock, + }) + ).toThrow( + new FakerError( + `Invalid CIDR block provided: ${cidrBlock}. Must be in the format x.x.x.x/y.` + ) + ); + } + ); + it.each([ [IPv4Network.Any, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/], [IPv4Network.Loopback, /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/], From 5e2f0b94a929f7c7e30f93f2b7b97ef73e093fef Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Thu, 1 Aug 2024 23:07:15 +0200 Subject: [PATCH 4/5] chore: fix docs --- src/modules/internet/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/internet/index.ts b/src/modules/internet/index.ts index cc3d124e156..a455135b00d 100644 --- a/src/modules/internet/index.ts +++ b/src/modules/internet/index.ts @@ -563,7 +563,7 @@ export class InternetModule extends ModuleBase { * Generates a random IPv4 address. * * @param options The optional options object. - * @param options.cidrBlock The optional CIDR block to use. Defaults to `'0.0.0.0/0'`. + * @param options.cidrBlock The optional CIDR block to use. Must be in the format `x.x.x.x/y`. Defaults to `'0.0.0.0/0'`. * * @example * faker.internet.ipv4() // '245.108.222.0' @@ -573,7 +573,7 @@ export class InternetModule extends ModuleBase { */ ipv4(options?: { /** - * The optional CIDR block to use. Should follow the format `x.x.x.x/y`. + * The optional CIDR block to use. Must be in the format `x.x.x.x/y`. * * @default '0.0.0.0/0' */ @@ -603,7 +603,7 @@ export class InternetModule extends ModuleBase { * Generates a random IPv4 address. * * @param options The optional options object. - * @param options.cidrBlock The optional CIDR block to use. Defaults to `'0.0.0.0/0'`. + * @param options.cidrBlock The optional CIDR block to use. Must be in the format `x.x.x.x/y`. Defaults to `'0.0.0.0/0'`. * @param options.network The optional network to use. This is intended as an alias for well-known `cidrBlock`s. Defaults to `'any'`. * * @example @@ -617,11 +617,11 @@ export class InternetModule extends ModuleBase { options?: | { /** - * The optional CIDR block to use. + * The optional CIDR block to use. Must be in the format `x.x.x.x/y`. * * @default '0.0.0.0/0' */ - cidrBlock?: `${number}.${number}.${number}.${number}/${number}`; + cidrBlock?: string; } | { /** From 5b91c4f12834a0a6fc45873cc1c544acff3cb4a8 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Thu, 1 Aug 2024 23:10:49 +0200 Subject: [PATCH 5/5] chore: fix import --- test/modules/internet.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/modules/internet.spec.ts b/test/modules/internet.spec.ts index bf30777d55b..04b255fd48f 100644 --- a/test/modules/internet.spec.ts +++ b/test/modules/internet.spec.ts @@ -1,6 +1,7 @@ import validator from 'validator'; import { describe, expect, it } from 'vitest'; -import { allFakers, faker, FakerError } from '../../src'; +import { allFakers, faker } from '../../src'; +import { FakerError } from '../../src/errors/faker-error'; import { IPv4Network } from '../../src/modules/internet'; import { seededTests } from '../support/seeded-runs'; import { times } from './../support/times';