appium-ios-remotexpc 0.13.2 → 0.14.0

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/src/index.d.ts +1 -1
  3. package/build/src/index.d.ts.map +1 -1
  4. package/build/src/lib/plist/binary-plist-creator.d.ts.map +1 -1
  5. package/build/src/lib/plist/binary-plist-creator.js +30 -0
  6. package/build/src/lib/plist/index.d.ts +1 -0
  7. package/build/src/lib/plist/index.d.ts.map +1 -1
  8. package/build/src/lib/plist/index.js +1 -0
  9. package/build/src/lib/plist/plist-uid.d.ts +10 -0
  10. package/build/src/lib/plist/plist-uid.d.ts.map +1 -0
  11. package/build/src/lib/plist/plist-uid.js +10 -0
  12. package/build/src/lib/types.d.ts +165 -2
  13. package/build/src/lib/types.d.ts.map +1 -1
  14. package/build/src/services/ios/dvt/channel-fragmenter.d.ts +21 -0
  15. package/build/src/services/ios/dvt/channel-fragmenter.d.ts.map +1 -0
  16. package/build/src/services/ios/dvt/channel-fragmenter.js +37 -0
  17. package/build/src/services/ios/dvt/channel.d.ts +32 -0
  18. package/build/src/services/ios/dvt/channel.d.ts.map +1 -0
  19. package/build/src/services/ios/dvt/channel.js +44 -0
  20. package/build/src/services/ios/dvt/dtx-message.d.ts +88 -0
  21. package/build/src/services/ios/dvt/dtx-message.d.ts.map +1 -0
  22. package/build/src/services/ios/dvt/dtx-message.js +113 -0
  23. package/build/src/services/ios/dvt/index.d.ts +119 -0
  24. package/build/src/services/ios/dvt/index.d.ts.map +1 -0
  25. package/build/src/services/ios/dvt/index.js +552 -0
  26. package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts +37 -0
  27. package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts.map +1 -0
  28. package/build/src/services/ios/dvt/instruments/condition-inducer.js +99 -0
  29. package/build/src/services/ios/dvt/instruments/location-simulation.d.ts +43 -0
  30. package/build/src/services/ios/dvt/instruments/location-simulation.d.ts.map +1 -0
  31. package/build/src/services/ios/dvt/instruments/location-simulation.js +60 -0
  32. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts +41 -0
  33. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts.map +1 -0
  34. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js +190 -0
  35. package/build/src/services/ios/dvt/utils.d.ts +19 -0
  36. package/build/src/services/ios/dvt/utils.d.ts.map +1 -0
  37. package/build/src/services/ios/dvt/utils.js +67 -0
  38. package/build/src/services.d.ts +2 -1
  39. package/build/src/services.d.ts.map +1 -1
  40. package/build/src/services.js +23 -0
  41. package/package.json +4 -1
  42. package/src/index.ts +6 -0
  43. package/src/lib/plist/binary-plist-creator.ts +30 -0
  44. package/src/lib/plist/index.ts +2 -0
  45. package/src/lib/plist/plist-uid.ts +9 -0
  46. package/src/lib/types.ts +179 -1
  47. package/src/services/ios/dvt/channel-fragmenter.ts +42 -0
  48. package/src/services/ios/dvt/channel.ts +58 -0
  49. package/src/services/ios/dvt/dtx-message.ts +162 -0
  50. package/src/services/ios/dvt/index.ts +727 -0
  51. package/src/services/ios/dvt/instruments/condition-inducer.ts +140 -0
  52. package/src/services/ios/dvt/instruments/location-simulation.ts +83 -0
  53. package/src/services/ios/dvt/nskeyedarchiver-decoder.ts +219 -0
  54. package/src/services/ios/dvt/utils.ts +89 -0
  55. package/src/services.ts +33 -0
