appium-ios-remotexpc 0.16.1 → 0.18.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.
@@ -509,11 +509,15 @@ export class DVTSecureSocketProxyService extends BaseService {
509
509
  * Archive a value using NSKeyedArchiver format for DTX protocol
510
510
  */
511
511
  private archiveValue(value: any): Buffer {
512
+ // Handle null values by referencing the $null marker
513
+ const rootIndex = value === null ? 0 : 1;
514
+ const objects = value === null ? ['$null'] : ['$null', value];
515
+
512
516
  const archived = {
513
517
  $version: 100000,
514
518
  $archiver: 'NSKeyedArchiver',
515
- $top: { root: new PlistUID(1) },
516
- $objects: ['$null', value],
519
+ $top: { root: new PlistUID(rootIndex) },
520
+ $objects: objects,
517
521
  };
518
522
 
519
523
  return createBinaryPlist(archived);
@@ -0,0 +1,97 @@
1
+ import { getLogger } from '../../../../lib/logger.js';
2
+ import type { Channel } from '../channel.js';
3
+ import { MessageAux } from '../dtx-message.js';
4
+ import type { DVTSecureSocketProxyService } from '../index.js';
5
+
6
+ const log = getLogger('ApplicationListing');
7
+
8
+ export interface iOSApplication {
9
+ /** Display name of the application/plugin */
10
+ DisplayName: string;
11
+
12
+ /** Bundle identifier in reverse domain notation */
13
+ CFBundleIdentifier: string;
14
+
15
+ /** Full path to the application bundle */
16
+ BundlePath: string;
17
+
18
+ /** Version string of the application */
19
+ Version: string;
20
+
21
+ /** Name of the main executable file */
22
+ ExecutableName: string;
23
+
24
+ /** Access restriction flag (0 = unrestricted, 1 = restricted) */
25
+ Restricted: number;
26
+
27
+ /** Bundle type (e.g., 'PluginKit', 'Application') */
28
+ Type: string;
29
+
30
+ /** Unique identifier for plugins */
31
+ PluginIdentifier: string;
32
+
33
+ /** UUID for the plugin instance */
34
+ PluginUUID: string;
35
+
36
+ /** Extension configuration with variable structure */
37
+ ExtensionDictionary?: Record<string, any>;
38
+
39
+ /** Bundle identifier of the containing app (plugins only) */
40
+ ContainerBundleIdentifier?: string;
41
+
42
+ /** Path to the container app bundle (plugins only) */
43
+ ContainerBundlePath?: string;
44
+ }
45
+
46
+ /**
47
+ * Application Listing service for retrieving installed applications
48
+ */
49
+ export class ApplicationListing {
50
+ static readonly IDENTIFIER =
51
+ 'com.apple.instruments.server.services.device.applictionListing';
52
+
53
+ private channel: Channel | null = null;
54
+
55
+ constructor(private readonly dvt: DVTSecureSocketProxyService) {}
56
+
57
+ /**
58
+ * Initialize the application listing channel
59
+ */
60
+ async initialize(): Promise<void> {
61
+ if (this.channel) {
62
+ return;
63
+ }
64
+ this.channel = await this.dvt.makeChannel(ApplicationListing.IDENTIFIER);
65
+ }
66
+
67
+ /**
68
+ * Get the list of installed applications from the device
69
+ * @returns {Promise<iOSApplication[]>}
70
+ */
71
+ async list(): Promise<iOSApplication[]> {
72
+ await this.initialize();
73
+
74
+ const args = new MessageAux().appendObj(null).appendObj(null);
75
+
76
+ await this.channel!.call(
77
+ 'installedApplicationsMatching_registerUpdateToken_',
78
+ )(args);
79
+
80
+ const result = await this.channel!.receivePlist();
81
+
82
+ if (!result) {
83
+ log.warn(
84
+ 'Received null/undefined response from installedApplicationsMatching',
85
+ );
86
+ return [];
87
+ }
88
+
89
+ if (Array.isArray(result)) {
90
+ return result;
91
+ }
92
+
93
+ throw new Error(
94
+ `Unexpected response format from installedApplicationsMatching: ${JSON.stringify(result)}`,
95
+ );
96
+ }
97
+ }
@@ -0,0 +1,217 @@
1
+ import { getLogger } from '../../../../lib/logger.js';
2
+ import { parseBinaryPlist } from '../../../../lib/plist/index.js';
3
+ import type { ProcessInfo } from '../../../../lib/types.js';
4
+ import type { Channel } from '../channel.js';
5
+ import { MessageAux } from '../dtx-message.js';
6
+ import type { DVTSecureSocketProxyService } from '../index.js';
7
+
8
+ const log = getLogger('DeviceInfo');
9
+
10
+ /**
11
+ * DeviceInfo service provides access to device information, file system,
12
+ * and process management through the DTX protocol.
13
+ *
14
+ * Available methods:
15
+ * - ls(path): List directory contents
16
+ * - execnameForPid(pid): Get executable path for a process ID
17
+ * - proclist(): Get list of running processes
18
+ * - isRunningPid(pid): Check if a process is running
19
+ * - hardwareInformation(): Get hardware details
20
+ * - networkInformation(): Get network configuration
21
+ * - machTimeInfo(): Get mach time information
22
+ * - machKernelName(): Get kernel name
23
+ * - kpepDatabase(): Get kernel performance event database
24
+ * - traceCodes(): Get trace code mappings
25
+ * - nameForUid(uid): Get username for UID
26
+ * - nameForGid(gid): Get group name for GID
27
+ */
28
+ export class DeviceInfo {
29
+ static readonly IDENTIFIER =
30
+ 'com.apple.instruments.server.services.deviceinfo';
31
+
32
+ private channel: Channel | null = null;
33
+ constructor(private readonly dvt: DVTSecureSocketProxyService) {}
34
+
35
+ /**
36
+ * Initialize the device info channel
37
+ */
38
+ async initialize(): Promise<void> {
39
+ if (!this.channel) {
40
+ this.channel = await this.dvt.makeChannel(DeviceInfo.IDENTIFIER);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * List directory contents at the specified path.
46
+ * @param path - The directory path to list
47
+ * @returns Array of filenames
48
+ * @throws {Error} If the directory doesn't exist or cannot be accessed
49
+ */
50
+ async ls(path: string): Promise<string[]> {
51
+ const result = await this.requestInformation(
52
+ 'directoryListingForPath_',
53
+ path,
54
+ );
55
+
56
+ if (result === null || result === undefined) {
57
+ throw new Error(`Failed to list directory: ${path}`);
58
+ }
59
+
60
+ log.debug(`Listed directory ${path}: ${result.length} entries`);
61
+ return result;
62
+ }
63
+
64
+ /**
65
+ * Get the full executable path for a given process ID.
66
+ * @param pid - The process identifier
67
+ * @returns The full path to the executable
68
+ */
69
+ async execnameForPid(pid: number): Promise<string> {
70
+ return this.requestInformation('execnameForPid_', pid);
71
+ }
72
+
73
+ /**
74
+ * Get the list of all running processes on the device.
75
+ * @returns Array of process information objects
76
+ */
77
+ async proclist(): Promise<ProcessInfo[]> {
78
+ const result = await this.requestInformation('runningProcesses');
79
+
80
+ if (!Array.isArray(result)) {
81
+ throw new Error(
82
+ `proclist returned invalid data: expected an array, got ${typeof result} (${JSON.stringify(result)})`,
83
+ );
84
+ }
85
+
86
+ log.debug(`Retrieved ${result.length} running processes`);
87
+ return result;
88
+ }
89
+
90
+ /**
91
+ * Check if a process with the given PID is currently running.
92
+ * @param pid - The process identifier to check
93
+ * @returns true if the process is running, false otherwise
94
+ */
95
+ async isRunningPid(pid: number): Promise<boolean> {
96
+ return this.requestInformation('isRunningPid_', pid);
97
+ }
98
+
99
+ /**
100
+ * Get hardware information about the device.
101
+ * @returns Object containing hardware information
102
+ */
103
+ async hardwareInformation(): Promise<any> {
104
+ return this.requestInformation('hardwareInformation');
105
+ }
106
+
107
+ /**
108
+ * Get network configuration information.
109
+ * @returns Object containing network information
110
+ */
111
+ async networkInformation(): Promise<any> {
112
+ return this.requestInformation('networkInformation');
113
+ }
114
+
115
+ /**
116
+ * Get mach kernel time information.
117
+ * @returns Object containing mach time info
118
+ */
119
+ async machTimeInfo(): Promise<any> {
120
+ return this.requestInformation('machTimeInfo');
121
+ }
122
+
123
+ /**
124
+ * Get the mach kernel name.
125
+ * @returns The kernel name string
126
+ */
127
+ async machKernelName(): Promise<string> {
128
+ return this.requestInformation('machKernelName');
129
+ }
130
+
131
+ /**
132
+ * Get the kernel performance event (kpep) database.
133
+ * @returns Object containing kpep database or null if not available
134
+ */
135
+ async kpepDatabase(): Promise<any | null> {
136
+ const kpepData = await this.requestInformation('kpepDatabase');
137
+
138
+ if (kpepData === null || kpepData === undefined) {
139
+ return null;
140
+ }
141
+
142
+ // The kpepDatabase is returned as binary plist data
143
+ if (Buffer.isBuffer(kpepData)) {
144
+ try {
145
+ return parseBinaryPlist(kpepData);
146
+ } catch (error) {
147
+ log.warn('Failed to parse kpep database:', error);
148
+ return null;
149
+ }
150
+ }
151
+
152
+ return kpepData;
153
+ }
154
+
155
+ /**
156
+ * Get trace code mappings.
157
+ * @returns Object mapping trace codes (as hex strings) to descriptions
158
+ */
159
+ async traceCodes(): Promise<Record<string, string>> {
160
+ const codesFile = await this.requestInformation('traceCodesFile');
161
+ if (typeof codesFile !== 'string') {
162
+ return {};
163
+ }
164
+
165
+ const codes: Record<string, string> = {};
166
+
167
+ for (const line of codesFile.split('\n')) {
168
+ const match = line.trim().match(/^(\S+)\s+(.+)$/);
169
+ if (match) {
170
+ const [, hex, description] = match;
171
+ codes[hex] = description;
172
+ }
173
+ }
174
+
175
+ log.debug(`Retrieved ${Object.keys(codes).length} trace codes`);
176
+ return codes;
177
+ }
178
+
179
+ /**
180
+ * Get the username for a given user ID (UID).
181
+ * @param uid - The user identifier
182
+ * @returns The username string
183
+ */
184
+ async nameForUid(uid: number): Promise<string> {
185
+ return this.requestInformation('nameForUID_', uid);
186
+ }
187
+
188
+ /**
189
+ * Get the group name for a given group ID (GID).
190
+ * @param gid - The group identifier
191
+ * @returns The group name string
192
+ */
193
+ async nameForGid(gid: number): Promise<string> {
194
+ return this.requestInformation('nameForGID_', gid);
195
+ }
196
+
197
+ /**
198
+ * Generic method to request information from the device.
199
+ * @param selectorName - The selector name to call
200
+ * @param arg - Optional argument to pass to the selector
201
+ * @returns The information object or value returned by the selector
202
+ * @private
203
+ */
204
+ private async requestInformation(
205
+ selectorName: string,
206
+ arg?: any,
207
+ ): Promise<any> {
208
+ await this.initialize();
209
+
210
+ const call = this.channel!.call(selectorName);
211
+ const args =
212
+ arg !== undefined ? new MessageAux().appendObj(arg) : undefined;
213
+
214
+ await call(args);
215
+ return this.channel!.receivePlist();
216
+ }
217
+ }
@@ -1,6 +1,7 @@
1
1
  import { getLogger } from '../../../lib/logger.js';
2
2
 
3
3
  const log = getLogger('NSKeyedArchiverDecoder');
4
+ const MAX_DECODE_DEPTH = 1000;
4
5
 
5
6
  /**
6
7
  * Decode NSKeyedArchiver formatted data into native JavaScript objects
@@ -79,50 +80,77 @@ export class NSKeyedArchiverDecoder {
79
80
  /**
80
81
  * Decode an object at a specific index
81
82
  */
82
- private decodeObject(index: number): any {
83
+ private decodeObject(
84
+ index: number,
85
+ visited: Set<number> = new Set(),
86
+ depth: number = 0,
87
+ ): any {
88
+ // Prevent stack overflow with depth limit
89
+ if (depth > MAX_DECODE_DEPTH) {
90
+ log.warn(`Maximum decode depth exceeded at index ${index}`);
91
+ return null;
92
+ }
83
93
  if (index < 0 || index >= this.objects.length) {
84
94
  return null;
85
95
  }
86
96
 
97
+ // Prevent infinite recursion
98
+ if (visited.has(index)) {
99
+ return null; // Return null for circular references
100
+ }
101
+
87
102
  // Check cache
88
103
  if (this.decoded.has(index)) {
89
104
  return this.decoded.get(index);
90
105
  }
91
106
 
107
+ visited.add(index);
92
108
  const obj = this.objects[index];
93
109
 
94
110
  // Handle null marker
95
111
  if (obj === '$null' || obj === null) {
112
+ visited.delete(index);
96
113
  return null;
97
114
  }
98
115
 
99
116
  // Handle primitive types
100
117
  if (typeof obj !== 'object') {
118
+ visited.delete(index);
101
119
  return obj;
102
120
  }
103
121
 
104
122
  // Handle Buffer/binary data (eg. screenshots)
105
123
  if (Buffer.isBuffer(obj)) {
106
124
  this.decoded.set(index, obj);
125
+ visited.delete(index);
107
126
  return obj;
108
127
  }
109
128
 
110
129
  // Handle UID references
111
130
  if ('CF$UID' in obj) {
112
- return this.decodeObject(obj.CF$UID);
131
+ const result = this.decodeObject(obj.CF$UID, visited, depth + 1);
132
+ visited.delete(index);
133
+ return result;
113
134
  }
114
135
 
115
136
  // Handle NSDictionary (NS.keys + NS.objects) - check this FIRST before NSArray
116
137
  if ('NS.keys' in obj && 'NS.objects' in obj) {
117
- const result = this.decodeDictionary(obj['NS.keys'], obj['NS.objects']);
138
+ const result = this.decodeDictionary(
139
+ obj['NS.keys'],
140
+ obj['NS.objects'],
141
+ visited,
142
+ depth,
143
+ );
118
144
  this.decoded.set(index, result);
145
+ visited.delete(index);
119
146
  return result;
120
147
  }
121
148
 
122
149
  // Handle NSArray (NS.objects only, without NS.keys)
123
150
  if ('NS.objects' in obj) {
124
- const result = this.decodeArray(obj['NS.objects']);
151
+ const result = this.decodeArray(obj['NS.objects'], visited, depth);
125
152
  this.decoded.set(index, result);
153
+ visited.delete(index);
126
154
  return result;
127
155
  }
128
156
 
@@ -135,20 +163,25 @@ export class NSKeyedArchiverDecoder {
135
163
 
136
164
  if (typeof value === 'number') {
137
165
  // Could be a reference or primitive
138
- const referenced = this.objects[value];
139
- if (
140
- referenced &&
141
- typeof referenced === 'object' &&
142
- referenced !== '$null'
143
- ) {
144
- result[key] = this.decodeObject(value);
166
+ if (value < this.objects.length && value >= 0) {
167
+ const referenced = this.objects[value];
168
+ if (
169
+ referenced &&
170
+ typeof referenced === 'object' &&
171
+ referenced !== '$null' &&
172
+ !visited.has(value)
173
+ ) {
174
+ result[key] = this.decodeObject(value, visited, depth + 1);
175
+ } else {
176
+ result[key] = value;
177
+ }
145
178
  } else {
146
179
  result[key] = value;
147
180
  }
148
181
  } else if (typeof value === 'object' && value && 'CF$UID' in value) {
149
182
  const uid = (value as any).CF$UID;
150
- if (typeof uid === 'number') {
151
- result[key] = this.decodeObject(uid);
183
+ if (typeof uid === 'number' && !visited.has(uid)) {
184
+ result[key] = this.decodeObject(uid, visited, depth + 1);
152
185
  } else {
153
186
  result[key] = value;
154
187
  }
@@ -158,22 +191,27 @@ export class NSKeyedArchiverDecoder {
158
191
  }
159
192
 
160
193
  this.decoded.set(index, result);
194
+ visited.delete(index);
161
195
  return result;
162
196
  }
163
197
 
164
198
  /**
165
199
  * Decode an NSArray
166
200
  */
167
- private decodeArray(refs: any): any[] {
201
+ private decodeArray(
202
+ refs: any,
203
+ visited: Set<number> = new Set(),
204
+ depth: number = 0,
205
+ ): any[] {
168
206
  if (!Array.isArray(refs)) {
169
207
  return [];
170
208
  }
171
209
 
172
210
  return refs.map((ref) => {
173
211
  if (typeof ref === 'number') {
174
- return this.decodeObject(ref);
212
+ return this.decodeObject(ref, visited, depth + 1);
175
213
  } else if (typeof ref === 'object' && ref && 'CF$UID' in ref) {
176
- return this.decodeObject(ref.CF$UID);
214
+ return this.decodeObject(ref.CF$UID, visited, depth + 1);
177
215
  }
178
216
  return ref;
179
217
  });
@@ -182,7 +220,12 @@ export class NSKeyedArchiverDecoder {
182
220
  /**
183
221
  * Decode an NSDictionary
184
222
  */
185
- private decodeDictionary(keyRefs: any, valueRefs: any): any {
223
+ private decodeDictionary(
224
+ keyRefs: any,
225
+ valueRefs: any,
226
+ visited: Set<number> = new Set(),
227
+ depth: number = 0,
228
+ ): any {
186
229
  if (!Array.isArray(keyRefs) || !Array.isArray(valueRefs)) {
187
230
  return {};
188
231
  }
@@ -190,8 +233,8 @@ export class NSKeyedArchiverDecoder {
190
233
  const result: any = {};
191
234
 
192
235
  for (let i = 0; i < keyRefs.length && i < valueRefs.length; i++) {
193
- const key = this.decodeObject(keyRefs[i]);
194
- const value = this.decodeObject(valueRefs[i]);
236
+ const key = this.decodeObject(keyRefs[i], visited, depth + 1);
237
+ const value = this.decodeObject(valueRefs[i], visited, depth + 1);
195
238
 
196
239
  if (typeof key === 'string') {
197
240
  result[key] = value;
package/src/services.ts CHANGED
@@ -18,7 +18,9 @@ import type {
18
18
  import AfcService from './services/ios/afc/index.js';
19
19
  import DiagnosticsService from './services/ios/diagnostic-service/index.js';
20
20
  import { DVTSecureSocketProxyService } from './services/ios/dvt/index.js';
21
+ import { ApplicationListing } from './services/ios/dvt/instruments/application-listing.js';
21
22
  import { ConditionInducer } from './services/ios/dvt/instruments/condition-inducer.js';
23
+ import { DeviceInfo } from './services/ios/dvt/instruments/device-info.js';
22
24
  import { Graphics } from './services/ios/dvt/instruments/graphics.js';
23
25
  import { LocationSimulation } from './services/ios/dvt/instruments/location-simulation.js';
24
26
  import { Screenshot } from './services/ios/dvt/instruments/screenshot.js';
@@ -203,7 +205,9 @@ export async function startDVTService(
203
205
  const locationSimulation = new LocationSimulation(dvtService);
204
206
  const conditionInducer = new ConditionInducer(dvtService);
205
207
  const screenshot = new Screenshot(dvtService);
208
+ const appListing = new ApplicationListing(dvtService);
206
209
  const graphics = new Graphics(dvtService);
210
+ const deviceInfo = new DeviceInfo(dvtService);
207
211
 
208
212
  return {
209
213
  remoteXPC: remoteXPC as RemoteXpcConnection,
@@ -211,7 +215,9 @@ export async function startDVTService(
211
215
  locationSimulation,
212
216
  conditionInducer,
213
217
  screenshot,
218
+ appListing,
214
219
  graphics,
220
+ deviceInfo,
215
221
  };
216
222
  }
217
223