Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Generate dangerous strings by default #3043

Merged
merged 17 commits into from
Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
200 changes: 97 additions & 103 deletions packages/fast-check/documentation/Arbitraries.md

Large diffs are not rendered by default.

80 changes: 77 additions & 3 deletions packages/fast-check/src/arbitrary/_internals/ArrayArbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {
depthIdentifier: DepthIdentifier | string | undefined,
// Whenever passing a isEqual to ArrayArbitrary, you also have to filter
// it's output just in case produced values are too small (below minLength)
readonly setBuilder?: CustomSetBuilder<Value<T>>
readonly setBuilder: CustomSetBuilder<Value<T>> | undefined,
readonly getCustomSlices: (() => T[][]) | undefined
) {
super();
this.lengthArb = integer({ min: minLength, max: maxGeneratedLength });
Expand Down Expand Up @@ -75,13 +76,15 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {
): Value<T>[] {
let numSkippedInRow = 0;
const s = setBuilder();
const slices = this.getCustomSlices !== undefined ? this.getCustomSlices() : [];
const slicedGenerator = buildSlicedGenerator(this.arb, mrng, slices, biasFactorItems);
// Try to append into items up to the target size
// We may reject some items as they are already part of the set
// so we need to retry and generate other ones. In order to prevent infinite loop,
// we accept a max of maxGeneratedLength consecutive failures. This circuit breaker may cause
// generated to be smaller than the minimal accepted one.
while (s.size() < N && numSkippedInRow < this.maxGeneratedLength) {
const current = this.arb.generate(mrng, biasFactorItems);
const current = slicedGenerator.next();
if (s.tryAdd(current)) {
numSkippedInRow = 0;
} else {
Expand All @@ -108,8 +111,11 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {

private generateNItems(N: number, mrng: Random, biasFactorItems: number | undefined): Value<T>[] {
const items: Value<T>[] = [];
const slices = this.getCustomSlices !== undefined ? this.getCustomSlices() : [];
const slicedGenerator = buildSlicedGenerator(this.arb, mrng, slices, biasFactorItems);
slicedGenerator.attemptExact(N);
for (let index = 0; index !== N; ++index) {
const current = this.arb.generate(mrng, biasFactorItems);
const current = slicedGenerator.next();
items.push(current);
}
return items;
Expand Down Expand Up @@ -316,3 +322,71 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {
);
}
}

type SlicedGenerator<T> = {
attemptExact: (targetLength: number) => void;
next: () => Value<T>;
};

function buildSlicedGenerator<T>(
arb: Arbitrary<T>,
mrng: Random,
slices: T[][],
biasFactor: number | undefined
): SlicedGenerator<T> {
if (biasFactor === undefined || slices.length === 0) {
return {
attemptExact: () => {},
next: () => arb.generate(mrng, biasFactor),
};
}
// WARNING: The code below makes the assumptions that we only receive non-empty slices!
let activeSliceIndex = 0;
let nextIndexInSlice = 0; // the next index to take from the slice
let lastIndexInSlice = -1; // the last index accepted for the current slice
return {
attemptExact: (targetLength) => {
if (targetLength !== 0 && mrng.nextInt(1, biasFactor) === 1) {
// Let's setup the generator for exact matching if any possible
const eligibleIndices: number[] = [];
for (let index = 0; index !== slices.length; ++index) {
const slice = slices[index];
if (slice.length === targetLength) {
eligibleIndices.push(index);
}
}
if (eligibleIndices.length === 0) {
return;
}
activeSliceIndex = mrng.nextInt(0, eligibleIndices.length - 1);
dubzzz marked this conversation as resolved.
Show resolved Hide resolved
nextIndexInSlice = 0;
lastIndexInSlice = targetLength - 1;
}
},
next: (): Value<T> => {
if (nextIndexInSlice <= lastIndexInSlice) {
// We continue on the previously selected slice
return new Value(slices[activeSliceIndex][nextIndexInSlice++], undefined);
}
if (mrng.nextInt(1, biasFactor) !== 1) {
// We don't use the slices
return arb.generate(mrng, biasFactor);
}
// We update the active slice
activeSliceIndex = mrng.nextInt(0, slices.length - 1);
if (mrng.nextInt(1, biasFactor) !== 1) {
// We will consider the whole slice and not a sub-set of it
nextIndexInSlice = 1;
lastIndexInSlice = slices.length - 1;
return new Value(slices[activeSliceIndex][0], undefined);
}
const slice = slices[activeSliceIndex];
const rangeBoundaryA = mrng.nextInt(0, slice.length - 1);
const rangeBoundaryB = mrng.nextInt(0, slice.length - 1);
nextIndexInSlice = Math.min(rangeBoundaryA, rangeBoundaryB);
lastIndexInSlice = Math.max(rangeBoundaryA, rangeBoundaryB);
lastIndexInSlice = slices.length - 1;
return new Value(slices[activeSliceIndex][nextIndexInSlice++], undefined);
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Arbitrary } from '../../../check/arbitrary/definition/Arbitrary';

const dangerousStrings = [
// JavaScript
'__prototype__',
'__proto__',
'proto',
'constructor',
'set',
'get',
'break',
'case',
'class',
'catch',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'export',
'extends',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'new',
'return',
'super',
'switch',
'this',
'throw',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield',
'enum',
'await',
'implements',
'let',
'package',
'protected',
'static',
'interface',
'private',
'public',
'abstract',
'boolean',
'byte',
'char',
'double',
'final',
'float',
'goto',
'int',
'long',
'native',
'short',
// React
'key',
'ref',
];

export function createSlicesForStringBuilder(
charArbitrary: Arbitrary<string>,
stringSplitter: (value: string) => string[]
): () => string[][] {
const slicesForString: string[][] = dangerousStrings
.map((dangerous) => {
try {
return stringSplitter(dangerous);
} catch (err) {
return [];
}
})
.filter((entry) => entry.length > 0 && entry.every((c) => charArbitrary.canShrinkWithoutContext(c)));
return function buildSlicesForString(): string[][] {
return slicesForString;
};
}
25 changes: 24 additions & 1 deletion packages/fast-check/src/arbitrary/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ export interface ArrayConstraints {
depthIdentifier?: DepthIdentifier | string;
}

/**
* Extra but internal constraints that can be passed to array.
* This extra set is made of constraints mostly targets experimental and internal features not yet mature to be exposed.
* @internal
*/
export interface ArrayConstraintsInternal<T> extends ArrayConstraints {
/**
* Extra user-definable and hardcoded values.
* Each entry in the array could be used to build the final generated value outputed by the arbitrary of array on generate.
* Each entry must have at least one element of type T into it.
* Each T must be a value acceptable for the arbitrary passed to the array.
*/
getCustomSlices?: () => T[][];
}

/**
* For arrays of values coming from `arb`
*
Expand All @@ -66,6 +81,14 @@ function array<T>(arb: Arbitrary<T>, constraints: ArrayConstraints = {}): Arbitr
const maxLength = maxLengthOrUnset !== undefined ? maxLengthOrUnset : MaxLengthUpperBound;
const specifiedMaxLength = maxLengthOrUnset !== undefined;
const maxGeneratedLength = maxGeneratedLengthFromSizeForArbitrary(size, minLength, maxLength, specifiedMaxLength);
return new ArrayArbitrary<T>(arb, minLength, maxGeneratedLength, maxLength, depthIdentifier);
return new ArrayArbitrary<T>(
arb,
minLength,
maxGeneratedLength,
maxLength,
depthIdentifier,
undefined,
(constraints as ArrayConstraintsInternal<T>).getCustomSlices
);
}
export { array };
8 changes: 6 additions & 2 deletions packages/fast-check/src/arbitrary/asciiString.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { array } from './array';
import { array, ArrayConstraintsInternal } from './array';
import { ascii } from './ascii';
import { StringSharedConstraints } from './_shared/StringSharedConstraints';
import { codePointsToStringMapper, codePointsToStringUnmapper } from './_internals/mappers/CodePointsToString';
import { createSlicesForStringBuilder } from './_internals/helpers/SlicesForStringBuilder';
export { StringSharedConstraints } from './_shared/StringSharedConstraints';

/**
Expand All @@ -14,5 +15,8 @@ export { StringSharedConstraints } from './_shared/StringSharedConstraints';
* @public
*/
export function asciiString(constraints: StringSharedConstraints = {}): Arbitrary<string> {
return array(ascii(), constraints).map(codePointsToStringMapper, codePointsToStringUnmapper);
const charArbitrary = ascii();
const slicesBuilder = createSlicesForStringBuilder(charArbitrary, codePointsToStringUnmapper);
const enrichedConstraints: ArrayConstraintsInternal<string> = { ...constraints, getCustomSlices: slicesBuilder };
return array(charArbitrary, enrichedConstraints).map(codePointsToStringMapper, codePointsToStringUnmapper);
}
13 changes: 11 additions & 2 deletions packages/fast-check/src/arbitrary/base64String.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { array } from './array';
import { array, ArrayConstraintsInternal } from './array';
import { base64 } from './base64';
import { MaxLengthUpperBound } from './_internals/helpers/MaxLengthFromMinLength';
import { StringSharedConstraints } from './_shared/StringSharedConstraints';
import { codePointsToStringMapper, codePointsToStringUnmapper } from './_internals/mappers/CodePointsToString';
import { stringToBase64Mapper, stringToBase64Unmapper } from './_internals/mappers/StringToBase64';
import { createSlicesForStringBuilder } from './_internals/helpers/SlicesForStringBuilder';
export { StringSharedConstraints } from './_shared/StringSharedConstraints';

/**
Expand All @@ -28,7 +29,15 @@ function base64String(constraints: StringSharedConstraints = {}): Arbitrary<stri
if (minLength % 4 !== 0) throw new Error('Minimal length of base64 strings must be a multiple of 4');
if (maxLength % 4 !== 0) throw new Error('Maximal length of base64 strings must be a multiple of 4');

return array(base64(), { minLength, maxLength, size: requestedSize })
const charArbitrary = base64();
const slicesBuilder = createSlicesForStringBuilder(charArbitrary, codePointsToStringUnmapper);
const enrichedConstraints: ArrayConstraintsInternal<string> = {
minLength,
maxLength,
size: requestedSize,
getCustomSlices: slicesBuilder,
};
return array(charArbitrary, enrichedConstraints)
.map(codePointsToStringMapper, codePointsToStringUnmapper)
.map(stringToBase64Mapper, stringToBase64Unmapper);
}
Expand Down
8 changes: 6 additions & 2 deletions packages/fast-check/src/arbitrary/fullUnicodeString.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { array } from './array';
import { array, ArrayConstraintsInternal } from './array';
import { fullUnicode } from './fullUnicode';
import { StringSharedConstraints } from './_shared/StringSharedConstraints';
import { codePointsToStringMapper, codePointsToStringUnmapper } from './_internals/mappers/CodePointsToString';
import { createSlicesForStringBuilder } from './_internals/helpers/SlicesForStringBuilder';
export { StringSharedConstraints } from './_shared/StringSharedConstraints';

/**
Expand All @@ -14,5 +15,8 @@ export { StringSharedConstraints } from './_shared/StringSharedConstraints';
* @public
*/
export function fullUnicodeString(constraints: StringSharedConstraints = {}): Arbitrary<string> {
return array(fullUnicode(), constraints).map(codePointsToStringMapper, codePointsToStringUnmapper);
const charArbitrary = fullUnicode();
const slicesBuilder = createSlicesForStringBuilder(charArbitrary, codePointsToStringUnmapper);
const enrichedConstraints: ArrayConstraintsInternal<string> = { ...constraints, getCustomSlices: slicesBuilder };
return array(charArbitrary, enrichedConstraints).map(codePointsToStringMapper, codePointsToStringUnmapper);
}
8 changes: 6 additions & 2 deletions packages/fast-check/src/arbitrary/hexaString.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { array } from './array';
import { array, ArrayConstraintsInternal } from './array';
import { hexa } from './hexa';
import { StringSharedConstraints } from './_shared/StringSharedConstraints';
import { codePointsToStringMapper, codePointsToStringUnmapper } from './_internals/mappers/CodePointsToString';
import { createSlicesForStringBuilder } from './_internals/helpers/SlicesForStringBuilder';
export { StringSharedConstraints } from './_shared/StringSharedConstraints';

/**
Expand All @@ -14,6 +15,9 @@ export { StringSharedConstraints } from './_shared/StringSharedConstraints';
* @public
*/
function hexaString(constraints: StringSharedConstraints = {}): Arbitrary<string> {
return array(hexa(), constraints).map(codePointsToStringMapper, codePointsToStringUnmapper);
const charArbitrary = hexa();
const slicesBuilder = createSlicesForStringBuilder(charArbitrary, codePointsToStringUnmapper);
const enrichedConstraints: ArrayConstraintsInternal<string> = { ...constraints, getCustomSlices: slicesBuilder };
return array(charArbitrary, enrichedConstraints).map(codePointsToStringMapper, codePointsToStringUnmapper);
}
export { hexaString };
8 changes: 6 additions & 2 deletions packages/fast-check/src/arbitrary/string.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { array } from './array';
import { array, ArrayConstraintsInternal } from './array';
import { char } from './char';
import { StringSharedConstraints } from './_shared/StringSharedConstraints';
import { codePointsToStringMapper, codePointsToStringUnmapper } from './_internals/mappers/CodePointsToString';
import { createSlicesForStringBuilder } from './_internals/helpers/SlicesForStringBuilder';
export { StringSharedConstraints } from './_shared/StringSharedConstraints';

/**
Expand All @@ -14,5 +15,8 @@ export { StringSharedConstraints } from './_shared/StringSharedConstraints';
* @public
*/
export function string(constraints: StringSharedConstraints = {}): Arbitrary<string> {
return array(char(), constraints).map(codePointsToStringMapper, codePointsToStringUnmapper);
const charArbitrary = char();
const slicesBuilder = createSlicesForStringBuilder(charArbitrary, codePointsToStringUnmapper);
const enrichedConstraints: ArrayConstraintsInternal<string> = { ...constraints, getCustomSlices: slicesBuilder };
return array(charArbitrary, enrichedConstraints).map(codePointsToStringMapper, codePointsToStringUnmapper);
}
8 changes: 6 additions & 2 deletions packages/fast-check/src/arbitrary/string16bits.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { array } from './array';
import { array, ArrayConstraintsInternal } from './array';
import { char16bits } from './char16bits';
import { StringSharedConstraints } from './_shared/StringSharedConstraints';
import { charsToStringMapper, charsToStringUnmapper } from './_internals/mappers/CharsToString';
import { createSlicesForStringBuilder } from './_internals/helpers/SlicesForStringBuilder';
export { StringSharedConstraints } from './_shared/StringSharedConstraints';

/**
Expand All @@ -14,5 +15,8 @@ export { StringSharedConstraints } from './_shared/StringSharedConstraints';
* @public
*/
export function string16bits(constraints: StringSharedConstraints = {}): Arbitrary<string> {
return array(char16bits(), constraints).map(charsToStringMapper, charsToStringUnmapper);
const charArbitrary = char16bits();
const slicesBuilder = createSlicesForStringBuilder(charArbitrary, charsToStringUnmapper);
const enrichedConstraints: ArrayConstraintsInternal<string> = { ...constraints, getCustomSlices: slicesBuilder };
return array(charArbitrary, enrichedConstraints).map(charsToStringMapper, charsToStringUnmapper);
}
8 changes: 6 additions & 2 deletions packages/fast-check/src/arbitrary/stringOf.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { array } from './array';
import { array, ArrayConstraintsInternal } from './array';
import { StringSharedConstraints } from './_shared/StringSharedConstraints';
import { patternsToStringMapper, patternsToStringUnmapperFor } from './_internals/mappers/PatternsToString';
import { createSlicesForStringBuilder } from './_internals/helpers/SlicesForStringBuilder';
export { StringSharedConstraints } from './_shared/StringSharedConstraints';

/**
Expand All @@ -14,5 +15,8 @@ export { StringSharedConstraints } from './_shared/StringSharedConstraints';
* @public
*/
export function stringOf(charArb: Arbitrary<string>, constraints: StringSharedConstraints = {}): Arbitrary<string> {
return array(charArb, constraints).map(patternsToStringMapper, patternsToStringUnmapperFor(charArb, constraints));
const unmapper = patternsToStringUnmapperFor(charArb, constraints);
const slicesBuilder = createSlicesForStringBuilder(charArb, unmapper);
const enrichedConstraints: ArrayConstraintsInternal<string> = { ...constraints, getCustomSlices: slicesBuilder };
return array(charArb, enrichedConstraints).map(patternsToStringMapper, unmapper);
}
Loading