From c9c4dacf5c67bd50e9cde50348fd54a887dc88fd Mon Sep 17 00:00:00 2001 From: Hendrik Vorwerk Date: Fri, 2 Apr 2021 11:39:49 +0200 Subject: [PATCH] very basic implementation of protobuf decoding --- .../Sidebar/ValueRenderer/Protobuf.tsx | 162 ++++++++++++++++++ .../Sidebar/ValueRenderer/ValueRenderer.tsx | 14 ++ backend/src/Model/Base64Message.ts | 8 + package.json | 4 +- yarn.lock | 8 +- 5 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 app/src/components/Sidebar/ValueRenderer/Protobuf.tsx diff --git a/app/src/components/Sidebar/ValueRenderer/Protobuf.tsx b/app/src/components/Sidebar/ValueRenderer/Protobuf.tsx new file mode 100644 index 00000000..8ffd9124 --- /dev/null +++ b/app/src/components/Sidebar/ValueRenderer/Protobuf.tsx @@ -0,0 +1,162 @@ +interface Field { + key: number, + value: any +} + +class Protobuf { + TYPE: number; + NUMBER: number; + MSB: number; + VALUE: number; + offset: number; + LENGTH: number; + data: (Int8Array | Uint8Array); + + constructor(data: (Int8Array | Uint8Array)) { + this.data = data; + + // Set up masks + this.TYPE = 0x07; + this.NUMBER = 0x78; + this.MSB = 0x80; + this.VALUE = 0x7f; + + // Declare offset and length + this.offset = 0; + this.LENGTH = data.length; + } + + + static decode(input: (Int8Array | Uint8Array)) { + const pb = new Protobuf(input); + return pb._parse(); + } + + _parse() { + let object = {}; + // Continue reading whilst we still have data + while (this.offset < this.LENGTH) { + const field = this._parseField(); + object = this._addField(field, object); + } + // Throw an error if we have gone beyond the end of the data + if (this.offset > this.LENGTH) { + throw new Error("Exhausted Buffer"); + } + return object; + } + + _addField(field: Field, object: any) { + // Get the field key/values + const key = field.key; + const value = field.value; + object[key] = Object.prototype.hasOwnProperty.call(object, key) ? + object[key] instanceof Array ? + object[key].concat([value]) : + [object[key], value] : + value; + return object; + } + + _parseField() { + // Get the field headers + const header = this._fieldHeader(); + const type = header.type; + const key = header.key; + switch (type) { + // varint + case 0: + return { "key": key, "value": this._varInt() }; + // fixed 64 + case 1: + return { "key": key, "value": this._uint64() }; + // length delimited + case 2: + return { "key": key, "value": this._lenDelim() }; + // fixed 32 + case 5: + return { "key": key, "value": this._uint32() }; + // unknown type + default: + throw new Error("Unknown type 0x" + type.toString(16)); + } + } + + _fieldHeader() { + // Make sure we call type then number to preserve offset + return { "type": this._fieldType(), "key": this._fieldNumber() }; + } + + _fieldType() { + // Field type stored in lower 3 bits of tag byte + return this.data[this.offset] & this.TYPE; + } + + _fieldNumber() { + let shift = -3; + let fieldNumber = 0; + do { + fieldNumber += shift < 28 ? + shift === -3 ? + (this.data[this.offset] & this.NUMBER) >> -shift : + (this.data[this.offset] & this.VALUE) << shift : + (this.data[this.offset] & this.VALUE) * Math.pow(2, shift); + shift += 7; + } while ((this.data[this.offset++] & this.MSB) === this.MSB); + return fieldNumber; + } + + _varInt() { + let value = 0; + let shift = 0; + // Keep reading while upper bit set + do { + value += shift < 28 ? + (this.data[this.offset] & this.VALUE) << shift : + (this.data[this.offset] & this.VALUE) * Math.pow(2, shift); + shift += 7; + } while ((this.data[this.offset++] & this.MSB) === this.MSB); + return value; + } + _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; + } + _lenDelim() { + // Read off the field length + const length = this._varInt(); + const fieldBytes = this.data.slice(this.offset, this.offset + length); + let field; + try { + // Attempt to parse as a new Protobuf Object + const pbObject = new Protobuf(fieldBytes); + field = pbObject._parse(); + } catch (err) { + // Otherwise treat as bytes + field = this._byteArrayToChars(fieldBytes); + } + // Move the offset and return the field + this.offset += length; + return field; + } + _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); + this.offset += 4; + return value; + } + _byteArrayToChars(byteArray: (Int8Array | Uint8Array)) { + if (!byteArray) return ""; + let str = ""; + // String concatenation appears to be faster than an array join + for (let i = 0; i < byteArray.length;) { + str += String.fromCharCode(byteArray[i++]); + } + return str; + } +} + +export default Protobuf; \ No newline at end of file diff --git a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx index 3a4f9ffc..e0b054d3 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx @@ -7,6 +7,7 @@ import { connect } from 'react-redux' import { default as ReactResizeDetector } from 'react-resize-detector' import { ValueRendererDisplayMode } from '../../../reducers/Settings' import { Typography, Fade, Grow } from '@material-ui/core' +import Protobuf from './Protobuf'; interface Props { message: q.Message @@ -43,6 +44,19 @@ class ValueRenderer extends React.Component { return [undefined, undefined] } + let obj = {} + try { + const byteArray = Base64Message.ToByteArray(msg); + obj = Protobuf.decode(byteArray); + } + catch (e) { + console.log("Caught exception while decoding protobuf: ", e); + } + + if (obj) { + return [JSON.stringify(obj, undefined, ' '), 'json']; + } + const str = Base64Message.toUnicodeString(msg) try { JSON.parse(str) diff --git a/backend/src/Model/Base64Message.ts b/backend/src/Model/Base64Message.ts index b3763fed..80f8318e 100644 --- a/backend/src/Model/Base64Message.ts +++ b/backend/src/Model/Base64Message.ts @@ -12,10 +12,18 @@ export class Base64Message { this.length = base64Str.length } + public static toBase64(message: Base64Message) { + return message.base64Message || '' + } + public static toUnicodeString(message: Base64Message) { return message.unicodeValue || '' } + public static ToByteArray(message: Base64Message): Uint8Array { + return Base64.toUint8Array(message.base64Message) + } + public static fromBuffer(buffer: Buffer) { return new Base64Message(buffer.toString('base64')) } diff --git a/package.json b/package.json index 7153f80d..74ca7f9b 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist", "electron-updater": "^4.0.6", "fs-extra": "9", - "js-base64": "^2.5.1", + "js-base64": "^2.6.3", "json-to-ast": "^2.1.0", "lowdb": "^1.0.0", "mime": "^2.4.4", @@ -119,4 +119,4 @@ "yarn-run-all": "^3.1.1" }, "optionalDependencies": {} -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 00446f08..46b4e57e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3026,10 +3026,10 @@ jake@^10.6.1: filelist "^1.0.1" minimatch "^3.0.4" -js-base64@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209" - integrity sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ== +js-base64@^2.6.3: + version "2.6.4" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" + integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== js-tokens@^4.0.0: version "4.0.0"