appium-ios-remotexpc 0.13.2 → 0.15.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 (59) hide show
  1. package/CHANGELOG.md +12 -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 +177 -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/instruments/screenshot.d.ts +17 -0
  33. package/build/src/services/ios/dvt/instruments/screenshot.d.ts.map +1 -0
  34. package/build/src/services/ios/dvt/instruments/screenshot.js +35 -0
  35. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts +41 -0
  36. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts.map +1 -0
  37. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js +195 -0
  38. package/build/src/services/ios/dvt/utils.d.ts +19 -0
  39. package/build/src/services/ios/dvt/utils.d.ts.map +1 -0
  40. package/build/src/services/ios/dvt/utils.js +67 -0
  41. package/build/src/services.d.ts +2 -1
  42. package/build/src/services.d.ts.map +1 -1
  43. package/build/src/services.js +26 -0
  44. package/package.json +5 -1
  45. package/src/index.ts +7 -0
  46. package/src/lib/plist/binary-plist-creator.ts +30 -0
  47. package/src/lib/plist/index.ts +2 -0
  48. package/src/lib/plist/plist-uid.ts +9 -0
  49. package/src/lib/types.ts +192 -1
  50. package/src/services/ios/dvt/channel-fragmenter.ts +42 -0
  51. package/src/services/ios/dvt/channel.ts +58 -0
  52. package/src/services/ios/dvt/dtx-message.ts +162 -0
  53. package/src/services/ios/dvt/index.ts +727 -0
  54. package/src/services/ios/dvt/instruments/condition-inducer.ts +140 -0
  55. package/src/services/ios/dvt/instruments/location-simulation.ts +83 -0
  56. package/src/services/ios/dvt/instruments/screenshot.ts +47 -0
  57. package/src/services/ios/dvt/nskeyedarchiver-decoder.ts +225 -0
  58. package/src/services/ios/dvt/utils.ts +89 -0
  59. package/src/services.ts +36 -0
