Skip to content

Commit c9c4dac

Browse files
very basic implementation of protobuf decoding
1 parent 9cdfa2d commit c9c4dac

File tree

5 files changed

+190
-6
lines changed

5 files changed

+190
-6
lines changed
+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
interface Field {
2+
key: number,
3+
value: any
4+
}
5+
6+
class Protobuf {
7+
TYPE: number;
8+
NUMBER: number;
9+
MSB: number;
10+
VALUE: number;
11+
offset: number;
12+
LENGTH: number;
13+
data: (Int8Array | Uint8Array);
14+
15+
constructor(data: (Int8Array | Uint8Array)) {
16+
this.data = data;
17+
18+
// Set up masks
19+
this.TYPE = 0x07;
20+
this.NUMBER = 0x78;
21+
this.MSB = 0x80;
22+
this.VALUE = 0x7f;
23+
24+
// Declare offset and length
25+
this.offset = 0;
26+
this.LENGTH = data.length;
27+
}
28+
29+
30+
static decode(input: (Int8Array | Uint8Array)) {
31+
const pb = new Protobuf(input);
32+
return pb._parse();
33+
}
34+
35+
_parse() {
36+
let object = {};
37+
// Continue reading whilst we still have data
38+
while (this.offset < this.LENGTH) {
39+
const field = this._parseField();
40+
object = this._addField(field, object);
41+
}
42+
// Throw an error if we have gone beyond the end of the data
43+
if (this.offset > this.LENGTH) {
44+
throw new Error("Exhausted Buffer");
45+
}
46+
return object;
47+
}
48+
49+
_addField(field: Field, object: any) {
50+
// Get the field key/values
51+
const key = field.key;
52+
const value = field.value;
53+
object[key] = Object.prototype.hasOwnProperty.call(object, key) ?
54+
object[key] instanceof Array ?
55+
object[key].concat([value]) :
56+
[object[key], value] :
57+
value;
58+
return object;
59+
}
60+
61+
_parseField() {
62+
// Get the field headers
63+
const header = this._fieldHeader();
64+
const type = header.type;
65+
const key = header.key;
66+
switch (type) {
67+
// varint
68+
case 0:
69+
return { "key": key, "value": this._varInt() };
70+
// fixed 64
71+
case 1:
72+
return { "key": key, "value": this._uint64() };
73+
// length delimited
74+
case 2:
75+
return { "key": key, "value": this._lenDelim() };
76+
// fixed 32
77+
case 5:
78+
return { "key": key, "value": this._uint32() };
79+
// unknown type
80+
default:
81+
throw new Error("Unknown type 0x" + type.toString(16));
82+
}
83+
}
84+
85+
_fieldHeader() {
86+
// Make sure we call type then number to preserve offset
87+
return { "type": this._fieldType(), "key": this._fieldNumber() };
88+
}
89+
90+
_fieldType() {
91+
// Field type stored in lower 3 bits of tag byte
92+
return this.data[this.offset] & this.TYPE;
93+
}
94+
95+
_fieldNumber() {
96+
let shift = -3;
97+
let fieldNumber = 0;
98+
do {
99+
fieldNumber += shift < 28 ?
100+
shift === -3 ?
101+
(this.data[this.offset] & this.NUMBER) >> -shift :
102+
(this.data[this.offset] & this.VALUE) << shift :
103+
(this.data[this.offset] & this.VALUE) * Math.pow(2, shift);
104+
shift += 7;
105+
} while ((this.data[this.offset++] & this.MSB) === this.MSB);
106+
return fieldNumber;
107+
}
108+
109+
_varInt() {
110+
let value = 0;
111+
let shift = 0;
112+
// Keep reading while upper bit set
113+
do {
114+
value += shift < 28 ?
115+
(this.data[this.offset] & this.VALUE) << shift :
116+
(this.data[this.offset] & this.VALUE) * Math.pow(2, shift);
117+
shift += 7;
118+
} while ((this.data[this.offset++] & this.MSB) === this.MSB);
119+
return value;
120+
}
121+
_uint64() {
122+
// Read off a Uint64
123+
let num = this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++];
124+
num = num * 0x100000000 + this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++];
125+
return num;
126+
}
127+
_lenDelim() {
128+
// Read off the field length
129+
const length = this._varInt();
130+
const fieldBytes = this.data.slice(this.offset, this.offset + length);
131+
let field;
132+
try {
133+
// Attempt to parse as a new Protobuf Object
134+
const pbObject = new Protobuf(fieldBytes);
135+
field = pbObject._parse();
136+
} catch (err) {
137+
// Otherwise treat as bytes
138+
field = this._byteArrayToChars(fieldBytes);
139+
}
140+
// Move the offset and return the field
141+
this.offset += length;
142+
return field;
143+
}
144+
_uint32() {
145+
// Use a dataview to read off the integer
146+
const dataview = new DataView(new Uint8Array(this.data.slice(this.offset, this.offset + 4)).buffer);
147+
const value = dataview.getUint32(0);
148+
this.offset += 4;
149+
return value;
150+
}
151+
_byteArrayToChars(byteArray: (Int8Array | Uint8Array)) {
152+
if (!byteArray) return "";
153+
let str = "";
154+
// String concatenation appears to be faster than an array join
155+
for (let i = 0; i < byteArray.length;) {
156+
str += String.fromCharCode(byteArray[i++]);
157+
}
158+
return str;
159+
}
160+
}
161+
162+
export default Protobuf;