@@ -0,0 +1,727 @@
1
+ import net from 'node:net';
2
+
3
+ import { getLogger } from '../../../lib/logger.js';
4
+ import {
5
+ PlistUID,
6
+ createBinaryPlist,
7
+ parseBinaryPlist,
8
+ } from '../../../lib/plist/index.js';
9
+ import type { PlistDictionary } from '../../../lib/types.js';
10
+ import { ServiceConnection } from '../../../service-connection.js';
11
+ import { BaseService, type Service } from '../base-service.js';
12
+ import { ChannelFragmenter } from './channel-fragmenter.js';
13
+ import { Channel } from './channel.js';
14
+ import { DTXMessage, DTX_CONSTANTS, MessageAux } from './dtx-message.js';
15
+ import { decodeNSKeyedArchiver } from './nskeyedarchiver-decoder.js';
16
+ import {
17
+ extractCapabilityStrings,
18
+ extractNSDictionary,
19
+ extractNSKeyedArchiverObjects,
20
+ hasNSErrorIndicators,
21
+ isNSDictionaryFormat,
22
+ } from './utils.js';
23
+
24
+ const log = getLogger('DVTSecureSocketProxyService');
25
+
26
+ const MIN_ERROR_DESCRIPTION_LENGTH = 20;
27
+
28
+ /**
29
+ * DVTSecureSocketProxyService provides access to Apple's DTServiceHub functionality
30
+ * This service enables various instruments and debugging capabilities through the DTX protocol
31
+ */
32
+ export class DVTSecureSocketProxyService extends BaseService {
33
+ static readonly RSD_SERVICE_NAME = 'com.apple.instruments.dtservicehub';
34
+ static readonly BROADCAST_CHANNEL = 0;
35
+
36
+ private connection: ServiceConnection | null = null;
37
+ private socket: net.Socket | null = null;
38
+ private supportedIdentifiers: PlistDictionary = {};
39
+ private lastChannelCode: number = 0;
40
+ private curMessageId: number = 0;
41
+ private readonly channelCache: Map<string, Channel> = new Map();
42
+ private readonly channelMessages: Map<number, ChannelFragmenter> = new Map();
43
+ private isHandshakeComplete: boolean = false;
44
+ private readBuffer: Buffer = Buffer.alloc(0);
45
+
46
+ constructor(address: [string, number]) {
47
+ super(address);
48
+ this.channelMessages.set(
49
+ DVTSecureSocketProxyService.BROADCAST_CHANNEL,
50
+ new ChannelFragmenter(),
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Connect to the DVT service and perform handshake
56
+ */
57
+ async connect(): Promise<void> {
58
+ if (this.connection) {
59
+ return;
60
+ }
61
+
62
+ const service: Service = {
63
+ serviceName: DVTSecureSocketProxyService.RSD_SERVICE_NAME,
64
+ port: this.address[1].toString(),
65
+ };
66
+
67
+ // DVT uses DTX binary protocol, connect without plist-based RSDCheckin
68
+ this.connection = await this.startLockdownWithoutCheckin(service);
69
+ this.socket = this.connection.getSocket();
70
+
71
+ // Remove SSL context if present for raw DTX communication
72
+ if ('_sslobj' in this.socket) {
73
+ (this.socket as any)._sslobj = null;
74
+ }
75
+
76
+ await this.performHandshake();
77
+ }
78
+
79
+ /**
80
+ * Get supported service identifiers (capabilities)
81
+ */
82
+ getSupportedIdentifiers(): PlistDictionary {
83
+ return this.supportedIdentifiers;
84
+ }
85
+
86
+ /**
87
+ * Create a communication channel for a specific service identifier
88
+ * @param identifier The service identifier (e.g., 'com.apple.instruments.server.services.LocationSimulation')
89
+ * @returns The created channel instance
90
+ */
91
+ async makeChannel(identifier: string): Promise<Channel> {
92
+ if (!this.isHandshakeComplete) {
93
+ throw new Error('Handshake not complete. Call connect() first.');
94
+ }
95
+
96
+ if (this.channelCache.has(identifier)) {
97
+ return this.channelCache.get(identifier)!;
98
+ }
99
+
100
+ this.lastChannelCode++;
101
+ const channelCode = this.lastChannelCode;
102
+
103
+ const args = new MessageAux();
104
+ args.appendInt(channelCode);
105
+ args.appendObj(identifier);
106
+
107
+ await this.sendMessage(0, '_requestChannelWithCode:identifier:', args);
108
+
109
+ const [ret] = await this.recvPlist();
110
+
111
+ // Check for NSError in response
112
+ this.checkForNSError(ret, 'Failed to create channel');
113
+
114
+ const channel = new Channel(channelCode, this);
115
+ this.channelCache.set(identifier, channel);
116
+ this.channelMessages.set(channelCode, new ChannelFragmenter());
117
+
118
+ return channel;
119
+ }
120
+
121
+ /**
122
+ * Send a DTX message on a channel
123
+ * @param channel The channel code
124
+ * @param selector The ObjectiveC method selector
125
+ * @param args Optional message arguments
126
+ * @param expectsReply Whether a reply is expected
127
+ */
128
+ async sendMessage(
129
+ channel: number,
130
+ selector: string | null = null,
131
+ args: MessageAux | null = null,
132
+ expectsReply: boolean = true,
133
+ ): Promise<void> {
134
+ if (!this.socket) {
135
+ throw new Error('Not connected to DVT service');
136
+ }
137
+
138
+ this.curMessageId++;
139
+
140
+ const auxBuffer = args ? this.buildAuxiliaryData(args) : Buffer.alloc(0);
141
+ const selectorBuffer = selector
142
+ ? this.archiveSelector(selector)
143
+ : Buffer.alloc(0);
144
+
145
+ let flags = DTX_CONSTANTS.INSTRUMENTS_MESSAGE_TYPE;
146
+ if (expectsReply) {
147
+ flags |= DTX_CONSTANTS.EXPECTS_REPLY_MASK;
148
+ }
149
+
150
+ const payloadHeader = DTXMessage.buildPayloadHeader({
151
+ flags,
152
+ auxiliaryLength: auxBuffer.length,
153
+ totalLength: BigInt(auxBuffer.length + selectorBuffer.length),
154
+ });
155
+
156
+ const messageHeader = DTXMessage.buildMessageHeader({
157
+ magic: DTX_CONSTANTS.MESSAGE_HEADER_MAGIC,
158
+ cb: DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
159
+ fragmentId: 0,
160
+ fragmentCount: 1,
161
+ length:
162
+ DTX_CONSTANTS.PAYLOAD_HEADER_SIZE +
163
+ auxBuffer.length +
164
+ selectorBuffer.length,
165
+ identifier: this.curMessageId,
166
+ conversationIndex: 0,
167
+ channelCode: channel,
168
+ expectsReply: expectsReply ? 1 : 0,
169
+ });
170
+
171
+ const message = Buffer.concat([
172
+ messageHeader,
173
+ payloadHeader,
174
+ auxBuffer,
175
+ selectorBuffer,
176
+ ]);
177
+
178
+ await new Promise<void>((resolve, reject) => {
179
+ this.socket!.write(message, (err) => {
180
+ if (err) {
181
+ reject(err);
182
+ } else {
183
+ resolve();
184
+ }
185
+ });
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Receive a plist message from a channel
191
+ * @param channel The channel to receive from
192
+ * @returns Tuple of [decoded data, auxiliary values]
193
+ */
194
+ async recvPlist(
195
+ channel: number = DVTSecureSocketProxyService.BROADCAST_CHANNEL,
196
+ ): Promise<[any, any[]]> {
197
+ const [data, aux] = await this.recvMessage(channel);
198
+
199
+ let decodedData = null;
200
+ if (data?.length) {
201
+ try {
202
+ decodedData = parseBinaryPlist(data);
203
+ // decode NSKeyedArchiver format
204
+ decodedData = decodeNSKeyedArchiver(decodedData);
205
+ } catch (error) {
206
+ log.warn('Failed to parse plist data:', error);
207
+ }
208
+ }
209
+
210
+ return [decodedData, aux];
211
+ }
212
+
213
+ /**
214
+ * Receive a raw message from a channel
215
+ * @param channel The channel to receive from
216
+ * @returns Tuple of [raw data, auxiliary values]
217
+ */
218
+ async recvMessage(
219
+ channel: number = DVTSecureSocketProxyService.BROADCAST_CHANNEL,
220
+ ): Promise<[Buffer | null, any[]]> {
221
+ const packetData = await this.recvPacketFragments(channel);
222
+
223
+ const payloadHeader = DTXMessage.parsePayloadHeader(packetData);
224
+
225
+ const compression = (payloadHeader.flags & 0xff000) >> 12;
226
+ if (compression) {
227
+ throw new Error('Compressed messages not supported');
228
+ }
229
+
230
+ let offset = DTX_CONSTANTS.PAYLOAD_HEADER_SIZE;
231
+
232
+ // Parse auxiliary data if present
233
+ let aux: any[] = [];
234
+ if (payloadHeader.auxiliaryLength > 0) {
235
+ const auxBuffer = packetData.subarray(
236
+ offset,
237
+ offset + payloadHeader.auxiliaryLength,
238
+ );
239
+ aux = this.parseAuxiliaryData(auxBuffer);
240
+ offset += payloadHeader.auxiliaryLength;
241
+ }
242
+
243
+ // Extract object data
244
+ const objSize =
245
+ Number(payloadHeader.totalLength) - payloadHeader.auxiliaryLength;
246
+ const data =
247
+ objSize > 0 ? packetData.subarray(offset, offset + objSize) : null;
248
+
249
+ return [data, aux];
250
+ }
251
+
252
+ /**
253
+ * Close the DVT service connection
254
+ */
255
+ async close(): Promise<void> {
256
+ if (!this.connection) {
257
+ return;
258
+ }
259
+
260
+ // Send channel cancellation for all active channels
261
+ const activeCodes = Array.from(this.channelMessages.keys()).filter(
262
+ (code) => code > 0,
263
+ );
264
+
265
+ if (activeCodes.length > 0) {
266
+ const args = new MessageAux();
267
+ for (const code of activeCodes) {
268
+ args.appendInt(code);
269
+ }
270
+
271
+ try {
272
+ await this.sendMessage(
273
+ DVTSecureSocketProxyService.BROADCAST_CHANNEL,
274
+ '_channelCanceled:',
275
+ args,
276
+ false,
277
+ );
278
+ } catch (error) {
279
+ log.debug('Error sending channel canceled message:', error);
280
+ }
281
+ }
282
+
283
+ this.connection.close();
284
+ this.connection = null;
285
+ this.socket = null;
286
+ this.isHandshakeComplete = false;
287
+ this.channelCache.clear();
288
+ this.channelMessages.clear();
289
+ this.channelMessages.set(
290
+ DVTSecureSocketProxyService.BROADCAST_CHANNEL,
291
+ new ChannelFragmenter(),
292
+ );
293
+ }
294
+
295
+ /**
296
+ * Perform DTX protocol handshake to establish connection and retrieve capabilities
297
+ */
298
+ private async performHandshake(): Promise<void> {
299
+ const args = new MessageAux();
300
+ args.appendObj({
301
+ 'com.apple.private.DTXBlockCompression': 0,
302
+ 'com.apple.private.DTXConnection': 1,
303
+ });
304
+ await this.sendMessage(0, '_notifyOfPublishedCapabilities:', args, false);
305
+
306
+ const [retData, aux] = await this.recvMessage();
307
+ const ret = retData ? parseBinaryPlist(retData) : null;
308
+
309
+ const selectorName = this.extractSelectorFromResponse(ret);
310
+ if (selectorName !== '_notifyOfPublishedCapabilities:') {
311
+ throw new Error(`Invalid handshake response selector: ${selectorName}`);
312
+ }
313
+
314
+ if (!aux || aux.length === 0) {
315
+ throw new Error('Invalid handshake response: missing capabilities');
316
+ }
317
+
318
+ // Extract server capabilities from auxiliary data
319
+ this.supportedIdentifiers = this.extractCapabilitiesFromAuxData(aux[0]);
320
+
321
+ this.isHandshakeComplete = true;
322
+
323
+ log.debug(
324
+ `DVT handshake complete. Found ${Object.keys(this.supportedIdentifiers).length} supported identifiers`,
325
+ );
326
+
327
+ // Consume any additional messages buffered after handshake
328
+ await this.drainBufferedMessages();
329
+ }
330
+
331
+ private extractSelectorFromResponse(ret: any): string {
332
+ if (typeof ret === 'string') {
333
+ return ret;
334
+ }
335
+ const objects = extractNSKeyedArchiverObjects(ret);
336
+ if (objects) {
337
+ return objects[1];
338
+ }
339
+
340
+ throw new Error('Invalid handshake response');
341
+ }
342
+
343
+ private extractCapabilitiesFromAuxData(
344
+ capabilitiesData: any,
345
+ ): PlistDictionary {
346
+ const objects = extractNSKeyedArchiverObjects(capabilitiesData);
347
+ if (!objects) {
348
+ return capabilitiesData || {};
349
+ }
350
+
351
+ const dictObj = objects[1];
352
+
353
+ if (isNSDictionaryFormat(dictObj)) {
354
+ return extractNSDictionary(dictObj, objects);
355
+ }
356
+
357
+ return extractCapabilityStrings(objects);
358
+ }
359
+
360
+ /**
361
+ * Drain any buffered messages that arrived during handshake
362
+ */
363
+ private async drainBufferedMessages(): Promise<void> {
364
+ if (this.readBuffer.length === 0) {
365
+ return;
366
+ }
367
+
368
+ try {
369
+ while (this.readBuffer.length >= DTX_CONSTANTS.MESSAGE_HEADER_SIZE) {
370
+ const headerData = this.readBuffer.subarray(
371
+ 0,
372
+ DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
373
+ );
374
+ const header = DTXMessage.parseMessageHeader(headerData);
375
+
376
+ const totalSize = DTX_CONSTANTS.MESSAGE_HEADER_SIZE + header.length;
377
+ if (this.readBuffer.length >= totalSize) {
378
+ // Consume complete buffered message
379
+ this.readBuffer = this.readBuffer.subarray(
380
+ DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
381
+ );
382
+ this.readBuffer = this.readBuffer.subarray(header.length);
383
+ } else {
384
+ break;
385
+ }
386
+ }
387
+ } catch (error) {
388
+ log.debug('Error while draining buffer:', error);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Receive packet fragments until a complete message is available for the specified channel
394
+ */
395
+ private async recvPacketFragments(channel: number): Promise<Buffer> {
396
+ while (true) {
397
+ const fragmenter = this.channelMessages.get(channel);
398
+ if (!fragmenter) {
399
+ throw new Error(`No fragmenter for channel ${channel}`);
400
+ }
401
+
402
+ // Check if we have a complete message
403
+ const message = fragmenter.get();
404
+ if (message) {
405
+ return message;
406
+ }
407
+
408
+ // Read next message header
409
+ const headerData = await this.readExact(
410
+ DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
411
+ );
412
+ const header = DTXMessage.parseMessageHeader(headerData);
413
+
414
+ const receivedChannel = Math.abs(header.channelCode);
415
+
416
+ if (!this.channelMessages.has(receivedChannel)) {
417
+ this.channelMessages.set(receivedChannel, new ChannelFragmenter());
418
+ }
419
+
420
+ // Update message ID tracker
421
+ if (!header.conversationIndex && header.identifier > this.curMessageId) {
422
+ this.curMessageId = header.identifier;
423
+ }
424
+
425
+ // Skip first fragment header for multi-fragment messages
426
+ if (header.fragmentCount > 1 && header.fragmentId === 0) {
427
+ continue;
428
+ }
429
+
430
+ // Read message payload
431
+ const messageData = await this.readExact(header.length);
432
+
433
+ // Add fragment to appropriate channel
434
+ const targetFragmenter = this.channelMessages.get(receivedChannel)!;
435
+ targetFragmenter.addFragment(header, messageData);
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Read exact number of bytes from socket with buffering
441
+ */
442
+ private async readExact(length: number): Promise<Buffer> {
443
+ if (!this.socket) {
444
+ throw new Error(
445
+ `${this.constructor.name} is not initialized. Call connect() before sending messages.`,
446
+ );
447
+ }
448
+
449
+ // Keep reading until we have enough data
450
+ while (this.readBuffer.length < length) {
451
+ const chunk = await new Promise<Buffer>((resolve, reject) => {
452
+ const onData = (data: Buffer) => {
453
+ this.socket!.off('data', onData);
454
+ this.socket!.off('error', onError);
455
+ resolve(data);
456
+ };
457
+
458
+ const onError = (err: Error) => {
459
+ this.socket!.off('data', onData);
460
+ this.socket!.off('error', onError);
461
+ reject(err);
462
+ };
463
+
464
+ this.socket!.once('data', onData);
465
+ this.socket!.once('error', onError);
466
+ });
467
+
468
+ this.readBuffer = Buffer.concat([this.readBuffer, chunk]);
469
+ }
470
+
471
+ // Extract exact amount requested
472
+ const result = this.readBuffer.subarray(0, length);
473
+ this.readBuffer = this.readBuffer.subarray(length);
474
+
475
+ return result;
476
+ }
477
+
478
+ /**
479
+ * Check if response contains an NSError and throw if present
480
+ */
481
+ private checkForNSError(response: any, context: string): void {
482
+ if (!response || typeof response !== 'object') {
483
+ return;
484
+ }
485
+
486
+ // Check NSKeyedArchiver format
487
+ const objects = extractNSKeyedArchiverObjects(response);
488
+ if (objects) {
489
+ // Check for NSError indicators in $objects
490
+ const hasNSError = objects.some((o) => hasNSErrorIndicators(o));
491
+
492
+ if (hasNSError) {
493
+ const errorMsg =
494
+ objects.find(
495
+ (o: any) =>
496
+ typeof o === 'string' && o.length > MIN_ERROR_DESCRIPTION_LENGTH,
497
+ ) || 'Unknown error';
498
+ throw new Error(`${context}: ${errorMsg}`);
499
+ }
500
+ }
501
+
502
+ // Check direct NSError format
503
+ if (hasNSErrorIndicators(response)) {
504
+ throw new Error(`${context}: ${JSON.stringify(response)}`);
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Archive a value using NSKeyedArchiver format for DTX protocol
510
+ */
511
+ private archiveValue(value: any): Buffer {
512
+ const archived = {
513
+ $version: 100000,
514
+ $archiver: 'NSKeyedArchiver',
515
+ $top: { root: new PlistUID(1) },
516
+ $objects: ['$null', value],
517
+ };
518
+
519
+ return createBinaryPlist(archived);
520
+ }
521
+
522
+ /**
523
+ * Archive a selector string for DTX messages
524
+ */
525
+ private archiveSelector(selector: string): Buffer {
526
+ return this.archiveValue(selector);
527
+ }
528
+
529
+ /**
530
+ * Build auxiliary data buffer with NSKeyedArchiver encoding for objects
531
+ */
532
+ private buildAuxiliaryData(args: MessageAux): Buffer {
533
+ const values = args.getValues();
534
+
535
+ if (values.length === 0) {
536
+ return Buffer.alloc(0);
537
+ }
538
+
539
+ const itemBuffers: Buffer[] = [];
540
+
541
+ for (const auxValue of values) {
542
+ // Empty dictionary marker
543
+ const dictMarker = Buffer.alloc(4);
544
+ dictMarker.writeUInt32LE(DTX_CONSTANTS.EMPTY_DICTIONARY, 0);
545
+ itemBuffers.push(dictMarker);
546
+
547
+ // Type marker
548
+ const typeBuffer = Buffer.alloc(4);
549
+ typeBuffer.writeUInt32LE(auxValue.type, 0);
550
+ itemBuffers.push(typeBuffer);
551
+
552
+ // Value data
553
+ switch (auxValue.type) {
554
+ case DTX_CONSTANTS.AUX_TYPE_INT32: {
555
+ const valueBuffer = Buffer.alloc(4);
556
+ valueBuffer.writeUInt32LE(auxValue.value, 0);
557
+ itemBuffers.push(valueBuffer);
558
+ break;
559
+ }
560
+
561
+ case DTX_CONSTANTS.AUX_TYPE_INT64: {
562
+ const valueBuffer = Buffer.alloc(8);
563
+ valueBuffer.writeBigUInt64LE(BigInt(auxValue.value), 0);
564
+ itemBuffers.push(valueBuffer);
565
+ break;
566
+ }
567
+
568
+ case DTX_CONSTANTS.AUX_TYPE_OBJECT: {
569
+ const encodedPlist = this.archiveValue(auxValue.value);
570
+ const lengthBuffer = Buffer.alloc(4);
571
+ lengthBuffer.writeUInt32LE(encodedPlist.length, 0);
572
+ itemBuffers.push(lengthBuffer);
573
+ itemBuffers.push(encodedPlist);
574
+ break;
575
+ }
576
+
577
+ default:
578
+ throw new Error(`Unsupported auxiliary type: ${auxValue.type}`);
579
+ }
580
+ }
581
+
582
+ const itemsData = Buffer.concat(itemBuffers);
583
+
584
+ // Build header: magic + total size of items
585
+ const header = Buffer.alloc(16);
586
+ header.writeBigUInt64LE(BigInt(DTX_CONSTANTS.MESSAGE_AUX_MAGIC), 0);
587
+ header.writeBigUInt64LE(BigInt(itemsData.length), 8);
588
+
589
+ return Buffer.concat([header, itemsData]);
590
+ }
591
+
592
+ /**
593
+ * Parse auxiliary data from buffer
594
+ *
595
+ * The auxiliary data format can be:
596
+ * 1. Standard format: [magic:8][size:8][items...]
597
+ * 2. NSKeyedArchiver bplist format (for handshake responses)
598
+ */
599
+ private parseAuxiliaryData(buffer: Buffer): any[] {
600
+ if (buffer.length < 16) {
601
+ return [];
602
+ }
603
+
604
+ const magic = buffer.readBigUInt64LE(0);
605
+
606
+ // Check if this is NSKeyedArchiver bplist format (handshake response)
607
+ if (magic !== BigInt(DTX_CONSTANTS.MESSAGE_AUX_MAGIC)) {
608
+ return this.parseAuxiliaryAsBplist(buffer);
609
+ }
610
+
611
+ // Standard auxiliary format
612
+ return this.parseAuxiliaryStandard(buffer);
613
+ }
614
+
615
+ /**
616
+ * Parse auxiliary data in NSKeyedArchiver bplist format
617
+ */
618
+ private parseAuxiliaryAsBplist(buffer: Buffer): any[] {
619
+ // Find bplist header in buffer
620
+ const bplistMagic = 'bplist00';
621
+ for (let i = 0; i < Math.min(100, buffer.length - 8); i++) {
622
+ if (buffer.toString('ascii', i, i + 8) === bplistMagic) {
623
+ try {
624
+ const plistBuffer = buffer.subarray(i);
625
+ const parsed = parseBinaryPlist(plistBuffer);
626
+ return Array.isArray(parsed) ? parsed : [parsed];
627
+ } catch (error) {
628
+ log.warn('Failed to parse auxiliary bplist:', error);
629
+ }
630
+ break;
631
+ }
632
+ }
633
+ return [];
634
+ }
635
+
636
+ /**
637
+ * Parse auxiliary data in standard DTX format
638
+ */
639
+ private parseAuxiliaryStandard(buffer: Buffer): any[] {
640
+ const values: any[] = [];
641
+ let offset = 16; // Skip magic (8) + size (8)
642
+
643
+ const totalSize = buffer.readBigUInt64LE(8);
644
+ const endOffset = offset + Number(totalSize);
645
+
646
+ while (offset < endOffset && offset < buffer.length) {
647
+ // Read and validate empty dictionary marker
648
+ const marker = buffer.readUInt32LE(offset);
649
+ offset += 4;
650
+
651
+ if (marker !== DTX_CONSTANTS.EMPTY_DICTIONARY) {
652
+ offset -= 4; // Rewind if not the expected marker
653
+ }
654
+
655
+ // Read type
656
+ const type = buffer.readUInt32LE(offset);
657
+ offset += 4;
658
+
659
+ // Read value based on type
660
+ try {
661
+ const value = this.parseAuxiliaryValue(buffer, type, offset);
662
+ values.push(value.data);
663
+ offset = value.newOffset;
664
+ } catch (error) {
665
+ log.warn(`Failed to parse auxiliary value at offset ${offset}:`, error);
666
+ break;
667
+ }
668
+ }
669
+
670
+ return values;
671
+ }
672
+
673
+ /**
674
+ * Parse a single auxiliary value
675
+ */
676
+ private parseAuxiliaryValue(
677
+ buffer: Buffer,
678
+ type: number,
679
+ offset: number,
680
+ ): { data: any; newOffset: number } {
681
+ switch (type) {
682
+ case DTX_CONSTANTS.AUX_TYPE_INT32:
683
+ return {
684
+ data: buffer.readUInt32LE(offset),
685
+ newOffset: offset + 4,
686
+ };
687
+
688
+ case DTX_CONSTANTS.AUX_TYPE_INT64:
689
+ return {
690
+ data: buffer.readBigUInt64LE(offset),
691
+ newOffset: offset + 8,
692
+ };
693
+
694
+ case DTX_CONSTANTS.AUX_TYPE_OBJECT: {
695
+ const length = buffer.readUInt32LE(offset);
696
+ const plistData = buffer.subarray(offset + 4, offset + 4 + length);
697
+
698
+ let parsed: any;
699
+ try {
700
+ parsed = parseBinaryPlist(plistData);
701
+ } catch (error) {
702
+ log.warn('Failed to parse auxiliary object plist:', error);
703
+ parsed = plistData;
704
+ }
705
+
706
+ return {
707
+ data: parsed,
708
+ newOffset: offset + 4 + length,
709
+ };
710
+ }
711
+
712
+ default:
713
+ throw new Error(`Unknown auxiliary type: ${type}`);
714
+ }
715
+ }
716
+ }
717
+
718
+ export { Channel, ChannelFragmenter, DTXMessage, MessageAux, DTX_CONSTANTS };
719
+ export {
720
+ decodeNSKeyedArchiver,
721
+ NSKeyedArchiverDecoder,
722
+ } from './nskeyedarchiver-decoder.js';
723
+ export type {
724
+ DTXMessageHeader,
725
+ DTXMessagePayloadHeader,
726
+ MessageAuxValue,
727
+ } from './dtx-message.js';