@@ -0,0 +1,552 @@
1
+ import net from 'node:net';
2
+ import { getLogger } from '../../../lib/logger.js';
3
+ import { PlistUID, createBinaryPlist, parseBinaryPlist, } from '../../../lib/plist/index.js';
4
+ import { ServiceConnection } from '../../../service-connection.js';
5
+ import { BaseService } from '../base-service.js';
6
+ import { ChannelFragmenter } from './channel-fragmenter.js';
7
+ import { Channel } from './channel.js';
8
+ import { DTXMessage, DTX_CONSTANTS, MessageAux } from './dtx-message.js';
9
+ import { decodeNSKeyedArchiver } from './nskeyedarchiver-decoder.js';
10
+ import { extractCapabilityStrings, extractNSDictionary, extractNSKeyedArchiverObjects, hasNSErrorIndicators, isNSDictionaryFormat, } from './utils.js';
11
+ const log = getLogger('DVTSecureSocketProxyService');
12
+ const MIN_ERROR_DESCRIPTION_LENGTH = 20;
13
+ /**
14
+ * DVTSecureSocketProxyService provides access to Apple's DTServiceHub functionality
15
+ * This service enables various instruments and debugging capabilities through the DTX protocol
16
+ */
17
+ export class DVTSecureSocketProxyService extends BaseService {
18
+ static RSD_SERVICE_NAME = 'com.apple.instruments.dtservicehub';
19
+ static BROADCAST_CHANNEL = 0;
20
+ connection = null;
21
+ socket = null;
22
+ supportedIdentifiers = {};
23
+ lastChannelCode = 0;
24
+ curMessageId = 0;
25
+ channelCache = new Map();
26
+ channelMessages = new Map();
27
+ isHandshakeComplete = false;
28
+ readBuffer = Buffer.alloc(0);
29
+ constructor(address) {
30
+ super(address);
31
+ this.channelMessages.set(DVTSecureSocketProxyService.BROADCAST_CHANNEL, new ChannelFragmenter());
32
+ }
33
+ /**
34
+ * Connect to the DVT service and perform handshake
35
+ */
36
+ async connect() {
37
+ if (this.connection) {
38
+ return;
39
+ }
40
+ const service = {
41
+ serviceName: DVTSecureSocketProxyService.RSD_SERVICE_NAME,
42
+ port: this.address[1].toString(),
43
+ };
44
+ // DVT uses DTX binary protocol, connect without plist-based RSDCheckin
45
+ this.connection = await this.startLockdownWithoutCheckin(service);
46
+ this.socket = this.connection.getSocket();
47
+ // Remove SSL context if present for raw DTX communication
48
+ if ('_sslobj' in this.socket) {
49
+ this.socket._sslobj = null;
50
+ }
51
+ await this.performHandshake();
52
+ }
53
+ /**
54
+ * Get supported service identifiers (capabilities)
55
+ */
56
+ getSupportedIdentifiers() {
57
+ return this.supportedIdentifiers;
58
+ }
59
+ /**
60
+ * Create a communication channel for a specific service identifier
61
+ * @param identifier The service identifier (e.g., 'com.apple.instruments.server.services.LocationSimulation')
62
+ * @returns The created channel instance
63
+ */
64
+ async makeChannel(identifier) {
65
+ if (!this.isHandshakeComplete) {
66
+ throw new Error('Handshake not complete. Call connect() first.');
67
+ }
68
+ if (this.channelCache.has(identifier)) {
69
+ return this.channelCache.get(identifier);
70
+ }
71
+ this.lastChannelCode++;
72
+ const channelCode = this.lastChannelCode;
73
+ const args = new MessageAux();
74
+ args.appendInt(channelCode);
75
+ args.appendObj(identifier);
76
+ await this.sendMessage(0, '_requestChannelWithCode:identifier:', args);
77
+ const [ret] = await this.recvPlist();
78
+ // Check for NSError in response
79
+ this.checkForNSError(ret, 'Failed to create channel');
80
+ const channel = new Channel(channelCode, this);
81
+ this.channelCache.set(identifier, channel);
82
+ this.channelMessages.set(channelCode, new ChannelFragmenter());
83
+ return channel;
84
+ }
85
+ /**
86
+ * Send a DTX message on a channel
87
+ * @param channel The channel code
88
+ * @param selector The ObjectiveC method selector
89
+ * @param args Optional message arguments
90
+ * @param expectsReply Whether a reply is expected
91
+ */
92
+ async sendMessage(channel, selector = null, args = null, expectsReply = true) {
93
+ if (!this.socket) {
94
+ throw new Error('Not connected to DVT service');
95
+ }
96
+ this.curMessageId++;
97
+ const auxBuffer = args ? this.buildAuxiliaryData(args) : Buffer.alloc(0);
98
+ const selectorBuffer = selector
99
+ ? this.archiveSelector(selector)
100
+ : Buffer.alloc(0);
101
+ let flags = DTX_CONSTANTS.INSTRUMENTS_MESSAGE_TYPE;
102
+ if (expectsReply) {
103
+ flags |= DTX_CONSTANTS.EXPECTS_REPLY_MASK;
104
+ }
105
+ const payloadHeader = DTXMessage.buildPayloadHeader({
106
+ flags,
107
+ auxiliaryLength: auxBuffer.length,
108
+ totalLength: BigInt(auxBuffer.length + selectorBuffer.length),
109
+ });
110
+ const messageHeader = DTXMessage.buildMessageHeader({
111
+ magic: DTX_CONSTANTS.MESSAGE_HEADER_MAGIC,
112
+ cb: DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
113
+ fragmentId: 0,
114
+ fragmentCount: 1,
115
+ length: DTX_CONSTANTS.PAYLOAD_HEADER_SIZE +
116
+ auxBuffer.length +
117
+ selectorBuffer.length,
118
+ identifier: this.curMessageId,
119
+ conversationIndex: 0,
120
+ channelCode: channel,
121
+ expectsReply: expectsReply ? 1 : 0,
122
+ });
123
+ const message = Buffer.concat([
124
+ messageHeader,
125
+ payloadHeader,
126
+ auxBuffer,
127
+ selectorBuffer,
128
+ ]);
129
+ await new Promise((resolve, reject) => {
130
+ this.socket.write(message, (err) => {
131
+ if (err) {
132
+ reject(err);
133
+ }
134
+ else {
135
+ resolve();
136
+ }
137
+ });
138
+ });
139
+ }
140
+ /**
141
+ * Receive a plist message from a channel
142
+ * @param channel The channel to receive from
143
+ * @returns Tuple of [decoded data, auxiliary values]
144
+ */
145
+ async recvPlist(channel = DVTSecureSocketProxyService.BROADCAST_CHANNEL) {
146
+ const [data, aux] = await this.recvMessage(channel);
147
+ let decodedData = null;
148
+ if (data?.length) {
149
+ try {
150
+ decodedData = parseBinaryPlist(data);
151
+ // decode NSKeyedArchiver format
152
+ decodedData = decodeNSKeyedArchiver(decodedData);
153
+ }
154
+ catch (error) {
155
+ log.warn('Failed to parse plist data:', error);
156
+ }
157
+ }
158
+ return [decodedData, aux];
159
+ }
160
+ /**
161
+ * Receive a raw message from a channel
162
+ * @param channel The channel to receive from
163
+ * @returns Tuple of [raw data, auxiliary values]
164
+ */
165
+ async recvMessage(channel = DVTSecureSocketProxyService.BROADCAST_CHANNEL) {
166
+ const packetData = await this.recvPacketFragments(channel);
167
+ const payloadHeader = DTXMessage.parsePayloadHeader(packetData);
168
+ const compression = (payloadHeader.flags & 0xff000) >> 12;
169
+ if (compression) {
170
+ throw new Error('Compressed messages not supported');
171
+ }
172
+ let offset = DTX_CONSTANTS.PAYLOAD_HEADER_SIZE;
173
+ // Parse auxiliary data if present
174
+ let aux = [];
175
+ if (payloadHeader.auxiliaryLength > 0) {
176
+ const auxBuffer = packetData.subarray(offset, offset + payloadHeader.auxiliaryLength);
177
+ aux = this.parseAuxiliaryData(auxBuffer);
178
+ offset += payloadHeader.auxiliaryLength;
179
+ }
180
+ // Extract object data
181
+ const objSize = Number(payloadHeader.totalLength) - payloadHeader.auxiliaryLength;
182
+ const data = objSize > 0 ? packetData.subarray(offset, offset + objSize) : null;
183
+ return [data, aux];
184
+ }
185
+ /**
186
+ * Close the DVT service connection
187
+ */
188
+ async close() {
189
+ if (!this.connection) {
190
+ return;
191
+ }
192
+ // Send channel cancellation for all active channels
193
+ const activeCodes = Array.from(this.channelMessages.keys()).filter((code) => code > 0);
194
+ if (activeCodes.length > 0) {
195
+ const args = new MessageAux();
196
+ for (const code of activeCodes) {
197
+ args.appendInt(code);
198
+ }
199
+ try {
200
+ await this.sendMessage(DVTSecureSocketProxyService.BROADCAST_CHANNEL, '_channelCanceled:', args, false);
201
+ }
202
+ catch (error) {
203
+ log.debug('Error sending channel canceled message:', error);
204
+ }
205
+ }
206
+ this.connection.close();
207
+ this.connection = null;
208
+ this.socket = null;
209
+ this.isHandshakeComplete = false;
210
+ this.channelCache.clear();
211
+ this.channelMessages.clear();
212
+ this.channelMessages.set(DVTSecureSocketProxyService.BROADCAST_CHANNEL, new ChannelFragmenter());
213
+ }
214
+ /**
215
+ * Perform DTX protocol handshake to establish connection and retrieve capabilities
216
+ */
217
+ async performHandshake() {
218
+ const args = new MessageAux();
219
+ args.appendObj({
220
+ 'com.apple.private.DTXBlockCompression': 0,
221
+ 'com.apple.private.DTXConnection': 1,
222
+ });
223
+ await this.sendMessage(0, '_notifyOfPublishedCapabilities:', args, false);
224
+ const [retData, aux] = await this.recvMessage();
225
+ const ret = retData ? parseBinaryPlist(retData) : null;
226
+ const selectorName = this.extractSelectorFromResponse(ret);
227
+ if (selectorName !== '_notifyOfPublishedCapabilities:') {
228
+ throw new Error(`Invalid handshake response selector: ${selectorName}`);
229
+ }
230
+ if (!aux || aux.length === 0) {
231
+ throw new Error('Invalid handshake response: missing capabilities');
232
+ }
233
+ // Extract server capabilities from auxiliary data
234
+ this.supportedIdentifiers = this.extractCapabilitiesFromAuxData(aux[0]);
235
+ this.isHandshakeComplete = true;
236
+ log.debug(`DVT handshake complete. Found ${Object.keys(this.supportedIdentifiers).length} supported identifiers`);
237
+ // Consume any additional messages buffered after handshake
238
+ await this.drainBufferedMessages();
239
+ }
240
+ extractSelectorFromResponse(ret) {
241
+ if (typeof ret === 'string') {
242
+ return ret;
243
+ }
244
+ const objects = extractNSKeyedArchiverObjects(ret);
245
+ if (objects) {
246
+ return objects[1];
247
+ }
248
+ throw new Error('Invalid handshake response');
249
+ }
250
+ extractCapabilitiesFromAuxData(capabilitiesData) {
251
+ const objects = extractNSKeyedArchiverObjects(capabilitiesData);
252
+ if (!objects) {
253
+ return capabilitiesData || {};
254
+ }
255
+ const dictObj = objects[1];
256
+ if (isNSDictionaryFormat(dictObj)) {
257
+ return extractNSDictionary(dictObj, objects);
258
+ }
259
+ return extractCapabilityStrings(objects);
260
+ }
261
+ /**
262
+ * Drain any buffered messages that arrived during handshake
263
+ */
264
+ async drainBufferedMessages() {
265
+ if (this.readBuffer.length === 0) {
266
+ return;
267
+ }
268
+ try {
269
+ while (this.readBuffer.length >= DTX_CONSTANTS.MESSAGE_HEADER_SIZE) {
270
+ const headerData = this.readBuffer.subarray(0, DTX_CONSTANTS.MESSAGE_HEADER_SIZE);
271
+ const header = DTXMessage.parseMessageHeader(headerData);
272
+ const totalSize = DTX_CONSTANTS.MESSAGE_HEADER_SIZE + header.length;
273
+ if (this.readBuffer.length >= totalSize) {
274
+ // Consume complete buffered message
275
+ this.readBuffer = this.readBuffer.subarray(DTX_CONSTANTS.MESSAGE_HEADER_SIZE);
276
+ this.readBuffer = this.readBuffer.subarray(header.length);
277
+ }
278
+ else {
279
+ break;
280
+ }
281
+ }
282
+ }
283
+ catch (error) {
284
+ log.debug('Error while draining buffer:', error);
285
+ }
286
+ }
287
+ /**
288
+ * Receive packet fragments until a complete message is available for the specified channel
289
+ */
290
+ async recvPacketFragments(channel) {
291
+ while (true) {
292
+ const fragmenter = this.channelMessages.get(channel);
293
+ if (!fragmenter) {
294
+ throw new Error(`No fragmenter for channel ${channel}`);
295
+ }
296
+ // Check if we have a complete message
297
+ const message = fragmenter.get();
298
+ if (message) {
299
+ return message;
300
+ }
301
+ // Read next message header
302
+ const headerData = await this.readExact(DTX_CONSTANTS.MESSAGE_HEADER_SIZE);
303
+ const header = DTXMessage.parseMessageHeader(headerData);
304
+ const receivedChannel = Math.abs(header.channelCode);
305
+ if (!this.channelMessages.has(receivedChannel)) {
306
+ this.channelMessages.set(receivedChannel, new ChannelFragmenter());
307
+ }
308
+ // Update message ID tracker
309
+ if (!header.conversationIndex && header.identifier > this.curMessageId) {
310
+ this.curMessageId = header.identifier;
311
+ }
312
+ // Skip first fragment header for multi-fragment messages
313
+ if (header.fragmentCount > 1 && header.fragmentId === 0) {
314
+ continue;
315
+ }
316
+ // Read message payload
317
+ const messageData = await this.readExact(header.length);
318
+ // Add fragment to appropriate channel
319
+ const targetFragmenter = this.channelMessages.get(receivedChannel);
320
+ targetFragmenter.addFragment(header, messageData);
321
+ }
322
+ }
323
+ /**
324
+ * Read exact number of bytes from socket with buffering
325
+ */
326
+ async readExact(length) {
327
+ if (!this.socket) {
328
+ throw new Error(`${this.constructor.name} is not initialized. Call connect() before sending messages.`);
329
+ }
330
+ // Keep reading until we have enough data
331
+ while (this.readBuffer.length < length) {
332
+ const chunk = await new Promise((resolve, reject) => {
333
+ const onData = (data) => {
334
+ this.socket.off('data', onData);
335
+ this.socket.off('error', onError);
336
+ resolve(data);
337
+ };
338
+ const onError = (err) => {
339
+ this.socket.off('data', onData);
340
+ this.socket.off('error', onError);
341
+ reject(err);
342
+ };
343
+ this.socket.once('data', onData);
344
+ this.socket.once('error', onError);
345
+ });
346
+ this.readBuffer = Buffer.concat([this.readBuffer, chunk]);
347
+ }
348
+ // Extract exact amount requested
349
+ const result = this.readBuffer.subarray(0, length);
350
+ this.readBuffer = this.readBuffer.subarray(length);
351
+ return result;
352
+ }
353
+ /**
354
+ * Check if response contains an NSError and throw if present
355
+ */
356
+ checkForNSError(response, context) {
357
+ if (!response || typeof response !== 'object') {
358
+ return;
359
+ }
360
+ // Check NSKeyedArchiver format
361
+ const objects = extractNSKeyedArchiverObjects(response);
362
+ if (objects) {
363
+ // Check for NSError indicators in $objects
364
+ const hasNSError = objects.some((o) => hasNSErrorIndicators(o));
365
+ if (hasNSError) {
366
+ const errorMsg = objects.find((o) => typeof o === 'string' && o.length > MIN_ERROR_DESCRIPTION_LENGTH) || 'Unknown error';
367
+ throw new Error(`${context}: ${errorMsg}`);
368
+ }
369
+ }
370
+ // Check direct NSError format
371
+ if (hasNSErrorIndicators(response)) {
372
+ throw new Error(`${context}: ${JSON.stringify(response)}`);
373
+ }
374
+ }
375
+ /**
376
+ * Archive a value using NSKeyedArchiver format for DTX protocol
377
+ */
378
+ archiveValue(value) {
379
+ const archived = {
380
+ $version: 100000,
381
+ $archiver: 'NSKeyedArchiver',
382
+ $top: { root: new PlistUID(1) },
383
+ $objects: ['$null', value],
384
+ };
385
+ return createBinaryPlist(archived);
386
+ }
387
+ /**
388
+ * Archive a selector string for DTX messages
389
+ */
390
+ archiveSelector(selector) {
391
+ return this.archiveValue(selector);
392
+ }
393
+ /**
394
+ * Build auxiliary data buffer with NSKeyedArchiver encoding for objects
395
+ */
396
+ buildAuxiliaryData(args) {
397
+ const values = args.getValues();
398
+ if (values.length === 0) {
399
+ return Buffer.alloc(0);
400
+ }
401
+ const itemBuffers = [];
402
+ for (const auxValue of values) {
403
+ // Empty dictionary marker
404
+ const dictMarker = Buffer.alloc(4);
405
+ dictMarker.writeUInt32LE(DTX_CONSTANTS.EMPTY_DICTIONARY, 0);
406
+ itemBuffers.push(dictMarker);
407
+ // Type marker
408
+ const typeBuffer = Buffer.alloc(4);
409
+ typeBuffer.writeUInt32LE(auxValue.type, 0);
410
+ itemBuffers.push(typeBuffer);
411
+ // Value data
412
+ switch (auxValue.type) {
413
+ case DTX_CONSTANTS.AUX_TYPE_INT32: {
414
+ const valueBuffer = Buffer.alloc(4);
415
+ valueBuffer.writeUInt32LE(auxValue.value, 0);
416
+ itemBuffers.push(valueBuffer);
417
+ break;
418
+ }
419
+ case DTX_CONSTANTS.AUX_TYPE_INT64: {
420
+ const valueBuffer = Buffer.alloc(8);
421
+ valueBuffer.writeBigUInt64LE(BigInt(auxValue.value), 0);
422
+ itemBuffers.push(valueBuffer);
423
+ break;
424
+ }
425
+ case DTX_CONSTANTS.AUX_TYPE_OBJECT: {
426
+ const encodedPlist = this.archiveValue(auxValue.value);
427
+ const lengthBuffer = Buffer.alloc(4);
428
+ lengthBuffer.writeUInt32LE(encodedPlist.length, 0);
429
+ itemBuffers.push(lengthBuffer);
430
+ itemBuffers.push(encodedPlist);
431
+ break;
432
+ }
433
+ default:
434
+ throw new Error(`Unsupported auxiliary type: ${auxValue.type}`);
435
+ }
436
+ }
437
+ const itemsData = Buffer.concat(itemBuffers);
438
+ // Build header: magic + total size of items
439
+ const header = Buffer.alloc(16);
440
+ header.writeBigUInt64LE(BigInt(DTX_CONSTANTS.MESSAGE_AUX_MAGIC), 0);
441
+ header.writeBigUInt64LE(BigInt(itemsData.length), 8);
442
+ return Buffer.concat([header, itemsData]);
443
+ }
444
+ /**
445
+ * Parse auxiliary data from buffer
446
+ *
447
+ * The auxiliary data format can be:
448
+ * 1. Standard format: [magic:8][size:8][items...]
449
+ * 2. NSKeyedArchiver bplist format (for handshake responses)
450
+ */
451
+ parseAuxiliaryData(buffer) {
452
+ if (buffer.length < 16) {
453
+ return [];
454
+ }
455
+ const magic = buffer.readBigUInt64LE(0);
456
+ // Check if this is NSKeyedArchiver bplist format (handshake response)
457
+ if (magic !== BigInt(DTX_CONSTANTS.MESSAGE_AUX_MAGIC)) {
458
+ return this.parseAuxiliaryAsBplist(buffer);
459
+ }
460
+ // Standard auxiliary format
461
+ return this.parseAuxiliaryStandard(buffer);
462
+ }
463
+ /**
464
+ * Parse auxiliary data in NSKeyedArchiver bplist format
465
+ */
466
+ parseAuxiliaryAsBplist(buffer) {
467
+ // Find bplist header in buffer
468
+ const bplistMagic = 'bplist00';
469
+ for (let i = 0; i < Math.min(100, buffer.length - 8); i++) {
470
+ if (buffer.toString('ascii', i, i + 8) === bplistMagic) {
471
+ try {
472
+ const plistBuffer = buffer.subarray(i);
473
+ const parsed = parseBinaryPlist(plistBuffer);
474
+ return Array.isArray(parsed) ? parsed : [parsed];
475
+ }
476
+ catch (error) {
477
+ log.warn('Failed to parse auxiliary bplist:', error);
478
+ }
479
+ break;
480
+ }
481
+ }
482
+ return [];
483
+ }
484
+ /**
485
+ * Parse auxiliary data in standard DTX format
486
+ */
487
+ parseAuxiliaryStandard(buffer) {
488
+ const values = [];
489
+ let offset = 16; // Skip magic (8) + size (8)
490
+ const totalSize = buffer.readBigUInt64LE(8);
491
+ const endOffset = offset + Number(totalSize);
492
+ while (offset < endOffset && offset < buffer.length) {
493
+ // Read and validate empty dictionary marker
494
+ const marker = buffer.readUInt32LE(offset);
495
+ offset += 4;
496
+ if (marker !== DTX_CONSTANTS.EMPTY_DICTIONARY) {
497
+ offset -= 4; // Rewind if not the expected marker
498
+ }
499
+ // Read type
500
+ const type = buffer.readUInt32LE(offset);
501
+ offset += 4;
502
+ // Read value based on type
503
+ try {
504
+ const value = this.parseAuxiliaryValue(buffer, type, offset);
505
+ values.push(value.data);
506
+ offset = value.newOffset;
507
+ }
508
+ catch (error) {
509
+ log.warn(`Failed to parse auxiliary value at offset ${offset}:`, error);
510
+ break;
511
+ }
512
+ }
513
+ return values;
514
+ }
515
+ /**
516
+ * Parse a single auxiliary value
517
+ */
518
+ parseAuxiliaryValue(buffer, type, offset) {
519
+ switch (type) {
520
+ case DTX_CONSTANTS.AUX_TYPE_INT32:
521
+ return {
522
+ data: buffer.readUInt32LE(offset),
523
+ newOffset: offset + 4,
524
+ };
525
+ case DTX_CONSTANTS.AUX_TYPE_INT64:
526
+ return {
527
+ data: buffer.readBigUInt64LE(offset),
528
+ newOffset: offset + 8,
529
+ };
530
+ case DTX_CONSTANTS.AUX_TYPE_OBJECT: {
531
+ const length = buffer.readUInt32LE(offset);
532
+ const plistData = buffer.subarray(offset + 4, offset + 4 + length);
533
+ let parsed;
534
+ try {
535
+ parsed = parseBinaryPlist(plistData);
536
+ }
537
+ catch (error) {
538
+ log.warn('Failed to parse auxiliary object plist:', error);
539
+ parsed = plistData;
540
+ }
541
+ return {
542
+ data: parsed,
543
+ newOffset: offset + 4 + length,
544
+ };
545
+ }
546
+ default:
547
+ throw new Error(`Unknown auxiliary type: ${type}`);
548
+ }
549
+ }
550
+ }
551
+ export { Channel, ChannelFragmenter, DTXMessage, MessageAux, DTX_CONSTANTS };
552
+ export { decodeNSKeyedArchiver, NSKeyedArchiverDecoder, } from './nskeyedarchiver-decoder.js';
@@ -0,0 +1,37 @@
1
+ import type { ConditionGroup } from '../../../../lib/types.js';
2
+ import type { DVTSecureSocketProxyService } from '../index.js';
3
+ /**
4
+ * Condition Inducer service for simulating various device conditions
5
+ * such as network conditions, thermal states, etc.
6
+ */
7
+ export declare class ConditionInducer {
8
+ private readonly dvt;
9
+ static readonly IDENTIFIER = "com.apple.instruments.server.services.ConditionInducer";
10
+ private channel;
11
+ constructor(dvt: DVTSecureSocketProxyService);
12
+ /**
13
+ * Initialize the condition inducer channel
14
+ */
15
+ initialize(): Promise<void>;
16
+ /**
17
+ * List all available condition inducers and their profiles
18
+ * @returns Array of condition groups with their available profiles
19
+ */
20
+ list(): Promise<ConditionGroup[]>;
21
+ /**
22
+ * Set a specific condition profile
23
+ * @param profileIdentifier The identifier of the profile to enable
24
+ * @throws Error if the profile identifier is not found
25
+ * @throws Error if a condition is already active
26
+ */
27
+ set(profileIdentifier: string): Promise<void>;
28
+ /**
29
+ * Disable the currently active condition
30
+ *
31
+ * Note: This method is idempotent - calling it when no condition is active
32
+ * will not throw an error.
33
+ */
34
+ disable(): Promise<void>;
35
+ private isNSError;
36
+ }
37
+ //# sourceMappingURL=condition-inducer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"condition-inducer.d.ts","sourceRoot":"","sources":["../../../../../../src/services/ios/dvt/instruments/condition-inducer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAI/D;;;GAGG;AACH,qBAAa,gBAAgB;IAMf,OAAO,CAAC,QAAQ,CAAC,GAAG;IALhC,MAAM,CAAC,QAAQ,CAAC,UAAU,4DACiC;IAE3D,OAAO,CAAC,OAAO,CAAwB;gBAEV,GAAG,EAAE,2BAA2B;IAE7D;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAOjC;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAwBvC;;;;;OAKG;IACG,GAAG,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4CnD;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB9B,OAAO,CAAC,SAAS;CAKlB"}
@@ -0,0 +1,99 @@
1
+ import { getLogger } from '../../../../lib/logger.js';
2
+ import { MessageAux } from '../dtx-message.js';
3
+ const log = getLogger('ConditionInducer');
4
+ /**
5
+ * Condition Inducer service for simulating various device conditions
6
+ * such as network conditions, thermal states, etc.
7
+ */
8
+ export class ConditionInducer {
9
+ dvt;
10
+ static IDENTIFIER = 'com.apple.instruments.server.services.ConditionInducer';
11
+ channel = null;
12
+ constructor(dvt) {
13
+ this.dvt = dvt;
14
+ }
15
+ /**
16
+ * Initialize the condition inducer channel
17
+ */
18
+ async initialize() {
19
+ if (this.channel) {
20
+ return;
21
+ }
22
+ this.channel = await this.dvt.makeChannel(ConditionInducer.IDENTIFIER);
23
+ }
24
+ /**
25
+ * List all available condition inducers and their profiles
26
+ * @returns Array of condition groups with their available profiles
27
+ */
28
+ async list() {
29
+ await this.initialize();
30
+ await this.channel.call('availableConditionInducers')();
31
+ const result = await this.channel.receivePlist();
32
+ // Handle different response formats
33
+ if (!result) {
34
+ log.warn('Received null/undefined response from availableConditionInducers');
35
+ return [];
36
+ }
37
+ // If result is already an array, return it
38
+ if (Array.isArray(result)) {
39
+ return result;
40
+ }
41
+ throw new Error(`Unexpected response format from availableConditionInducers: ${JSON.stringify(result)}`);
42
+ }
43
+ /**
44
+ * Set a specific condition profile
45
+ * @param profileIdentifier The identifier of the profile to enable
46
+ * @throws Error if the profile identifier is not found
47
+ * @throws Error if a condition is already active
48
+ */
49
+ async set(profileIdentifier) {
50
+ await this.initialize();
51
+ const groups = await this.list();
52
+ // Find the profile in the available groups
53
+ for (const group of groups) {
54
+ const profiles = group.profiles || [];
55
+ for (const profile of profiles) {
56
+ if (profileIdentifier !== profile.identifier) {
57
+ continue;
58
+ }
59
+ log.info(`Enabling condition: ${profile.description || profile.identifier}`);
60
+ const args = new MessageAux()
61
+ .appendObj(group.identifier)
62
+ .appendObj(profile.identifier);
63
+ await this.channel.call('enableConditionWithIdentifier_profileIdentifier_')(args);
64
+ // Wait for response which may be a raised NSError
65
+ await this.channel.receivePlist();
66
+ log.info(`Successfully enabled condition profile: ${profileIdentifier}`);
67
+ return;
68
+ }
69
+ }
70
+ const availableProfiles = groups.flatMap((group) => (group.profiles || []).map((p) => p.identifier));
71
+ throw new Error(`Invalid profile identifier: ${profileIdentifier}. Available profiles: ${availableProfiles.join(', ')}`);
72
+ }
73
+ /**
74
+ * Disable the currently active condition
75
+ *
76
+ * Note: This method is idempotent - calling it when no condition is active
77
+ * will not throw an error.
78
+ */
79
+ async disable() {
80
+ await this.initialize();
81
+ await this.channel.call('disableActiveCondition')();
82
+ const response = await this.channel.receivePlist();
83
+ // Response can be:
84
+ // - true (successfully disabled condition)
85
+ // - NSError object, when no condition is active
86
+ if (response === true) {
87
+ log.info('Disabled active condition');
88
+ }
89
+ else if (this.isNSError(response)) {
90
+ log.debug('No active condition to disable');
91
+ }
92
+ else {
93
+ throw new Error(`Unexpected response from disableActiveCondition: ${JSON.stringify(response)}`);
94
+ }
95
+ }
96
+ isNSError(obj) {
97
+ return ['NSCode', 'NSUserInfo', 'NSDomain'].some((prop) => obj?.[prop] !== undefined);
98
+ }
99
+ }