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
+ ]
}
]
},