Skip to content

Commit c044688

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

File tree

5 files changed

+199
-6
lines changed

5 files changed

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

app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx

Lines changed: 13 additions & 0 deletions
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,18 @@ 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+
} catch (e) {
52+
console.log('Caught exception while decoding protobuf: ', e)
53+
}
54+
55+
if (obj) {
56+
return [JSON.stringify(obj, undefined, ' '), 'json']
57+
}
58+
4659
const str = Base64Message.toUnicodeString(msg)
4760
try {
4861
JSON.parse(str)

backend/src/Model/Base64Message.ts

Lines changed: 8 additions & 0 deletions
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
}

package.json

Lines changed: 2 additions & 2 deletions
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+
}

yarn.lock

Lines changed: 4 additions & 4 deletions
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)