@onekeyfe/hd-transport 1.1.28 → 1.2.0-alpha.1
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/README.md +2 -4
- package/__tests__/build-receive.test.js +6 -8
- package/__tests__/decode-features.test.js +3 -2
- package/__tests__/messages.test.js +8 -0
- package/__tests__/protocol-v2.test.js +754 -0
- package/dist/constants.d.ts +14 -5
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +905 -41
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +811 -84
- package/dist/protocols/index.d.ts +45 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/v1/decode.d.ts +11 -0
- package/dist/protocols/v1/decode.d.ts.map +1 -0
- package/dist/protocols/v1/encode.d.ts +11 -0
- package/dist/protocols/v1/encode.d.ts.map +1 -0
- package/dist/protocols/v1/index.d.ts +5 -0
- package/dist/protocols/v1/index.d.ts.map +1 -0
- package/dist/protocols/v1/packets.d.ts +7 -0
- package/dist/protocols/v1/packets.d.ts.map +1 -0
- package/dist/{serialization → protocols/v1}/receive.d.ts +1 -1
- package/dist/protocols/v1/receive.d.ts.map +1 -0
- package/dist/protocols/v2/constants.d.ts +7 -0
- package/dist/protocols/v2/constants.d.ts.map +1 -0
- package/dist/protocols/v2/crc8.d.ts +3 -0
- package/dist/protocols/v2/crc8.d.ts.map +1 -0
- package/dist/protocols/v2/debug.d.ts +13 -0
- package/dist/protocols/v2/debug.d.ts.map +1 -0
- package/dist/protocols/v2/decode.d.ts +8 -0
- package/dist/protocols/v2/decode.d.ts.map +1 -0
- package/dist/protocols/v2/encode.d.ts +5 -0
- package/dist/protocols/v2/encode.d.ts.map +1 -0
- package/dist/protocols/v2/frame-assembler.d.ts +12 -0
- package/dist/protocols/v2/frame-assembler.d.ts.map +1 -0
- package/dist/protocols/v2/index.d.ts +7 -0
- package/dist/protocols/v2/index.d.ts.map +1 -0
- package/dist/protocols/v2/session.d.ts +50 -0
- package/dist/protocols/v2/session.d.ts.map +1 -0
- package/dist/serialization/index.d.ts +6 -3
- package/dist/serialization/index.d.ts.map +1 -1
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/serialization/protobuf/messages.d.ts +1 -1
- package/dist/serialization/protobuf/messages.d.ts.map +1 -1
- package/dist/types/messages.d.ts +441 -11
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +14 -2
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/logBlockCommand.d.ts.map +1 -1
- package/messages-protocol-v2.json +13375 -0
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +314 -20
- package/scripts/protobuf-patches/TxInputType.js +1 -0
- package/scripts/protobuf-patches/index.js +2 -0
- package/scripts/protobuf-types.js +224 -18
- package/src/constants.ts +42 -6
- package/src/index.ts +39 -11
- package/src/protocols/index.ts +144 -0
- package/src/{serialization/protocol → protocols/v1}/decode.ts +4 -4
- package/src/{serialization/protocol → protocols/v1}/encode.ts +18 -13
- package/src/protocols/v1/index.ts +4 -0
- package/src/protocols/v1/packets.ts +53 -0
- package/src/{serialization → protocols/v1}/receive.ts +5 -5
- package/src/protocols/v2/constants.ts +6 -0
- package/src/protocols/v2/crc8.ts +34 -0
- package/src/protocols/v2/debug.ts +26 -0
- package/src/protocols/v2/decode.ts +92 -0
- package/src/protocols/v2/encode.ts +116 -0
- package/src/protocols/v2/frame-assembler.ts +98 -0
- package/src/protocols/v2/index.ts +6 -0
- package/src/protocols/v2/session.ts +429 -0
- package/src/serialization/index.ts +6 -5
- package/src/serialization/protobuf/decode.ts +7 -0
- package/src/serialization/protobuf/messages.ts +8 -4
- package/src/types/messages.ts +579 -13
- package/src/types/transport.ts +26 -2
- package/src/utils/logBlockCommand.ts +9 -1
- package/dist/serialization/protocol/decode.d.ts +0 -11
- package/dist/serialization/protocol/decode.d.ts.map +0 -1
- package/dist/serialization/protocol/encode.d.ts +0 -11
- package/dist/serialization/protocol/encode.d.ts.map +0 -1
- package/dist/serialization/protocol/index.d.ts +0 -3
- package/dist/serialization/protocol/index.d.ts.map +0 -1
- package/dist/serialization/receive.d.ts.map +0 -1
- package/dist/serialization/send.d.ts +0 -7
- package/dist/serialization/send.d.ts.map +0 -1
- package/src/serialization/protocol/index.ts +0 -2
- package/src/serialization/send.ts +0 -58
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
const { ProtocolV2 } = require('../src/protocols');
|
|
2
|
+
const { parseConfigure } = require('../src/serialization/protobuf/messages');
|
|
3
|
+
const sessionModule = require('../src/protocols/v2/session');
|
|
4
|
+
|
|
5
|
+
const { ProtocolV2FrameAssembler, ProtocolV2Session, probeProtocolV2 } = sessionModule;
|
|
6
|
+
const protocolV2 = require('../src/protocols/v2');
|
|
7
|
+
|
|
8
|
+
const protocolV1Messages = parseConfigure({
|
|
9
|
+
nested: {
|
|
10
|
+
Success: {
|
|
11
|
+
fields: {
|
|
12
|
+
message: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
id: 1,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
ButtonRequest: {
|
|
19
|
+
fields: {
|
|
20
|
+
code: {
|
|
21
|
+
type: 'uint32',
|
|
22
|
+
id: 1,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
OnekeyGetFeatures: {
|
|
27
|
+
fields: {},
|
|
28
|
+
},
|
|
29
|
+
OnekeyFeatures: {
|
|
30
|
+
fields: {},
|
|
31
|
+
},
|
|
32
|
+
MessageType: {
|
|
33
|
+
values: {
|
|
34
|
+
MessageType_Success: 2,
|
|
35
|
+
MessageType_ButtonRequest: 26,
|
|
36
|
+
MessageType_OnekeyGetFeatures: 10025,
|
|
37
|
+
MessageType_OnekeyFeatures: 10026,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const protocolV2Messages = parseConfigure({
|
|
44
|
+
nested: {
|
|
45
|
+
GetProtoVersion: {
|
|
46
|
+
fields: {},
|
|
47
|
+
},
|
|
48
|
+
ProtoVersion: {
|
|
49
|
+
fields: {
|
|
50
|
+
major_version: {
|
|
51
|
+
type: 'uint32',
|
|
52
|
+
id: 1,
|
|
53
|
+
},
|
|
54
|
+
minor_version: {
|
|
55
|
+
type: 'uint32',
|
|
56
|
+
id: 2,
|
|
57
|
+
},
|
|
58
|
+
patch_version: {
|
|
59
|
+
type: 'uint32',
|
|
60
|
+
id: 3,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
Ping: {
|
|
65
|
+
fields: {
|
|
66
|
+
message: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
id: 1,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
Success: {
|
|
73
|
+
fields: {
|
|
74
|
+
message: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
id: 1,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
DevFirmwareUpdate: {
|
|
81
|
+
fields: {},
|
|
82
|
+
},
|
|
83
|
+
DevFirmwareInstallProgress: {
|
|
84
|
+
fields: {
|
|
85
|
+
target_id: {
|
|
86
|
+
type: 'uint32',
|
|
87
|
+
id: 1,
|
|
88
|
+
},
|
|
89
|
+
progress: {
|
|
90
|
+
type: 'uint32',
|
|
91
|
+
id: 2,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
FileWrite: {
|
|
96
|
+
fields: {},
|
|
97
|
+
},
|
|
98
|
+
PartialNested: {
|
|
99
|
+
fields: {
|
|
100
|
+
child: {
|
|
101
|
+
type: 'NestedChild',
|
|
102
|
+
id: 1,
|
|
103
|
+
},
|
|
104
|
+
label: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
id: 2,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
NestedChild: {
|
|
111
|
+
fields: {
|
|
112
|
+
value: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
id: 1,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
MessageType: {
|
|
119
|
+
values: {
|
|
120
|
+
MessageType_GetProtoVersion: 60200,
|
|
121
|
+
MessageType_ProtoVersion: 60201,
|
|
122
|
+
MessageType_Ping: 60206,
|
|
123
|
+
MessageType_Success: 60207,
|
|
124
|
+
MessageType_FileWrite: 60805,
|
|
125
|
+
MessageType_DevFirmwareUpdate: 61000,
|
|
126
|
+
MessageType_DevFirmwareInstallProgress: 61001,
|
|
127
|
+
MessageType_PartialNested: 62000,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const schemas = {
|
|
134
|
+
protocolV1: protocolV1Messages,
|
|
135
|
+
protocolV2: protocolV2Messages,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const rewriteSeq = (frame, seq) => {
|
|
139
|
+
const copy = new Uint8Array(frame);
|
|
140
|
+
copy[6] = seq;
|
|
141
|
+
copy[copy.length - 1] = protocolV2.crc8(copy, copy.length - 1);
|
|
142
|
+
return copy;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
describe('Protocol V2 framing and session', () => {
|
|
146
|
+
test('encodes and decodes Protocol V2 protobuf frames', () => {
|
|
147
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
|
|
148
|
+
major_version: 1,
|
|
149
|
+
minor_version: 2,
|
|
150
|
+
patch_version: 3,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const parsed = protocolV2.decodeFrame(frame);
|
|
154
|
+
expect(parsed.messageTypeId).toBe(60201);
|
|
155
|
+
|
|
156
|
+
const decoded = ProtocolV2.decodeFrame(schemas, frame);
|
|
157
|
+
expect(decoded).toEqual({
|
|
158
|
+
type: 'ProtoVersion',
|
|
159
|
+
messageName: 'ProtoVersion',
|
|
160
|
+
messageTypeId: 60201,
|
|
161
|
+
pbPayload: parsed.pbPayload,
|
|
162
|
+
seq: parsed.seq,
|
|
163
|
+
message: {
|
|
164
|
+
major_version: 1,
|
|
165
|
+
minor_version: 2,
|
|
166
|
+
patch_version: 3,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('does not encode V1-only messages into Protocol V2 frames', () => {
|
|
172
|
+
expect(() => ProtocolV2.encodeFrame(schemas, 'Ping', { message: 'ok' })).not.toThrow();
|
|
173
|
+
expect(() => ProtocolV2.encodeFrame(schemas, 'OnekeyGetFeatures', {})).toThrow(
|
|
174
|
+
'Protocol V2 message "OnekeyGetFeatures" is not defined'
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('decodes Protocol V2 frames with the Protocol V2 catalog first', () => {
|
|
179
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
180
|
+
message: 'ok',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const parsed = protocolV2.decodeFrame(frame);
|
|
184
|
+
expect(parsed.messageTypeId).toBe(60207);
|
|
185
|
+
|
|
186
|
+
const decoded = ProtocolV2.decodeFrame(schemas, frame);
|
|
187
|
+
expect(decoded.type).toBe('Success');
|
|
188
|
+
expect(decoded.message).toEqual({ message: 'ok' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('decodes missing optional nested messages as null', () => {
|
|
192
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'PartialNested', {
|
|
193
|
+
label: 'only label',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const decoded = ProtocolV2.decodeFrame(schemas, frame);
|
|
197
|
+
expect(decoded.message).toEqual({
|
|
198
|
+
child: null,
|
|
199
|
+
label: 'only label',
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('reassembles split Protocol V2 frames and rejects oversized frames', () => {
|
|
204
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
|
|
205
|
+
major_version: 1,
|
|
206
|
+
minor_version: 0,
|
|
207
|
+
patch_version: 0,
|
|
208
|
+
});
|
|
209
|
+
const assembler = new ProtocolV2FrameAssembler();
|
|
210
|
+
|
|
211
|
+
expect(assembler.push(frame.slice(0, 4))).toBeUndefined();
|
|
212
|
+
expect(assembler.push(frame.slice(4))).toEqual(frame);
|
|
213
|
+
|
|
214
|
+
const oversized = new Uint8Array([0x5a, 0xff, 0xff]);
|
|
215
|
+
expect(() => assembler.push(oversized)).toThrow('Protocol V2 frame too large');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('keeps bytes after the first complete frame for the next read', () => {
|
|
219
|
+
const first = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
|
|
220
|
+
major_version: 1,
|
|
221
|
+
minor_version: 0,
|
|
222
|
+
patch_version: 0,
|
|
223
|
+
});
|
|
224
|
+
const second = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
|
|
225
|
+
major_version: 2,
|
|
226
|
+
minor_version: 0,
|
|
227
|
+
patch_version: 0,
|
|
228
|
+
});
|
|
229
|
+
const assembler = new ProtocolV2FrameAssembler();
|
|
230
|
+
const combined = new Uint8Array(first.length + second.length);
|
|
231
|
+
combined.set(first, 0);
|
|
232
|
+
combined.set(second, first.length);
|
|
233
|
+
|
|
234
|
+
expect(assembler.push(combined)).toEqual(first);
|
|
235
|
+
expect(assembler.push(new Uint8Array(0))).toEqual(second);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('session writes one encoded frame and decodes the response frame', async () => {
|
|
239
|
+
const written = [];
|
|
240
|
+
const response = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
|
|
241
|
+
major_version: 2,
|
|
242
|
+
minor_version: 0,
|
|
243
|
+
patch_version: 1,
|
|
244
|
+
});
|
|
245
|
+
const session = new ProtocolV2Session({
|
|
246
|
+
schemas,
|
|
247
|
+
router: 1,
|
|
248
|
+
writeFrame: frame => {
|
|
249
|
+
written.push(frame);
|
|
250
|
+
return Promise.resolve();
|
|
251
|
+
},
|
|
252
|
+
readFrame: () =>
|
|
253
|
+
Promise.resolve(rewriteSeq(response, protocolV2.decodeFrame(written[0]).seq)),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const result = await session.call('GetProtoVersion', {});
|
|
257
|
+
|
|
258
|
+
expect(written).toHaveLength(1);
|
|
259
|
+
expect(written[0][4]).toBe(1);
|
|
260
|
+
expect(written[0][5]).toBe(0);
|
|
261
|
+
expect(protocolV2.decodeFrame(written[0]).messageTypeId).toBe(60200);
|
|
262
|
+
expect(result).toEqual({
|
|
263
|
+
type: 'ProtoVersion',
|
|
264
|
+
message: {
|
|
265
|
+
major_version: 2,
|
|
266
|
+
minor_version: 0,
|
|
267
|
+
patch_version: 1,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('session starts response timeout after the frame is written', async () => {
|
|
273
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
274
|
+
message: 'ok',
|
|
275
|
+
});
|
|
276
|
+
const session = new ProtocolV2Session({
|
|
277
|
+
schemas,
|
|
278
|
+
router: 1,
|
|
279
|
+
writeFrame: () =>
|
|
280
|
+
new Promise(resolve => {
|
|
281
|
+
setTimeout(resolve, 30);
|
|
282
|
+
}),
|
|
283
|
+
readFrame: () => Promise.resolve(response),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await expect(
|
|
287
|
+
session.call('Ping', { message: 'hello' }, { timeoutMs: 10, expectedTypes: ['Success'] })
|
|
288
|
+
).resolves.toEqual({
|
|
289
|
+
type: 'Success',
|
|
290
|
+
message: {
|
|
291
|
+
message: 'ok',
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('session accepts response frames with a device-owned seq', async () => {
|
|
297
|
+
const response = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
|
|
298
|
+
major_version: 2,
|
|
299
|
+
minor_version: 0,
|
|
300
|
+
patch_version: 1,
|
|
301
|
+
});
|
|
302
|
+
const logger = {
|
|
303
|
+
debug: jest.fn(),
|
|
304
|
+
};
|
|
305
|
+
const session = new ProtocolV2Session({
|
|
306
|
+
schemas,
|
|
307
|
+
router: 1,
|
|
308
|
+
writeFrame: () => Promise.resolve(),
|
|
309
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 200)),
|
|
310
|
+
logger,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await expect(session.call('GetProtoVersion', {})).resolves.toEqual({
|
|
314
|
+
type: 'ProtoVersion',
|
|
315
|
+
message: {
|
|
316
|
+
major_version: 2,
|
|
317
|
+
minor_version: 0,
|
|
318
|
+
patch_version: 1,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('seq differs'));
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('session logs decoded transmit and receive payloads', async () => {
|
|
325
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
326
|
+
message: 'accepted',
|
|
327
|
+
});
|
|
328
|
+
const logger = {
|
|
329
|
+
debug: jest.fn(),
|
|
330
|
+
};
|
|
331
|
+
const session = new ProtocolV2Session({
|
|
332
|
+
schemas,
|
|
333
|
+
router: 1,
|
|
334
|
+
writeFrame: () => Promise.resolve(),
|
|
335
|
+
readFrame: () => Promise.resolve(response),
|
|
336
|
+
logger,
|
|
337
|
+
logPrefix: 'ProtocolV2 Test',
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await expect(session.call('Ping', { message: 'hello' })).resolves.toEqual({
|
|
341
|
+
type: 'Success',
|
|
342
|
+
message: {
|
|
343
|
+
message: 'accepted',
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(logger.debug).toHaveBeenCalledWith('[ProtocolV2 Test] TX payload name=Ping', {
|
|
348
|
+
message: 'hello',
|
|
349
|
+
});
|
|
350
|
+
expect(logger.debug).toHaveBeenCalledWith(
|
|
351
|
+
'[ProtocolV2 Test] encode raw frame',
|
|
352
|
+
expect.objectContaining({
|
|
353
|
+
context: 'tx:Ping',
|
|
354
|
+
messageTypeId: 60206,
|
|
355
|
+
router: 1,
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
expect(logger.debug).toHaveBeenCalledWith(
|
|
359
|
+
'[ProtocolV2 Test] decode raw frame',
|
|
360
|
+
expect.objectContaining({
|
|
361
|
+
context: 'rx:Ping',
|
|
362
|
+
messageTypeId: 60207,
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
expect(logger.debug).toHaveBeenCalledWith(
|
|
366
|
+
'[ProtocolV2 Test] RX payload type=Success messageTypeId=60207',
|
|
367
|
+
{
|
|
368
|
+
message: 'accepted',
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('session suppresses debug logs for file transfer calls', async () => {
|
|
374
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
375
|
+
message: 'ok',
|
|
376
|
+
});
|
|
377
|
+
const logger = {
|
|
378
|
+
debug: jest.fn(),
|
|
379
|
+
};
|
|
380
|
+
const session = new ProtocolV2Session({
|
|
381
|
+
schemas,
|
|
382
|
+
router: 1,
|
|
383
|
+
writeFrame: () => Promise.resolve(),
|
|
384
|
+
readFrame: () => Promise.resolve(response),
|
|
385
|
+
logger,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
await expect(session.call('FileWrite', {})).resolves.toEqual({
|
|
389
|
+
type: 'Success',
|
|
390
|
+
message: {
|
|
391
|
+
message: 'ok',
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('session skips unrelated terminal frames when expected response types are provided', async () => {
|
|
399
|
+
const stale = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
400
|
+
message: 'stale response',
|
|
401
|
+
});
|
|
402
|
+
const response = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
|
|
403
|
+
major_version: 2,
|
|
404
|
+
minor_version: 0,
|
|
405
|
+
patch_version: 1,
|
|
406
|
+
});
|
|
407
|
+
const logger = {
|
|
408
|
+
debug: jest.fn(),
|
|
409
|
+
};
|
|
410
|
+
const readFrame = jest.fn().mockResolvedValueOnce(stale).mockResolvedValueOnce(response);
|
|
411
|
+
const session = new ProtocolV2Session({
|
|
412
|
+
schemas,
|
|
413
|
+
router: 1,
|
|
414
|
+
writeFrame: () => Promise.resolve(),
|
|
415
|
+
readFrame,
|
|
416
|
+
logger,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await expect(
|
|
420
|
+
session.call('GetProtoVersion', {}, { expectedTypes: ['ProtoVersion'] })
|
|
421
|
+
).resolves.toEqual({
|
|
422
|
+
type: 'ProtoVersion',
|
|
423
|
+
message: {
|
|
424
|
+
major_version: 2,
|
|
425
|
+
minor_version: 0,
|
|
426
|
+
patch_version: 1,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
431
|
+
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('skip unexpected response'));
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('session consumes intermediate response frames before returning the final response', async () => {
|
|
435
|
+
const written = [];
|
|
436
|
+
const progress = ProtocolV2.encodeFrame(schemas, 'DevFirmwareInstallProgress', {
|
|
437
|
+
target_id: 0,
|
|
438
|
+
progress: 42,
|
|
439
|
+
});
|
|
440
|
+
const success = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
441
|
+
message: 'ok',
|
|
442
|
+
});
|
|
443
|
+
const onIntermediateResponse = jest.fn();
|
|
444
|
+
const readFrame = jest.fn(() => {
|
|
445
|
+
const [writtenFrame] = written;
|
|
446
|
+
const { seq } = protocolV2.decodeFrame(writtenFrame);
|
|
447
|
+
return Promise.resolve(
|
|
448
|
+
readFrame.mock.calls.length === 1 ? rewriteSeq(progress, seq) : rewriteSeq(success, seq)
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
const session = new ProtocolV2Session({
|
|
452
|
+
schemas,
|
|
453
|
+
router: 1,
|
|
454
|
+
writeFrame: frame => {
|
|
455
|
+
written.push(frame);
|
|
456
|
+
return Promise.resolve();
|
|
457
|
+
},
|
|
458
|
+
readFrame,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const result = await session.call(
|
|
462
|
+
'DevFirmwareUpdate',
|
|
463
|
+
{},
|
|
464
|
+
{
|
|
465
|
+
intermediateTypes: ['DevFirmwareInstallProgress'],
|
|
466
|
+
onIntermediateResponse,
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
471
|
+
expect(onIntermediateResponse).toHaveBeenCalledWith({
|
|
472
|
+
type: 'DevFirmwareInstallProgress',
|
|
473
|
+
message: {
|
|
474
|
+
target_id: 0,
|
|
475
|
+
progress: 42,
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
expect(result).toEqual({
|
|
479
|
+
type: 'Success',
|
|
480
|
+
message: {
|
|
481
|
+
message: 'ok',
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test('probeProtocolV2 accepts Success as a normal V2 probe response', async () => {
|
|
487
|
+
await expect(
|
|
488
|
+
probeProtocolV2({
|
|
489
|
+
call: () => Promise.resolve({ type: 'Success', message: {} }),
|
|
490
|
+
timeoutMs: 1,
|
|
491
|
+
})
|
|
492
|
+
).resolves.toBe(true);
|
|
493
|
+
|
|
494
|
+
await expect(
|
|
495
|
+
probeProtocolV2({
|
|
496
|
+
call: () => Promise.resolve({ type: 'Failure', message: {} }),
|
|
497
|
+
timeoutMs: 1,
|
|
498
|
+
})
|
|
499
|
+
).resolves.toBe(false);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('decodeFrame rejects frames that are too short', () => {
|
|
503
|
+
expect(() => protocolV2.decodeFrame(new Uint8Array([0x5a, 0x08, 0x00]))).toThrow(
|
|
504
|
+
'Protocol V2 frame too short'
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('decodeFrame rejects frames with an invalid SOF byte', () => {
|
|
509
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
510
|
+
const corrupted = new Uint8Array(frame);
|
|
511
|
+
corrupted[0] = 0x00;
|
|
512
|
+
expect(() => protocolV2.decodeFrame(corrupted)).toThrow('Invalid SOF byte');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('decodeFrame rejects frames with a header CRC mismatch', () => {
|
|
516
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
517
|
+
const corrupted = new Uint8Array(frame);
|
|
518
|
+
corrupted[3] = (corrupted[3] + 1) % 256;
|
|
519
|
+
expect(() => protocolV2.decodeFrame(corrupted)).toThrow('Header CRC mismatch');
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test('decodeFrame rejects frames with a frame CRC mismatch', () => {
|
|
523
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
524
|
+
const corrupted = new Uint8Array(frame);
|
|
525
|
+
corrupted[corrupted.length - 1] = (corrupted[corrupted.length - 1] + 1) % 256;
|
|
526
|
+
expect(() => protocolV2.decodeFrame(corrupted)).toThrow('Frame CRC mismatch');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('decodeFrame rejects frames whose payload is too short for a messageTypeId', () => {
|
|
530
|
+
// Raw frame with empty payload: 8 bytes of overhead, no messageTypeId.
|
|
531
|
+
const frame = protocolV2.encodeFrame(null);
|
|
532
|
+
expect(() => protocolV2.decodeFrame(frame)).toThrow('payload too short');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test('session call rejects when no response frame arrives before the timeout', async () => {
|
|
536
|
+
const session = new ProtocolV2Session({
|
|
537
|
+
schemas,
|
|
538
|
+
router: 1,
|
|
539
|
+
writeFrame: () => Promise.resolve(),
|
|
540
|
+
readFrame: () => new Promise(() => {}),
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
await expect(session.call('Ping', { message: 'x' }, { timeoutMs: 20 })).rejects.toThrow(
|
|
544
|
+
'Protocol V2 response timeout after 20ms for Ping'
|
|
545
|
+
);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test('session stops the read loop after a timeout instead of consuming later frames', async () => {
|
|
549
|
+
const success = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'late' });
|
|
550
|
+
let resolveRead;
|
|
551
|
+
const readFrame = jest.fn(
|
|
552
|
+
() =>
|
|
553
|
+
new Promise(resolve => {
|
|
554
|
+
resolveRead = resolve;
|
|
555
|
+
})
|
|
556
|
+
);
|
|
557
|
+
const session = new ProtocolV2Session({
|
|
558
|
+
schemas,
|
|
559
|
+
router: 1,
|
|
560
|
+
writeFrame: () => Promise.resolve(),
|
|
561
|
+
readFrame,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
await expect(
|
|
565
|
+
session.call('GetProtoVersion', {}, { timeoutMs: 10, expectedTypes: ['ProtoVersion'] })
|
|
566
|
+
).rejects.toThrow('Protocol V2 response timeout');
|
|
567
|
+
|
|
568
|
+
// Without cancellation the loop would skip this unexpected Success frame
|
|
569
|
+
// and call readFrame again, stealing frames from the next call.
|
|
570
|
+
resolveRead(success);
|
|
571
|
+
await new Promise(resolve => {
|
|
572
|
+
setTimeout(resolve, 20);
|
|
573
|
+
});
|
|
574
|
+
expect(readFrame).toHaveBeenCalledTimes(1);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('session serializes concurrent calls so responses cannot be stolen', async () => {
|
|
578
|
+
const events = [];
|
|
579
|
+
const written = [];
|
|
580
|
+
const session = new ProtocolV2Session({
|
|
581
|
+
schemas,
|
|
582
|
+
router: 1,
|
|
583
|
+
writeFrame: frame => {
|
|
584
|
+
written.push(frame);
|
|
585
|
+
events.push(`write:${written.length}`);
|
|
586
|
+
return new Promise(resolve => {
|
|
587
|
+
setTimeout(resolve, 10);
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
readFrame: () => {
|
|
591
|
+
events.push(`read:${written.length}`);
|
|
592
|
+
const [frame] = written.slice(-1);
|
|
593
|
+
const { seq } = protocolV2.decodeFrame(frame);
|
|
594
|
+
const response =
|
|
595
|
+
written.length === 1
|
|
596
|
+
? ProtocolV2.encodeFrame(schemas, 'Success', { message: 'first' })
|
|
597
|
+
: ProtocolV2.encodeFrame(schemas, 'Success', { message: 'second' });
|
|
598
|
+
return Promise.resolve(rewriteSeq(response, seq));
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const [first, second] = await Promise.all([
|
|
603
|
+
session.call('Ping', { message: '1' }, { expectedTypes: ['Success'] }),
|
|
604
|
+
session.call('Ping', { message: '2' }, { expectedTypes: ['Success'] }),
|
|
605
|
+
]);
|
|
606
|
+
|
|
607
|
+
expect(first.message).toEqual({ message: 'first' });
|
|
608
|
+
expect(second.message).toEqual({ message: 'second' });
|
|
609
|
+
// The second call must not start writing before the first call finished.
|
|
610
|
+
expect(events).toEqual(['write:1', 'read:1', 'write:2', 'read:2']);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test('session keeps serving calls after a previous call failed', async () => {
|
|
614
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
615
|
+
let shouldFail = true;
|
|
616
|
+
const session = new ProtocolV2Session({
|
|
617
|
+
schemas,
|
|
618
|
+
router: 1,
|
|
619
|
+
writeFrame: () => {
|
|
620
|
+
if (shouldFail) {
|
|
621
|
+
shouldFail = false;
|
|
622
|
+
return Promise.reject(new Error('transport write failed'));
|
|
623
|
+
}
|
|
624
|
+
return Promise.resolve();
|
|
625
|
+
},
|
|
626
|
+
readFrame: () => Promise.resolve(response),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
await expect(session.call('Ping', { message: '1' })).rejects.toThrow('transport write failed');
|
|
630
|
+
await expect(session.call('Ping', { message: '2' })).resolves.toEqual({
|
|
631
|
+
type: 'Success',
|
|
632
|
+
message: { message: 'ok' },
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test('session uses a per-session sequence counter starting at 1', async () => {
|
|
637
|
+
const written = [];
|
|
638
|
+
const makeSession = () =>
|
|
639
|
+
new ProtocolV2Session({
|
|
640
|
+
schemas,
|
|
641
|
+
router: 1,
|
|
642
|
+
writeFrame: frame => {
|
|
643
|
+
written.push(frame);
|
|
644
|
+
return Promise.resolve();
|
|
645
|
+
},
|
|
646
|
+
readFrame: () => {
|
|
647
|
+
const [frame] = written.slice(-1);
|
|
648
|
+
const { seq } = protocolV2.decodeFrame(frame);
|
|
649
|
+
return Promise.resolve(
|
|
650
|
+
rewriteSeq(ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }), seq)
|
|
651
|
+
);
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const sessionA = makeSession();
|
|
656
|
+
await sessionA.call('Ping', { message: '1' });
|
|
657
|
+
await sessionA.call('Ping', { message: '2' });
|
|
658
|
+
const sessionB = makeSession();
|
|
659
|
+
await sessionB.call('Ping', { message: '3' });
|
|
660
|
+
|
|
661
|
+
expect(written.map(frame => frame[6])).toEqual([1, 2, 1]);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test('assembler throws and resets on frames with an impossible length field', () => {
|
|
665
|
+
const assembler = new ProtocolV2FrameAssembler();
|
|
666
|
+
// expectedLen = 0 < 8-byte minimum: without the guard this poisons the
|
|
667
|
+
// buffer forever and deadlocks drain loops.
|
|
668
|
+
expect(() => assembler.push(new Uint8Array([0x5a, 0x00, 0x00]))).toThrow(
|
|
669
|
+
'Protocol V2 frame length too small: 0'
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
// Buffer must have been reset so the next valid frame goes through.
|
|
673
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
674
|
+
expect(assembler.push(frame)).toEqual(frame);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test('assembler validates the header CRC as soon as 4 bytes arrive', () => {
|
|
678
|
+
const assembler = new ProtocolV2FrameAssembler();
|
|
679
|
+
const header = new Uint8Array([0x5a, 0x10, 0x00, 0x00]);
|
|
680
|
+
header[3] = (protocolV2.crc8(header, 3) + 1) % 256;
|
|
681
|
+
|
|
682
|
+
expect(() => assembler.push(header)).toThrow('Protocol V2 header CRC mismatch');
|
|
683
|
+
|
|
684
|
+
// Buffer was reset: a valid frame parses afterwards.
|
|
685
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
686
|
+
expect(assembler.push(frame)).toEqual(frame);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test('assembler drain returns every buffered complete frame', () => {
|
|
690
|
+
const first = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
|
|
691
|
+
major_version: 1,
|
|
692
|
+
minor_version: 0,
|
|
693
|
+
patch_version: 0,
|
|
694
|
+
});
|
|
695
|
+
const second = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
696
|
+
const third = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'last' });
|
|
697
|
+
const assembler = new ProtocolV2FrameAssembler();
|
|
698
|
+
|
|
699
|
+
const combined = new Uint8Array(first.length + second.length + 3);
|
|
700
|
+
combined.set(first, 0);
|
|
701
|
+
combined.set(second, first.length);
|
|
702
|
+
combined.set(third.slice(0, 3), first.length + second.length);
|
|
703
|
+
|
|
704
|
+
expect(assembler.drain(combined)).toEqual([first, second]);
|
|
705
|
+
expect(assembler.drain()).toEqual([]);
|
|
706
|
+
expect(assembler.drain(third.slice(3))).toEqual([third]);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test('decodes allowlisted legacy V1 interaction messages as a fallback', () => {
|
|
710
|
+
// ButtonRequest only exists in the V1 schema; the V2 decoder should fall
|
|
711
|
+
// back to it because it is on the legacy decode allowlist.
|
|
712
|
+
const frame = protocolV2.encodeProtobufFrame(26, new Uint8Array(0));
|
|
713
|
+
const decoded = ProtocolV2.decodeFrame(schemas, frame);
|
|
714
|
+
expect(decoded.type).toBe('ButtonRequest');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test('does not fall back to legacy V1 messages outside the allowlist', () => {
|
|
718
|
+
// OnekeyFeatures exists only in the V1 schema and is not allowlisted.
|
|
719
|
+
const frame = protocolV2.encodeProtobufFrame(10026, new Uint8Array(0));
|
|
720
|
+
expect(() => ProtocolV2.decodeFrame(schemas, frame)).toThrow();
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test('hexToBytes converts valid hex and rejects malformed input', () => {
|
|
724
|
+
const { hexToBytes } = sessionModule;
|
|
725
|
+
expect(hexToBytes('5a0102')).toEqual(new Uint8Array([0x5a, 0x01, 0x02]));
|
|
726
|
+
expect(hexToBytes('')).toEqual(new Uint8Array(0));
|
|
727
|
+
expect(() => hexToBytes('abc')).toThrow('Invalid hex string: odd length');
|
|
728
|
+
expect(() => hexToBytes('zz')).toThrow('contains non-hex characters');
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test('probeProtocolV2 only uses Ping for acquire probing', async () => {
|
|
732
|
+
const call = jest.fn().mockRejectedValue(new Error('Ping timeout'));
|
|
733
|
+
const onProbeFailed = jest.fn();
|
|
734
|
+
|
|
735
|
+
await expect(
|
|
736
|
+
probeProtocolV2({
|
|
737
|
+
call,
|
|
738
|
+
timeoutMs: 1,
|
|
739
|
+
onProbeFailed,
|
|
740
|
+
})
|
|
741
|
+
).resolves.toBe(false);
|
|
742
|
+
expect(call).toHaveBeenNthCalledWith(
|
|
743
|
+
1,
|
|
744
|
+
'Ping',
|
|
745
|
+
{ message: 'protocol-v2-probe' },
|
|
746
|
+
{
|
|
747
|
+
timeoutMs: 1,
|
|
748
|
+
expectedTypes: ['Success'],
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
752
|
+
expect(onProbeFailed).toHaveBeenCalledWith(expect.any(Error));
|
|
753
|
+
});
|
|
754
|
+
});
|