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.
- package/CHANGELOG.md +6 -0
- package/build/src/index.d.ts +1 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/lib/plist/binary-plist-creator.d.ts.map +1 -1
- package/build/src/lib/plist/binary-plist-creator.js +30 -0
- package/build/src/lib/plist/index.d.ts +1 -0
- package/build/src/lib/plist/index.d.ts.map +1 -1
- package/build/src/lib/plist/index.js +1 -0
- package/build/src/lib/plist/plist-uid.d.ts +10 -0
- package/build/src/lib/plist/plist-uid.d.ts.map +1 -0
- package/build/src/lib/plist/plist-uid.js +10 -0
- package/build/src/lib/types.d.ts +165 -2
- package/build/src/lib/types.d.ts.map +1 -1
- package/build/src/services/ios/dvt/channel-fragmenter.d.ts +21 -0
- package/build/src/services/ios/dvt/channel-fragmenter.d.ts.map +1 -0
- package/build/src/services/ios/dvt/channel-fragmenter.js +37 -0
- package/build/src/services/ios/dvt/channel.d.ts +32 -0
- package/build/src/services/ios/dvt/channel.d.ts.map +1 -0
- package/build/src/services/ios/dvt/channel.js +44 -0
- package/build/src/services/ios/dvt/dtx-message.d.ts +88 -0
- package/build/src/services/ios/dvt/dtx-message.d.ts.map +1 -0
- package/build/src/services/ios/dvt/dtx-message.js +113 -0
- package/build/src/services/ios/dvt/index.d.ts +119 -0
- package/build/src/services/ios/dvt/index.d.ts.map +1 -0
- package/build/src/services/ios/dvt/index.js +552 -0
- package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts +37 -0
- package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts.map +1 -0
- package/build/src/services/ios/dvt/instruments/condition-inducer.js +99 -0
- package/build/src/services/ios/dvt/instruments/location-simulation.d.ts +43 -0
- package/build/src/services/ios/dvt/instruments/location-simulation.d.ts.map +1 -0
- package/build/src/services/ios/dvt/instruments/location-simulation.js +60 -0
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts +41 -0
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts.map +1 -0
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js +190 -0
- package/build/src/services/ios/dvt/utils.d.ts +19 -0
- package/build/src/services/ios/dvt/utils.d.ts.map +1 -0
- package/build/src/services/ios/dvt/utils.js +67 -0
- package/build/src/services.d.ts +2 -1
- package/build/src/services.d.ts.map +1 -1
- package/build/src/services.js +23 -0
- package/package.json +4 -1
- package/src/index.ts +6 -0
- package/src/lib/plist/binary-plist-creator.ts +30 -0
- package/src/lib/plist/index.ts +2 -0
- package/src/lib/plist/plist-uid.ts +9 -0
- package/src/lib/types.ts +179 -1
- package/src/services/ios/dvt/channel-fragmenter.ts +42 -0
- package/src/services/ios/dvt/channel.ts +58 -0
- package/src/services/ios/dvt/dtx-message.ts +162 -0
- package/src/services/ios/dvt/index.ts +727 -0
- package/src/services/ios/dvt/instruments/condition-inducer.ts +140 -0
- package/src/services/ios/dvt/instruments/location-simulation.ts +83 -0
- package/src/services/ios/dvt/nskeyedarchiver-decoder.ts +219 -0
- package/src/services/ios/dvt/utils.ts +89 -0
- package/src/services.ts +33 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { getLogger } from '../../../../lib/logger.js';
|
|
2
|
+
import type { ConditionGroup } from '../../../../lib/types.js';
|
|
3
|
+
import type { Channel } from '../channel.js';
|
|
4
|
+
import { MessageAux } from '../dtx-message.js';
|
|
5
|
+
import type { DVTSecureSocketProxyService } from '../index.js';
|
|
6
|
+
|
|
7
|
+
const log = getLogger('ConditionInducer');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Condition Inducer service for simulating various device conditions
|
|
11
|
+
* such as network conditions, thermal states, etc.
|
|
12
|
+
*/
|
|
13
|
+
export class ConditionInducer {
|
|
14
|
+
static readonly IDENTIFIER =
|
|
15
|
+
'com.apple.instruments.server.services.ConditionInducer';
|
|
16
|
+
|
|
17
|
+
private channel: Channel | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(private readonly dvt: DVTSecureSocketProxyService) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize the condition inducer channel
|
|
23
|
+
*/
|
|
24
|
+
async initialize(): Promise<void> {
|
|
25
|
+
if (this.channel) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.channel = await this.dvt.makeChannel(ConditionInducer.IDENTIFIER);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* List all available condition inducers and their profiles
|
|
33
|
+
* @returns Array of condition groups with their available profiles
|
|
34
|
+
*/
|
|
35
|
+
async list(): Promise<ConditionGroup[]> {
|
|
36
|
+
await this.initialize();
|
|
37
|
+
|
|
38
|
+
await this.channel!.call('availableConditionInducers')();
|
|
39
|
+
const result = await this.channel!.receivePlist();
|
|
40
|
+
|
|
41
|
+
// Handle different response formats
|
|
42
|
+
if (!result) {
|
|
43
|
+
log.warn(
|
|
44
|
+
'Received null/undefined response from availableConditionInducers',
|
|
45
|
+
);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If result is already an array, return it
|
|
50
|
+
if (Array.isArray(result)) {
|
|
51
|
+
return result as ConditionGroup[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Unexpected response format from availableConditionInducers: ${JSON.stringify(result)}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set a specific condition profile
|
|
61
|
+
* @param profileIdentifier The identifier of the profile to enable
|
|
62
|
+
* @throws Error if the profile identifier is not found
|
|
63
|
+
* @throws Error if a condition is already active
|
|
64
|
+
*/
|
|
65
|
+
async set(profileIdentifier: string): Promise<void> {
|
|
66
|
+
await this.initialize();
|
|
67
|
+
|
|
68
|
+
const groups = await this.list();
|
|
69
|
+
|
|
70
|
+
// Find the profile in the available groups
|
|
71
|
+
for (const group of groups) {
|
|
72
|
+
const profiles = group.profiles || [];
|
|
73
|
+
for (const profile of profiles) {
|
|
74
|
+
if (profileIdentifier !== profile.identifier) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
log.info(
|
|
79
|
+
`Enabling condition: ${profile.description || profile.identifier}`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const args = new MessageAux()
|
|
83
|
+
.appendObj(group.identifier)
|
|
84
|
+
.appendObj(profile.identifier);
|
|
85
|
+
|
|
86
|
+
await this.channel!.call(
|
|
87
|
+
'enableConditionWithIdentifier_profileIdentifier_',
|
|
88
|
+
)(args);
|
|
89
|
+
|
|
90
|
+
// Wait for response which may be a raised NSError
|
|
91
|
+
await this.channel!.receivePlist();
|
|
92
|
+
|
|
93
|
+
log.info(
|
|
94
|
+
`Successfully enabled condition profile: ${profileIdentifier}`,
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const availableProfiles = groups.flatMap((group) =>
|
|
101
|
+
(group.profiles || []).map((p) => p.identifier),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Invalid profile identifier: ${profileIdentifier}. Available profiles: ${availableProfiles.join(', ')}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Disable the currently active condition
|
|
111
|
+
*
|
|
112
|
+
* Note: This method is idempotent - calling it when no condition is active
|
|
113
|
+
* will not throw an error.
|
|
114
|
+
*/
|
|
115
|
+
async disable(): Promise<void> {
|
|
116
|
+
await this.initialize();
|
|
117
|
+
|
|
118
|
+
await this.channel!.call('disableActiveCondition')();
|
|
119
|
+
const response = await this.channel!.receivePlist();
|
|
120
|
+
|
|
121
|
+
// Response can be:
|
|
122
|
+
// - true (successfully disabled condition)
|
|
123
|
+
// - NSError object, when no condition is active
|
|
124
|
+
if (response === true) {
|
|
125
|
+
log.info('Disabled active condition');
|
|
126
|
+
} else if (this.isNSError(response)) {
|
|
127
|
+
log.debug('No active condition to disable');
|
|
128
|
+
} else {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Unexpected response from disableActiveCondition: ${JSON.stringify(response)}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private isNSError(obj: any): boolean {
|
|
136
|
+
return ['NSCode', 'NSUserInfo', 'NSDomain'].some(
|
|
137
|
+
(prop) => obj?.[prop] !== undefined,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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('LocationSimulation');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Geographic coordinates
|
|
10
|
+
*/
|
|
11
|
+
export interface LocationCoordinates {
|
|
12
|
+
latitude: number;
|
|
13
|
+
longitude: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Location simulation service for simulating device GPS location
|
|
18
|
+
*/
|
|
19
|
+
export class LocationSimulation {
|
|
20
|
+
static readonly IDENTIFIER =
|
|
21
|
+
'com.apple.instruments.server.services.LocationSimulation';
|
|
22
|
+
|
|
23
|
+
private channel: Channel | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(private readonly dvt: DVTSecureSocketProxyService) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize the location simulation channel
|
|
29
|
+
*/
|
|
30
|
+
async initialize(): Promise<void> {
|
|
31
|
+
if (this.channel) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.channel = await this.dvt.makeChannel(LocationSimulation.IDENTIFIER);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set the simulated GPS location
|
|
40
|
+
* @param coordinates The location coordinates
|
|
41
|
+
*/
|
|
42
|
+
async set(coordinates: LocationCoordinates): Promise<void> {
|
|
43
|
+
await this.initialize();
|
|
44
|
+
|
|
45
|
+
const args = new MessageAux()
|
|
46
|
+
.appendObj(coordinates.latitude)
|
|
47
|
+
.appendObj(coordinates.longitude);
|
|
48
|
+
|
|
49
|
+
await this.channel!.call('simulateLocationWithLatitude_longitude_')(args);
|
|
50
|
+
await this.channel!.receivePlist();
|
|
51
|
+
|
|
52
|
+
log.info(
|
|
53
|
+
`Location set to: ${coordinates.latitude}, ${coordinates.longitude}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set the simulated GPS location
|
|
59
|
+
* @param latitude The latitude coordinate
|
|
60
|
+
* @param longitude The longitude coordinate
|
|
61
|
+
*/
|
|
62
|
+
async setLocation(latitude: number, longitude: number): Promise<void> {
|
|
63
|
+
await this.set({ latitude, longitude });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Stop location simulation and restore the actual device location
|
|
68
|
+
*
|
|
69
|
+
* Note: This method is safe to call even if no location simulation is currently active.
|
|
70
|
+
*/
|
|
71
|
+
async clear(): Promise<void> {
|
|
72
|
+
await this.initialize();
|
|
73
|
+
await this.channel!.call('stopLocationSimulation')();
|
|
74
|
+
log.info('Location simulation stopped');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Stop location simulation (alias for clear)
|
|
79
|
+
*/
|
|
80
|
+
async stop(): Promise<void> {
|
|
81
|
+
await this.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { getLogger } from '../../../lib/logger.js';
|
|
2
|
+
|
|
3
|
+
const log = getLogger('NSKeyedArchiverDecoder');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decode NSKeyedArchiver formatted data into native JavaScript objects
|
|
7
|
+
*
|
|
8
|
+
* NSKeyedArchiver is Apple's serialization format that stores object graphs
|
|
9
|
+
* with references. The format includes:
|
|
10
|
+
* - $version: Archive version (typically 100000)
|
|
11
|
+
* - $archiver: "NSKeyedArchiver"
|
|
12
|
+
* - $top: Root object references
|
|
13
|
+
* - $objects: Array of all objects with cross-references
|
|
14
|
+
*/
|
|
15
|
+
export class NSKeyedArchiverDecoder {
|
|
16
|
+
private readonly objects: any[];
|
|
17
|
+
private readonly decoded: Map<number, any>;
|
|
18
|
+
private readonly archive: any;
|
|
19
|
+
|
|
20
|
+
constructor(data: any) {
|
|
21
|
+
if (!NSKeyedArchiverDecoder.isNSKeyedArchive(data)) {
|
|
22
|
+
throw new Error('Data is not in NSKeyedArchiver format');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.archive = data;
|
|
26
|
+
this.objects = data.$objects || [];
|
|
27
|
+
this.decoded = new Map();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if data is in NSKeyedArchiver format
|
|
32
|
+
*/
|
|
33
|
+
static isNSKeyedArchive(data: any): boolean {
|
|
34
|
+
if (!data || typeof data !== 'object') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
'$archiver' in data &&
|
|
40
|
+
data.$archiver === 'NSKeyedArchiver' &&
|
|
41
|
+
'$objects' in data &&
|
|
42
|
+
Array.isArray(data.$objects)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decode the entire archive starting from the root
|
|
48
|
+
*/
|
|
49
|
+
decode(): any {
|
|
50
|
+
if (!this.objects || this.objects.length === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extract root reference from $top
|
|
55
|
+
let rootIndex: number | null = null;
|
|
56
|
+
|
|
57
|
+
if (this.archive.$top && typeof this.archive.$top === 'object') {
|
|
58
|
+
const top = this.archive.$top;
|
|
59
|
+
if ('root' in top) {
|
|
60
|
+
const root = top.root;
|
|
61
|
+
if (typeof root === 'number') {
|
|
62
|
+
rootIndex = root;
|
|
63
|
+
} else if (typeof root === 'object' && root && 'CF$UID' in root) {
|
|
64
|
+
rootIndex = (root as any).CF$UID;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// If we found the root index, decode it
|
|
70
|
+
if (rootIndex !== null) {
|
|
71
|
+
return this.decodeObject(rootIndex);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fallback: decode first non-null object
|
|
75
|
+
log.warn('Could not find root reference, using fallback');
|
|
76
|
+
return this.decodeObject(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decode an object at a specific index
|
|
81
|
+
*/
|
|
82
|
+
private decodeObject(index: number): any {
|
|
83
|
+
if (index < 0 || index >= this.objects.length) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check cache
|
|
88
|
+
if (this.decoded.has(index)) {
|
|
89
|
+
return this.decoded.get(index);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const obj = this.objects[index];
|
|
93
|
+
|
|
94
|
+
// Handle null marker
|
|
95
|
+
if (obj === '$null' || obj === null) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle primitive types
|
|
100
|
+
if (typeof obj !== 'object') {
|
|
101
|
+
return obj;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle UID references
|
|
105
|
+
if ('CF$UID' in obj) {
|
|
106
|
+
return this.decodeObject(obj.CF$UID);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle NSDictionary (NS.keys + NS.objects) - check this FIRST before NSArray
|
|
110
|
+
if ('NS.keys' in obj && 'NS.objects' in obj) {
|
|
111
|
+
const result = this.decodeDictionary(obj['NS.keys'], obj['NS.objects']);
|
|
112
|
+
this.decoded.set(index, result);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Handle NSArray (NS.objects only, without NS.keys)
|
|
117
|
+
if ('NS.objects' in obj) {
|
|
118
|
+
const result = this.decodeArray(obj['NS.objects']);
|
|
119
|
+
this.decoded.set(index, result);
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Handle regular objects - just return as-is but resolve references
|
|
124
|
+
const result: any = {};
|
|
125
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
126
|
+
if (key === '$class') {
|
|
127
|
+
continue; // Skip class metadata
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (typeof value === 'number') {
|
|
131
|
+
// Could be a reference or primitive
|
|
132
|
+
const referenced = this.objects[value];
|
|
133
|
+
if (
|
|
134
|
+
referenced &&
|
|
135
|
+
typeof referenced === 'object' &&
|
|
136
|
+
referenced !== '$null'
|
|
137
|
+
) {
|
|
138
|
+
result[key] = this.decodeObject(value);
|
|
139
|
+
} else {
|
|
140
|
+
result[key] = value;
|
|
141
|
+
}
|
|
142
|
+
} else if (typeof value === 'object' && value && 'CF$UID' in value) {
|
|
143
|
+
const uid = (value as any).CF$UID;
|
|
144
|
+
if (typeof uid === 'number') {
|
|
145
|
+
result[key] = this.decodeObject(uid);
|
|
146
|
+
} else {
|
|
147
|
+
result[key] = value;
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
result[key] = value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.decoded.set(index, result);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Decode an NSArray
|
|
160
|
+
*/
|
|
161
|
+
private decodeArray(refs: any): any[] {
|
|
162
|
+
if (!Array.isArray(refs)) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return refs.map((ref) => {
|
|
167
|
+
if (typeof ref === 'number') {
|
|
168
|
+
return this.decodeObject(ref);
|
|
169
|
+
} else if (typeof ref === 'object' && ref && 'CF$UID' in ref) {
|
|
170
|
+
return this.decodeObject(ref.CF$UID);
|
|
171
|
+
}
|
|
172
|
+
return ref;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Decode an NSDictionary
|
|
178
|
+
*/
|
|
179
|
+
private decodeDictionary(keyRefs: any, valueRefs: any): any {
|
|
180
|
+
if (!Array.isArray(keyRefs) || !Array.isArray(valueRefs)) {
|
|
181
|
+
return {};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const result: any = {};
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < keyRefs.length && i < valueRefs.length; i++) {
|
|
187
|
+
const key = this.decodeObject(keyRefs[i]);
|
|
188
|
+
const value = this.decodeObject(valueRefs[i]);
|
|
189
|
+
|
|
190
|
+
if (typeof key === 'string') {
|
|
191
|
+
result[key] = value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Decode NSKeyedArchiver data or return as-is if not archived
|
|
201
|
+
*/
|
|
202
|
+
export function decodeNSKeyedArchiver(data: any): any {
|
|
203
|
+
if (!data) {
|
|
204
|
+
return data;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check if this is NSKeyedArchiver format
|
|
208
|
+
if (!NSKeyedArchiverDecoder.isNSKeyedArchive(data)) {
|
|
209
|
+
return data;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const decoder = new NSKeyedArchiverDecoder(data);
|
|
214
|
+
return decoder.decode();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
log.warn('Failed to decode NSKeyedArchiver data:', error);
|
|
217
|
+
return data;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { PlistDictionary } from '../../../lib/types.js';
|
|
2
|
+
|
|
3
|
+
export function isPlainObject(value: any): value is Record<string, any> {
|
|
4
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function hasProperties(obj: any, ...props: string[]): boolean {
|
|
8
|
+
return isPlainObject(obj) ? props.every((prop) => prop in obj) : false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isNSKeyedArchiverFormat(data: any): boolean {
|
|
12
|
+
return (
|
|
13
|
+
hasProperties(data, '$objects') &&
|
|
14
|
+
Array.isArray(data.$objects) &&
|
|
15
|
+
data.$objects.length > 0
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isNSDictionaryFormat(obj: any): boolean {
|
|
20
|
+
return hasProperties(obj, 'NS.keys', 'NS.objects');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function hasNSErrorIndicators(obj: any): boolean {
|
|
24
|
+
if (!isPlainObject(obj)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const errorProps = ['NSCode', 'NSUserInfo', 'NSDomain'];
|
|
29
|
+
return errorProps.some((prop) => prop in obj);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract $objects array from NSKeyedArchiver format, returns null if invalid
|
|
34
|
+
*/
|
|
35
|
+
export function extractNSKeyedArchiverObjects(data: any): any[] | null {
|
|
36
|
+
if (!isNSKeyedArchiverFormat(data)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const objects = data.$objects;
|
|
41
|
+
return objects.length > 1 ? objects : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract NSDictionary from NSKeyedArchiver objects using key/value references
|
|
46
|
+
*/
|
|
47
|
+
export function extractNSDictionary(
|
|
48
|
+
dictObj: any,
|
|
49
|
+
objects: any[],
|
|
50
|
+
): PlistDictionary {
|
|
51
|
+
if (!isNSDictionaryFormat(dictObj)) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const keysRef = dictObj['NS.keys'];
|
|
56
|
+
const valuesRef = dictObj['NS.objects'];
|
|
57
|
+
|
|
58
|
+
if (!Array.isArray(keysRef) || !Array.isArray(valuesRef)) {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result: PlistDictionary = {};
|
|
63
|
+
for (let i = 0; i < keysRef.length; i++) {
|
|
64
|
+
const key = objects[keysRef[i]];
|
|
65
|
+
const value = objects[valuesRef[i]];
|
|
66
|
+
if (typeof key === 'string') {
|
|
67
|
+
result[key] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract strings from NSKeyedArchiver objects array as dictionary keys
|
|
76
|
+
*/
|
|
77
|
+
export function extractCapabilityStrings(objects: any[]): PlistDictionary {
|
|
78
|
+
const result: PlistDictionary = {};
|
|
79
|
+
|
|
80
|
+
// Start from index 1 because index 0 is always '$null' in NSKeyedArchiver format
|
|
81
|
+
for (let i = 1; i < objects.length; i++) {
|
|
82
|
+
const obj = objects[i];
|
|
83
|
+
if (typeof obj === 'string' && obj !== '$null') {
|
|
84
|
+
result[obj] = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
package/src/services.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { RemoteXpcConnection } from './lib/remote-xpc/remote-xpc-connection.js';
|
|
|
4
4
|
import { TunnelManager } from './lib/tunnel/index.js';
|
|
5
5
|
import { TunnelApiClient } from './lib/tunnel/tunnel-api-client.js';
|
|
6
6
|
import type {
|
|
7
|
+
DVTServiceWithConnection,
|
|
7
8
|
DiagnosticsServiceWithConnection,
|
|
8
9
|
MisagentServiceWithConnection,
|
|
9
10
|
MobileConfigServiceWithConnection,
|
|
@@ -16,6 +17,9 @@ import type {
|
|
|
16
17
|
} from './lib/types.js';
|
|
17
18
|
import AfcService from './services/ios/afc/index.js';
|
|
18
19
|
import DiagnosticsService from './services/ios/diagnostic-service/index.js';
|
|
20
|
+
import { DVTSecureSocketProxyService } from './services/ios/dvt/index.js';
|
|
21
|
+
import { ConditionInducer } from './services/ios/dvt/instruments/condition-inducer.js';
|
|
22
|
+
import { LocationSimulation } from './services/ios/dvt/instruments/location-simulation.js';
|
|
19
23
|
import { MisagentService } from './services/ios/misagent/index.js';
|
|
20
24
|
import { MobileConfigService } from './services/ios/mobile-config/index.js';
|
|
21
25
|
import MobileImageMounterService from './services/ios/mobile-image-mounter/index.js';
|
|
@@ -176,6 +180,35 @@ export async function startWebInspectorService(
|
|
|
176
180
|
};
|
|
177
181
|
}
|
|
178
182
|
|
|
183
|
+
export async function startDVTService(
|
|
184
|
+
udid: string,
|
|
185
|
+
): Promise<DVTServiceWithConnection> {
|
|
186
|
+
const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
|
|
187
|
+
const dvtServiceDescriptor = remoteXPC.findService(
|
|
188
|
+
DVTSecureSocketProxyService.RSD_SERVICE_NAME,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Create DVT service instance
|
|
192
|
+
const dvtService = new DVTSecureSocketProxyService([
|
|
193
|
+
tunnelConnection.host,
|
|
194
|
+
parseInt(dvtServiceDescriptor.port, 10),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
// Connect to DVT service
|
|
198
|
+
await dvtService.connect();
|
|
199
|
+
|
|
200
|
+
// Create instrument services
|
|
201
|
+
const locationSimulation = new LocationSimulation(dvtService);
|
|
202
|
+
const conditionInducer = new ConditionInducer(dvtService);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
remoteXPC: remoteXPC as RemoteXpcConnection,
|
|
206
|
+
dvtService,
|
|
207
|
+
locationSimulation,
|
|
208
|
+
conditionInducer,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
179
212
|
export async function createRemoteXPCConnection(udid: string) {
|
|
180
213
|
const tunnelConnection = await getTunnelInformation(udid);
|
|
181
214
|
const remoteXPC = await startService(
|