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.
- package/CHANGELOG.md +12 -0
- package/build/src/index.d.ts +1 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/lib/types.d.ts +234 -0
- package/build/src/lib/types.d.ts.map +1 -1
- package/build/src/services/ios/dvt/index.d.ts.map +1 -1
- package/build/src/services/ios/dvt/index.js +5 -2
- package/build/src/services/ios/dvt/instruments/application-listing.d.ts +46 -0
- package/build/src/services/ios/dvt/instruments/application-listing.d.ts.map +1 -0
- package/build/src/services/ios/dvt/instruments/application-listing.js +41 -0
- package/build/src/services/ios/dvt/instruments/device-info.d.ts +105 -0
- package/build/src/services/ios/dvt/instruments/device-info.d.ts.map +1 -0
- package/build/src/services/ios/dvt/instruments/device-info.js +179 -0
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts.map +1 -1
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js +42 -17
- package/build/src/services.d.ts.map +1 -1
- package/build/src/services.js +6 -0
- package/package.json +3 -1
- package/src/index.ts +2 -0
- package/src/lib/types.ts +253 -0
- package/src/services/ios/dvt/index.ts +6 -2
- package/src/services/ios/dvt/instruments/application-listing.ts +97 -0
- package/src/services/ios/dvt/instruments/device-info.ts +217 -0
- package/src/services/ios/dvt/nskeyedarchiver-decoder.ts +62 -19
- package/src/services.ts +6 -0
|
@@ -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(
|
|
516
|
-
$objects:
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
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(
|
|
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
|
|