diff --git a/app/src/components/Sidebar/ValueRenderer/Protobuf.tsx b/app/src/components/Sidebar/ValueRenderer/Protobuf.tsx new file mode 100644 index 00000000..daa7d213 --- /dev/null +++ b/app/src/components/Sidebar/ValueRenderer/Protobuf.tsx @@ -0,0 +1,172 @@ +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 diff --git a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx index 3a4f9ffc..ec91af67 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,18 @@ 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"