diff --git a/packages/protons-runtime/src/codec.ts b/packages/protons-runtime/src/codec.ts index 74ff90a..8de608b 100644 --- a/packages/protons-runtime/src/codec.ts +++ b/packages/protons-runtime/src/codec.ts @@ -19,11 +19,40 @@ export interface EncodeFunction { (value: Partial, writer: Writer, opts?: EncodeOptions): void } +// protobuf types that contain multiple values +type CollectionTypes = any[] | Map + +// protobuf types that are not collections or messages +type PrimitiveTypes = boolean | number | string | bigint | Uint8Array + +// recursive array/map field length limits +type CollectionLimits = { + [K in keyof T]: T[K] extends CollectionTypes ? number : + T[K] extends PrimitiveTypes ? never : Limits +} + +// recursive array member array/map field length limits +type ArrayElementLimits = { + [K in keyof T as `${string & K}$`]: T[K] extends Array ? + (ElementType extends PrimitiveTypes ? never : Limits) : + (T[K] extends PrimitiveTypes ? never : Limits) +} + +// recursive map value array/map field length limits +type MapValueLimits = { + [K in keyof T as `${string & K}$value`]: T[K] extends Map ? + (MapValueType extends PrimitiveTypes ? never : Limits) : + (T[K] extends PrimitiveTypes ? never : Limits) +} + +// union of collection and array elements +type Limits = Partial & ArrayElementLimits & MapValueLimits> + export interface DecodeOptions { /** * Runtime-specified limits for lengths of repeated/map fields */ - limits?: Partial> + limits?: Limits } export interface DecodeFunction { diff --git a/packages/protons-runtime/src/decode.ts b/packages/protons-runtime/src/decode.ts index 6a05d92..bb59ec2 100644 --- a/packages/protons-runtime/src/decode.ts +++ b/packages/protons-runtime/src/decode.ts @@ -2,7 +2,7 @@ import { createReader } from './utils/reader.js' import type { Codec, DecodeOptions } from './codec.js' import type { Uint8ArrayList } from 'uint8arraylist' -export function decodeMessage (buf: Uint8Array | Uint8ArrayList, codec: Codec, opts?: DecodeOptions): T { +export function decodeMessage (buf: Uint8Array | Uint8ArrayList, codec: Pick, 'decode'>, opts?: DecodeOptions): T { const reader = createReader(buf) return codec.decode(reader, undefined, opts) diff --git a/packages/protons-runtime/src/encode.ts b/packages/protons-runtime/src/encode.ts index 9f23828..0127c53 100644 --- a/packages/protons-runtime/src/encode.ts +++ b/packages/protons-runtime/src/encode.ts @@ -1,7 +1,7 @@ import { createWriter } from './utils/writer.js' import type { Codec } from './codec.js' -export function encodeMessage (message: T, codec: Codec): Uint8Array { +export function encodeMessage (message: Partial, codec: Pick, 'encode'>): Uint8Array { const w = createWriter() codec.encode(message, w, { diff --git a/packages/protons/README.md b/packages/protons/README.md index d543327..47ce43e 100644 --- a/packages/protons/README.md +++ b/packages/protons/README.md @@ -94,6 +94,80 @@ const message = MyMessage.decode(buf, { }) ``` +#### Limiting repeating fields of nested messages at runtime + +Sub messages with repeating elements can be limited in a similar way: + +```protobuf +message SubMessage { + repeated uint32 repeatedField = 1; +} + +message MyMessage { + SubMessage message = 1; +} +``` + +```TypeScript +const message = MyMessage.decode(buf, { + limits: { + messages: { + repeatedField: 5 // the SubMessage can not have more than 5 repeatedField entries + } + } +}) +``` + +#### Limiting repeating fields of repeating messages at runtime + +Sub messages defined in repeating elements can be limited by appending `$` to the field name in the runtime limit options: + +```protobuf +message SubMessage { + repeated uint32 repeatedField = 1; +} + +message MyMessage { + repeated SubMessage messages = 1; +} +``` + +```TypeScript +const message = MyMessage.decode(buf, { + limits: { + messages: 5 // max 5x SubMessages + messages$: { + repeatedField: 5 // no SubMessage can have more than 5 repeatedField entries + } + } +}) +``` + +#### Limiting repeating fields of map entries at runtime + +Repeating fields in map entries can be limited by appending `$value` to the field name in the runtime limit options: + +```protobuf +message SubMessage { + repeated uint32 repeatedField = 1; +} + +message MyMessage { + map messages = 1; +} +``` + +```TypeScript +const message = MyMessage.decode(buf, { + limits: { + messages: 5 // max 5x SubMessages in the map + messages$value: { + repeatedField: 5 // no SubMessage in the map can have more than 5 repeatedField entries + } + } +}) +``` + ### Overriding 64 bit types By default 64 bit types are implemented as [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)s. diff --git a/packages/protons/src/index.ts b/packages/protons/src/index.ts index 21a626e..532c611 100644 --- a/packages/protons/src/index.ts +++ b/packages/protons/src/index.ts @@ -88,6 +88,80 @@ * }) * ``` * + * #### Limiting repeating fields of nested messages at runtime + * + * Sub messages with repeating elements can be limited in a similar way: + * + * ```protobuf + * message SubMessage { + * repeated uint32 repeatedField = 1; + * } + * + * message MyMessage { + * SubMessage message = 1; + * } + * ``` + * + * ```TypeScript + * const message = MyMessage.decode(buf, { + * limits: { + * messages: { + * repeatedField: 5 // the SubMessage can not have more than 5 repeatedField entries + * } + * } + * }) + * ``` + * + * #### Limiting repeating fields of repeating messages at runtime + * + * Sub messages defined in repeating elements can be limited by appending `$` to the field name in the runtime limit options: + * + * ```protobuf + * message SubMessage { + * repeated uint32 repeatedField = 1; + * } + * + * message MyMessage { + * repeated SubMessage messages = 1; + * } + * ``` + * + * ```TypeScript + * const message = MyMessage.decode(buf, { + * limits: { + * messages: 5 // max 5x SubMessages + * messages$: { + * repeatedField: 5 // no SubMessage can have more than 5 repeatedField entries + * } + * } + * }) + * ``` + * + * #### Limiting repeating fields of map entries at runtime + * + * Repeating fields in map entries can be limited by appending `$value` to the field name in the runtime limit options: + * + * ```protobuf + * message SubMessage { + * repeated uint32 repeatedField = 1; + * } + * + * message MyMessage { + * map messages = 1; + * } + * ``` + * + * ```TypeScript + * const message = MyMessage.decode(buf, { + * limits: { + * messages: 5 // max 5x SubMessages in the map + * messages$value: { + * repeatedField: 5 // no SubMessage in the map can have more than 5 repeatedField entries + * } + * } + * }) + * ``` + * * ### Overriding 64 bit types * * By default 64 bit types are implemented as [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)s. @@ -806,7 +880,47 @@ export interface ${messageDef.name} { // override setting type on js object const jsTypeOverride = findJsTypeOverride(fieldDef.type, fieldDef) - const parseValue = `${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type](jsTypeOverride)}` + let fieldOpts = '' + + if (fieldDef.message) { + let suffix = '' + + if (fieldDef.repeated) { + suffix = '$' + } + + fieldOpts = `, { + limits: opts.limits?.${fieldName}${suffix} + }` + } + + if (fieldDef.map) { + fieldOpts = `, { + limits: { + value: opts.limits?.${fieldName}$value + } + }` + + // do not pass limit opts to map value types that are enums or + // primitives - only support messages + if (types[fieldDef.valueType] != null) { + // primmitive type + fieldOpts = '' + } else { + const valueType = findDef(fieldDef.valueType, messageDef, moduleDef) + + if (isEnumDef(valueType)) { + // enum type + fieldOpts = '' + } + } + } + + const parseValue = `${decoderGenerators[type] == null + ? `${codec}.decode(reader${type === 'message' + ? `, reader.uint32()${fieldOpts}` + : ''})` + : decoderGenerators[type](jsTypeOverride)}` if (fieldDef.map) { moduleDef.addImport('protons-runtime', 'CodeError') diff --git a/packages/protons/test/fixtures/bitswap.ts b/packages/protons/test/fixtures/bitswap.ts index 8253723..f679e40 100644 --- a/packages/protons/test/fixtures/bitswap.ts +++ b/packages/protons/test/fixtures/bitswap.ts @@ -183,7 +183,9 @@ export namespace Message { throw new CodeError('decode error - map field "entries" had too many elements', 'ERR_MAX_LENGTH') } - obj.entries.push(Message.Wantlist.Entry.codec().decode(reader, reader.uint32())) + obj.entries.push(Message.Wantlist.Entry.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.entries$ + })) break } case 2: { @@ -429,7 +431,9 @@ export namespace Message { switch (tag >>> 3) { case 1: { - obj.wantlist = Message.Wantlist.codec().decode(reader, reader.uint32()) + obj.wantlist = Message.Wantlist.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.wantlist + }) break } case 2: { @@ -445,7 +449,9 @@ export namespace Message { throw new CodeError('decode error - map field "payload" had too many elements', 'ERR_MAX_LENGTH') } - obj.payload.push(Message.Block.codec().decode(reader, reader.uint32())) + obj.payload.push(Message.Block.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.payload$ + })) break } case 4: { @@ -453,7 +459,9 @@ export namespace Message { throw new CodeError('decode error - map field "blockPresences" had too many elements', 'ERR_MAX_LENGTH') } - obj.blockPresences.push(Message.BlockPresence.codec().decode(reader, reader.uint32())) + obj.blockPresences.push(Message.BlockPresence.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.blockPresences$ + })) break } case 5: { diff --git a/packages/protons/test/fixtures/circuit.ts b/packages/protons/test/fixtures/circuit.ts index f854a2c..f21362a 100644 --- a/packages/protons/test/fixtures/circuit.ts +++ b/packages/protons/test/fixtures/circuit.ts @@ -203,11 +203,15 @@ export namespace CircuitRelay { break } case 2: { - obj.srcPeer = CircuitRelay.Peer.codec().decode(reader, reader.uint32()) + obj.srcPeer = CircuitRelay.Peer.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.srcPeer + }) break } case 3: { - obj.dstPeer = CircuitRelay.Peer.codec().decode(reader, reader.uint32()) + obj.dstPeer = CircuitRelay.Peer.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.dstPeer + }) break } case 4: { diff --git a/packages/protons/test/fixtures/daemon.ts b/packages/protons/test/fixtures/daemon.ts index e12e220..395cd03 100644 --- a/packages/protons/test/fixtures/daemon.ts +++ b/packages/protons/test/fixtures/daemon.ts @@ -126,35 +126,51 @@ export namespace Request { break } case 2: { - obj.connect = ConnectRequest.codec().decode(reader, reader.uint32()) + obj.connect = ConnectRequest.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.connect + }) break } case 3: { - obj.streamOpen = StreamOpenRequest.codec().decode(reader, reader.uint32()) + obj.streamOpen = StreamOpenRequest.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.streamOpen + }) break } case 4: { - obj.streamHandler = StreamHandlerRequest.codec().decode(reader, reader.uint32()) + obj.streamHandler = StreamHandlerRequest.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.streamHandler + }) break } case 5: { - obj.dht = DHTRequest.codec().decode(reader, reader.uint32()) + obj.dht = DHTRequest.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.dht + }) break } case 6: { - obj.connManager = ConnManagerRequest.codec().decode(reader, reader.uint32()) + obj.connManager = ConnManagerRequest.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.connManager + }) break } case 7: { - obj.disconnect = DisconnectRequest.codec().decode(reader, reader.uint32()) + obj.disconnect = DisconnectRequest.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.disconnect + }) break } case 8: { - obj.pubsub = PSRequest.codec().decode(reader, reader.uint32()) + obj.pubsub = PSRequest.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.pubsub + }) break } case 9: { - obj.peerStore = PeerstoreRequest.codec().decode(reader, reader.uint32()) + obj.peerStore = PeerstoreRequest.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.peerStore + }) break } default: { @@ -279,19 +295,27 @@ export namespace Response { break } case 2: { - obj.error = ErrorResponse.codec().decode(reader, reader.uint32()) + obj.error = ErrorResponse.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.error + }) break } case 3: { - obj.streamInfo = StreamInfo.codec().decode(reader, reader.uint32()) + obj.streamInfo = StreamInfo.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.streamInfo + }) break } case 4: { - obj.identify = IdentifyResponse.codec().decode(reader, reader.uint32()) + obj.identify = IdentifyResponse.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.identify + }) break } case 5: { - obj.dht = DHTResponse.codec().decode(reader, reader.uint32()) + obj.dht = DHTResponse.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.dht + }) break } case 6: { @@ -299,15 +323,21 @@ export namespace Response { throw new CodeError('decode error - map field "peers" had too many elements', 'ERR_MAX_LENGTH') } - obj.peers.push(PeerInfo.codec().decode(reader, reader.uint32())) + obj.peers.push(PeerInfo.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.peers$ + })) break } case 7: { - obj.pubsub = PSResponse.codec().decode(reader, reader.uint32()) + obj.pubsub = PSResponse.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.pubsub + }) break } case 8: { - obj.peerStore = PeerstoreResponse.codec().decode(reader, reader.uint32()) + obj.peerStore = PeerstoreResponse.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.peerStore + }) break } default: { @@ -1021,7 +1051,9 @@ export namespace DHTResponse { break } case 2: { - obj.peer = PeerInfo.codec().decode(reader, reader.uint32()) + obj.peer = PeerInfo.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.peer + }) break } case 3: { @@ -1742,7 +1774,9 @@ export namespace PeerstoreResponse { switch (tag >>> 3) { case 1: { - obj.peer = PeerInfo.codec().decode(reader, reader.uint32()) + obj.peer = PeerInfo.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.peer + }) break } case 2: { diff --git a/packages/protons/test/fixtures/dht.ts b/packages/protons/test/fixtures/dht.ts index d13a028..ebe259c 100644 --- a/packages/protons/test/fixtures/dht.ts +++ b/packages/protons/test/fixtures/dht.ts @@ -324,7 +324,9 @@ export namespace Message { throw new CodeError('decode error - map field "closerPeers" had too many elements', 'ERR_MAX_LENGTH') } - obj.closerPeers.push(Message.Peer.codec().decode(reader, reader.uint32())) + obj.closerPeers.push(Message.Peer.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.closerPeers$ + })) break } case 9: { @@ -332,7 +334,9 @@ export namespace Message { throw new CodeError('decode error - map field "providerPeers" had too many elements', 'ERR_MAX_LENGTH') } - obj.providerPeers.push(Message.Peer.codec().decode(reader, reader.uint32())) + obj.providerPeers.push(Message.Peer.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.providerPeers$ + })) break } default: { diff --git a/packages/protons/test/fixtures/maps.proto b/packages/protons/test/fixtures/maps.proto index cca3338..69c7868 100644 --- a/packages/protons/test/fixtures/maps.proto +++ b/packages/protons/test/fixtures/maps.proto @@ -1,7 +1,14 @@ syntax = "proto3"; +enum EnumValue { + NO_VALUE = 0; + VALUE_1 = 1; + VALUE_2 = 2; +} + message SubMessage { string foo = 1; + repeated uint32 bar = 2; } message MapTypes { @@ -9,4 +16,5 @@ message MapTypes { map intMap = 2; map boolMap = 3; map messageMap = 4; + map enumMap = 5; } diff --git a/packages/protons/test/fixtures/maps.ts b/packages/protons/test/fixtures/maps.ts index b0a6833..001cf81 100644 --- a/packages/protons/test/fixtures/maps.ts +++ b/packages/protons/test/fixtures/maps.ts @@ -4,11 +4,29 @@ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ /* eslint-disable @typescript-eslint/no-empty-interface */ -import { type Codec, CodeError, decodeMessage, type DecodeOptions, encodeMessage, message } from 'protons-runtime' +import { type Codec, CodeError, decodeMessage, type DecodeOptions, encodeMessage, enumeration, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' +export enum EnumValue { + NO_VALUE = 'NO_VALUE', + VALUE_1 = 'VALUE_1', + VALUE_2 = 'VALUE_2' +} + +enum __EnumValueValues { + NO_VALUE = 0, + VALUE_1 = 1, + VALUE_2 = 2 +} + +export namespace EnumValue { + export const codec = (): Codec => { + return enumeration(__EnumValueValues) + } +} export interface SubMessage { foo: string + bar: number[] } export namespace SubMessage { @@ -26,12 +44,20 @@ export namespace SubMessage { w.string(obj.foo) } + if (obj.bar != null) { + for (const value of obj.bar) { + w.uint32(16) + w.uint32(value) + } + } + if (opts.lengthDelimited !== false) { w.ldelim() } }, (reader, length, opts = {}) => { const obj: any = { - foo: '' + foo: '', + bar: [] } const end = length == null ? reader.len : reader.pos + length @@ -44,6 +70,14 @@ export namespace SubMessage { obj.foo = reader.string() break } + case 2: { + if (opts.limits?.bar != null && obj.bar.length === opts.limits.bar) { + throw new CodeError('decode error - map field "bar" had too many elements', 'ERR_MAX_LENGTH') + } + + obj.bar.push(reader.uint32()) + break + } default: { reader.skipType(tag & 7) break @@ -72,6 +106,7 @@ export interface MapTypes { intMap: Map boolMap: Map messageMap: Map + enumMap: Map } export namespace MapTypes { @@ -332,7 +367,9 @@ export namespace MapTypes { break } case 2: { - obj.value = SubMessage.codec().decode(reader, reader.uint32()) + obj.value = SubMessage.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.value + }) break } default: { @@ -358,6 +395,77 @@ export namespace MapTypes { } } + export interface MapTypes$enumMapEntry { + key: string + value: EnumValue + } + + export namespace MapTypes$enumMapEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key) + } + + if (obj.value != null && __EnumValueValues[obj.value] !== 0) { + w.uint32(16) + EnumValue.codec().encode(obj.value, w) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length, opts = {}) => { + const obj: any = { + key: '', + value: EnumValue.NO_VALUE + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: { + obj.key = reader.string() + break + } + case 2: { + obj.value = EnumValue.codec().decode(reader) + break + } + default: { + reader.skipType(tag & 7) + break + } + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, MapTypes$enumMapEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions): MapTypes$enumMapEntry => { + return decodeMessage(buf, MapTypes$enumMapEntry.codec(), opts) + } + } + let _codec: Codec export const codec = (): Codec => { @@ -395,6 +503,13 @@ export namespace MapTypes { } } + if (obj.enumMap != null && obj.enumMap.size !== 0) { + for (const [key, value] of obj.enumMap.entries()) { + w.uint32(42) + MapTypes.MapTypes$enumMapEntry.codec().encode({ key, value }, w) + } + } + if (opts.lengthDelimited !== false) { w.ldelim() } @@ -403,7 +518,8 @@ export namespace MapTypes { stringMap: new Map(), intMap: new Map(), boolMap: new Map(), - messageMap: new Map() + messageMap: new Map(), + enumMap: new Map() } const end = length == null ? reader.len : reader.pos + length @@ -444,10 +560,23 @@ export namespace MapTypes { throw new CodeError('decode error - map field "messageMap" had too many elements', 'ERR_MAX_SIZE') } - const entry = MapTypes.MapTypes$messageMapEntry.codec().decode(reader, reader.uint32()) + const entry = MapTypes.MapTypes$messageMapEntry.codec().decode(reader, reader.uint32(), { + limits: { + value: opts.limits?.messageMap$value + } + }) obj.messageMap.set(entry.key, entry.value) break } + case 5: { + if (opts.limits?.enumMap != null && obj.enumMap.size === opts.limits.enumMap) { + throw new CodeError('decode error - map field "enumMap" had too many elements', 'ERR_MAX_SIZE') + } + + const entry = MapTypes.MapTypes$enumMapEntry.codec().decode(reader, reader.uint32()) + obj.enumMap.set(entry.key, entry.value) + break + } default: { reader.skipType(tag & 7) break diff --git a/packages/protons/test/fixtures/optional.ts b/packages/protons/test/fixtures/optional.ts index 53dc250..0c80289 100644 --- a/packages/protons/test/fixtures/optional.ts +++ b/packages/protons/test/fixtures/optional.ts @@ -284,7 +284,9 @@ export namespace Optional { break } case 17: { - obj.subMessage = OptionalSubMessage.codec().decode(reader, reader.uint32()) + obj.subMessage = OptionalSubMessage.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.subMessage + }) break } default: { diff --git a/packages/protons/test/fixtures/peer.ts b/packages/protons/test/fixtures/peer.ts index 86e62e1..2628df4 100644 --- a/packages/protons/test/fixtures/peer.ts +++ b/packages/protons/test/fixtures/peer.ts @@ -78,7 +78,9 @@ export namespace Peer { throw new CodeError('decode error - map field "addresses" had too many elements', 'ERR_MAX_LENGTH') } - obj.addresses.push(Address.codec().decode(reader, reader.uint32())) + obj.addresses.push(Address.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.addresses$ + })) break } case 2: { @@ -94,7 +96,9 @@ export namespace Peer { throw new CodeError('decode error - map field "metadata" had too many elements', 'ERR_MAX_LENGTH') } - obj.metadata.push(Metadata.codec().decode(reader, reader.uint32())) + obj.metadata.push(Metadata.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.metadata$ + })) break } case 4: { diff --git a/packages/protons/test/fixtures/protons-options.proto b/packages/protons/test/fixtures/protons-options.proto index 21bfbe8..ed6536b 100644 --- a/packages/protons/test/fixtures/protons-options.proto +++ b/packages/protons/test/fixtures/protons-options.proto @@ -1,7 +1,5 @@ syntax = "proto3"; -import "protons.proto"; - message MessageWithSizeLimitedRepeatedField { repeated string repeatedField = 1 [(protons.options).limit = 1]; } diff --git a/packages/protons/test/fixtures/repeated.proto b/packages/protons/test/fixtures/repeated.proto index 79c1d54..9310f7a 100644 --- a/packages/protons/test/fixtures/repeated.proto +++ b/packages/protons/test/fixtures/repeated.proto @@ -1,11 +1,21 @@ syntax = "proto3"; +message SubSubMessage { + repeated string foo = 1; + optional uint32 nonRepeating = 2; +} + message SubMessage { - string foo = 1; + repeated string foo = 1; + optional uint32 nonRepeating = 2; + optional SubSubMessage message = 3; + repeated SubSubMessage messages = 4; } message RepeatedTypes { repeated uint32 number = 1; repeated uint32 limitedNumber = 2 [(protons.options).limit = 1]; - repeated SubMessage message = 3; + repeated SubMessage messages = 3; + optional SubMessage message = 4; + optional uint32 nonRepeating = 5; } diff --git a/packages/protons/test/fixtures/repeated.ts b/packages/protons/test/fixtures/repeated.ts index 3e29034..97489c9 100644 --- a/packages/protons/test/fixtures/repeated.ts +++ b/packages/protons/test/fixtures/repeated.ts @@ -7,8 +7,87 @@ import { type Codec, CodeError, decodeMessage, type DecodeOptions, encodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' +export interface SubSubMessage { + foo: string[] + nonRepeating?: number +} + +export namespace SubSubMessage { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.foo != null) { + for (const value of obj.foo) { + w.uint32(10) + w.string(value) + } + } + + if (obj.nonRepeating != null) { + w.uint32(16) + w.uint32(obj.nonRepeating) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length, opts = {}) => { + const obj: any = { + foo: [] + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: { + if (opts.limits?.foo != null && obj.foo.length === opts.limits.foo) { + throw new CodeError('decode error - map field "foo" had too many elements', 'ERR_MAX_LENGTH') + } + + obj.foo.push(reader.string()) + break + } + case 2: { + obj.nonRepeating = reader.uint32() + break + } + default: { + reader.skipType(tag & 7) + break + } + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, SubSubMessage.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions): SubSubMessage => { + return decodeMessage(buf, SubSubMessage.codec(), opts) + } +} + export interface SubMessage { - foo: string + foo: string[] + nonRepeating?: number + message?: SubSubMessage + messages: SubSubMessage[] } export namespace SubMessage { @@ -21,9 +100,28 @@ export namespace SubMessage { w.fork() } - if ((obj.foo != null && obj.foo !== '')) { - w.uint32(10) - w.string(obj.foo) + if (obj.foo != null) { + for (const value of obj.foo) { + w.uint32(10) + w.string(value) + } + } + + if (obj.nonRepeating != null) { + w.uint32(16) + w.uint32(obj.nonRepeating) + } + + if (obj.message != null) { + w.uint32(26) + SubSubMessage.codec().encode(obj.message, w) + } + + if (obj.messages != null) { + for (const value of obj.messages) { + w.uint32(34) + SubSubMessage.codec().encode(value, w) + } } if (opts.lengthDelimited !== false) { @@ -31,7 +129,8 @@ export namespace SubMessage { } }, (reader, length, opts = {}) => { const obj: any = { - foo: '' + foo: [], + messages: [] } const end = length == null ? reader.len : reader.pos + length @@ -41,7 +140,31 @@ export namespace SubMessage { switch (tag >>> 3) { case 1: { - obj.foo = reader.string() + if (opts.limits?.foo != null && obj.foo.length === opts.limits.foo) { + throw new CodeError('decode error - map field "foo" had too many elements', 'ERR_MAX_LENGTH') + } + + obj.foo.push(reader.string()) + break + } + case 2: { + obj.nonRepeating = reader.uint32() + break + } + case 3: { + obj.message = SubSubMessage.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.message + }) + break + } + case 4: { + if (opts.limits?.messages != null && obj.messages.length === opts.limits.messages) { + throw new CodeError('decode error - map field "messages" had too many elements', 'ERR_MAX_LENGTH') + } + + obj.messages.push(SubSubMessage.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.messages$ + })) break } default: { @@ -70,7 +193,9 @@ export namespace SubMessage { export interface RepeatedTypes { number: number[] limitedNumber: number[] - message: SubMessage[] + messages: SubMessage[] + message?: SubMessage + nonRepeating?: number } export namespace RepeatedTypes { @@ -97,13 +222,23 @@ export namespace RepeatedTypes { } } - if (obj.message != null) { - for (const value of obj.message) { + if (obj.messages != null) { + for (const value of obj.messages) { w.uint32(26) SubMessage.codec().encode(value, w) } } + if (obj.message != null) { + w.uint32(34) + SubMessage.codec().encode(obj.message, w) + } + + if (obj.nonRepeating != null) { + w.uint32(40) + w.uint32(obj.nonRepeating) + } + if (opts.lengthDelimited !== false) { w.ldelim() } @@ -111,7 +246,7 @@ export namespace RepeatedTypes { const obj: any = { number: [], limitedNumber: [], - message: [] + messages: [] } const end = length == null ? reader.len : reader.pos + length @@ -141,11 +276,23 @@ export namespace RepeatedTypes { break } case 3: { - if (opts.limits?.message != null && obj.message.length === opts.limits.message) { - throw new CodeError('decode error - map field "message" had too many elements', 'ERR_MAX_LENGTH') + if (opts.limits?.messages != null && obj.messages.length === opts.limits.messages) { + throw new CodeError('decode error - map field "messages" had too many elements', 'ERR_MAX_LENGTH') } - obj.message.push(SubMessage.codec().decode(reader, reader.uint32())) + obj.messages.push(SubMessage.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.messages$ + })) + break + } + case 4: { + obj.message = SubMessage.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.message + }) + break + } + case 5: { + obj.nonRepeating = reader.uint32() break } default: { diff --git a/packages/protons/test/fixtures/singular.ts b/packages/protons/test/fixtures/singular.ts index 91bdfb8..020cf8a 100644 --- a/packages/protons/test/fixtures/singular.ts +++ b/packages/protons/test/fixtures/singular.ts @@ -305,7 +305,9 @@ export namespace Singular { break } case 17: { - obj.subMessage = SingularSubMessage.codec().decode(reader, reader.uint32()) + obj.subMessage = SingularSubMessage.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.subMessage + }) break } default: { diff --git a/packages/protons/test/fixtures/test.ts b/packages/protons/test/fixtures/test.ts index 6cbee45..cd86ecd 100644 --- a/packages/protons/test/fixtures/test.ts +++ b/packages/protons/test/fixtures/test.ts @@ -268,7 +268,9 @@ export namespace AllTheTypes { break } case 13: { - obj.field13 = SubMessage.codec().decode(reader, reader.uint32()) + obj.field13 = SubMessage.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.field13 + }) break } case 14: { diff --git a/packages/protons/test/maps.spec.ts b/packages/protons/test/maps.spec.ts index 996a33f..707458c 100644 --- a/packages/protons/test/maps.spec.ts +++ b/packages/protons/test/maps.spec.ts @@ -3,7 +3,7 @@ import { expect } from 'aegir/chai' import Long from 'long' import protobufjs from 'protobufjs' -import { MapTypes, type SubMessage } from './fixtures/maps.js' +import { MapTypes, type SubMessage, type EnumValue } from './fixtures/maps.js' function longifyBigInts (obj: any): any { const output = { @@ -128,7 +128,8 @@ describe('maps', () => { stringMap: new Map(), intMap: new Map(), boolMap: new Map(), - messageMap: new Map() + messageMap: new Map(), + enumMap: new Map() } testEncodings(obj, MapTypes, './test/fixtures/maps.proto', 'MapTypes') @@ -139,7 +140,8 @@ describe('maps', () => { stringMap: new Map([['key', 'value']]), intMap: new Map(), // protobuf.js only supports strings as keys boolMap: new Map(), // protobuf.js only supports strings as keys - messageMap: new Map([['key', { foo: 'bar' }]]) + messageMap: new Map([['key', { foo: 'bar', bar: [] }]]), + enumMap: new Map() } testEncodings(obj, MapTypes, './test/fixtures/maps.proto', 'MapTypes') @@ -150,7 +152,8 @@ describe('maps', () => { stringMap: new Map([['key', 'value'], ['foo', 'bar']]), intMap: new Map(), boolMap: new Map(), - messageMap: new Map() + messageMap: new Map(), + enumMap: new Map() } const buf = MapTypes.encode(obj) @@ -160,4 +163,23 @@ describe('maps', () => { } })).to.throw(/too many elements/) }) + + it('should limit nested message collection sizes using runtime options', () => { + const obj: MapTypes = { + stringMap: new Map(), + intMap: new Map(), + boolMap: new Map(), + messageMap: new Map([['foo', { foo: 'hello', bar: [1, 2, 3, 4, 5] }]]), + enumMap: new Map() + } + + const buf = MapTypes.encode(obj) + expect(() => MapTypes.decode(buf, { + limits: { + messageMap$value: { + bar: 1 + } + } + })).to.throw(/too many elements/) + }) }) diff --git a/packages/protons/test/repeated.spec.ts b/packages/protons/test/repeated.spec.ts index 49bc43b..8527a75 100644 --- a/packages/protons/test/repeated.spec.ts +++ b/packages/protons/test/repeated.spec.ts @@ -8,7 +8,7 @@ describe('repeated', () => { const obj: RepeatedTypes = { number: [], limitedNumber: [], - message: [] + messages: [] } const buf = RepeatedTypes.encode(obj) @@ -19,7 +19,7 @@ describe('repeated', () => { const obj: RepeatedTypes = { number: [], limitedNumber: [1, 2], - message: [] + messages: [] } const buf = RepeatedTypes.encode(obj) @@ -30,7 +30,7 @@ describe('repeated', () => { const obj: RepeatedTypes = { number: [1, 2], limitedNumber: [], - message: [] + messages: [] } const buf = RepeatedTypes.encode(obj) @@ -40,4 +40,101 @@ describe('repeated', () => { } })).to.throw(/too many elements/) }) + + it('should limit repeating repeating fields using runtime options', () => { + const obj: RepeatedTypes = { + number: [], + limitedNumber: [], + messages: [{ + foo: ['one', 'two'], + nonRepeating: 0, + messages: [] + }], + nonRepeating: 5 + } + + const buf = RepeatedTypes.encode(obj) + expect(() => RepeatedTypes.decode(buf, { + limits: { + messages$: { + foo: 1 + } + } + })).to.throw(/too many elements/) + }) + + it('should limit repeating nested repeating fields using runtime options', () => { + const obj: RepeatedTypes = { + number: [], + limitedNumber: [], + messages: [{ + foo: [], + nonRepeating: 0, + messages: [{ + foo: ['one', 'two'], + nonRepeating: 0 + }] + }], + nonRepeating: 5 + } + + const buf = RepeatedTypes.encode(obj) + expect(() => RepeatedTypes.decode(buf, { + limits: { + messages$: { + messages$: { + foo: 1 + } + } + } + })).to.throw(/too many elements/) + }) + + it('should limit nested repeating fields using runtime options', () => { + const obj: RepeatedTypes = { + number: [], + limitedNumber: [], + messages: [], + nonRepeating: 5, + message: { + foo: ['one', 'two'], + messages: [] + } + } + + const buf = RepeatedTypes.encode(obj) + expect(() => RepeatedTypes.decode(buf, { + limits: { + message: { + foo: 1 + } + } + })).to.throw(/too many elements/) + }) + + it('should limit nested repeating nested repeating fields using runtime options', () => { + const obj: RepeatedTypes = { + number: [], + limitedNumber: [], + messages: [], + nonRepeating: 5, + message: { + foo: [], + messages: [{ + foo: ['one', 'two'] + }] + } + } + + const buf = RepeatedTypes.encode(obj) + expect(() => RepeatedTypes.decode(buf, { + limits: { + message: { + messages$: { + foo: 1 + } + } + } + })).to.throw(/too many elements/) + }) }) diff --git a/packages/protons/tsconfig.json b/packages/protons/tsconfig.json index 315999b..9f58dc9 100644 --- a/packages/protons/tsconfig.json +++ b/packages/protons/tsconfig.json @@ -10,10 +10,6 @@ "src", "test" ], - "exclude": [ - "test/fixtures/*.pbjs.ts", - "test/fixtures/*.protobuf.js" - ], "references": [ { "path": "../protons-runtime"