From dd18e529939078b89867297b181a584e8b2cc7da Mon Sep 17 00:00:00 2001 From: n1474335 Date: Wed, 18 Aug 2021 17:22:09 +0100 Subject: [PATCH] Protobuf operations improved to enable full and partial schema support --- package-lock.json | 87 +++++++- package.json | 1 + src/core/config/Categories.json | 1 + src/core/lib/Protobuf.mjs | 297 ++++++++++++++++++++++++- src/core/operations/ProtobufDecode.mjs | 26 ++- src/core/operations/ProtobufEncode.mjs | 54 +++++ tests/operations/tests/Protobuf.mjs | 276 ++++++++++++++++++++++- 7 files changed, 723 insertions(+), 19 deletions(-) mode change 100755 => 100644 src/core/config/Categories.json create mode 100644 src/core/operations/ProtobufEncode.mjs diff --git a/package-lock.json b/package-lock.json index 45e9ce6f..c822614a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3084,6 +3084,60 @@ "integrity": "sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==", "dev": true }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, "@testim/chrome-version": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.0.7.tgz", @@ -3144,6 +3198,11 @@ "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", "dev": true }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -3153,8 +3212,7 @@ "@types/node": { "version": "14.14.22", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", - "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==", - "dev": true + "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==" }, "@types/parse-json": { "version": "4.0.0", @@ -10075,6 +10133,11 @@ "loglevel": "^1.4.0" } }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -12228,6 +12291,26 @@ "winston": "2.x" } }, + "protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", diff --git a/package.json b/package.json index 79c7ddca..d8a596f0 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "path": "^0.12.7", "popper.js": "^1.16.1", "process": "^0.11.10", + "protobufjs": "^6.11.2", "qr-image": "^3.2.0", "scryptsy": "^2.1.0", "snackbarjs": "^1.1.0", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json old mode 100755 new mode 100644 index d04ccb6b..09ee8d15 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -191,6 +191,7 @@ "URL Encode", "URL Decode", "Protobuf Decode", + "Protobuf Encode", "VarInt Encode", "VarInt Decode", "JA3 Fingerprint", diff --git a/src/core/lib/Protobuf.mjs b/src/core/lib/Protobuf.mjs index 0cdf41f2..4f3609ef 100644 --- a/src/core/lib/Protobuf.mjs +++ b/src/core/lib/Protobuf.mjs @@ -1,4 +1,5 @@ import Utils from "../Utils.mjs"; +import protobuf from "protobufjs"; /** * Protobuf lib. Contains functions to decode protobuf serialised @@ -32,9 +33,10 @@ class Protobuf { this.MSB = 0x80; this.VALUE = 0x7f; - // Declare offset and length + // Declare offset, length, and field type object this.offset = 0; this.LENGTH = data.length; + this.fieldTypes = {}; } // Public Functions @@ -76,15 +78,281 @@ class Protobuf { return pb._varInt(); } + /** + * Encode input JSON according to the given schema + * + * @param {Object} input + * @param {Object []} args + * @returns {Object} + */ + static encode(input, args) { + this.updateProtoRoot(args[0]); + if (!this.mainMessageName) { + throw new Error("Schema Error: Schema not defined"); + } + const message = this.parsedProto.root.nested[this.mainMessageName]; + + // Convert input into instance of message, and verify instance + input = message.fromObject(input); + const error = message.verify(input); + if (error) { + throw new Error("Input Error: " + error); + } + // Encode input + const output = message.encode(input).finish(); + return new Uint8Array(output).buffer; + } + /** * Parse Protobuf data * * @param {byteArray} input * @returns {Object} */ - static decode(input) { + static decode(input, args) { + this.updateProtoRoot(args[0]); + this.showUnknownFields = args[1]; + this.showTypes = args[2]; + return this.mergeDecodes(input); + } + + /** + * Update the parsedProto, throw parsing errors + * + * @param {string} protoText + */ + static updateProtoRoot(protoText) { + try { + this.parsedProto = protobuf.parse(protoText); + if (this.parsedProto.package) { + this.parsedProto.root = this.parsedProto.root.nested[this.parsedProto.package]; + } + this.updateMainMessageName(); + } catch (error) { + throw new Error("Schema " + error); + } + } + + /** + * Set mainMessageName to the first instance of a message defined in the schema that is not a submessage + * + */ + static updateMainMessageName() { + const messageNames = []; + const fieldTypes = []; + this.parsedProto.root.nestedArray.forEach(block => { + if (block.constructor.name === "Type") { + messageNames.push(block.name); + this.parsedProto.root.nested[block.name].fieldsArray.forEach(field => { + fieldTypes.push(field.type); + }); + } + }); + + if (messageNames.length === 0) { + this.mainMessageName = null; + } else { + for (const name of messageNames) { + if (!fieldTypes.includes(name)) { + this.mainMessageName = name; + break; + } + } + this.mainMessageName = messageNames[0]; + } + } + + /** + * Decode input using Protobufjs package and raw methods, compare, and merge results + * + * @param {byteArray} input + * @returns {Object} + */ + static mergeDecodes(input) { const pb = new Protobuf(input); - return pb._parse(); + let rawDecode = pb._parse(); + let message; + + if (this.showTypes) { + rawDecode = this.showRawTypes(rawDecode, pb.fieldTypes); + this.parsedProto.root = this.appendTypesToFieldNames(this.parsedProto.root); + } + + try { + message = this.parsedProto.root.nested[this.mainMessageName]; + const packageDecode = message.toObject(message.decode(input), { + bytes: String, + longs: Number, + enums: String, + defualts: true + }); + const output = {}; + + if (this.showUnknownFields) { + output[message.name] = packageDecode; + output["Unknown Fields"] = this.compareFields(rawDecode, message); + return output; + } else { + return packageDecode; + } + + } catch (error) { + if (message) { + throw new Error("Input " + error); + } else { + return rawDecode; + } + } + } + + /** + * Replace fieldnames with fieldname and type + * + * @param {Object} schemaRoot + * @returns {Object} + */ + static appendTypesToFieldNames(schemaRoot) { + for (const block of schemaRoot.nestedArray) { + if (block.constructor.name === "Type") { + for (const [fieldName, fieldData] of Object.entries(block.fields)) { + schemaRoot.nested[block.name].remove(block.fields[fieldName]); + schemaRoot.nested[block.name].add(new protobuf.Field(`${fieldName} (${fieldData.type})`, fieldData.id, fieldData.type, fieldData.rule)); + } + } + } + return schemaRoot; + } + + /** + * Add field type to field name for fields in the raw decoded output + * + * @param {Object} rawDecode + * @param {Object} fieldTypes + * @returns {Object} + */ + static showRawTypes(rawDecode, fieldTypes) { + for (const [fieldNum, value] of Object.entries(rawDecode)) { + const fieldType = fieldTypes[fieldNum]; + let outputFieldValue; + let outputFieldType; + + // Submessages + if (isNaN(fieldType)) { + outputFieldType = 2; + + // Repeated submessages + if (Array.isArray(value)) { + const fieldInstances = []; + for (const instance of Object.keys(value)) { + if (typeof(value[instance]) !== "string") { + fieldInstances.push(this.showRawTypes(value[instance], fieldType)); + } else { + fieldInstances.push(value[instance]); + } + } + outputFieldValue = fieldInstances; + + // Single submessage + } else { + outputFieldValue = this.showRawTypes(value, fieldType); + } + + // Non-submessage field + } else { + outputFieldType = fieldType; + outputFieldValue = value; + } + + // Substitute fieldNum with field number and type + rawDecode[`field #${fieldNum}: ${this.getTypeInfo(outputFieldType)}`] = outputFieldValue; + delete rawDecode[fieldNum]; + } + return rawDecode; + } + + /** + * Compare raw decode to package decode and return discrepancies + * + * @param rawDecodedMessage + * @param schemaMessage + * @returns {Object} + */ + static compareFields(rawDecodedMessage, schemaMessage) { + // Define message data using raw decode output and schema + const schemaFieldProperties = {}; + const schemaFieldNames = Object.keys(schemaMessage.fields); + schemaFieldNames.forEach(field => schemaFieldProperties[schemaMessage.fields[field].id] = field); + + // Loop over each field present in the raw decode output + for (const fieldName in rawDecodedMessage) { + let fieldId; + if (isNaN(fieldName)) { + fieldId = fieldName.match(/^field #(\d+)/)[1]; + } else { + fieldId = fieldName; + } + + // Check if this field is defined in the schema + if (fieldId in schemaFieldProperties) { + const schemaFieldName = schemaFieldProperties[fieldId]; + + // Extract the current field data from the raw decode and schema + const rawFieldData = rawDecodedMessage[fieldName]; + const schemaField = schemaMessage.fields[schemaFieldName]; + + // Check for repeated fields + if (Array.isArray(rawFieldData) && !schemaField.repeated) { + rawDecodedMessage[`(${schemaMessage.name}) ${schemaFieldName} is a repeated field`] = rawFieldData; + } + + // Check for submessage fields + if (schemaField.resolvedType !== null && schemaField.resolvedType.constructor.name === "Type") { + const subMessageType = schemaMessage.fields[schemaFieldName].type; + const schemaSubMessage = this.parsedProto.root.nested[subMessageType]; + const rawSubMessages = rawDecodedMessage[fieldName]; + let rawDecodedSubMessage = {}; + + // Squash multiple submessage instances into one submessage + if (Array.isArray(rawSubMessages)) { + rawSubMessages.forEach(subMessageInstance => { + const instanceFields = Object.entries(subMessageInstance); + instanceFields.forEach(subField => { + rawDecodedSubMessage[subField[0]] = subField[1]; + }); + }); + } else { + rawDecodedSubMessage = rawSubMessages; + } + + // Treat submessage as own message and compare its fields + rawDecodedSubMessage = Protobuf.compareFields(rawDecodedSubMessage, schemaSubMessage); + if (Object.entries(rawDecodedSubMessage).length !== 0) { + rawDecodedMessage[`${schemaFieldName} (${subMessageType}) has missing fields`] = rawDecodedSubMessage; + } + } + delete rawDecodedMessage[fieldName]; + } + } + return rawDecodedMessage; + } + + /** + * Returns wiretype information for input wiretype number + * + * @param {number} wireType + * @returns {string} + */ + static getTypeInfo(wireType) { + switch (wireType) { + case 0: + return "VarInt (e.g. int32, bool)"; + case 1: + return "64-Bit (e.g. fixed64, double)"; + case 2: + return "L-delim (e.g. string, message)"; + case 5: + return "32-Bit (e.g. fixed32, float)"; + } } // Private Class Functions @@ -143,6 +411,11 @@ class Protobuf { const header = this._fieldHeader(); const type = header.type; const key = header.key; + + if (typeof(this.fieldTypes[key]) !== "object") { + this.fieldTypes[key] = type; + } + switch (type) { // varint case 0: @@ -152,7 +425,7 @@ class Protobuf { return { "key": key, "value": this._uint64() }; // length delimited case 2: - return { "key": key, "value": this._lenDelim() }; + return { "key": key, "value": this._lenDelim(key) }; // fixed 32 case 5: return { "key": key, "value": this._uint32() }; @@ -237,10 +510,10 @@ class Protobuf { * @returns {number} */ _uint64() { - // Read off a Uint64 - let num = this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++]; - num = num * 0x100000000 + this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++]; - return num; + // Read off a Uint64 with little-endian + const lowerHalf = this.data[this.offset++] + (this.data[this.offset++] * 0x100) + (this.data[this.offset++] * 0x10000) + this.data[this.offset++] * 0x1000000; + const upperHalf = this.data[this.offset++] + (this.data[this.offset++] * 0x100) + (this.data[this.offset++] * 0x10000) + this.data[this.offset++] * 0x1000000; + return upperHalf * 0x100000000 + lowerHalf; } /** @@ -249,7 +522,7 @@ class Protobuf { * @private * @returns {Object|string} */ - _lenDelim() { + _lenDelim(fieldNum) { // Read off the field length const length = this._varInt(); const fieldBytes = this.data.slice(this.offset, this.offset + length); @@ -258,6 +531,10 @@ class Protobuf { // Attempt to parse as a new Protobuf Object const pbObject = new Protobuf(fieldBytes); field = pbObject._parse(); + + // Set field types object + this.fieldTypes[fieldNum] = {...this.fieldTypes[fieldNum], ...pbObject.fieldTypes}; + } catch (err) { // Otherwise treat as bytes field = Utils.byteArrayToChars(fieldBytes); @@ -276,7 +553,7 @@ class Protobuf { _uint32() { // Use a dataview to read off the integer const dataview = new DataView(new Uint8Array(this.data.slice(this.offset, this.offset + 4)).buffer); - const value = dataview.getUint32(0); + const value = dataview.getUint32(0, true); this.offset += 4; return value; } diff --git a/src/core/operations/ProtobufDecode.mjs b/src/core/operations/ProtobufDecode.mjs index 8470bdb7..fbc16dc4 100644 --- a/src/core/operations/ProtobufDecode.mjs +++ b/src/core/operations/ProtobufDecode.mjs @@ -20,12 +20,30 @@ class ProtobufDecode extends Operation { super(); this.name = "Protobuf Decode"; - this.module = "Default"; - this.description = "Decodes any Protobuf encoded data to a JSON representation of the data using the field number as the field key."; + this.module = "Protobuf"; + this.description = "Decodes any Protobuf encoded data to a JSON representation of the data using the field number as the field key.

If a .proto schema is defined, the encoded data will be decoded with reference to the schema. Only one message instance will be decoded.

Show Unknown Fields
When a schema is used, this option shows fields that are present in the input data but not defined in the schema.

Show Types
Show the type of a field next to its name. For undefined fields, the wiretype and example types are shown instead."; this.infoURL = "https://wikipedia.org/wiki/Protocol_Buffers"; this.inputType = "ArrayBuffer"; this.outputType = "JSON"; - this.args = []; + this.args = [ + { + name: "Schema (.proto text)", + type: "text", + value: "", + rows: 8, + hint: "Drag and drop is enabled on this ingredient" + }, + { + name: "Show Unknown Fields", + type: "boolean", + value: false + }, + { + name: "Show Types", + type: "boolean", + value: false + } + ]; } /** @@ -36,7 +54,7 @@ class ProtobufDecode extends Operation { run(input, args) { input = new Uint8Array(input); try { - return Protobuf.decode(input); + return Protobuf.decode(input, args); } catch (err) { throw new OperationError(err); } diff --git a/src/core/operations/ProtobufEncode.mjs b/src/core/operations/ProtobufEncode.mjs new file mode 100644 index 00000000..eaf4d6c4 --- /dev/null +++ b/src/core/operations/ProtobufEncode.mjs @@ -0,0 +1,54 @@ +/** + * @author GCHQ Contributor [3] + * @copyright Crown Copyright 2021 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import Protobuf from "../lib/Protobuf.mjs"; + +/** + * Protobuf Encode operation + */ +class ProtobufEncode extends Operation { + + /** + * ProtobufEncode constructor + */ + constructor() { + super(); + + this.name = "Protobuf Encode"; + this.module = "Protobuf"; + this.description = "Encodes a valid JSON object into a protobuf byte array using the input .proto schema."; + this.infoURL = "https://developers.google.com/protocol-buffers/docs/encoding"; + this.inputType = "JSON"; + this.outputType = "ArrayBuffer"; + this.args = [ + { + name: "Schema (.proto text)", + type: "text", + value: "", + rows: 8, + hint: "Drag and drop is enabled on this ingredient" + } + ]; + } + + /** + * @param {Object} input + * @param {Object[]} args + * @returns {ArrayBuffer} + */ + run(input, args) { + try { + return Protobuf.encode(input, args); + } catch (error) { + throw new OperationError(error); + } + } + +} + +export default ProtobufEncode; diff --git a/tests/operations/tests/Protobuf.mjs b/tests/operations/tests/Protobuf.mjs index 0bdd6b19..17adfd88 100644 --- a/tests/operations/tests/Protobuf.mjs +++ b/tests/operations/tests/Protobuf.mjs @@ -10,10 +10,10 @@ import TestRegister from "../../lib/TestRegister.mjs"; TestRegister.addTests([ { - name: "Protobuf Decode", + name: "Protobuf Decode: no schema", input: "0d1c0000001203596f751a024d65202b2a0a0a066162633132331200", expectedOutput: JSON.stringify({ - "1": 469762048, + "1": 28, "2": "You", "3": "Me", "4": 43, @@ -29,7 +29,277 @@ TestRegister.addTests([ }, { "op": "Protobuf Decode", - "args": [] + "args": ["", false, false] + } + ] + }, + { + name: "Protobuf Decode: partial schema, no unknown fields", + input: "0d1c0000001203596f751a024d65202b2a0a0a066162633132331200", + expectedOutput: JSON.stringify({ + "Apple": [ + 28 + ], + "Banana": "You", + "Carrot": [ + "Me" + ] + }, null, 4), + recipeConfig: [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "Protobuf Decode", + "args": [ + `message Test { + repeated fixed32 Apple = 1; + optional string Banana = 2; + repeated string Carrot = 3; + }`, + false, + false + ] + } + ] + }, + { + name: "Protobuf Decode: partial schema, show unknown fields", + input: "0d1c0000001203596f751a024d65202b2a0a0a066162633132331200", + expectedOutput: JSON.stringify({ + "Test": { + "Apple": [ + 28 + ], + "Banana": "You", + "Carrot": [ + "Me" + ] + }, + "Unknown Fields": { + "4": 43, + "5": { + "1": "abc123", + "2": {} + } + } + }, null, 4), + recipeConfig: [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "Protobuf Decode", + "args": [ + `message Test { + repeated fixed32 Apple = 1; + optional string Banana = 2; + repeated string Carrot = 3; + }`, + true, + false + ] + } + ] + }, + { + name: "Protobuf Decode: full schema, no unknown fields", + input: "0d1c0000001203596f751a024d65202b2a0a0a06616263313233120031ff00000000000000", + expectedOutput: JSON.stringify({ + "Apple": [ + 28 + ], + "Banana": "You", + "Carrot": [ + "Me" + ], + "Date": 43, + "Elderberry": { + "Fig": "abc123", + "Grape": {} + }, + "Huckleberry": 255 + }, null, 4), + recipeConfig: [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "Protobuf Decode", + "args": [ + `message Test { + repeated fixed32 Apple = 1; + optional string Banana = 2; + repeated string Carrot = 3; + optional int32 Date = 4; + optional subTest Elderberry = 5; + optional fixed64 Huckleberry = 6; + } + message subTest { + optional string Fig = 1; + optional subSubTest Grape = 2; + } + message subSubTest {}`, + false, + false + ] + } + ] + }, + { + name: "Protobuf Decode: partial schema, show unknown fields, show types", + input: "0d1c0000001203596f751a024d65202b2a0a0a06616263313233120031ba32a96cc10200003801", + expectedOutput: JSON.stringify({ + "Test": { + "Banana (string)": "You", + "Carrot (string)": [ + "Me" + ], + "Date (int32)": 43, + "Imbe (Options)": "Option1" + }, + "Unknown Fields": { + "field #1: 32-Bit (e.g. fixed32, float)": 28, + "field #5: L-delim (e.g. string, message)": { + "field #1: L-delim (e.g. string, message)": "abc123", + "field #2: L-delim (e.g. string, message)": {} + }, + "field #6: 64-Bit (e.g. fixed64, double)": 3029774971578 + } + }, null, 4), + recipeConfig: [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "Protobuf Decode", + "args": [ + `message Test { + optional string Banana = 2; + repeated string Carrot = 3; + optional int32 Date = 4; + optional Options Imbe = 7; + } + message subTest { + optional string Fig = 1; + optional subSubTest Grape = 2; + } + message subSubTest {} + enum Options { + Option0 = 0; + Option1 = 1; + Option2 = 2; + }`, + true, + true + ] + } + ] + }, + { + name: "Protobuf Encode", + input: JSON.stringify({ + "Apple": [ + 28 + ], + "Banana": "You", + "Carrot": [ + "Me" + ], + "Date": 43, + "Elderberry": { + "Fig": "abc123", + "Grape": {} + }, + "Huckleberry": [3029774971578], + "Imbe": 1 + }, null, 4), + expectedOutput: "0d1c0000001203596f751a024d65202b2a0a0a06616263313233120031ba32a96cc10200003801", + recipeConfig: [ + { + "op": "Protobuf Encode", + "args": [ + `message Test { + repeated fixed32 Apple = 1; + optional string Banana = 2; + repeated string Carrot = 3; + optional int32 Date = 4; + optional subTest Elderberry = 5; + repeated fixed64 Huckleberry = 6; + optional Options Imbe = 7; + } + message subTest { + optional string Fig = 1; + optional subSubTest Grape = 2; + } + message subSubTest {} + enum Options { + Option0 = 0; + Option1 = 1; + Option2 = 2; + }` + ] + }, + { + "op": "To Hex", + "args": [ + "None", + 0 + ] + } + ] + }, + { + name: "Protobuf Encode: incomplete schema", + input: JSON.stringify({ + "Apple": [ + 28 + ], + "Banana": "You", + "Carrot": [ + "Me" + ], + "Date": 43, + "Elderberry": { + "Fig": "abc123", + "Grape": {} + }, + "Huckleberry": [3029774971578], + "Imbe": 1 + }, null, 4), + expectedOutput: "1203596f75202b2a0a0a06616263313233120031ba32a96cc1020000", + recipeConfig: [ + { + "op": "Protobuf Encode", + "args": [ + `message Test { + optional string Banana = 2; + optional int32 Date = 4; + optional subTest Elderberry = 5; + repeated fixed64 Huckleberry = 6; + } + message subTest { + optional string Fig = 1; + optional subSubTest Grape = 2; + } + message subSubTest {} + enum Options { + Option0 = 0; + Option1 = 1; + Option2 = 2; + }` + ] + }, + { + "op": "To Hex", + "args": [ + "None", + 0 + ] } ] },