-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbytestruct.js
318 lines (277 loc) · 8.39 KB
/
bytestruct.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/// <reference types="./bytestruct.d.ts" />
// types
// f32, f64
// s8, s16, s32, s64
// u8, u16, u32, u64
// todo?: byte (u8)?, pad (u8)?
const
// globalThis.__DEBUG__ will be replaced with false and optimized away at build time.
DEBUG = globalThis.__DEBUG__ ?? true
const
TOKENS = /(le|be)|(byte)|(?:([fsu])(8|16|32|64))|(\d+)|(\w+):|(\s+)|(.)/g
const
TokenEndianness = 1,
TokenByte = 2,
TokenPartTypeName = 3,
TokenPartTypeSize = 4,
TokenNum = 5,
TokenLabel = 6,
TokenIgnore = 7,
TokenSymbol = 8
const
ModeTypes = 0,
ModeRepeat = 1
const ByteSizes = {
8: 1,
16: 2,
32: 4,
64: 8,
b: 1
}
const FullName = {
f: 'Float',
s: 'Int',
u: 'Uint',
b: 'Uint'
}
export function bytes(strings, ...values) {
let mode = ModeTypes
let littleEndian
let parts = []
let label
const handleValue = (value, token) => {
({
[ModeTypes]: () => {
if (DEBUG) {
if (!token && !Array.isArray(value)) {
throw new Error(`Invalid interpolation for ${value}`)
}
if (littleEndian == null && !token[TokenEndianness]) {
throw new Error(`Pattern much start by declaring endianness with 'le' or 'be'`)
}
if (label != null && !Array.isArray(value) && !token[TokenPartTypeName] && !token[TokenByte]) {
throw new Error(`Label '${label}' requires a type after it`)
}
}
if (Array.isArray(value)) {
parts.push({ name: value, label })
label = undefined
} else if (token[TokenEndianness]) {
littleEndian = token[TokenEndianness] === 'le'
} else if (token[TokenByte]) {
let part = { name: 'b' }
if (label != null) {
part.label = label
label = undefined
}
parts.push(part)
} else if (token[TokenPartTypeName]) {
let name = token[TokenPartTypeName]
let size = token[TokenPartTypeSize]
let part = { name, size, littleEndian }
if (label != null) {
part.label = label
label = undefined
}
parts.push(part)
} else if (token[TokenLabel]) {
label = token[TokenLabel]
} else if (token[TokenSymbol] === '*') {
mode = ModeRepeat
} else {
if (DEBUG) throw new Error(`Invalid token for '${token[0]}' for ModeTypes`)
}
},
[ModeRepeat]: () => {
if (DEBUG && !token && typeof value !== 'number')
throw new Error(`Invalid interpolation for ${value}`)
if (!token || token[TokenNum]) {
const times = value ?? parseInt(token[TokenNum])
parts.at(-1).repeat = times
mode = ModeTypes
} else {
if (DEBUG) throw new Error(`Invalid token '${token[0]}' for ModeRepeat`)
}
}
})[mode]()
}
for (let i = 0; i < strings.length; i++) {
const string = strings[i];
const tokens = string.matchAll(TOKENS);
for (const token of tokens) {
if (token[TokenIgnore]) continue;
handleValue(undefined, token)
}
if (i < values.length) handleValue(values[i])
}
return parts
}
export function sizeOf(fields) {
let totalSize = 0
for (const field of fields) {
let byteSize = Array.isArray(field.name)
? sizeOf(field.name)
: ByteSizes[field.size ?? field.name]
if (field.repeat) byteSize *= field.repeat
totalSize += byteSize
}
return totalSize
}
export function writeStructInto(view, fields, struct, offset) {
let pos = offset
for (const { name: type, size, littleEndian, repeat, label } of fields) {
if (DEBUG && !label) {
// we do not continue here because we do not want a difference in behavior between debug and final builds
console.warn(`You're trying to write a struct that has unnamed fields. Unnamed fields may not be written, please name them or use 'writeBytesInto' instead if this is not the intended behavior.`)
}
// todo: benchmark
const write = Array.isArray(type)
? (value) => pos += writeStructInto(view, type, value, pos)
: (value) => {
const byteSize = ByteSizes[size ?? type]
const fullName = (type === 's' || type === 'u') && size === 64
? `Big${FullName[type]}`
: FullName[type]
view[`set${fullName}${type === 'b' ? 8 : size}`](pos, value, littleEndian)
pos += byteSize
}
const value = struct[label]
if (repeat) {
for (let i = 0; i < repeat; i++) {
write(value[i])
}
} else {
write(value)
}
}
return pos - offset
}
export function readStructFrom(view, fields, offset) {
const struct = {}
let pos = offset
// todo: test performance
for (const { name: type, size, littleEndian, repeat, label } of fields) {
const byteSize = ByteSizes[size ?? type]
if (DEBUG && label in struct) {
console.warn(`There are duplicate fields for '${label}' in ${struct}`)
}
if (!label) {
pos += byteSize
continue
}
// todo: simplify
if (Array.isArray(type)) {
const byteSize = sizeOf(type)
if (repeat) {
const structs = []
for (let i = 0; i < repeat; i++) {
structs[i] = readStructFrom(view, type, pos)
pos += byteSize
}
struct[label] = structs
} else {
struct[label] = readStructFrom(view, type, pos)
pos += byteSize
}
continue
}
if (type === 'b') {
const size = repeat ?? 1
const uint8 = new Uint8Array(view.buffer)
struct[label] = uint8.slice(pos, pos + size)
pos += size
} else {
const fullName = (type === 's' || type === 'u') && size === 64
? `Big${FullName[type]}`
: FullName[type]
const readData = view[`get${fullName}${size}`].bind(view)
if (repeat) {
const bytes = []
for (let j = 0; j < repeat; j++) {
bytes[j] = readData(pos, littleEndian)
pos += byteSize
}
struct[label] = bytes
} else {
struct[label] = readData(pos, littleEndian)
pos += byteSize
}
}
}
return struct
}
// todo: clean
export function readBytesFrom(view, fields, offset) {
let pos = offset
let output = []
for (let i = 0; i < fields.length; i++) {
const { name, size, littleEndian, repeat } = fields[i]
// TODO: not sure how I feel about the `byte` feature
// it feel like a feature to be a feature when 'good-enough' alternatives exist even if they're not perfect
if (name === 'b') {
const size = repeat ?? 1
const uint8 = new Uint8Array(view.buffer)
const slice = uint8.slice(pos, pos + size)
output.push.apply(output, slice)
pos += size
continue
}
const byteSize = ByteSizes[size]
const fullName = (name === 's' || name === 'u') && size === 64
? `Big${FullName[name]}`
: FullName[name]
const readData = view[`get${fullName}${size}`].bind(view)
if (repeat) {
for (let j = 0; j < repeat; j++) {
const data = readData(pos, littleEndian)
output.push(data)
pos += byteSize
}
} else {
const data = readData(pos, littleEndian)
output.push(data)
pos += byteSize
}
}
return output
}
export function writeBytesInto(view, fields, bytes, offset) {
let pos = offset
if (DEBUG) {
let entrySize = fields.reduce((prev, type) => prev + (type.repeat ?? 1), 0)
if (entrySize !== bytes.length) {
throw new Error(`Byte count and total pat byte count does not match.`)
}
}
let bytePos = 0
for (let i = 0; i < fields.length; i++) {
const { name, size, littleEndian, repeat } = fields[i]
if (name === 'b') {
const size = repeat ?? 1
const data = bytes.slice(bytePos, bytePos + size)
// todo: check if more specialized init is faster
const uint8 = new Uint8Array(view.buffer);
uint8.set(data, pos)
pos += size
bytePos += size
continue
}
const byteSize = ByteSizes[size]
const fullName = (name === 's' || name === 'u') && size === 64
? `Big${FullName[name]}`
: FullName[name]
const writeData = view[`set${fullName}${size}`].bind(view)
if (repeat) {
for (let i = 0; i < repeat; i++) {
writeData(pos, bytes[bytePos], littleEndian)
pos += byteSize
bytePos += 1
}
} else {
writeData(pos, bytes[bytePos], littleEndian)
pos += byteSize
bytePos += 1
}
}
return pos - offset
}