@robdobsn/raftjs 1.11.5 → 1.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react-native/RaftAttributeHandler.js +11 -9
- package/dist/react-native/RaftAttributeHandler.js.map +1 -1
- package/dist/react-native/RaftDeviceManager.d.ts +1 -1
- package/dist/react-native/RaftDeviceManager.js.map +1 -1
- package/dist/react-native/RaftDeviceStates.d.ts +1 -1
- package/dist/react-native/RaftDeviceStates.js +6 -2
- package/dist/react-native/RaftDeviceStates.js.map +1 -1
- package/dist/react-native/RaftStruct.d.ts +2 -2
- package/dist/react-native/RaftStruct.js +97 -26
- package/dist/react-native/RaftStruct.js.map +1 -1
- package/dist/web/RaftAttributeHandler.js +11 -9
- package/dist/web/RaftAttributeHandler.js.map +1 -1
- package/dist/web/RaftDeviceManager.d.ts +1 -1
- package/dist/web/RaftDeviceManager.js.map +1 -1
- package/dist/web/RaftDeviceStates.d.ts +1 -1
- package/dist/web/RaftDeviceStates.js +6 -2
- package/dist/web/RaftDeviceStates.js.map +1 -1
- package/dist/web/RaftStruct.d.ts +2 -2
- package/dist/web/RaftStruct.js +97 -26
- package/dist/web/RaftStruct.js.map +1 -1
- package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
- package/examples/dashboard/src/DeviceStatsPanel.tsx +17 -6
- package/package.json +8 -5
- package/src/RaftAttributeHandler.ts +25 -22
- package/src/RaftDeviceManager.ts +2 -2
- package/src/RaftDeviceStates.ts +7 -3
- package/src/RaftStruct.test.ts +229 -0
- package/src/RaftStruct.ts +101 -37
|
@@ -37,7 +37,7 @@ export default class AttributeHandler {
|
|
|
37
37
|
const msgDataStartIdx = msgBufIdx;
|
|
38
38
|
|
|
39
39
|
// New attribute values (in order as they appear in the attributes JSON)
|
|
40
|
-
let newAttrValues: number[][] = [];
|
|
40
|
+
let newAttrValues: (number | string)[][] = [];
|
|
41
41
|
if ("c" in pollRespMetadata) {
|
|
42
42
|
|
|
43
43
|
// Extract attribute values using custom handler
|
|
@@ -201,7 +201,7 @@ export default class AttributeHandler {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
private processMsgAttribute(attrDef: DeviceTypeAttribute, msgBuffer: Uint8Array, msgBufIdx: number, msgDataStartIdx: number): { values: number[], newMsgBufIdx: number} {
|
|
204
|
+
private processMsgAttribute(attrDef: DeviceTypeAttribute, msgBuffer: Uint8Array, msgBufIdx: number, msgDataStartIdx: number): { values: (number | string)[], newMsgBufIdx: number} {
|
|
205
205
|
|
|
206
206
|
// Current field message string index
|
|
207
207
|
let curFieldBufIdx = msgBufIdx;
|
|
@@ -253,11 +253,14 @@ export default class AttributeHandler {
|
|
|
253
253
|
|
|
254
254
|
// Extract the value using python-struct
|
|
255
255
|
const unpackValues = structUnpack(maskOnSignedValue ? attrTypesOnly.toUpperCase() : attrTypesOnly, attrBuf);
|
|
256
|
-
let attrValues = unpackValues as number[];
|
|
256
|
+
let attrValues = unpackValues as (number | string)[];
|
|
257
257
|
|
|
258
258
|
// Get number of bytes consumed
|
|
259
259
|
const numBytesConsumed = structSizeOf(attrTypesOnly);
|
|
260
260
|
|
|
261
|
+
// Check if any values are strings (from 's' format) — skip numeric transforms for those
|
|
262
|
+
const hasStringValues = attrValues.some(v => typeof v === 'string');
|
|
263
|
+
|
|
261
264
|
// // Check if sign extendable mask specified on signed value
|
|
262
265
|
// if (mmSpecifiedOnSignedValue) {
|
|
263
266
|
// const signBitMask = 1 << (signExtendableMaskSignPos - 1);
|
|
@@ -270,57 +273,57 @@ export default class AttributeHandler {
|
|
|
270
273
|
// }
|
|
271
274
|
|
|
272
275
|
// Check for XOR mask
|
|
273
|
-
if ("x" in attrDef) {
|
|
276
|
+
if (!hasStringValues && "x" in attrDef) {
|
|
274
277
|
const mask = typeof attrDef.x === "string" ? parseInt(attrDef.x, 16) : attrDef.x as number;
|
|
275
|
-
attrValues = attrValues.map((value) => (value >>> 0) ^ mask);
|
|
278
|
+
attrValues = attrValues.map((value) => ((value as number) >>> 0) ^ mask);
|
|
276
279
|
}
|
|
277
280
|
|
|
278
281
|
// Check for AND mask
|
|
279
|
-
if ("m" in attrDef) {
|
|
282
|
+
if (!hasStringValues && "m" in attrDef) {
|
|
280
283
|
const mask = typeof attrDef.m === "string" ? parseInt(attrDef.m, 16) : attrDef.m as number;
|
|
281
|
-
attrValues = attrValues.map((value) => (maskOnSignedValue ? this.signExtend(value, mask) : (value >>> 0) & mask));
|
|
284
|
+
attrValues = attrValues.map((value) => (maskOnSignedValue ? this.signExtend(value as number, mask) : ((value as number) >>> 0) & mask));
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
// Check for a sign-bit
|
|
285
|
-
if ("sb" in attrDef) {
|
|
288
|
+
if (!hasStringValues && "sb" in attrDef) {
|
|
286
289
|
const signBitPos = attrDef.sb as number;
|
|
287
290
|
const signBitMask = 1 << signBitPos;
|
|
288
291
|
if ("ss" in attrDef) {
|
|
289
292
|
const signBitSubtract = attrDef.ss as number;
|
|
290
|
-
attrValues = attrValues.map((value) => (value & signBitMask) ? signBitSubtract - value : value);
|
|
293
|
+
attrValues = attrValues.map((value) => ((value as number) & signBitMask) ? signBitSubtract - (value as number) : value);
|
|
291
294
|
} else {
|
|
292
|
-
attrValues = attrValues.map((value) => (value & signBitMask) ? value - (signBitMask << 1) : value);
|
|
295
|
+
attrValues = attrValues.map((value) => ((value as number) & signBitMask) ? (value as number) - (signBitMask << 1) : value);
|
|
293
296
|
}
|
|
294
297
|
}
|
|
295
298
|
|
|
296
299
|
// Check for bit shift required
|
|
297
|
-
if ("s" in attrDef && attrDef.s) {
|
|
300
|
+
if (!hasStringValues && "s" in attrDef && attrDef.s) {
|
|
298
301
|
const bitshift = attrDef.s as number;
|
|
299
302
|
if (bitshift > 0) {
|
|
300
|
-
attrValues = attrValues.map((value) => (value >>> 0) >>> bitshift);
|
|
303
|
+
attrValues = attrValues.map((value) => ((value as number) >>> 0) >>> bitshift);
|
|
301
304
|
} else if (bitshift < 0) {
|
|
302
|
-
attrValues = attrValues.map((value) => (value >>> 0) << -bitshift);
|
|
305
|
+
attrValues = attrValues.map((value) => ((value as number) >>> 0) << -bitshift);
|
|
303
306
|
}
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
// Check for divisor
|
|
307
|
-
if ("d" in attrDef && attrDef.d) {
|
|
310
|
+
if (!hasStringValues && "d" in attrDef && attrDef.d) {
|
|
308
311
|
const divisor = attrDef.d as number;
|
|
309
|
-
attrValues = attrValues.map((value) => (value) / divisor);
|
|
312
|
+
attrValues = attrValues.map((value) => (value as number) / divisor);
|
|
310
313
|
}
|
|
311
314
|
|
|
312
315
|
// Check for value to add
|
|
313
|
-
if ("a" in attrDef && attrDef.a !== undefined) {
|
|
316
|
+
if (!hasStringValues && "a" in attrDef && attrDef.a !== undefined) {
|
|
314
317
|
const addValue = attrDef.a as number;
|
|
315
|
-
attrValues = attrValues.map((value) => (value) + addValue);
|
|
318
|
+
attrValues = attrValues.map((value) => (value as number) + addValue);
|
|
316
319
|
}
|
|
317
320
|
|
|
318
321
|
// Apply lookup table if defined
|
|
319
|
-
if ("lut" in attrDef && attrDef.lut !== undefined) {
|
|
322
|
+
if (!hasStringValues && "lut" in attrDef && attrDef.lut !== undefined) {
|
|
320
323
|
attrValues = attrValues.map((value): number => {
|
|
321
324
|
// Skip NaN values
|
|
322
|
-
if (isNaN(value)) {
|
|
323
|
-
return value;
|
|
325
|
+
if (isNaN(value as number)) {
|
|
326
|
+
return value as number;
|
|
324
327
|
}
|
|
325
328
|
|
|
326
329
|
// Search through the lookup table rows for a match
|
|
@@ -334,7 +337,7 @@ export default class AttributeHandler {
|
|
|
334
337
|
}
|
|
335
338
|
|
|
336
339
|
// Parse the range string
|
|
337
|
-
if (this.isValueInRangeString(value, row.r)) {
|
|
340
|
+
if (this.isValueInRangeString(value as number, row.r)) {
|
|
338
341
|
return row.v;
|
|
339
342
|
}
|
|
340
343
|
}
|
|
@@ -345,7 +348,7 @@ export default class AttributeHandler {
|
|
|
345
348
|
}
|
|
346
349
|
|
|
347
350
|
// Otherwise keep the original value
|
|
348
|
-
return value;
|
|
351
|
+
return value as number;
|
|
349
352
|
});
|
|
350
353
|
}
|
|
351
354
|
|
package/src/RaftDeviceManager.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface DeviceDecodedData {
|
|
|
23
23
|
deviceAddress: string;
|
|
24
24
|
deviceType: string;
|
|
25
25
|
attrGroupName?: string;
|
|
26
|
-
attrValues: Record<string, number[]>;
|
|
26
|
+
attrValues: Record<string, (number | string)[]>;
|
|
27
27
|
timestampsUs: number[];
|
|
28
28
|
markers?: Record<string, unknown>;
|
|
29
29
|
fromOfflineBuffer?: boolean;
|
|
@@ -956,7 +956,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
956
956
|
return;
|
|
957
957
|
}
|
|
958
958
|
|
|
959
|
-
const attrValues: Record<string, number[]> = {};
|
|
959
|
+
const attrValues: Record<string, (number | string)[]> = {};
|
|
960
960
|
let hasValues = false;
|
|
961
961
|
|
|
962
962
|
pollRespMetadata.a.forEach((attr) => {
|
package/src/RaftDeviceStates.ts
CHANGED
|
@@ -14,10 +14,14 @@ export function deviceAttrGetLatestFormatted(attrState: DeviceAttributeState): s
|
|
|
14
14
|
if (attrState.values.length === 0) {
|
|
15
15
|
return 'N/A';
|
|
16
16
|
}
|
|
17
|
+
const value = attrState.values[attrState.values.length - 1];
|
|
18
|
+
// String values are returned directly
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
17
22
|
if (attrState.format.length === 0) {
|
|
18
|
-
return
|
|
23
|
+
return value.toString();
|
|
19
24
|
}
|
|
20
|
-
const value = attrState.values[attrState.values.length - 1];
|
|
21
25
|
let format = attrState.format;
|
|
22
26
|
if (format.startsWith("%")) {
|
|
23
27
|
format = format.slice(1);
|
|
@@ -52,7 +56,7 @@ export interface DeviceAttributeState {
|
|
|
52
56
|
newAttribute: boolean;
|
|
53
57
|
newData: boolean;
|
|
54
58
|
numNewValues: number;
|
|
55
|
-
values: number[];
|
|
59
|
+
values: (number | string)[];
|
|
56
60
|
units: string;
|
|
57
61
|
range: number[];
|
|
58
62
|
format: string;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { structUnpack, structPack, structSizeOf } from "./RaftStruct";
|
|
2
|
+
|
|
3
|
+
// Helper to create Uint8Array from byte values
|
|
4
|
+
function bytes(...vals: number[]): Uint8Array {
|
|
5
|
+
return new Uint8Array(vals);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// ===== Existing functionality (regression tests) =====
|
|
9
|
+
|
|
10
|
+
describe("structUnpack", () => {
|
|
11
|
+
test("single B", () => {
|
|
12
|
+
expect(structUnpack("B", bytes(0xff))).toEqual([255]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("single b (signed)", () => {
|
|
16
|
+
expect(structUnpack("b", bytes(0xff))).toEqual([-1]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("<H little-endian uint16", () => {
|
|
20
|
+
expect(structUnpack("<H", bytes(0x01, 0x02))).toEqual([0x0201]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test(">H big-endian uint16", () => {
|
|
24
|
+
expect(structUnpack(">H", bytes(0x01, 0x02))).toEqual([0x0102]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("BBBB four bytes", () => {
|
|
28
|
+
expect(structUnpack("BBBB", bytes(1, 2, 3, 4))).toEqual([1, 2, 3, 4]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("<I little-endian uint32", () => {
|
|
32
|
+
const buf = bytes(0x78, 0x56, 0x34, 0x12);
|
|
33
|
+
expect(structUnpack("<I", buf)).toEqual([0x12345678]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test(">I big-endian uint32", () => {
|
|
37
|
+
const buf = bytes(0x12, 0x34, 0x56, 0x78);
|
|
38
|
+
expect(structUnpack(">I", buf)).toEqual([0x12345678]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("[N] bracket repeat", () => {
|
|
42
|
+
expect(structUnpack("B[3]", bytes(10, 20, 30))).toEqual([10, 20, 30]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("x padding skips byte", () => {
|
|
46
|
+
expect(structUnpack("xB", bytes(0xff, 0x42))).toEqual([0x42]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("<f float32", () => {
|
|
50
|
+
const buf = new Uint8Array(4);
|
|
51
|
+
new DataView(buf.buffer).setFloat32(0, 3.14, true);
|
|
52
|
+
const result = structUnpack("<f", buf);
|
|
53
|
+
expect(result[0]).toBeCloseTo(3.14, 2);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ===== Prefix digit syntax =====
|
|
58
|
+
|
|
59
|
+
describe("prefix digit syntax", () => {
|
|
60
|
+
test("3B equivalent to BBB", () => {
|
|
61
|
+
expect(structUnpack("3B", bytes(1, 2, 3))).toEqual([1, 2, 3]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("2H two uint16s", () => {
|
|
65
|
+
expect(structUnpack(">2H", bytes(0, 1, 0, 2))).toEqual([1, 2]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("prefix and bracket multiply", () => {
|
|
69
|
+
// 2B[3] means repeat=6
|
|
70
|
+
expect(structUnpack("2B[3]", bytes(1, 2, 3, 4, 5, 6))).toEqual([1, 2, 3, 4, 5, 6]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("prefix digits in structSizeOf", () => {
|
|
74
|
+
expect(structSizeOf("3B")).toBe(3);
|
|
75
|
+
expect(structSizeOf(">2H")).toBe(4);
|
|
76
|
+
expect(structSizeOf("2I")).toBe(8);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("prefix digits in structPack", () => {
|
|
80
|
+
const packed = structPack("3B", [10, 20, 30]);
|
|
81
|
+
expect(Array.from(packed)).toEqual([10, 20, 30]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("multi-digit prefix count", () => {
|
|
85
|
+
expect(structSizeOf("16s")).toBe(16);
|
|
86
|
+
expect(structSizeOf("10B")).toBe(10);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ===== s format code =====
|
|
91
|
+
|
|
92
|
+
describe("s format (byte strings)", () => {
|
|
93
|
+
test("unpack 5s reads a string", () => {
|
|
94
|
+
const buf = bytes(0x48, 0x65, 0x6c, 0x6c, 0x6f); // "Hello"
|
|
95
|
+
expect(structUnpack("5s", buf)).toEqual(["Hello"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("unpack s with trailing nulls trims them", () => {
|
|
99
|
+
const buf = bytes(0x48, 0x69, 0x00, 0x00, 0x00); // "Hi\0\0\0"
|
|
100
|
+
expect(structUnpack("5s", buf)).toEqual(["Hi"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("unpack s produces one value regardless of count", () => {
|
|
104
|
+
const buf = bytes(0x41, 0x42, 0x43); // "ABC"
|
|
105
|
+
const result = structUnpack("3s", buf);
|
|
106
|
+
expect(result).toHaveLength(1);
|
|
107
|
+
expect(result[0]).toBe("ABC");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("s combined with other types", () => {
|
|
111
|
+
// B then 3s then B
|
|
112
|
+
const buf = bytes(0xff, 0x41, 0x42, 0x43, 0x01);
|
|
113
|
+
const result = structUnpack("B3sB", buf);
|
|
114
|
+
expect(result).toEqual([255, "ABC", 1]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("structSizeOf with s", () => {
|
|
118
|
+
expect(structSizeOf("5s")).toBe(5);
|
|
119
|
+
expect(structSizeOf("B5sH")).toBe(8); // 1 + 5 + 2
|
|
120
|
+
expect(structSizeOf(">16s")).toBe(16);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("structPack with string value", () => {
|
|
124
|
+
const packed = structPack("5s", ["Hello"]);
|
|
125
|
+
expect(Array.from(packed)).toEqual([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("structPack s zero-pads short strings", () => {
|
|
129
|
+
const packed = structPack("5s", ["Hi"]);
|
|
130
|
+
expect(Array.from(packed)).toEqual([0x48, 0x69, 0x00, 0x00, 0x00]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("structPack s with Uint8Array value", () => {
|
|
134
|
+
const packed = structPack("3s", [bytes(0x01, 0x02, 0x03)]);
|
|
135
|
+
expect(Array.from(packed)).toEqual([1, 2, 3]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("structPack s combined with numeric types", () => {
|
|
139
|
+
const packed = structPack("B3sB", [0xff, "ABC", 0x01]);
|
|
140
|
+
expect(Array.from(packed)).toEqual([0xff, 0x41, 0x42, 0x43, 0x01]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("round-trip pack/unpack s", () => {
|
|
144
|
+
const original = "Test";
|
|
145
|
+
const packed = structPack("8s", [original]);
|
|
146
|
+
const [unpacked] = structUnpack("8s", packed);
|
|
147
|
+
expect(unpacked).toBe(original);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ===== q/Q 64-bit integers =====
|
|
152
|
+
|
|
153
|
+
describe("q/Q 64-bit integers", () => {
|
|
154
|
+
test("unpack >q signed 64-bit", () => {
|
|
155
|
+
// 0x0000000000000001 = 1
|
|
156
|
+
const buf = bytes(0, 0, 0, 0, 0, 0, 0, 1);
|
|
157
|
+
expect(structUnpack(">q", buf)).toEqual([1]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("unpack >Q unsigned 64-bit", () => {
|
|
161
|
+
const buf = bytes(0, 0, 0, 0, 0, 0, 0, 42);
|
|
162
|
+
expect(structUnpack(">Q", buf)).toEqual([42]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("unpack <q little-endian", () => {
|
|
166
|
+
const buf = new Uint8Array(8);
|
|
167
|
+
new DataView(buf.buffer).setBigInt64(0, BigInt(-1000), true);
|
|
168
|
+
expect(structUnpack("<q", buf)).toEqual([-1000]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("unpack >q negative", () => {
|
|
172
|
+
const buf = new Uint8Array(8);
|
|
173
|
+
new DataView(buf.buffer).setBigInt64(0, BigInt(-1), false);
|
|
174
|
+
expect(structUnpack(">q", buf)).toEqual([-1]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("structSizeOf q and Q", () => {
|
|
178
|
+
expect(structSizeOf("q")).toBe(8);
|
|
179
|
+
expect(structSizeOf("Q")).toBe(8);
|
|
180
|
+
expect(structSizeOf(">qQ")).toBe(16);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("structPack >q", () => {
|
|
184
|
+
const packed = structPack(">q", [1]);
|
|
185
|
+
expect(Array.from(packed)).toEqual([0, 0, 0, 0, 0, 0, 0, 1]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("structPack <Q", () => {
|
|
189
|
+
const packed = structPack("<Q", [256]);
|
|
190
|
+
const view = new DataView(packed.buffer);
|
|
191
|
+
expect(Number(view.getBigUint64(0, true))).toBe(256);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("round-trip q", () => {
|
|
195
|
+
const packed = structPack(">q", [-123456789]);
|
|
196
|
+
const [unpacked] = structUnpack(">q", packed);
|
|
197
|
+
expect(unpacked).toBe(-123456789);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("round-trip Q", () => {
|
|
201
|
+
const packed = structPack(">Q", [123456789]);
|
|
202
|
+
const [unpacked] = structUnpack(">Q", packed);
|
|
203
|
+
expect(unpacked).toBe(123456789);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ===== Error handling =====
|
|
208
|
+
|
|
209
|
+
describe("error handling", () => {
|
|
210
|
+
test("unknown format code throws", () => {
|
|
211
|
+
expect(() => structUnpack("Z", bytes(0))).toThrow("Unknown format character");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("missing ] throws", () => {
|
|
215
|
+
expect(() => structUnpack("B[3", bytes(1, 2, 3))).toThrow("missing closing ]");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("digit without format code throws", () => {
|
|
219
|
+
expect(() => structUnpack("3", bytes(1, 2, 3))).toThrow("Expected format code after prefix count");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("structPack insufficient values throws", () => {
|
|
223
|
+
expect(() => structPack("HH", [1])).toThrow("Insufficient values");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("structPack s with number throws", () => {
|
|
227
|
+
expect(() => structPack("3s", [42])).toThrow("Expected string or Uint8Array");
|
|
228
|
+
});
|
|
229
|
+
});
|
package/src/RaftStruct.ts
CHANGED
|
@@ -33,22 +33,40 @@ function parseFormatInstructions(format: string): FormatInstruction[] {
|
|
|
33
33
|
continue;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Check for prefix digit count (e.g. 3H, 16s)
|
|
37
|
+
let repeat = 1;
|
|
38
|
+
if (/\d/.test(char)) {
|
|
39
|
+
let numStr = char;
|
|
40
|
+
idx++;
|
|
41
|
+
while (idx < format.length && /\d/.test(format[idx])) {
|
|
42
|
+
numStr += format[idx];
|
|
43
|
+
idx++;
|
|
44
|
+
}
|
|
45
|
+
repeat = parseInt(numStr, 10);
|
|
46
|
+
if (!Number.isFinite(repeat) || repeat <= 0) {
|
|
47
|
+
throw new Error(`Invalid prefix count "${numStr}" in format string "${format}"`);
|
|
48
|
+
}
|
|
49
|
+
if (idx >= format.length || /[\s<>\d]/.test(format[idx])) {
|
|
50
|
+
throw new Error(`Expected format code after prefix count "${numStr}" in format string "${format}"`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
// Attribute code
|
|
37
|
-
const code = char;
|
|
38
|
-
idx++;
|
|
55
|
+
const code = /\d/.test(char) ? format[idx] : char;
|
|
56
|
+
if (/\d/.test(char)) idx++; else idx++;
|
|
39
57
|
|
|
40
|
-
// Check for
|
|
41
|
-
let repeat = 1;
|
|
58
|
+
// Check for [N] suffix count (e.g. B[3]) — multiplied with any prefix count
|
|
42
59
|
if (idx < format.length && format[idx] === "[") {
|
|
43
60
|
const endIdx = format.indexOf("]", idx + 1);
|
|
44
61
|
if (endIdx === -1) {
|
|
45
62
|
throw new Error(`Invalid format string: missing closing ] in "${format}"`);
|
|
46
63
|
}
|
|
47
64
|
const repeatStr = format.slice(idx + 1, endIdx);
|
|
48
|
-
|
|
49
|
-
if (!Number.isFinite(
|
|
65
|
+
const bracketRepeat = parseInt(repeatStr, 10);
|
|
66
|
+
if (!Number.isFinite(bracketRepeat) || bracketRepeat <= 0) {
|
|
50
67
|
throw new Error(`Invalid repeat count "${repeatStr}" in format string "${format}"`);
|
|
51
68
|
}
|
|
69
|
+
repeat *= bracketRepeat;
|
|
52
70
|
idx = endIdx + 1;
|
|
53
71
|
}
|
|
54
72
|
|
|
@@ -58,9 +76,9 @@ function parseFormatInstructions(format: string): FormatInstruction[] {
|
|
|
58
76
|
return instructions;
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
export function structUnpack(format: string, data: Uint8Array): number[] {
|
|
79
|
+
export function structUnpack(format: string, data: Uint8Array): (number | string)[] {
|
|
62
80
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
63
|
-
const results: number[] = [];
|
|
81
|
+
const results: (number | string)[] = [];
|
|
64
82
|
let offset = 0;
|
|
65
83
|
let littleEndian = false;
|
|
66
84
|
|
|
@@ -107,14 +125,14 @@ export function structUnpack(format: string, data: Uint8Array): number[] {
|
|
|
107
125
|
results.push(view.getUint32(offset, littleEndian));
|
|
108
126
|
offset += 4;
|
|
109
127
|
break;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
case "q": // Signed 64-bit integer
|
|
129
|
+
results.push(Number(view.getBigInt64(offset, littleEndian)));
|
|
130
|
+
offset += 8;
|
|
131
|
+
break;
|
|
132
|
+
case "Q": // Unsigned 64-bit integer
|
|
133
|
+
results.push(Number(view.getBigUint64(offset, littleEndian)));
|
|
134
|
+
offset += 8;
|
|
135
|
+
break;
|
|
118
136
|
case "f": // 32-bit float
|
|
119
137
|
results.push(view.getFloat32(offset, littleEndian));
|
|
120
138
|
offset += 4;
|
|
@@ -123,9 +141,20 @@ export function structUnpack(format: string, data: Uint8Array): number[] {
|
|
|
123
141
|
results.push(view.getFloat64(offset, littleEndian));
|
|
124
142
|
offset += 8;
|
|
125
143
|
break;
|
|
144
|
+
case "s": { // Byte string (repeat = byte length, produces one string value)
|
|
145
|
+
const bytes = data.slice(offset, offset + repeat);
|
|
146
|
+
// Trim trailing null bytes for C-string compatibility
|
|
147
|
+
let end = bytes.length;
|
|
148
|
+
while (end > 0 && bytes[end - 1] === 0) end--;
|
|
149
|
+
results.push(new TextDecoder().decode(bytes.subarray(0, end)));
|
|
150
|
+
offset += repeat;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
126
153
|
default:
|
|
127
154
|
throw new Error(`Unknown format character: ${code}`);
|
|
128
155
|
}
|
|
156
|
+
// For 's', the repeat is consumed as byte-length in one go
|
|
157
|
+
if (code === "s") break;
|
|
129
158
|
}
|
|
130
159
|
}
|
|
131
160
|
|
|
@@ -162,16 +191,19 @@ export function structSizeOf(format: string): number {
|
|
|
162
191
|
case "L": // Unsigned 32-bit integer
|
|
163
192
|
unitSize = 4;
|
|
164
193
|
break;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
194
|
+
case "q": // Signed 64-bit integer
|
|
195
|
+
case "Q": // Unsigned 64-bit integer
|
|
196
|
+
unitSize = 8;
|
|
197
|
+
break;
|
|
169
198
|
case "f": // 32-bit float
|
|
170
199
|
unitSize = 4;
|
|
171
200
|
break;
|
|
172
201
|
case "d": // 64-bit float
|
|
173
202
|
unitSize = 8;
|
|
174
203
|
break;
|
|
204
|
+
case "s": // Byte string (repeat = byte length)
|
|
205
|
+
size += repeat;
|
|
206
|
+
continue;
|
|
175
207
|
default:
|
|
176
208
|
throw new Error(`Unknown format character: ${code}`);
|
|
177
209
|
}
|
|
@@ -181,7 +213,7 @@ export function structSizeOf(format: string): number {
|
|
|
181
213
|
return size;
|
|
182
214
|
}
|
|
183
215
|
|
|
184
|
-
export function structPack(format: string, values: number[]): Uint8Array {
|
|
216
|
+
export function structPack(format: string, values: (number | string | Uint8Array)[]): Uint8Array {
|
|
185
217
|
const size = structSizeOf(format);
|
|
186
218
|
const buffer = new ArrayBuffer(size);
|
|
187
219
|
const view = new DataView(buffer);
|
|
@@ -209,7 +241,7 @@ export function structPack(format: string, values: number[]): Uint8Array {
|
|
|
209
241
|
if (valueIdx >= values.length) {
|
|
210
242
|
throw new Error("Insufficient values provided for structPack");
|
|
211
243
|
}
|
|
212
|
-
view.setInt8(offset, values[valueIdx++]);
|
|
244
|
+
view.setInt8(offset, values[valueIdx++] as number);
|
|
213
245
|
offset += 1;
|
|
214
246
|
break;
|
|
215
247
|
case "B": // Unsigned 8-bit integer
|
|
@@ -217,21 +249,21 @@ export function structPack(format: string, values: number[]): Uint8Array {
|
|
|
217
249
|
if (valueIdx >= values.length) {
|
|
218
250
|
throw new Error("Insufficient values provided for structPack");
|
|
219
251
|
}
|
|
220
|
-
view.setUint8(offset, values[valueIdx++]);
|
|
252
|
+
view.setUint8(offset, values[valueIdx++] as number);
|
|
221
253
|
offset += 1;
|
|
222
254
|
break;
|
|
223
255
|
case "h": // Signed 16-bit integer
|
|
224
256
|
if (valueIdx >= values.length) {
|
|
225
257
|
throw new Error("Insufficient values provided for structPack");
|
|
226
258
|
}
|
|
227
|
-
view.setInt16(offset, values[valueIdx++], littleEndian);
|
|
259
|
+
view.setInt16(offset, values[valueIdx++] as number, littleEndian);
|
|
228
260
|
offset += 2;
|
|
229
261
|
break;
|
|
230
262
|
case "H": // Unsigned 16-bit integer
|
|
231
263
|
if (valueIdx >= values.length) {
|
|
232
264
|
throw new Error("Insufficient values provided for structPack");
|
|
233
265
|
}
|
|
234
|
-
view.setUint16(offset, values[valueIdx++], littleEndian);
|
|
266
|
+
view.setUint16(offset, values[valueIdx++] as number, littleEndian);
|
|
235
267
|
offset += 2;
|
|
236
268
|
break;
|
|
237
269
|
case "i": // Signed 32-bit integer
|
|
@@ -239,7 +271,7 @@ export function structPack(format: string, values: number[]): Uint8Array {
|
|
|
239
271
|
if (valueIdx >= values.length) {
|
|
240
272
|
throw new Error("Insufficient values provided for structPack");
|
|
241
273
|
}
|
|
242
|
-
view.setInt32(offset, values[valueIdx++], littleEndian);
|
|
274
|
+
view.setInt32(offset, values[valueIdx++] as number, littleEndian);
|
|
243
275
|
offset += 4;
|
|
244
276
|
break;
|
|
245
277
|
case "I": // Unsigned 32-bit integer
|
|
@@ -247,34 +279,66 @@ export function structPack(format: string, values: number[]): Uint8Array {
|
|
|
247
279
|
if (valueIdx >= values.length) {
|
|
248
280
|
throw new Error("Insufficient values provided for structPack");
|
|
249
281
|
}
|
|
250
|
-
view.setUint32(offset, values[valueIdx++], littleEndian);
|
|
282
|
+
view.setUint32(offset, values[valueIdx++] as number, littleEndian);
|
|
251
283
|
offset += 4;
|
|
252
284
|
break;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
//
|
|
285
|
+
case "q": // Signed 64-bit integer
|
|
286
|
+
if (valueIdx >= values.length) {
|
|
287
|
+
throw new Error("Insufficient values provided for structPack");
|
|
288
|
+
}
|
|
289
|
+
view.setBigInt64(offset, BigInt(values[valueIdx++] as number), littleEndian);
|
|
290
|
+
offset += 8;
|
|
291
|
+
break;
|
|
292
|
+
case "Q": // Unsigned 64-bit integer
|
|
293
|
+
if (valueIdx >= values.length) {
|
|
294
|
+
throw new Error("Insufficient values provided for structPack");
|
|
295
|
+
}
|
|
296
|
+
view.setBigUint64(offset, BigInt(values[valueIdx++] as number), littleEndian);
|
|
297
|
+
offset += 8;
|
|
298
|
+
break;
|
|
261
299
|
case "f": // 32-bit float
|
|
262
300
|
if (valueIdx >= values.length) {
|
|
263
301
|
throw new Error("Insufficient values provided for structPack");
|
|
264
302
|
}
|
|
265
|
-
view.setFloat32(offset, values[valueIdx++], littleEndian);
|
|
303
|
+
view.setFloat32(offset, values[valueIdx++] as number, littleEndian);
|
|
266
304
|
offset += 4;
|
|
267
305
|
break;
|
|
268
306
|
case "d": // 64-bit float
|
|
269
307
|
if (valueIdx >= values.length) {
|
|
270
308
|
throw new Error("Insufficient values provided for structPack");
|
|
271
309
|
}
|
|
272
|
-
view.setFloat64(offset, values[valueIdx++], littleEndian);
|
|
310
|
+
view.setFloat64(offset, values[valueIdx++] as number, littleEndian);
|
|
273
311
|
offset += 8;
|
|
274
312
|
break;
|
|
313
|
+
case "s": { // Byte string (repeat = byte length, consumes one value)
|
|
314
|
+
if (valueIdx >= values.length) {
|
|
315
|
+
throw new Error("Insufficient values provided for structPack");
|
|
316
|
+
}
|
|
317
|
+
const val = values[valueIdx++];
|
|
318
|
+
let bytes: Uint8Array;
|
|
319
|
+
if (val instanceof Uint8Array) {
|
|
320
|
+
bytes = val;
|
|
321
|
+
} else if (typeof val === "string") {
|
|
322
|
+
bytes = new TextEncoder().encode(val);
|
|
323
|
+
} else {
|
|
324
|
+
throw new Error(`Expected string or Uint8Array for 's' format, got number`);
|
|
325
|
+
}
|
|
326
|
+
// Copy bytes, zero-pad if shorter than repeat
|
|
327
|
+
const copyLen = Math.min(bytes.length, repeat);
|
|
328
|
+
for (let j = 0; j < copyLen; j++) {
|
|
329
|
+
view.setUint8(offset + j, bytes[j]);
|
|
330
|
+
}
|
|
331
|
+
for (let j = copyLen; j < repeat; j++) {
|
|
332
|
+
view.setUint8(offset + j, 0);
|
|
333
|
+
}
|
|
334
|
+
offset += repeat;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
275
337
|
default:
|
|
276
338
|
throw new Error(`Unknown format character: ${code}`);
|
|
277
339
|
}
|
|
340
|
+
// For 's', the repeat is consumed as byte-length in one go
|
|
341
|
+
if (code === "s") break;
|
|
278
342
|
}
|
|
279
343
|
}
|
|
280
344
|
|