Skip to content

Commit

Permalink
fix: size limits
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Aug 26, 2022
1 parent 3d1a71a commit e0ff2a1
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 108 deletions.
321 changes: 238 additions & 83 deletions packages/store/src/patterns/patternMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {

const { quote: q, details: X } = assert;
const { entries, values } = Object;
const { ownKeys } = Reflect;

/** @type WeakSet<Pattern> */
const patternMemo = new WeakSet();
Expand Down Expand Up @@ -534,6 +535,72 @@ const makePatternKit = () => {
return check(false, details);
};

// /////////////////////// Match Helpers Helpers /////////////////////////////

const defaultLimits = harden({
decimalDigitsLimit: 100,
stringLengthLimit: 100_000,
symbolNameLengthLimit: 40,
numPropertiesLimit: 80,
propertyNameLengthLimit: 100,
arrayLengthLimit: 10_000,
numSetElementsLimit: 10_000,
numUniqueBagElementsLimit: 10_000,
numMapEntriesLimit: 5000,
});

/**
* Use the result only to get the limits you need by destructuring.
* Thus, the result only needs to support destructuring. The current
* implementation uses inheritance as a cheap hack.
*
* @param {Limits} [limits]
* @returns {AllLimits}
*/
const limit = (limits = {}) =>
/** @type {AllLimits} */ (harden({ __proto__: defaultLimits, ...limits }));

const checkIsLimitPayload = (payload, mainPayloadShape, check, label) => {
assert(Array.isArray(mainPayloadShape));
if (!Array.isArray(payload)) {
return check(false, X`${q(label)} payload must be an array: ${payload}`);
}

// Was the following, but its overuse of patterns caused an infinite regress
// const payloadLimitShape = harden(
// M.split(
// mainPayloadShape,
// M.partial(harden([M.recordOf(M.string(), M.number())]), harden([])),
// ),
// );
// return checkMatches(payload, payloadLimitShape, check, label);

const mainLength = mainPayloadShape.length;
if (payload.length < mainLength || payload.length > mainLength + 1) {
return check(false, X`${q(label)} payload unexpected size: ${payload}`);
}
const limits = payload[mainLength];
payload = harden(payload.slice(0, mainLength));
if (!checkMatches(payload, mainPayloadShape, check, label)) {
return false;
}
if (limits === undefined) {
return true;
}
return (
check(
passStyleOf(limits) === 'copyRecord',
X`Limits must be a record: ${q(limits)}`,
) &&
entries(limits).every(([key, value]) =>
check(
passStyleOf(value) === 'number',
X`Value of limit ${q(key)} but be a number: ${q(value)}`,
),
)
);
};

// /////////////////////// Match Helpers /////////////////////////////////////

/** @type {MatchHelper} */
Expand Down Expand Up @@ -821,36 +888,46 @@ const makePatternKit = () => {
checkKeyPattern(rightOperand, check),
});

/** @type {MatchHelper} */
const matchArrayOfHelper = Far('match:arrayOf helper', {
checkMatches: (specimen, subPatt, check = x => x) =>
check(
passStyleOf(specimen) === 'copyArray',
X`${specimen} - Must be an array`,
) && specimen.every((el, i) => checkMatches(el, subPatt, check, i)),

checkIsMatcherPayload: checkPattern,

getRankCover: () => getPassStyleCover('copyArray'),

checkKeyPattern: (_, check = x => x) =>
check(false, X`Arrays not yet supported as keys`),
});

