Skip to content

Commit 3758e63

Browse files
committed
feat: add custom protons options for limiting list/map sizes
Adds the capability to define protons-specific custom options that control decoding behaviour initially around limiting the sizes of maps and lists. ```protobuf // import the options definition - it will work without this but some // code editor plugins may report an error if the def can't be found import "protons-rutime/options.proto"; message MessageWithSizeLimitedRepeatedField { // define the size limit - here more than 10 repeated items will // cause decoding to fail repeated string repeatedField = 1 [(protons.limit) = 10]; } ``` The defintion is shipped with the `protons-runtime` module. There is a [pending PR](protocolbuffers/protobuf#14505) to reserve the `1186` field ID. This should be merged first and/or the field ID updated here if it changes due to that PR. Fixes #113
1 parent 47a4dcb commit 3758e63

File tree

7 files changed

+306
-2
lines changed

7 files changed

+306
-2
lines changed
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
syntax = "proto3";
2+
3+
import "google/protobuf/descriptor.proto";
4+
5+
package protons;
6+
7+
message ProtonsOptions {
8+
// limit the number of repeated fields or map entries that will be decoded
9+
optional int32 limit = 1;
10+
}
11+
12+
// custom options available for use by protons
13+
extend google.protobuf.FieldOptions {
14+
optional ProtonsOptions protons_options = 1186;
15+
}

packages/protons-runtime/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"type": "module",
1515
"types": "./dist/src/index.d.ts",
1616
"files": [
17+
"options.proto",
1718
"src",
1819
"dist",
1920
"!dist/test",

packages/protons-runtime/src/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,13 @@ export interface Reader {
326326
*/
327327
sfixed64String(): string
328328
}
329+
330+
export class CodeError extends Error {
331+
public code: string
332+
333+
constructor (message: string, code: string, options?: ErrorOptions) {
334+
super(message, options)
335+
336+
this.code = code
337+
}
338+
}

packages/protons/src/index.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ interface FieldDef {
430430
rule: string
431431
optional: boolean
432432
repeated: boolean
433+
lengthLimit?: number
433434
message: boolean
434435
enum: boolean
435436
map: boolean
@@ -685,13 +686,37 @@ export interface ${messageDef.name} {
685686
const parseValue = `${decoderGenerators[type] == null ? `${codec}.decode(reader${type === 'message' ? ', reader.uint32()' : ''})` : decoderGenerators[type](jsTypeOverride)}`
686687

687688
if (fieldDef.map) {
688-
return `case ${fieldDef.id}: {
689+
let limit = ''
690+
691+
if (fieldDef.lengthLimit != null) {
692+
moduleDef.imports.add('CodeError')
693+
694+
limit = `
695+
if (obj.${fieldName}.size === ${fieldDef.lengthLimit}) {
696+
throw new CodeError('decode error - map field "${fieldName}" had too many elements', 'ERR_MAX_SIZE')
697+
}
698+
`
699+
}
700+
701+
return `case ${fieldDef.id}: {${limit}
689702
const entry = ${parseValue}
690703
obj.${fieldName}.set(entry.key, entry.value)
691704
break
692705
}`
693706
} else if (fieldDef.repeated) {
694-
return `case ${fieldDef.id}: {
707+
let limit = ''
708+
709+
if (fieldDef.lengthLimit != null) {
710+
moduleDef.imports.add('CodeError')
711+
712+
limit = `
713+
if (obj.${fieldName}.length === ${fieldDef.lengthLimit}) {
714+
throw new CodeError('decode error - repeated field "${fieldName}" had too many elements', 'ERR_MAX_LENGTH')
715+
}
716+
`
717+
}
718+
719+
return `case ${fieldDef.id}: {${limit}
695720
obj.${fieldName}.push(${parseValue})
696721
break
697722
}`
@@ -801,6 +826,7 @@ function defineModule (def: ClassDef, flags: Flags): ModuleDef {
801826
fieldDef.repeated = fieldDef.rule === 'repeated'
802827
fieldDef.optional = !fieldDef.repeated && fieldDef.options?.proto3_optional === true
803828
fieldDef.map = fieldDef.keyType != null
829+
fieldDef.lengthLimit = fieldDef.options?.['(protons.limit)']
804830
fieldDef.proto2Required = false
805831

806832
if (fieldDef.rule === 'required') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
syntax = "proto3";
2+
3+
import "protons-rutime/options.proto";
4+
5+
message MessageWithSizeLimitedRepeatedField {
6+
repeated string repeatedField = 1 [(protons.limit) = 1];
7+
}
8+
9+
message MessageWithSizeLimitedMap {
10+
map<string, string> mapField = 1 [(protons.limit) = 1];
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/* eslint-disable import/export */
2+
/* eslint-disable complexity */
3+
/* eslint-disable @typescript-eslint/no-namespace */
4+
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
5+
/* eslint-disable @typescript-eslint/no-empty-interface */
6+
7+
import { encodeMessage, decodeMessage, message, CodeError } from 'protons-runtime'
8+
import type { Codec } from 'protons-runtime'
9+
import type { Uint8ArrayList } from 'uint8arraylist'
10+
11+
export interface MessageWithSizeLimitedRepeatedField {
12+
repeatedField: string[]
13+
}
14+
15+
export namespace MessageWithSizeLimitedRepeatedField {
16+
let _codec: Codec<MessageWithSizeLimitedRepeatedField>
17+
18+
export const codec = (): Codec<MessageWithSizeLimitedRepeatedField> => {
19+
if (_codec == null) {
20+
_codec = message<MessageWithSizeLimitedRepeatedField>((obj, w, opts = {}) => {
21+
if (opts.lengthDelimited !== false) {
22+
w.fork()
23+
}
24+
25+
if (obj.repeatedField != null) {
26+
for (const value of obj.repeatedField) {
27+
w.uint32(10)
28+
w.string(value)
29+
}
30+
}
31+
32+
if (opts.lengthDelimited !== false) {
33+
w.ldelim()
34+
}
35+
}, (reader, length) => {
36+
const obj: any = {
37+
repeatedField: []
38+
}
39+
40+
const end = length == null ? reader.len : reader.pos + length
41+
42+
while (reader.pos < end) {
43+
const tag = reader.uint32()
44+
45+
switch (tag >>> 3) {
46+
case 1: {
47+
if (obj.repeatedField.length === 1) {
48+
throw new CodeError('decode error - repeated field "repeatedField" had too many elements', 'ERR_MAX_LENGTH')
49+
}
50+
51+
obj.repeatedField.push(reader.string())
52+
break
53+
}
54+
default: {
55+
reader.skipType(tag & 7)
56+
break
57+
}
58+
}
59+
}
60+
61+
return obj
62+
})
63+
}
64+
65+
return _codec
66+
}
67+
68+
export const encode = (obj: Partial<MessageWithSizeLimitedRepeatedField>): Uint8Array => {
69+
return encodeMessage(obj, MessageWithSizeLimitedRepeatedField.codec())
70+
}
71+
72+
export const decode = (buf: Uint8Array | Uint8ArrayList): MessageWithSizeLimitedRepeatedField => {
73+
return decodeMessage(buf, MessageWithSizeLimitedRepeatedField.codec())
74+
}
75+
}
76+
77+
export interface MessageWithSizeLimitedMap {
78+
mapField: Map<string, string>
79+
}
80+
81+
export namespace MessageWithSizeLimitedMap {
82+
export interface MessageWithSizeLimitedMap$mapFieldEntry {
83+
key: string
84+
value: string
85+
}
86+
87+
export namespace MessageWithSizeLimitedMap$mapFieldEntry {
88+
let _codec: Codec<MessageWithSizeLimitedMap$mapFieldEntry>
89+
90+
export const codec = (): Codec<MessageWithSizeLimitedMap$mapFieldEntry> => {
91+
if (_codec == null) {
92+
_codec = message<MessageWithSizeLimitedMap$mapFieldEntry>((obj, w, opts = {}) => {
93+
if (opts.lengthDelimited !== false) {
94+
w.fork()
95+
}
96+
97+
if ((obj.key != null && obj.key !== '')) {
98+
w.uint32(10)
99+
w.string(obj.key)
100+
}
101+
102+
if ((obj.value != null && obj.value !== '')) {
103+
w.uint32(18)
104+
w.string(obj.value)
105+
}
106+
107+
if (opts.lengthDelimited !== false) {
108+
w.ldelim()
109+
}
110+
}, (reader, length) => {
111+
const obj: any = {
112+
key: '',
113+
value: ''
114+
}
115+
116+
const end = length == null ? reader.len : reader.pos + length
117+
118+
while (reader.pos < end) {
119+
const tag = reader.uint32()
120+
121+
switch (tag >>> 3) {
122+
case 1: {
123+
obj.key = reader.string()
124+
break
125+
}
126+
case 2: {
127+
obj.value = reader.string()
128+
break
129+
}
130+
default: {
131+
reader.skipType(tag & 7)
132+
break
133+
}
134+
}
135+
}
136+
137+
return obj
138+
})
139+
}
140+
141+
return _codec
142+
}
143+
144+
export const encode = (obj: Partial<MessageWithSizeLimitedMap$mapFieldEntry>): Uint8Array => {
145+
return encodeMessage(obj, MessageWithSizeLimitedMap$mapFieldEntry.codec())
146+
}
147+
148+
export const decode = (buf: Uint8Array | Uint8ArrayList): MessageWithSizeLimitedMap$mapFieldEntry => {
149+
return decodeMessage(buf, MessageWithSizeLimitedMap$mapFieldEntry.codec())
150+
}
151+
}
152+
153+
let _codec: Codec<MessageWithSizeLimitedMap>
154+
155+
export const codec = (): Codec<MessageWithSizeLimitedMap> => {
156+
if (_codec == null) {
157+
_codec = message<MessageWithSizeLimitedMap>((obj, w, opts = {}) => {
158+
if (opts.lengthDelimited !== false) {
159+
w.fork()
160+
}
161+
162+
if (obj.mapField != null && obj.mapField.size !== 0) {
163+
for (const [key, value] of obj.mapField.entries()) {
164+
w.uint32(10)
165+
MessageWithSizeLimitedMap.MessageWithSizeLimitedMap$mapFieldEntry.codec().encode({ key, value }, w)
166+
}
167+
}
168+
169+
if (opts.lengthDelimited !== false) {
170+
w.ldelim()
171+
}
172+
}, (reader, length) => {
173+
const obj: any = {
174+
mapField: new Map<string, string>()
175+
}
176+
177+
const end = length == null ? reader.len : reader.pos + length
178+
179+
while (reader.pos < end) {
180+
const tag = reader.uint32()
181+
182+
switch (tag >>> 3) {
183+
case 1: {
184+
if (obj.mapField.size === 1) {
185+
throw new CodeError('decode error - map field "mapField" had too many elements', 'ERR_MAX_SIZE')
186+
}
187+
188+
const entry = MessageWithSizeLimitedMap.MessageWithSizeLimitedMap$mapFieldEntry.codec().decode(reader, reader.uint32())
189+
obj.mapField.set(entry.key, entry.value)
190+
break
191+
}
192+
default: {
193+
reader.skipType(tag & 7)
194+
break
195+
}
196+
}
197+
}
198+
199+
return obj
200+
})
201+
}
202+
203+
return _codec
204+
}
205+
206+
export const encode = (obj: Partial<MessageWithSizeLimitedMap>): Uint8Array => {
207+
return encodeMessage(obj, MessageWithSizeLimitedMap.codec())
208+
}
209+
210+
export const decode = (buf: Uint8Array | Uint8ArrayList): MessageWithSizeLimitedMap => {
211+
return decodeMessage(buf, MessageWithSizeLimitedMap.codec())
212+
}
213+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-env mocha */
2+
3+
import { expect } from 'aegir/chai'
4+
import { MessageWithSizeLimitedMap, MessageWithSizeLimitedRepeatedField } from './fixtures/protons-options.js'
5+
6+
describe('protons options', () => {
7+
it('should not decode message with map that is too big', () => {
8+
const obj: MessageWithSizeLimitedMap = {
9+
mapField: new Map<string, string>([['one', 'two'], ['three', 'four']])
10+
}
11+
12+
const buf = MessageWithSizeLimitedMap.encode(obj)
13+
14+
expect(() => MessageWithSizeLimitedMap.decode(buf))
15+
.to.throw().with.property('code', 'ERR_MAX_SIZE')
16+
})
17+
18+
it('should not decode message with list that is too big', () => {
19+
const obj: MessageWithSizeLimitedRepeatedField = {
20+
repeatedField: ['0', '1']
21+
}
22+
23+
const buf = MessageWithSizeLimitedRepeatedField.encode(obj)
24+
25+
expect(() => MessageWithSizeLimitedRepeatedField.decode(buf))
26+
.to.throw().with.property('code', 'ERR_MAX_LENGTH')
27+
})
28+
})

0 commit comments

Comments
 (0)