diff --git a/packages/ERTP/test/unitTests/test-mintObj.js b/packages/ERTP/test/unitTests/test-mintObj.js index 84af9f5b8548..8ee13ec1ab89 100644 --- a/packages/ERTP/test/unitTests/test-mintObj.js +++ b/packages/ERTP/test/unitTests/test-mintObj.js @@ -46,7 +46,7 @@ test('mint.mintPayment set w strings AssetKind', async t => { const badAmount = AmountMath.make(brand, harden([['badElement']])); t.throws(() => mint.mintPayment(badAmount), { message: - 'In "mintPayment" method of (items mint) arg 0: value: [0]: copyArray ["badElement"] - Must be a string', + 'In "mintPayment" method of (items mint) arg 0: value: [0]: ["badElement"] - Must be a string', }); }); diff --git a/packages/SwingSet/test/gc-helpers.js b/packages/SwingSet/test/gc-helpers.js index d45329826465..2bcea11f0d9a 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 anyScalarSchema = 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 20195d923ea3..7db4572a130f 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 { @@ -33,6 +34,7 @@ import { const { quote: q, details: X } = assert; const { entries, values } = Object; +const { ownKeys } = Reflect; /** @type WeakSet */ const patternMemo = new WeakSet(); @@ -534,6 +536,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} */ @@ -705,6 +773,118 @@ const makePatternKit = () => { }, }); + /** @type {MatchHelper} */ + const matchBigintHelper = Far('match:bigint helper', { + checkMatches: (specimen, [limits = undefined], check = x => x) => { + const { decimalDigitsLimit } = limit(limits); + return ( + check( + passStyleOf(specimen) === 'bigint', + X`${specimen} - Must be a bigint`, + ) && + check( + `${specimen}`.length <= decimalDigitsLimit, + X`bigint ${specimen} must not have more than ${decimalDigitsLimit} digits`, + ) + ); + }, + + checkIsMatcherPayload: (payload, check = x => x) => + checkIsLimitPayload(payload, harden([]), check, 'match:bigint payload'), + + getRankCover: (_matchPayload, _encodePassable) => + getPassStyleCover('bigint'), + + checkKeyPattern: (_matcherPayload, _check = x => x) => true, + }); + + /** @type {MatchHelper} */ + const matchNatHelper = Far('match:nat helper', { + checkMatches: (specimen, [limits = undefined], check = x => x) => { + const { decimalDigitsLimit } = limit(limits); + return ( + check( + passStyleOf(specimen) === 'bigint', + X`${specimen} - Must be a bigint`, + ) && + 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 = x => x) => + checkIsLimitPayload(payload, harden([]), check, 'match:nat payload'), + + getRankCover: (_matchPayload, _encodePassable) => + // TODO Could be more precise + getPassStyleCover('bigint'), + + checkKeyPattern: (_matcherPayload, _check = x => x) => true, + }); + + /** @type {MatchHelper} */ + const matchStringHelper = Far('match:string helper', { + checkMatches: (specimen, [limits = undefined], check = x => x) => { + const { stringLengthLimit } = limit(limits); + return ( + check( + passStyleOf(specimen) === 'string', + X`${specimen} - Must be a string`, + ) && + check( + specimen.length <= stringLengthLimit, + X`string ${specimen} must not be bigger than ${stringLengthLimit}`, + ) + ); + }, + + checkIsMatcherPayload: (payload, check = x => x) => + checkIsLimitPayload(payload, harden([]), check, 'match:string payload'), + + getRankCover: (_matchPayload, _encodePassable) => + getPassStyleCover('string'), + + checkKeyPattern: (_matcherPayload, _check = x => x) => true, + }); + + /** @type {MatchHelper} */ + const matchSymbolHelper = Far('match:symbol helper', { + checkMatches: (specimen, [limits = undefined], check = x => x) => { + const { symbolNameLengthLimit } = limit(limits); + if ( + !check( + passStyleOf(specimen) === 'symbol', + X`${specimen} - Must be a symbol`, + ) + ) { + 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 = x => x) => + checkIsLimitPayload(payload, harden([]), check, 'match:bigint payload'), + + getRankCover: (_matchPayload, _encodePassable) => + getPassStyleCover('symbol'), + + checkKeyPattern: (_matcherPayload, _check = x => x) => true, + }); + /** @type {MatchHelper} */ const matchRemotableHelper = Far('match:remotable helper', { checkMatches: (specimen, remotableDesc, check = x => x) => { @@ -821,36 +1001,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', @@ -862,16 +1052,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'), @@ -881,20 +1118,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', @@ -908,21 +1165,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', @@ -1074,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, @@ -1104,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 @@ -1189,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, @@ -1214,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 7cebf5f41b24..24c647901c78 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 e203cc3130a7..146e02251a26 100644 --- a/packages/store/test/test-patterns.js +++ b/packages/store/test/test-patterns.js @@ -38,7 +38,7 @@ const matchTests = harden([ [4, '3 - Must be: 4'], [M.not(3), '3 - must fail negated pattern: 3'], [M.not(M.any()), '3 - must fail negated pattern: "[match:any]"'], - [M.string(), 'number 3 - Must be a string'], + [M.string(), '3 - Must be a string'], [[3, 4], '3 - Must be: [3,4]'], [M.gte(7), '3 - Must be >= 7'], [M.lte(2), '3 - Must be <= 2'], @@ -80,7 +80,7 @@ const matchTests = harden([ noPatterns: [ [[4, 3], '[3,4] - Must be: [4,3]'], [[3], '[3,4] - Must be: [3]'], - [[M.string(), M.any()], '[0]: number 3 - Must be a string'], + [[M.string(), M.any()], '[0]: 3 - Must be a string'], [M.lte([3, 3]), '[3,4] - Must be <= [3,3]'], [M.gte([4, 4]), '[3,4] - Must be >= [4,4]'], [M.lte([3]), '[3,4] - Must be <= [3]'], @@ -94,8 +94,8 @@ const matchTests = harden([ [M.partial([5]), 'optional-parts: [3] - Must be: [5]'], [M.scalar(), 'A "copyArray" cannot be a scalar key: [3,4]'], - [M.set(), 'copyArray [3,4] - Must be a copySet'], - [M.arrayOf(M.string()), '[0]: number 3 - Must be a string'], + [M.set(), '[3,4] - Must be a CopySet'], + [M.arrayOf(M.string()), '[0]: 3 - Must be a string'], ], }, { @@ -126,7 +126,7 @@ const matchTests = harden([ ], noPatterns: [ [{ foo: 4, bar: 3 }, '{"foo":3,"bar":4} - Must be: {"foo":4,"bar":3}'], - [{ foo: M.string(), bar: M.any() }, 'foo: number 3 - Must be a string'], + [{ foo: M.string(), bar: M.any() }, 'foo: 3 - Must be a string'], [ M.lte({ foo: 3, bar: 3 }), '{"foo":3,"bar":4} - Must be <= {"foo":3,"bar":3}', @@ -169,15 +169,12 @@ const matchTests = harden([ ], [M.scalar(), 'A "copyRecord" cannot be a scalar key: {"foo":3,"bar":4}'], - [M.map(), 'copyRecord {"foo":3,"bar":4} - Must be a copyMap'], + [M.map(), '{"foo":3,"bar":4} - Must be a CopyMap'], [ M.recordOf(M.number(), M.number()), 'foo: [0]: string "foo" - Must be a number', ], - [ - M.recordOf(M.string(), M.string()), - 'foo: [1]: number 3 - Must be a string', - ], + [M.recordOf(M.string(), M.string()), 'foo: [1]: 3 - Must be a string'], ], }, { @@ -194,8 +191,8 @@ const matchTests = harden([ [makeCopySet([3, 4, 5]), '"[copySet]" - Must be: "[copySet]"'], [M.lte(makeCopySet([])), '"[copySet]" - Must be <= "[copySet]"'], [M.gte(makeCopySet([3, 4, 5])), '"[copySet]" - Must be >= "[copySet]"'], - [M.bag(), 'copySet "[copySet]" - Must be a copyBag'], - [M.setOf(M.string()), '[0]: number 4 - Must be a string'], + [M.bag(), '"[copySet]" - Must be a CopyBag'], + [M.setOf(M.string()), '[0]: 4 - Must be a string'], ], }, { @@ -252,11 +249,11 @@ const matchTests = harden([ M.mapOf(M.record(), M.string()), ], noPatterns: [ - [M.bag(), 'copyMap "[copyMap]" - Must be a copyBag'], - [M.set(), 'copyMap "[copyMap]" - Must be a copySet'], + [M.bag(), '"[copyMap]" - Must be a CopyBag'], + [M.set(), '"[copyMap]" - Must be a CopySet'], [ M.mapOf(M.string(), M.string()), - 'keys[0]: copyRecord {"foo":3} - Must be a string', + 'keys[0]: {"foo":3} - Must be a string', ], [ M.mapOf(M.record(), M.number()), @@ -292,7 +289,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]"}', ], ], }, @@ -300,10 +297,7 @@ const matchTests = harden([ specimen: makeTagged('match:remotable', harden({ label: 88 })), yesPatterns: [M.any(), M.not(M.pattern())], noPatterns: [ - [ - M.pattern(), - 'match:remotable payload: label: number 88 - Must be a string', - ], + [M.pattern(), 'match:remotable payload: label: 88 - Must be a string'], ], }, { @@ -329,6 +323,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 => { @@ -351,6 +375,6 @@ test('test simple matches', t => { test('well formed patterns', t => { // @ts-expect-error purposeful type violation for testing t.throws(() => M.remotable(88), { - message: 'match:remotable payload: label: number 88 - Must be a string', + message: 'match:remotable payload: label: 88 - Must be a string', }); }); diff --git a/packages/zoe/test/unitTests/test-cleanProposal.js b/packages/zoe/test/unitTests/test-cleanProposal.js index 6ab8bbb2e5bf..e6c15a405cea 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: "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: 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(