/** @type {MatchHelper} */
const matchRecordOfHelper = Far('match:recordOf helper', {
checkMatches: (specimen, entryPatt, check = x => x) =>
check(
passStyleOf(specimen) === 'copyRecord',
X`${specimen} - Must be a record`,
) &&
entries(specimen).every(el =>
checkMatches(harden(el), entryPatt, check, el[0]),
),
checkMatches: (
specimen,
[keyPatt, valuePatt, limits = undefined],
check = x => x,
) => {
const { numPropertiesLimit, propertyNameLengthLimit } = limit(limits);
return (
check(
passStyleOf(specimen) === 'copyRecord',
X`${specimen} - Must be a record`,
) &&
check(
ownKeys(specimen).length <= numPropertiesLimit,
X`Must not have more than ${q(
numPropertiesLimit,
)} properties: ${specimen}`,
) &&
entries(specimen).every(
([key, value]) =>
check(
key.length <= propertyNameLengthLimit,
X`Property name ${q(key)} but not be longer than ${q(
propertyNameLengthLimit,
)}`,
) &&
checkMatches(
harden([key, value]),
harden([keyPatt, valuePatt]),
check,
key,
),
)
);
},

checkIsMatcherPayload: (entryPatt, check = x => x) =>
checkMatches(
entryPatt,
checkIsMatcherPayload: (payload, check = x => x) =>
checkIsLimitPayload(
payload,
harden([M.pattern(), M.pattern()]),
check,
'match:recordOf payload',
Expand All @@ -862,16 +939,63 @@ const makePatternKit = () => {
check(false, X`Records not yet supported as keys`),
});

/** @type {MatchHelper} */
const matchArrayOfHelper = Far('match:arrayOf helper', {
checkMatches: (specimen, [subPatt, limits = undefined], check = x => x) => {
const { arrayLengthLimit } = limit(limits);
return (
check(
passStyleOf(specimen) === 'copyArray',
X`${specimen} - Must be an array`,
) &&
check(
specimen.length <= arrayLengthLimit,
X`Array length ${specimen.length} must be <= limit ${arrayLengthLimit}`,
) &&
specimen.every((el, i) => checkMatches(el, subPatt, check, i))
);
},

checkIsMatcherPayload: (payload, check = x => x) =>
checkIsLimitPayload(
payload,
harden([M.pattern()]),
check,
'match:arrayOf payload',
),

getRankCover: () => getPassStyleCover('copyArray'),

checkKeyPattern: (_, check = x => x) =>
check(false, X`Arrays not yet supported as keys`),
});

/** @type {MatchHelper} */
const matchSetOfHelper = Far('match:setOf helper', {
checkMatches: (specimen, keyPatt, check = x => x) =>
check(
passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copySet',
X`${specimen} - Must be a a CopySet`,
) &&
specimen.payload.every((el, i) => checkMatches(el, keyPatt, check, i)),
checkMatches: (specimen, [keyPatt, limits = undefined], check = x => x) => {
const { numSetElementsLimit } = limit(limits);
return (
check(
passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copySet',
X`${specimen} - Must be a CopySet`,
) &&
check(
specimen.payload.length < numSetElementsLimit,
X`Set must not have more than ${q(numSetElementsLimit)} elements: ${
specimen.payload.length
}`,
) &&
specimen.payload.every((el, i) => checkMatches(el, keyPatt, check, i))
);
},

checkIsMatcherPayload: checkPattern,
checkIsMatcherPayload: (payload, check = x => x) =>
checkIsLimitPayload(
payload,
harden([M.pattern()]),
check,
'match:setOf payload',
),

getRankCover: () => getPassStyleCover('tagged'),

Expand All @@ -881,20 +1005,40 @@ const makePatternKit = () => {

/** @type {MatchHelper} */
const matchBagOfHelper = Far('match:bagOf helper', {
checkMatches: (specimen, [keyPatt, countPatt], check = x => x) =>
check(
passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copyBag',
X`${specimen} - Must be a a CopyBag`,
) &&
specimen.payload.every(
([key, count], i) =>
checkMatches(key, keyPatt, check, `keys[${i}]`) &&
checkMatches(count, countPatt, check, `counts[${i}]`),
),
checkMatches: (
specimen,
[keyPatt, countPatt, limits = undefined],
check = x => x,
) => {
const { numUniqueBagElementsLimit, decimalDigitsLimit } = limit(limits);
return (
check(
passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copyBag',
X`${specimen} - Must be a CopyBag`,
) &&
check(
specimen.payload.length <= numUniqueBagElementsLimit,
X`Bag must not have more than ${q(
numUniqueBagElementsLimit,
)} unique elements: ${specimen}`,
) &&
specimen.payload.every(
([key, count], i) =>
checkMatches(key, keyPatt, check, `keys[${i}]`) &&
check(
`${count}`.length <= decimalDigitsLimit,
X`Each bag element may be appear at most ${q(
decimalDigitsLimit,
)} times: ${specimen}`,
) &&
checkMatches(count, countPatt, check, `counts[${i}]`),
)
);
},

checkIsMatcherPayload: (entryPatt, check = x => x) =>
checkMatches(
entryPatt,
checkIsMatcherPayload: (payload, check = x => x) =>
checkIsLimitPayload(
payload,
harden([M.pattern(), M.pattern()]),
check,
'match:bagOf payload',
Expand All @@ -908,21 +1052,35 @@ const makePatternKit = () => {

/** @type {MatchHelper} */
const matchMapOfHelper = Far('match:mapOf helper', {
checkMatches: (specimen, [keyPatt, valuePatt], check = x => x) =>
check(
passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copyMap',
X`${specimen} - Must be a CopyMap`,
) &&
specimen.payload.keys.every((k, i) =>
checkMatches(k, keyPatt, check, `keys[${i}]`),
) &&
specimen.payload.values.every((v, i) =>
checkMatches(v, valuePatt, check, `values[${i}]`),
),
checkMatches: (
specimen,
[keyPatt, valuePatt, limits = undefined],
check = x => x,
) => {
const { numMapEntriesLimit } = limit(limits);
return (
check(
passStyleOf(specimen) === 'tagged' && getTag(specimen) === 'copyMap',
X`${specimen} - Must be a CopyMap`,
) &&
check(
specimen.payload.keys.length <= numMapEntriesLimit,
X`CopyMap must have no more than ${q(
numMapEntriesLimit,
)} entries: ${specimen}`,
) &&
specimen.payload.keys.every((k, i) =>
checkMatches(k, keyPatt, check, `keys[${i}]`),
) &&
specimen.payload.values.every((v, i) =>
checkMatches(v, valuePatt, check, `values[${i}]`),
)
);
},

checkIsMatcherPayload: (entryPatt, check = x => x) =>
checkMatches(
entryPatt,
checkIsMatcherPayload: (payload, check = x => x) =>
checkIsLimitPayload(
payload,
harden([M.pattern(), M.pattern()]),
check,
'match:mapOf payload',
Expand Down Expand Up @@ -1108,11 +1266,6 @@ const makePatternKit = () => {
const NatShape = makeMatcher('match:gte', 0n);
const StringShape = makeKindMatcher('string');
const SymbolShape = makeKindMatcher('symbol');
const RecordShape = makeKindMatcher('copyRecord');
const ArrayShape = makeKindMatcher('copyArray');
const SetShape = makeKindMatcher('copySet');
const BagShape = makeKindMatcher('copyBag');
const MapShape = makeKindMatcher('copyMap');
const RemotableShape = makeKindMatcher('remotable');
const ErrorShape = makeKindMatcher('error');
const PromiseShape = makeKindMatcher('promise');
Expand Down Expand Up @@ -1189,15 +1342,15 @@ const makePatternKit = () => {
kind: makeKindMatcher,
boolean: () => BooleanShape,
number: () => NumberShape,
bigint: () => BigintShape,
nat: () => NatShape,
string: () => StringShape,
symbol: () => SymbolShape,
record: () => RecordShape,
array: () => ArrayShape,
set: () => SetShape,
bag: () => BagShape,
map: () => MapShape,
bigint: (_limits = undefined) => BigintShape,
nat: (_limits = undefined) => NatShape,
string: (_limits = undefined) => StringShape,
symbol: (_limits = undefined) => SymbolShape,
record: (limits = undefined) => M.recordOf(M.any(), M.any(), limits),
array: (limits = undefined) => M.arrayOf(M.any(), limits),
set: (limits = undefined) => M.setOf(M.any(), limits),
bag: (limits = undefined) => M.bagOf(M.any(), limits),
map: (limits = undefined) => M.mapOf(M.any(), M.any(), limits),
remotable: makeRemotableMatcher,
error: () => ErrorShape,
promise: () => PromiseShape,
Expand All @@ -1214,14 +1367,16 @@ const makePatternKit = () => {
gte: rightOperand => makeMatcher('match:gte', rightOperand),
gt: rightOperand => makeMatcher('match:gt', rightOperand),

arrayOf: (subPatt = M.any()) => makeMatcher('match:arrayOf', subPatt),
recordOf: (keyPatt = M.any(), valuePatt = M.any()) =>
makeMatcher('match:recordOf', [keyPatt, valuePatt]),
setOf: (keyPatt = M.any()) => makeMatcher('match:setOf', keyPatt),
bagOf: (keyPatt = M.any(), countPatt = M.any()) =>
makeMatcher('match:bagOf', [keyPatt, countPatt]),
mapOf: (keyPatt = M.any(), valuePatt = M.any()) =>
makeMatcher('match:mapOf', [keyPatt, valuePatt]),
recordOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) =>
makeMatcher('match:recordOf', [keyPatt, valuePatt, limits]),
arrayOf: (subPatt = M.any(), limits = undefined) =>
makeMatcher('match:arrayOf', [subPatt, limits]),
setOf: (keyPatt = M.any(), limits = undefined) =>
makeMatcher('match:setOf', [keyPatt, limits]),
bagOf: (keyPatt = M.any(), countPatt = M.any(), limits = undefined) =>
makeMatcher('match:bagOf', [keyPatt, countPatt, limits]),
mapOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) =>
makeMatcher('match:mapOf', [keyPatt, valuePatt, limits]),
split: (base, rest = undefined) =>
makeMatcher('match:split', rest === undefined ? [base] : [base, rest]),
partial: (base, rest = undefined) =>
Expand Down
Loading

0 comments on commit e0ff2a1

Please sign in to comment.