diff --git a/packages/SwingSet/test/gc-helpers.js b/packages/SwingSet/test/gc-helpers.js index 51c020753c79..b26e51385603 100644 --- a/packages/SwingSet/test/gc-helpers.js +++ b/packages/SwingSet/test/gc-helpers.js @@ -157,7 +157,7 @@ export const anySchema = JSON.stringify( ); export const stringSchema = JSON.stringify( - capargs([{ '@qclass': 'tagged', tag: 'match:kind', payload: 'string' }]), + capargs([{ '@qclass': 'tagged', tag: 'match:string', payload: [] }]), ); export const scalarSchema = JSON.stringify( diff --git a/packages/SwingSet/test/virtualObjects/test-representatives.js b/packages/SwingSet/test/virtualObjects/test-representatives.js index 305a68c6a44c..556a9c384171 100644 --- a/packages/SwingSet/test/virtualObjects/test-representatives.js +++ b/packages/SwingSet/test/virtualObjects/test-representatives.js @@ -396,7 +396,7 @@ test('virtual object gc', async t => { [`${v}.vs.vc.1.|label`]: 'baggage', [`${v}.vs.vc.1.|nextOrdinal`]: '1', [`${v}.vs.vc.1.|schemata`]: - '{"body":"[{\\"@qclass\\":\\"tagged\\",\\"tag\\":\\"match:kind\\",\\"payload\\":\\"string\\"}]","slots":[]}', + '{"body":"[{\\"@qclass\\":\\"tagged\\",\\"tag\\":\\"match:string\\",\\"payload\\":[]}]","slots":[]}', [`${v}.vs.vc.2.|entryCount`]: '0', [`${v}.vs.vc.2.|label`]: 'promiseRegistrations', [`${v}.vs.vc.2.|nextOrdinal`]: '1', @@ -411,7 +411,7 @@ test('virtual object gc', async t => { [`${v}.vs.vc.4.|label`]: 'watchedPromises', [`${v}.vs.vc.4.|nextOrdinal`]: '1', [`${v}.vs.vc.4.|schemata`]: - '{"body":"[{\\"@qclass\\":\\"tagged\\",\\"tag\\":\\"match:kind\\",\\"payload\\":\\"string\\"}]","slots":[]}', + '{"body":"[{\\"@qclass\\":\\"tagged\\",\\"tag\\":\\"match:string\\",\\"payload\\":[]}]","slots":[]}', [`${v}.vs.vom.es.o+10/3`]: 'r', [`${v}.vs.vom.o+10/2`]: '{"label":{"body":"\\"thing #2\\"","slots":[]}}', [`${v}.vs.vom.o+10/3`]: '{"label":{"body":"\\"thing #3\\"","slots":[]}}', diff --git a/packages/store/src/patterns/patternMatchers.js b/packages/store/src/patterns/patternMatchers.js index 06f69c22fc84..7d03c7e0f683 100644 --- a/packages/store/src/patterns/patternMatchers.js +++ b/packages/store/src/patterns/patternMatchers.js @@ -8,6 +8,7 @@ import { makeTagged, passStyleOf, hasOwnPropertyOf, + nameForPassableSymbol, } from '@endo/marshal'; import { applyLabelingError, listDifference } from '@agoric/internal'; import { @@ -34,6 +35,7 @@ import { const { quote: q, details: X } = assert; const { entries, values } = Object; +const { ownKeys } = Reflect; /** @type WeakSet */ const patternMemo = new WeakSet(); @@ -558,6 +560,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} */ @@ -729,6 +797,104 @@ const makePatternKit = () => { }, }); + /** @type {MatchHelper} */ + const matchBigintHelper = Far('match:bigint helper', { + checkMatches: (specimen, [limits = undefined], check) => { + const { decimalDigitsLimit } = limit(limits); + return ( + checkKind(specimen, 'bigint', check) && + check( + `${specimen}`.length <= decimalDigitsLimit, + X`bigint ${specimen} must not have more than ${decimalDigitsLimit} digits`, + ) + ); + }, + + checkIsMatcherPayload: (payload, check) => + checkIsLimitPayload(payload, harden([]), check, 'match:bigint payload'), + + getRankCover: (_matchPayload, _encodePassable) => + getPassStyleCover('bigint'), + + checkKeyPattern: (_matcherPayload, _check) => true, + }); + + /** @type {MatchHelper} */ + const matchNatHelper = Far('match:nat helper', { + checkMatches: (specimen, [limits = undefined], check) => { + const { decimalDigitsLimit } = limit(limits); + return ( + checkKind(specimen, 'bigint', check) && + check(specimen >= 0n, X`${specimen} - Must be non-negative`) && + check( + `${specimen}`.length <= decimalDigitsLimit, + X`bigint ${specimen} must not have more than ${decimalDigitsLimit} digits`, + ) + ); + }, + + checkIsMatcherPayload: (payload, check) => + checkIsLimitPayload(payload, harden([]), check, 'match:nat payload'), + + getRankCover: (_matchPayload, _encodePassable) => + // TODO Could be more precise + getPassStyleCover('bigint'), + + checkKeyPattern: (_matcherPayload, _check) => true, + }); + + /** @type {MatchHelper} */ + const matchStringHelper = Far('match:string helper', { + checkMatches: (specimen, [limits = undefined], check) => { + const { stringLengthLimit } = limit(limits); + return ( + checkKind(specimen, 'string', check) && + check( + specimen.length <= stringLengthLimit, + X`string ${specimen} must not be bigger than ${stringLengthLimit}`, + ) + ); + }, + + checkIsMatcherPayload: (payload, check) => + checkIsLimitPayload(payload, harden([]), check, 'match:string payload'), + + getRankCover: (_matchPayload, _encodePassable) => + getPassStyleCover('string'), + + checkKeyPattern: (_matcherPayload, _check) => true, + }); + + /** @type {MatchHelper} */ + const matchSymbolHelper = Far('match:symbol helper', { + checkMatches: (specimen, [limits = undefined], check) => { + const { symbolNameLengthLimit } = limit(limits); + if (!checkKind(specimen, 'symbol', check)) { + return false; + } + const symbolName = nameForPassableSymbol(specimen); + assert.typeof( + symbolName, + 'string', + X`internal: Passable symbol ${specimen} must have a passable name`, + ); + return check( + symbolName.length <= symbolNameLengthLimit, + X`Symbol name ${q( + symbolName, + )} must not be bigger than ${symbolNameLengthLimit}`, + ); + }, + + checkIsMatcherPayload: (payload, check) => + checkIsLimitPayload(payload, harden([]), check, 'match:bigint payload'), + + getRankCover: (_matchPayload, _encodePassable) => + getPassStyleCover('symbol'), + + checkKeyPattern: (_matcherPayload, _check) => true, + }); + /** @type {MatchHelper} */ const matchRemotableHelper = Far('match:remotable helper', { checkMatches: (specimen, remotableDesc, check) => { @@ -761,7 +927,7 @@ const makePatternKit = () => { getRankCover: (_remotableDesc, _encodePassable) => getPassStyleCover('remotable'), - checkKeyPattern: (_remotableDesc, _check = x => x) => true, + checkKeyPattern: (_remotableDesc, _check) => true, }); /** @type {MatchHelper} */ @@ -852,15 +1018,41 @@ const makePatternKit = () => { /** @type {MatchHelper} */ const matchRecordOfHelper = Far('match:recordOf helper', { - checkMatches: (specimen, entryPatt, check) => - checkKind(specimen, 'copyRecord', check) && - entries(specimen).every(el => - checkMatches(harden(el), entryPatt, check, el[0]), - ), + checkMatches: ( + specimen, + [keyPatt, valuePatt, limits = undefined], + check, + ) => { + const { numPropertiesLimit, propertyNameLengthLimit } = limit(limits); + return ( + checkKind(specimen, 'copyRecord', check) && + 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) => - checkMatches( - entryPatt, + checkIsMatcherPayload: (payload, check) => + checkIsLimitPayload( + payload, harden([M.pattern(), M.pattern()]), check, 'match:recordOf payload', @@ -874,11 +1066,25 @@ const makePatternKit = () => { /** @type {MatchHelper} */ const matchArrayOfHelper = Far('match:arrayOf helper', { - checkMatches: (specimen, subPatt, check) => - checkKind(specimen, 'copyArray', check) && - specimen.every((el, i) => checkMatches(el, subPatt, check, i)), + checkMatches: (specimen, [subPatt, limits = undefined], check) => { + const { arrayLengthLimit } = limit(limits); + return ( + checkKind(specimen, 'copyArray', check) && + check( + specimen.length <= arrayLengthLimit, + X`Array length ${specimen.length} must be <= limit ${arrayLengthLimit}`, + ) && + specimen.every((el, i) => checkMatches(el, subPatt, check, i)) + ); + }, - checkIsMatcherPayload: checkPattern, + checkIsMatcherPayload: (payload, check) => + checkIsLimitPayload( + payload, + harden([M.pattern()]), + check, + 'match:arrayOf payload', + ), getRankCover: () => getPassStyleCover('copyArray'), @@ -888,11 +1094,27 @@ const makePatternKit = () => { /** @type {MatchHelper} */ const matchSetOfHelper = Far('match:setOf helper', { - checkMatches: (specimen, keyPatt, check) => - checkKind(specimen, 'copySet', check) && - specimen.payload.every((el, i) => checkMatches(el, keyPatt, check, i)), + checkMatches: (specimen, [keyPatt, limits = undefined], check) => { + const { numSetElementsLimit } = limit(limits); + return ( + checkKind(specimen, 'copySet', check) && + 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) => + checkIsLimitPayload( + payload, + harden([M.pattern()]), + check, + 'match:setOf payload', + ), getRankCover: () => getPassStyleCover('tagged'), @@ -902,17 +1124,37 @@ const makePatternKit = () => { /** @type {MatchHelper} */ const matchBagOfHelper = Far('match:bagOf helper', { - checkMatches: (specimen, [keyPatt, countPatt], check) => - checkKind(specimen, 'copyBag', check) && - 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, + ) => { + const { numUniqueBagElementsLimit, decimalDigitsLimit } = limit(limits); + return ( + checkKind(specimen, 'copyBag', check) && + 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) => - checkMatches( - entryPatt, + checkIsMatcherPayload: (payload, check) => + checkIsLimitPayload( + payload, harden([M.pattern(), M.pattern()]), check, 'match:bagOf payload', @@ -926,18 +1168,32 @@ const makePatternKit = () => { /** @type {MatchHelper} */ const matchMapOfHelper = Far('match:mapOf helper', { - checkMatches: (specimen, [keyPatt, valuePatt], check) => - checkKind(specimen, 'copyMap', check) && - 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, + ) => { + const { numMapEntriesLimit } = limit(limits); + return ( + checkKind(specimen, 'copyMap', check) && + 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) => - checkMatches( - entryPatt, + checkIsMatcherPayload: (payload, check) => + checkIsLimitPayload( + payload, harden([M.pattern(), M.pattern()]), check, 'match:mapOf payload', @@ -1089,6 +1345,10 @@ const makePatternKit = () => { 'match:key': matchKeyHelper, 'match:pattern': matchPatternHelper, 'match:kind': matchKindHelper, + 'match:bigint': matchBigintHelper, + 'match:nat': matchNatHelper, + 'match:string': matchStringHelper, + 'match:symbol': matchSymbolHelper, 'match:remotable': matchRemotableHelper, 'match:lt': matchLTHelper, @@ -1119,20 +1379,26 @@ const makePatternKit = () => { const PatternShape = makeMatcher('match:pattern', undefined); const BooleanShape = makeKindMatcher('boolean'); const NumberShape = makeKindMatcher('number'); - const BigintShape = makeKindMatcher('bigint'); - 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'); const UndefinedShape = makeKindMatcher('undefined'); + /** + * For when the last element of the payload is the optional limits, + * so that when it is `undefined` it is dropped from the end of the + * payloads array. + * + * @param {string} tag + * @param {Passable[]} payload + */ + const makeLimitsMatcher = (tag, payload) => { + if (payload[payload.length - 1] === undefined) { + payload = harden(payload.slice(0, payload.length - 1)); + } + return makeMatcher(tag, payload); + }; + const makeRemotableMatcher = (label = undefined) => label === undefined ? RemotableShape @@ -1204,15 +1470,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) => makeLimitsMatcher('match:bigint', [limits]), + nat: (limits = undefined) => makeLimitsMatcher('match:nat', [limits]), + string: (limits = undefined) => makeLimitsMatcher('match:string', [limits]), + symbol: (limits = undefined) => makeLimitsMatcher('match:symbol', [limits]), + 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, @@ -1229,14 +1495,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) => + makeLimitsMatcher('match:recordOf', [keyPatt, valuePatt, limits]), + arrayOf: (subPatt = M.any(), limits = undefined) => + makeLimitsMatcher('match:arrayOf', [subPatt, limits]), + setOf: (keyPatt = M.any(), limits = undefined) => + makeLimitsMatcher('match:setOf', [keyPatt, limits]), + bagOf: (keyPatt = M.any(), countPatt = M.any(), limits = undefined) => + makeLimitsMatcher('match:bagOf', [keyPatt, countPatt, limits]), + mapOf: (keyPatt = M.any(), valuePatt = M.any(), limits = undefined) => + makeLimitsMatcher('match:mapOf', [keyPatt, valuePatt, limits]), split: (base, rest = undefined) => makeMatcher('match:split', rest === undefined ? [base] : [base, rest]), partial: (base, rest = undefined) => diff --git a/packages/store/src/types.js b/packages/store/src/types.js index e1ff48b4c6b1..8f39add4f937 100644 --- a/packages/store/src/types.js +++ b/packages/store/src/types.js @@ -465,6 +465,23 @@ * @returns {RankCover} */ +/** + * @typedef {object} AllLimits + * @property {number} decimalDigitsLimit + * @property {number} stringLengthLimit + * @property {number} symbolNameLengthLimit + * @property {number} numPropertiesLimit + * @property {number} propertyNameLengthLimit + * @property {number} arrayLengthLimit + * @property {number} numSetElementsLimit + * @property {number} numUniqueBagElementsLimit + * @property {number} numMapEntriesLimit + */ + +/** + * @typedef {Partial} Limits + */ + /** * @typedef {object} MatcherNamespace * @property {() => Matcher} any @@ -490,16 +507,16 @@ * @property {(kind: string) => Matcher} kind * @property {() => Matcher} boolean * @property {() => Matcher} number Only floating point numbers - * @property {() => Matcher} bigint - * @property {() => Matcher} nat - * @property {() => Matcher} string - * @property {() => Matcher} symbol + * @property {(limits?: Limits) => Matcher} bigint + * @property {(limits?: Limits) => Matcher} nat + * @property {(limits?: Limits) => Matcher} string + * @property {(limits?: Limits) => Matcher} symbol * Only registered and well-known symbols are passable - * @property {() => Matcher} record A CopyRecord - * @property {() => Matcher} array A CopyArray - * @property {() => Matcher} set A CopySet - * @property {() => Matcher} bag A CopyBag - * @property {() => Matcher} map A CopyMap + * @property {(limits?: Limits) => Matcher} record A CopyRecord + * @property {(limits?: Limits) => Matcher} array A CopyArray + * @property {(limits?: Limits) => Matcher} set A CopySet + * @property {(limits?: Limits) => Matcher} bag A CopyBag + * @property {(limits?: Limits) => Matcher} map A CopyMap * @property {(label?: string) => Matcher} remotable * A far object or its remote presence. The optional `label` is purely for * diagnostic purpose. It does not enforce any constraint beyond the @@ -532,26 +549,33 @@ * @property {(rightOperand :Key) => Matcher} gt * Matches if > the right operand by compareKeys * - * @property {(subPatt?: Pattern) => Matcher} arrayOf - * @property {(keyPatt?: Pattern, valuePatt?: Pattern) => Matcher} recordOf - * @property {(keyPatt?: Pattern) => Matcher} setOf - * @property {(keyPatt?: Pattern, countPatt?: Pattern) => Matcher} bagOf + * @property {(subPatt?: Pattern, limits?: Limits) => Matcher} arrayOf + * @property {(keyPatt?: Pattern, + * valuePatt?: Pattern, + * limits?: Limits + * ) => Matcher} recordOf + * @property {(keyPatt?: Pattern, limits?: Limits) => Matcher} setOf + * @property {(keyPatt?: Pattern, + * countPatt?: Pattern, + * limits?: Limits + * ) => Matcher} bagOf * Parameterized by a keyPatt that is matched against every element of the * abstract bag. In terms of the bag representation, it is matched against * the first element of each pair. If the second `countPatt` is provided, * it is matched against the cardinality of each element. The `countPatt` * is rarely expected to be useful, but is provided to minimize surprise. - * @property {(keyPatt?: Pattern, valuePatt?: Pattern) => Matcher} mapOf - * @property {( - * base: CopyRecord<*> | CopyArray<*>, - * rest?: Pattern + * @property {(keyPatt?: Pattern, + * valuePatt?: Pattern, + * limits?: Limits + * ) => Matcher} mapOf + * @property {(base: CopyRecord<*> | CopyArray<*>, + * rest?: Pattern, * ) => Matcher} split * An array or record is split into the first part that matches the * base pattern, and the remainder, which matches against the optional * rest pattern if present. - * @property {( - * base: CopyRecord<*> | CopyArray<*>, - * rest?: Pattern + * @property {(base: CopyRecord<*> | CopyArray<*>, + * rest?: Pattern, * ) => Matcher} partial * An array or record is split into the first part that matches the * base pattern, and the remainder, which matches against the optional diff --git a/packages/store/test/test-patterns.js b/packages/store/test/test-patterns.js index 8dab91690d50..cedba8df6677 100644 --- a/packages/store/test/test-patterns.js +++ b/packages/store/test/test-patterns.js @@ -292,7 +292,7 @@ const matchTests = harden([ noPatterns: [ [ M.pattern(), - 'match:remotable payload: 88 - Must be a copyRecord to match a copyRecord pattern: {"label":"[match:kind]"}', + 'match:remotable payload: 88 - Must be a copyRecord to match a copyRecord pattern: {"label":"[match:string]"}', ], ], }, @@ -329,6 +329,36 @@ const matchTests = harden([ ], ], }, + // limit testing + { + specimen: [...'moderate length string'], + yesPatterns: [ + M.array(), + M.arrayOf(M.string()), + M.array(harden({ arrayLengthLimit: 40 })), + M.arrayOf(M.string(), harden({ arrayLengthLimit: 40 })), + ], + noPatterns: [ + [ + M.array(harden({ arrayLengthLimit: 10 })), + 'Array length 22 must be <= limit 10', + ], + [M.arrayOf(M.number()), '[0]: string "m" - Must be a number'], + [ + M.arrayOf(M.number(), harden({ arrayLengthLimit: 10 })), + 'Array length 22 must be <= limit 10', + ], + [ + M.arrayOf(M.string(), harden({ arrayLengthLimit: 10 })), + 'Array length 22 must be <= limit 10', + ], + ], + }, + { + specimen: Array(10_001).fill(1), + yesPatterns: [M.array(harden({ arrayLengthLimit: Infinity }))], + noPatterns: [[M.array(), 'Array length 10001 must be <= limit 10000']], + }, ]); test('test simple matches', t => { @@ -356,13 +386,13 @@ test('masking match failure', t => { harden({ keys: [M.string()], values: ['x'] }), ); t.throws(() => fit(nonSet, M.set()), { - message: 'A passable tagged "match:kind" is not a key: "[match:kind]"', + message: 'A passable tagged "match:string" is not a key: "[match:string]"', }); t.throws(() => fit(nonBag, M.bag()), { - message: 'A passable tagged "match:kind" is not a key: "[match:kind]"', + message: 'A passable tagged "match:string" is not a key: "[match:string]"', }); t.throws(() => fit(nonMap, M.map()), { - message: 'A passable tagged "match:kind" is not a key: "[match:kind]"', + message: 'A passable tagged "match:string" is not a key: "[match:string]"', }); }); diff --git a/packages/zoe/test/unitTests/test-cleanProposal.js b/packages/zoe/test/unitTests/test-cleanProposal.js index 6ab8bbb2e5bf..f4c239c90815 100644 --- a/packages/zoe/test/unitTests/test-cleanProposal.js +++ b/packages/zoe/test/unitTests/test-cleanProposal.js @@ -246,7 +246,7 @@ test('cleanProposal - other wrong stuff', t => { t, { exit: { afterDeadline: { timer, deadline: 'foo' } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: deadline: "foo" - Must be >= "[0n]"', + 'proposal: exit: optional-parts: afterDeadline: deadline: string "foo" - Must be a bigint', ); proposeBad( t, @@ -270,13 +270,13 @@ test('cleanProposal - other wrong stuff', t => { t, { exit: { afterDeadline: { timer, deadline: 3 } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: deadline: 3 - Must be >= "[0n]"', + 'proposal: exit: optional-parts: afterDeadline: deadline: number 3 - Must be a bigint', ); proposeBad( t, { exit: { afterDeadline: { timer, deadline: -3n } } }, 'nat', - 'proposal: exit: optional-parts: afterDeadline: deadline: "[-3n]" - Must be >= "[0n]"', + 'proposal: exit: optional-parts: afterDeadline: deadline: "[-3n]" - Must be non-negative', ); proposeBad(t, { exit: {} }, 'nat', /exit {} should only have one key/); proposeBad(