Skip to content

Commit 333a7a4

Browse files
committed
wokring on parser
1 parent bc7b76f commit 333a7a4

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed

test/ideas.test.js

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/* global describe test expect */
2+
3+
import { fileURLToPath } from 'url'
4+
import { join, dirname } from 'path'
5+
import { readFile } from 'fs/promises'
6+
7+
export class ProtobufDecoder {
8+
constructor (buffer) {
9+
this.buffer = new Uint8Array(buffer)
10+
this.pos = 0
11+
12+
this.debugging = false
13+
}
14+
15+
debug (...args) {
16+
this.debugging && console.log.apply(...args)
17+
}
18+
19+
readUInt32LE (pos) {
20+
return this.buffer[pos++] | (this.buffer[pos++] << 8) | (this.buffer[pos++] << 16) | (this.buffer[pos++] << 24)
21+
}
22+
23+
decodeVarint () {
24+
let result = 0
25+
let shift = 0
26+
const start = this.pos
27+
while (true) {
28+
if (this.pos >= this.buffer.length) {
29+
throw new Error('Unexpected end of buffer while decoding varint')
30+
}
31+
const byte = this.buffer[this.pos++]
32+
result |= (byte & 0x7f) << shift
33+
if ((byte & 0x80) === 0) {
34+
return { int: result, value: this.buffer.slice(start, this.pos) }
35+
}
36+
shift += 7
37+
}
38+
}
39+
40+
decode32Bit () {
41+
if (this.pos + 4 > this.buffer.length) {
42+
throw new Error('Unexpected end of buffer while decoding 32-bit field')
43+
}
44+
const value = { int: this.readUInt32LE(this.pos), value: this.buffer.slice(this.pos, this.pos + 4) }
45+
this.pos += 4
46+
return value
47+
}
48+
49+
decode64Bit () {
50+
if (this.pos + 8 > this.buffer.length) {
51+
throw new Error('Unexpected end of buffer while decoding 64-bit field')
52+
}
53+
const low = this.readUInt32LE(this.pos)
54+
const high = this.readUInt32LE(this.pos + 4)
55+
const value = { int: { low, high }, value: this.buffer.slice(this.pos, this.pos + 8) }
56+
this.pos += 8
57+
return value
58+
}
59+
60+
decodeBytes (length) {
61+
if (this.pos + length > this.buffer.length) {
62+
throw new Error('Unexpected end of buffer while decoding bytes')
63+
}
64+
const bytes = this.buffer.slice(this.pos, this.pos + length)
65+
this.pos += length
66+
return bytes
67+
}
68+
69+
decodeGroup (fieldNumber) {
70+
const group = {}
71+
const start = this.pos
72+
while (true) {
73+
if (this.pos >= this.buffer.length) {
74+
throw new Error('Unexpected end of buffer while decoding group')
75+
}
76+
const tag = this.decodeVarint().int
77+
const wireType = tag & 0x07
78+
const number = tag >> 3
79+
if (wireType === 4 && number === fieldNumber) {
80+
break // End of group
81+
}
82+
const value = this.decodeField(tag)
83+
if (group[number]) {
84+
if (!Array.isArray(group[number])) {
85+
group[number] = [group[number]]
86+
}
87+
group[number].push(value)
88+
} else {
89+
group[number] = value
90+
}
91+
}
92+
return { group, value: this.buffer.slice(start, this.pos) }
93+
}
94+
95+
decodeField (tag) {
96+
const wireType = tag & 0x07
97+
const fieldNumber = tag >> 3
98+
this.debug(`Decoding field number ${fieldNumber} with wire type ${wireType}`)
99+
100+
let out = { wireType, fieldNumber }
101+
102+
switch (wireType) {
103+
case 0: // Varint
104+
out = { ...out, ...this.decodeVarint() }; break
105+
case 1: // 64-bit
106+
out = { ...out, ...this.decode64Bit() }; break
107+
case 2: // Length-delimited (string, bytes, or nested message)
108+
out.value = this.decodeBytes(this.decodeVarint().int); break
109+
case 3: // Start group
110+
out = { ...out, ...this.decodeGroup(fieldNumber) }; break
111+
case 4: // End group
112+
throw new Error('Unexpected end group tag')
113+
case 5: // 32-bit
114+
out = { ...out, ...this.decode32Bit() }; break
115+
default:
116+
throw new Error(`Unsupported wire type: ${wireType}`)
117+
}
118+
119+
return out
120+
}
121+
122+
decode () {
123+
const result = {}
124+
while (this.pos < this.buffer.length) {
125+
const tag = this.decodeVarint().int
126+
const fieldNumber = tag >> 3
127+
const value = this.decodeField(tag)
128+
129+
if (result[fieldNumber]) {
130+
if (!Array.isArray(result[fieldNumber])) {
131+
result[fieldNumber] = [result[fieldNumber]]
132+
}
133+
result[fieldNumber].push(value)
134+
} else {
135+
result[fieldNumber] = value
136+
}
137+
}
138+
return result
139+
}
140+
}
141+
142+
export const decodeMessage = (buffer) => new ProtobufDecoder(buffer).decode()
143+
144+
const tdec = new TextDecoder()
145+
146+
export const decoders = {
147+
string: f => tdec.decode(f.value),
148+
bytes: f => f.value,
149+
sub: f => decodeMessage(f.value),
150+
raw: f => f,
151+
uint: f => {
152+
if (f.wireType === 0) {
153+
return f.int
154+
}
155+
const a = f.value.buffer
156+
const d = new DataView(a)
157+
if (a.byteLength === 4) {
158+
return d.getUint32(0, true)
159+
} else if (a.byteLength >= 8) {
160+
return d.getBigUint64(0, true)
161+
}
162+
},
163+
int: f => {
164+
if (f.wireType === 0) {
165+
return f.int
166+
}
167+
const a = f.value.buffer
168+
const d = new DataView(a)
169+
if (a.byteLength === 4) {
170+
return d.getInt32(0, true)
171+
} else if (a.byteLength >= 8) {
172+
return d.getBigInt64(0, true)
173+
}
174+
},
175+
float: f => {
176+
if (f.wireType === 0) {
177+
return f.int
178+
}
179+
const a = f.value.buffer
180+
const d = new DataView(a)
181+
if (a.byteLength === 4) {
182+
return d.getFloat32(0, true)
183+
} else if (a.byteLength >= 8) {
184+
return d.getFloat64(0, true)
185+
}
186+
}
187+
}
188+
189+
export function query (root, q) {
190+
const [path, renderType = 'bytes'] = q.split(':')
191+
let current = [root]
192+
const findPath = path.split('.')
193+
const fieldId = findPath.pop()
194+
195+
for (const p of findPath) {
196+
const nc = []
197+
for (const tree of current) {
198+
if (Array.isArray(tree[p])) {
199+
nc.push(...tree[p].map(b => decodeMessage(b.value)))
200+
} else {
201+
nc.push(decodeMessage(tree[p].value))
202+
}
203+
}
204+
current = nc
205+
}
206+
207+
return current.map(c => decoders[renderType](c[fieldId]))
208+
}
209+
210+
const root = decodeMessage(await readFile(join(dirname(fileURLToPath(import.meta.url)), 'hearthstone.bin')))
211+
212+
describe('Query', async () => {
213+
test('Basic Fields', () => {
214+
expect(query(root, '1.2.4.1:string').pop()).toEqual('com.blizzard.wtcg.hearthstone')
215+
expect(query(root, '1.2.4.5:string').pop()).toEqual('Hearthstone')
216+
})
217+
218+
test('Group Sub-query (media)', () => {
219+
const medias = query(root, '1.2.4.10:sub').map(r => ({
220+
type: query(r, '1:uint').pop(),
221+
url: query(r, '5:string').pop(),
222+
width: query(r, '2.3:uint').pop(),
223+
height: query(r, '2.4:uint').pop()
224+
}))
225+
expect(medias.length).toEqual(10)
226+
})
227+
})

0 commit comments

Comments
 (0)