From 8c039c50c0085bd496d684ffc42b30ae786f4a52 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Mon, 23 Oct 2023 15:19:34 +0300 Subject: [PATCH] feat: add strict option to CLI (#119) Instead of throwing when encountering proto2 syntax, log a warning. Add a `--strict` option to turn this warning into an error. This is required to support [custom options](https://protobuf.dev/programming-guides/proto3/#customoptions) which can necessitate compiling built in proto2 definitions (e.g. `extend google.protobuf.FileOptions`). --- packages/protons/bin/protons.ts | 5 + packages/protons/src/index.ts | 94 +++++-- packages/protons/test/bad-fixtures/enum.proto | 6 + packages/protons/test/fixtures/basic.ts | 12 +- packages/protons/test/fixtures/bitswap.ts | 63 +++-- packages/protons/test/fixtures/circuit.ts | 24 +- packages/protons/test/fixtures/daemon.ts | 246 ++++++++++++------ packages/protons/test/fixtures/dht.ts | 51 ++-- packages/protons/test/fixtures/maps.ts | 45 ++-- packages/protons/test/fixtures/noise.ts | 15 +- packages/protons/test/fixtures/optional.ts | 63 +++-- packages/protons/test/fixtures/peer.ts | 36 ++- packages/protons/test/fixtures/proto2.proto | 5 + packages/protons/test/fixtures/proto2.ts | 69 +++++ packages/protons/test/fixtures/singular.ts | 63 +++-- packages/protons/test/fixtures/test.ts | 63 +++-- packages/protons/test/proto2.spec.ts | 26 ++ packages/protons/test/unsupported.spec.ts | 15 +- 18 files changed, 655 insertions(+), 246 deletions(-) create mode 100644 packages/protons/test/bad-fixtures/enum.proto create mode 100644 packages/protons/test/fixtures/proto2.proto create mode 100644 packages/protons/test/fixtures/proto2.ts create mode 100644 packages/protons/test/proto2.spec.ts diff --git a/packages/protons/bin/protons.ts b/packages/protons/bin/protons.ts index d624696..ebb215b 100644 --- a/packages/protons/bin/protons.ts +++ b/packages/protons/bin/protons.ts @@ -10,6 +10,7 @@ async function main (): Promise { Options --output, -o Path to a directory to write transpiled typescript files into + --strict, -s Causes parsing warnings to become errors Examples $ protons ./path/to/file.proto ./path/to/other/file.proto @@ -20,6 +21,10 @@ async function main (): Promise { output: { type: 'string', shortFlag: 'o' + }, + strict: { + type: 'boolean', + shortFlag: 's' } } }) diff --git a/packages/protons/src/index.ts b/packages/protons/src/index.ts index e64c3ba..6801293 100644 --- a/packages/protons/src/index.ts +++ b/packages/protons/src/index.ts @@ -19,6 +19,16 @@ function pathWithExtension (input: string, extension: string, outputDir?: string return path.join(output, path.basename(input).split('.').slice(0, -1).join('.') + extension) } +export class CodeError extends Error { + public code: string + + constructor (message: string, code: string, options?: ErrorOptions) { + super(message, options) + + this.code = code + } +} + const types: Record = { bool: 'boolean', bytes: 'Uint8Array', @@ -287,6 +297,13 @@ interface FieldDef { map: boolean valueType: string keyType: string + + /** + * Support proto2 required field. This field means a value should always be + * in the serialized buffer, any message without it should be considered + * invalid. It was removed for proto3. + */ + proto2Required: boolean } function defineFields (fields: Record, messageDef: MessageDef, moduleDef: ModuleDef): string[] { @@ -299,10 +316,22 @@ function defineFields (fields: Record, messageDef: MessageDef, }) } -function compileMessage (messageDef: MessageDef, moduleDef: ModuleDef): string { +function compileMessage (messageDef: MessageDef, moduleDef: ModuleDef, flags?: Flags): string { if (isEnumDef(messageDef)) { moduleDef.imports.add('enumeration') + // check that the enum def values start from 0 + if (Object.values(messageDef.values)[0] !== 0) { + const message = `enum ${messageDef.name} does not contain a value that maps to zero as it's first element, this is required in proto3 - see https://protobuf.dev/programming-guides/proto3/#enum` + + if (flags?.strict === true) { + throw new CodeError(message, 'ERR_PARSE_ERROR') + } else { + // eslint-disable-next-line no-console + console.info(`[WARN] ${message}`) + } + } + return ` export enum ${messageDef.name} { ${ @@ -332,7 +361,7 @@ export namespace ${messageDef.name} { if (messageDef.nested != null) { nested = '\n' nested += Object.values(messageDef.nested) - .map(def => compileMessage(def, moduleDef).trim()) + .map(def => compileMessage(def, moduleDef, flags).trim()) .join('\n\n') .split('\n') .map(line => line.trim() === '' ? '' : ` ${line}`) @@ -391,13 +420,25 @@ export interface ${messageDef.name} { if (fieldDef.map) { valueTest = `obj.${name} != null && obj.${name}.size !== 0` - } else if (!fieldDef.optional && !fieldDef.repeated) { + } else if (!fieldDef.optional && !fieldDef.repeated && !fieldDef.proto2Required) { // proto3 singular fields should only be written out if they are not the default value if (defaultValueTestGenerators[type] != null) { valueTest = `${defaultValueTestGenerators[type](`obj.${name}`)}` } else if (type === 'enum') { // handle enums - valueTest = `obj.${name} != null && __${fieldDef.type}Values[obj.${name}] !== 0` + const def = findDef(fieldDef.type, messageDef, moduleDef) + + if (!isEnumDef(def)) { + throw new Error(`${fieldDef.type} was not enum def`) + } + + valueTest = `obj.${name} != null` + + // singular enums default to 0, but enums can be defined without a 0 + // value which is against the proto3 spec but is tolerated + if (Object.values(def.values)[0] === 0) { + valueTest += ` && __${fieldDef.type}Values[obj.${name}] !== 0` + } } } @@ -496,14 +537,16 @@ export interface ${messageDef.name} { break }` } else if (fieldDef.repeated) { - return `case ${fieldDef.id}: + return `case ${fieldDef.id}: { obj.${fieldName}.push(${parseValue}) - break` + break + }` } - return `case ${fieldDef.id}: + return `case ${fieldDef.id}: { obj.${fieldName} = ${parseValue} - break` + break + }` } return createReadField(fieldName, fieldDef) @@ -532,9 +575,10 @@ ${encodeFields === '' ? '' : `${encodeFields}\n`} const tag = reader.uint32() switch (tag >>> 3) {${decodeFields === '' ? '' : `\n ${decodeFields}`} - default: + default: { reader.skipType(tag & 7) break + } } } @@ -570,7 +614,7 @@ interface ModuleDef { globals: Record } -function defineModule (def: ClassDef): ModuleDef { +function defineModule (def: ClassDef, flags: Flags): ModuleDef { const moduleDef: ModuleDef = { imports: new Set(), importedTypes: new Set(), @@ -582,10 +626,10 @@ function defineModule (def: ClassDef): ModuleDef { const defs = def.nested if (defs == null) { - throw new Error('No top-level messages found in protobuf') + throw new CodeError('No top-level messages found in protobuf', 'ERR_NO_MESSAGES_FOUND') } - function defineMessage (defs: Record, parent?: ClassDef): void { + function defineMessage (defs: Record, parent?: ClassDef, flags?: Flags): void { for (const className of Object.keys(defs)) { const classDef = defs[className] @@ -603,9 +647,19 @@ function defineModule (def: ClassDef): ModuleDef { fieldDef.repeated = fieldDef.rule === 'repeated' fieldDef.optional = !fieldDef.repeated && fieldDef.options?.proto3_optional === true fieldDef.map = fieldDef.keyType != null + fieldDef.proto2Required = false if (fieldDef.rule === 'required') { - throw new Error('"required" fields are not allowed in proto3 - please convert your proto2 definitions to proto3') + const message = `field "${name}" is required, this is not allowed in proto3. Please convert your proto2 definitions to proto3 - see https://github.com/ipfs/protons/wiki/Required-fields-and-protobuf-3` + + if (flags?.strict === true) { + throw new CodeError(message, 'ERR_PARSE_ERROR') + } else { + fieldDef.proto2Required = true + + // eslint-disable-next-line no-console + console.info(`[WARN] ${message}`) + } } } } @@ -644,7 +698,7 @@ function defineModule (def: ClassDef): ModuleDef { } } - defineMessage(defs) + defineMessage(defs, undefined, flags) // set enum/message fields now all messages have been defined updateTypes(defs) @@ -652,14 +706,22 @@ function defineModule (def: ClassDef): ModuleDef { for (const className of Object.keys(defs)) { const classDef = defs[className] - moduleDef.compiled.push(compileMessage(classDef, moduleDef)) + moduleDef.compiled.push(compileMessage(classDef, moduleDef, flags)) } return moduleDef } interface Flags { + /** + * Specifies an output directory + */ output?: string + + /** + * If true, warnings will be thrown as errors + */ + strict?: boolean } export async function generate (source: string, flags: Flags): Promise { @@ -701,7 +763,7 @@ export async function generate (source: string, flags: Flags): Promise { } } - const moduleDef = defineModule(def) + const moduleDef = defineModule(def, flags) const ignores = [ '/* eslint-disable import/export */', diff --git a/packages/protons/test/bad-fixtures/enum.proto b/packages/protons/test/bad-fixtures/enum.proto new file mode 100644 index 0000000..f0808fc --- /dev/null +++ b/packages/protons/test/bad-fixtures/enum.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; + +enum AnEnum { + // enum values should start from 0 + value1 = 1; +} diff --git a/packages/protons/test/fixtures/basic.ts b/packages/protons/test/fixtures/basic.ts index 3cb1f7f..7a902bf 100644 --- a/packages/protons/test/fixtures/basic.ts +++ b/packages/protons/test/fixtures/basic.ts @@ -47,15 +47,18 @@ export namespace Basic { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.foo = reader.string() break - case 2: + } + case 2: { obj.num = reader.int32() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -99,9 +102,10 @@ export namespace Empty { const tag = reader.uint32() switch (tag >>> 3) { - default: + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/bitswap.ts b/packages/protons/test/fixtures/bitswap.ts index 4ec6946..e8687bb 100644 --- a/packages/protons/test/fixtures/bitswap.ts +++ b/packages/protons/test/fixtures/bitswap.ts @@ -99,24 +99,30 @@ export namespace Message { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.block = reader.bytes() break - case 2: + } + case 2: { obj.priority = reader.int32() break - case 3: + } + case 3: { obj.cancel = reader.bool() break - case 4: + } + case 4: { obj.wantType = Message.Wantlist.WantType.codec().decode(reader) break - case 5: + } + case 5: { obj.sendDontHave = reader.bool() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -172,15 +178,18 @@ export namespace Message { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.entries.push(Message.Wantlist.Entry.codec().decode(reader, reader.uint32())) break - case 2: + } + case 2: { obj.full = reader.bool() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -240,15 +249,18 @@ export namespace Message { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.prefix = reader.bytes() break - case 2: + } + case 2: { obj.data = reader.bytes() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -324,15 +336,18 @@ export namespace Message { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.cid = reader.bytes() break - case 2: + } + case 2: { obj.type = Message.BlockPresenceType.codec().decode(reader) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -409,24 +424,30 @@ export namespace Message { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.wantlist = Message.Wantlist.codec().decode(reader, reader.uint32()) break - case 2: + } + case 2: { obj.blocks.push(reader.bytes()) break - case 3: + } + case 3: { obj.payload.push(Message.Block.codec().decode(reader, reader.uint32())) break - case 4: + } + case 4: { obj.blockPresences.push(Message.BlockPresence.codec().decode(reader, reader.uint32())) break - case 5: + } + case 5: { obj.pendingBytes = reader.int32() break - default: + } + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/circuit.ts b/packages/protons/test/fixtures/circuit.ts index a32487a..046c06f 100644 --- a/packages/protons/test/fixtures/circuit.ts +++ b/packages/protons/test/fixtures/circuit.ts @@ -122,15 +122,18 @@ export namespace CircuitRelay { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.id = reader.bytes() break - case 2: + } + case 2: { obj.addrs.push(reader.bytes()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -191,21 +194,26 @@ export namespace CircuitRelay { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.type = CircuitRelay.Type.codec().decode(reader) break - case 2: + } + case 2: { obj.srcPeer = CircuitRelay.Peer.codec().decode(reader, reader.uint32()) break - case 3: + } + case 3: { obj.dstPeer = CircuitRelay.Peer.codec().decode(reader, reader.uint32()) break - case 4: + } + case 4: { obj.code = CircuitRelay.Status.codec().decode(reader) break - default: + } + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/daemon.ts b/packages/protons/test/fixtures/daemon.ts index 185ad47..a8a2108 100644 --- a/packages/protons/test/fixtures/daemon.ts +++ b/packages/protons/test/fixtures/daemon.ts @@ -121,36 +121,46 @@ export namespace Request { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.type = Request.Type.codec().decode(reader) break - case 2: + } + case 2: { obj.connect = ConnectRequest.codec().decode(reader, reader.uint32()) break - case 3: + } + case 3: { obj.streamOpen = StreamOpenRequest.codec().decode(reader, reader.uint32()) break - case 4: + } + case 4: { obj.streamHandler = StreamHandlerRequest.codec().decode(reader, reader.uint32()) break - case 5: + } + case 5: { obj.dht = DHTRequest.codec().decode(reader, reader.uint32()) break - case 6: + } + case 6: { obj.connManager = ConnManagerRequest.codec().decode(reader, reader.uint32()) break - case 7: + } + case 7: { obj.disconnect = DisconnectRequest.codec().decode(reader, reader.uint32()) break - case 8: + } + case 8: { obj.pubsub = PSRequest.codec().decode(reader, reader.uint32()) break - case 9: + } + case 9: { obj.peerStore = PeerstoreRequest.codec().decode(reader, reader.uint32()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -264,33 +274,42 @@ export namespace Response { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.type = Response.Type.codec().decode(reader) break - case 2: + } + case 2: { obj.error = ErrorResponse.codec().decode(reader, reader.uint32()) break - case 3: + } + case 3: { obj.streamInfo = StreamInfo.codec().decode(reader, reader.uint32()) break - case 4: + } + case 4: { obj.identify = IdentifyResponse.codec().decode(reader, reader.uint32()) break - case 5: + } + case 5: { obj.dht = DHTResponse.codec().decode(reader, reader.uint32()) break - case 6: + } + case 6: { obj.peers.push(PeerInfo.codec().decode(reader, reader.uint32())) break - case 7: + } + case 7: { obj.pubsub = PSResponse.codec().decode(reader, reader.uint32()) break - case 8: + } + case 8: { obj.peerStore = PeerstoreResponse.codec().decode(reader, reader.uint32()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -352,15 +371,18 @@ export namespace IdentifyResponse { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.id = reader.bytes() break - case 2: + } + case 2: { obj.addrs.push(reader.bytes()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -428,18 +450,22 @@ export namespace ConnectRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.peer = reader.bytes() break - case 2: + } + case 2: { obj.addrs.push(reader.bytes()) break - case 3: + } + case 3: { obj.timeout = reader.int64() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -507,18 +533,22 @@ export namespace StreamOpenRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.peer = reader.bytes() break - case 2: + } + case 2: { obj.proto.push(reader.string()) break - case 3: + } + case 3: { obj.timeout = reader.int64() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -580,15 +610,18 @@ export namespace StreamHandlerRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.addr = reader.bytes() break - case 2: + } + case 2: { obj.proto.push(reader.string()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -641,12 +674,14 @@ export namespace ErrorResponse { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.msg = reader.string() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -713,18 +748,22 @@ export namespace StreamInfo { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.peer = reader.bytes() break - case 2: + } + case 2: { obj.addr = reader.bytes() break - case 3: + } + case 3: { obj.proto = reader.string() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -843,30 +882,38 @@ export namespace DHTRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.type = DHTRequest.Type.codec().decode(reader) break - case 2: + } + case 2: { obj.peer = reader.bytes() break - case 3: + } + case 3: { obj.cid = reader.bytes() break - case 4: + } + case 4: { obj.key = reader.bytes() break - case 5: + } + case 5: { obj.value = reader.bytes() break - case 6: + } + case 6: { obj.count = reader.int32() break - case 7: + } + case 7: { obj.timeout = reader.int64() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -949,18 +996,22 @@ export namespace DHTResponse { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.type = DHTResponse.Type.codec().decode(reader) break - case 2: + } + case 2: { obj.peer = PeerInfo.codec().decode(reader, reader.uint32()) break - case 3: + } + case 3: { obj.value = reader.bytes() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -1022,15 +1073,18 @@ export namespace PeerInfo { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.id = reader.bytes() break - case 2: + } + case 2: { obj.addrs.push(reader.bytes()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -1119,21 +1173,26 @@ export namespace ConnManagerRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.type = ConnManagerRequest.Type.codec().decode(reader) break - case 2: + } + case 2: { obj.peer = reader.bytes() break - case 3: + } + case 3: { obj.tag = reader.string() break - case 4: + } + case 4: { obj.weight = reader.int64() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -1186,12 +1245,14 @@ export namespace DisconnectRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.peer = reader.bytes() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -1276,18 +1337,22 @@ export namespace PSRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.type = PSRequest.Type.codec().decode(reader) break - case 2: + } + case 2: { obj.topic = reader.string() break - case 3: + } + case 3: { obj.data = reader.bytes() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -1372,27 +1437,34 @@ export namespace PSMessage { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.from = reader.bytes() break - case 2: + } + case 2: { obj.data = reader.bytes() break - case 3: + } + case 3: { obj.seqno = reader.bytes() break - case 4: + } + case 4: { obj.topicIDs.push(reader.string()) break - case 5: + } + case 5: { obj.signature = reader.bytes() break - case 6: + } + case 6: { obj.key = reader.bytes() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -1456,15 +1528,18 @@ export namespace PSResponse { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.topics.push(reader.string()) break - case 2: + } + case 2: { obj.peerIDs.push(reader.bytes()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -1550,18 +1625,22 @@ export namespace PeerstoreRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.type = PeerstoreRequest.Type.codec().decode(reader) break - case 2: + } + case 2: { obj.id = reader.bytes() break - case 3: + } + case 3: { obj.protos.push(reader.string()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -1622,15 +1701,18 @@ export namespace PeerstoreResponse { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.peer = PeerInfo.codec().decode(reader, reader.uint32()) break - case 2: + } + case 2: { obj.protos.push(reader.string()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/dht.ts b/packages/protons/test/fixtures/dht.ts index af91930..abc18f9 100644 --- a/packages/protons/test/fixtures/dht.ts +++ b/packages/protons/test/fixtures/dht.ts @@ -63,24 +63,30 @@ export namespace Record { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.key = reader.bytes() break - case 2: + } + case 2: { obj.value = reader.bytes() break - case 3: + } + case 3: { obj.author = reader.bytes() break - case 4: + } + case 4: { obj.signature = reader.bytes() break - case 5: + } + case 5: { obj.timeReceived = reader.string() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -201,18 +207,22 @@ export namespace Message { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.id = reader.bytes() break - case 2: + } + case 2: { obj.addrs.push(reader.bytes()) break - case 3: + } + case 3: { obj.connection = Message.ConnectionType.codec().decode(reader) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -290,27 +300,34 @@ export namespace Message { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.type = Message.MessageType.codec().decode(reader) break - case 10: + } + case 10: { obj.clusterLevelRaw = reader.int32() break - case 2: + } + case 2: { obj.key = reader.bytes() break - case 3: + } + case 3: { obj.record = reader.bytes() break - case 8: + } + case 8: { obj.closerPeers.push(Message.Peer.codec().decode(reader, reader.uint32())) break - case 9: + } + case 9: { obj.providerPeers.push(Message.Peer.codec().decode(reader, reader.uint32())) break - default: + } + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/maps.ts b/packages/protons/test/fixtures/maps.ts index ad52943..8f8bb31 100644 --- a/packages/protons/test/fixtures/maps.ts +++ b/packages/protons/test/fixtures/maps.ts @@ -41,12 +41,14 @@ export namespace SubMessage { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.foo = reader.string() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -114,15 +116,18 @@ export namespace MapTypes { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.key = reader.string() break - case 2: + } + case 2: { obj.value = reader.string() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -182,15 +187,18 @@ export namespace MapTypes { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.key = reader.int32() break - case 2: + } + case 2: { obj.value = reader.int32() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -250,15 +258,18 @@ export namespace MapTypes { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.key = reader.bool() break - case 2: + } + case 2: { obj.value = reader.bool() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -317,15 +328,18 @@ export namespace MapTypes { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.key = reader.string() break - case 2: + } + case 2: { obj.value = SubMessage.codec().decode(reader, reader.uint32()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -419,9 +433,10 @@ export namespace MapTypes { obj.messageMap.set(entry.key, entry.value) break } - default: + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/noise.ts b/packages/protons/test/fixtures/noise.ts index 3e3699c..a9ab7c1 100644 --- a/packages/protons/test/fixtures/noise.ts +++ b/packages/protons/test/fixtures/noise.ts @@ -58,18 +58,22 @@ export namespace pb { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.identityKey = reader.bytes() break - case 2: + } + case 2: { obj.identitySig = reader.bytes() break - case 3: + } + case 3: { obj.data = reader.bytes() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -110,9 +114,10 @@ export namespace pb { const tag = reader.uint32() switch (tag >>> 3) { - default: + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/optional.ts b/packages/protons/test/fixtures/optional.ts index 8de9b5a..646b3bf 100644 --- a/packages/protons/test/fixtures/optional.ts +++ b/packages/protons/test/fixtures/optional.ts @@ -62,15 +62,18 @@ export namespace OptionalSubMessage { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.foo = reader.string() break - case 2: + } + case 2: { obj.bar = reader.int32() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -217,60 +220,78 @@ export namespace Optional { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.double = reader.double() break - case 2: + } + case 2: { obj.float = reader.float() break - case 3: + } + case 3: { obj.int32 = reader.int32() break - case 4: + } + case 4: { obj.int64 = reader.int64() break - case 5: + } + case 5: { obj.uint32 = reader.uint32() break - case 6: + } + case 6: { obj.uint64 = reader.uint64() break - case 7: + } + case 7: { obj.sint32 = reader.sint32() break - case 8: + } + case 8: { obj.sint64 = reader.sint64() break - case 9: + } + case 9: { obj.fixed32 = reader.fixed32() break - case 10: + } + case 10: { obj.fixed64 = reader.fixed64() break - case 11: + } + case 11: { obj.sfixed32 = reader.sfixed32() break - case 12: + } + case 12: { obj.sfixed64 = reader.sfixed64() break - case 13: + } + case 13: { obj.bool = reader.bool() break - case 14: + } + case 14: { obj.string = reader.string() break - case 15: + } + case 15: { obj.bytes = reader.bytes() break - case 16: + } + case 16: { obj.enum = OptionalEnum.codec().decode(reader) break - case 17: + } + case 17: { obj.subMessage = OptionalSubMessage.codec().decode(reader, reader.uint32()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/peer.ts b/packages/protons/test/fixtures/peer.ts index ac612d1..2954b2b 100644 --- a/packages/protons/test/fixtures/peer.ts +++ b/packages/protons/test/fixtures/peer.ts @@ -73,24 +73,30 @@ export namespace Peer { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.addresses.push(Address.codec().decode(reader, reader.uint32())) break - case 2: + } + case 2: { obj.protocols.push(reader.string()) break - case 3: + } + case 3: { obj.metadata.push(Metadata.codec().decode(reader, reader.uint32())) break - case 4: + } + case 4: { obj.pubKey = reader.bytes() break - case 5: + } + case 5: { obj.peerRecordEnvelope = reader.bytes() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -149,15 +155,18 @@ export namespace Address { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.multiaddr = reader.bytes() break - case 2: + } + case 2: { obj.isCertified = reader.bool() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -217,15 +226,18 @@ export namespace Metadata { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.key = reader.string() break - case 2: + } + case 2: { obj.value = reader.bytes() break - default: + } + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/proto2.proto b/packages/protons/test/fixtures/proto2.proto new file mode 100644 index 0000000..6583810 --- /dev/null +++ b/packages/protons/test/fixtures/proto2.proto @@ -0,0 +1,5 @@ +syntax = "proto2"; + +message MessageWithRequired { + required int32 scalarField = 1; +} diff --git a/packages/protons/test/fixtures/proto2.ts b/packages/protons/test/fixtures/proto2.ts new file mode 100644 index 0000000..6a6f242 --- /dev/null +++ b/packages/protons/test/fixtures/proto2.ts @@ -0,0 +1,69 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface MessageWithRequired { + scalarField: number +} + +export namespace MessageWithRequired { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.scalarField != null) { + w.uint32(8) + w.int32(obj.scalarField) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + scalarField: 0 + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: { + obj.scalarField = reader.int32() + break + } + default: { + reader.skipType(tag & 7) + break + } + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, MessageWithRequired.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): MessageWithRequired => { + return decodeMessage(buf, MessageWithRequired.codec()) + } +} diff --git a/packages/protons/test/fixtures/singular.ts b/packages/protons/test/fixtures/singular.ts index e3e4d90..bf4920a 100644 --- a/packages/protons/test/fixtures/singular.ts +++ b/packages/protons/test/fixtures/singular.ts @@ -65,15 +65,18 @@ export namespace SingularSubMessage { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.foo = reader.string() break - case 2: + } + case 2: { obj.bar = reader.int32() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -237,60 +240,78 @@ export namespace Singular { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.double = reader.double() break - case 2: + } + case 2: { obj.float = reader.float() break - case 3: + } + case 3: { obj.int32 = reader.int32() break - case 4: + } + case 4: { obj.int64 = reader.int64() break - case 5: + } + case 5: { obj.uint32 = reader.uint32() break - case 6: + } + case 6: { obj.uint64 = reader.uint64() break - case 7: + } + case 7: { obj.sint32 = reader.sint32() break - case 8: + } + case 8: { obj.sint64 = reader.sint64() break - case 9: + } + case 9: { obj.fixed32 = reader.fixed32() break - case 10: + } + case 10: { obj.fixed64 = reader.fixed64() break - case 11: + } + case 11: { obj.sfixed32 = reader.sfixed32() break - case 12: + } + case 12: { obj.sfixed64 = reader.sfixed64() break - case 13: + } + case 13: { obj.bool = reader.bool() break - case 14: + } + case 14: { obj.string = reader.string() break - case 15: + } + case 15: { obj.bytes = reader.bytes() break - case 16: + } + case 16: { obj.enum = SingularEnum.codec().decode(reader) break - case 17: + } + case 17: { obj.subMessage = SingularSubMessage.codec().decode(reader, reader.uint32()) break - default: + } + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/fixtures/test.ts b/packages/protons/test/fixtures/test.ts index 9a30e94..0ca31c4 100644 --- a/packages/protons/test/fixtures/test.ts +++ b/packages/protons/test/fixtures/test.ts @@ -56,12 +56,14 @@ export namespace SubMessage { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.foo = reader.string() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -218,63 +220,82 @@ export namespace AllTheTypes { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.field1 = reader.bool() break - case 2: + } + case 2: { obj.field2 = reader.int32() break - case 3: + } + case 3: { obj.field3 = reader.int64() break - case 4: + } + case 4: { obj.field4 = reader.uint32() break - case 5: + } + case 5: { obj.field5 = reader.uint64() break - case 6: + } + case 6: { obj.field6 = reader.sint32() break - case 7: + } + case 7: { obj.field7 = reader.sint64() break - case 8: + } + case 8: { obj.field8 = reader.double() break - case 9: + } + case 9: { obj.field9 = reader.float() break - case 10: + } + case 10: { obj.field10 = reader.string() break - case 11: + } + case 11: { obj.field11 = reader.bytes() break - case 12: + } + case 12: { obj.field12 = AnEnum.codec().decode(reader) break - case 13: + } + case 13: { obj.field13 = SubMessage.codec().decode(reader, reader.uint32()) break - case 14: + } + case 14: { obj.field14.push(reader.string()) break - case 15: + } + case 15: { obj.field15 = reader.fixed32() break - case 16: + } + case 16: { obj.field16 = reader.fixed64() break - case 17: + } + case 17: { obj.field17 = reader.sfixed32() break - case 18: + } + case 18: { obj.field18 = reader.sfixed64() break - default: + } + default: { reader.skipType(tag & 7) break + } } } diff --git a/packages/protons/test/proto2.spec.ts b/packages/protons/test/proto2.spec.ts new file mode 100644 index 0000000..3397a75 --- /dev/null +++ b/packages/protons/test/proto2.spec.ts @@ -0,0 +1,26 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { MessageWithRequired } from './fixtures/proto2.js' + +describe('proto2 support', () => { + it('should write a required field with a default value', () => { + const obj: MessageWithRequired = { + scalarField: 0 + } + + const buf = MessageWithRequired.encode(obj) + + expect(buf).to.equalBytes([8, 0]) + }) + + it('should write a required field with a non-default value', () => { + const obj: MessageWithRequired = { + scalarField: 5 + } + + const buf = MessageWithRequired.encode(obj) + + expect(buf).to.equalBytes([8, 5]) + }) +}) diff --git a/packages/protons/test/unsupported.spec.ts b/packages/protons/test/unsupported.spec.ts index 7f29154..c9862f0 100644 --- a/packages/protons/test/unsupported.spec.ts +++ b/packages/protons/test/unsupported.spec.ts @@ -5,12 +5,21 @@ import { generate } from '../src/index.js' describe('unsupported', () => { it('should refuse to generate source from proto2 definition', async () => { - await expect(generate('test/bad-fixtures/proto2.proto', {})).to.eventually.be.rejected - .with.property('message').that.contain('"required" fields are not allowed in proto3') + await expect(generate('test/bad-fixtures/proto2.proto', { + strict: true + })).to.eventually.be.rejected + .with.property('code', 'ERR_PARSE_ERROR') + }) + + it('should refuse to generate source from enum definition that does not start from 0', async () => { + await expect(generate('test/bad-fixtures/enum.proto', { + strict: true + })).to.eventually.be.rejected + .with.property('code', 'ERR_PARSE_ERROR') }) it('should refuse to generate source from empty definition', async () => { await expect(generate('test/bad-fixtures/empty.proto', {})).to.eventually.be.rejected - .with.property('message').that.contain('No top-level messages found in protobuf') + .with.property('code', 'ERR_NO_MESSAGES_FOUND') }) })