Diff for: app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { connect } from 'react-redux'
77
import { default as ReactResizeDetector } from 'react-resize-detector'
88
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
99
import { Typography, Fade, Grow } from '@material-ui/core'
10+
import Protobuf from './Protobuf';
1011

1112
interface Props {
1213
message: q.Message
@@ -43,6 +44,19 @@ class ValueRenderer extends React.Component<Props, State> {
4344
return [undefined, undefined]
4445
}
4546

47+
let obj = {}
48+
try {
49+
const byteArray = Base64Message.ToByteArray(msg);
50+
obj = Protobuf.decode(byteArray);
51+
}
52+
catch (e) {
53+
console.log("Caught exception while decoding protobuf: ", e);
54+
}
55+
56+
if (obj) {
57+
return [JSON.stringify(obj, undefined, ' '), 'json'];
58+
}
59+
4660
const str = Base64Message.toUnicodeString(msg)
4761
try {
4862
JSON.parse(str)

Diff for: backend/src/Model/Base64Message.ts

+8
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@ export class Base64Message {
1212
this.length = base64Str.length
1313
}
1414

15+
public static toBase64(message: Base64Message) {
16+
return message.base64Message || ''
17+
}
18+
1519
public static toUnicodeString(message: Base64Message) {
1620
return message.unicodeValue || ''
1721
}
1822

23+
public static ToByteArray(message: Base64Message): Uint8Array {
24+
return Base64.toUint8Array(message.base64Message)
25+
}
26+
1927
public static fromBuffer(buffer: Buffer) {
2028
return new Base64Message(buffer.toString('base64'))
2129
}

Diff for: package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
111111
"electron-updater": "^4.0.6",
112112
"fs-extra": "9",
113-
"js-base64": "^2.5.1",
113+
"js-base64": "^2.6.3",
114114
"json-to-ast": "^2.1.0",
115115
"lowdb": "^1.0.0",
116116
"mime": "^2.4.4",
@@ -119,4 +119,4 @@
119119
"yarn-run-all": "^3.1.1"
120120
},
121121
"optionalDependencies": {}
122-
}
122+
}

Diff for: yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -3026,10 +3026,10 @@ jake@^10.6.1:
30263026
filelist "^1.0.1"
30273027
minimatch "^3.0.4"
30283028

3029-
js-base64@^2.5.1:
3030-
version "2.5.2"
3031-
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209"
3032-
integrity sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==
3029+
js-base64@^2.6.3:
3030+
version "2.6.4"
3031+
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
3032+
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
30333033

30343034
js-tokens@^4.0.0:
30353035
version "4.0.0"

0 commit comments

Comments
 (0)