appium-ios-remotexpc 0.3.2 → 0.4.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 (33) hide show
  1. package/CHANGELOG.md +16 -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 +17 -5
  6. package/build/src/lib/plist/binary-plist-parser.d.ts.map +1 -1
  7. package/build/src/lib/plist/binary-plist-parser.js +19 -9
  8. package/build/src/lib/plist/plist-creator.d.ts.map +1 -1
  9. package/build/src/lib/plist/plist-creator.js +4 -0
  10. package/build/src/lib/tss/index.d.ts +71 -0
  11. package/build/src/lib/tss/index.d.ts.map +1 -0
  12. package/build/src/lib/tss/index.js +243 -0
  13. package/build/src/lib/types.d.ts +79 -0
  14. package/build/src/lib/types.d.ts.map +1 -1
  15. package/build/src/services/index.d.ts +2 -1
  16. package/build/src/services/index.d.ts.map +1 -1
  17. package/build/src/services/index.js +2 -1
  18. package/build/src/services/ios/mobile-image-mounter/index.d.ts +122 -0
  19. package/build/src/services/ios/mobile-image-mounter/index.d.ts.map +1 -0
  20. package/build/src/services/ios/mobile-image-mounter/index.js +363 -0
  21. package/build/src/services.d.ts +2 -1
  22. package/build/src/services.d.ts.map +1 -1
  23. package/build/src/services.js +12 -0
  24. package/package.json +5 -3
  25. package/src/index.ts +2 -0
  26. package/src/lib/plist/binary-plist-creator.ts +20 -5
  27. package/src/lib/plist/binary-plist-parser.ts +22 -12
  28. package/src/lib/plist/plist-creator.ts +4 -0
  29. package/src/lib/tss/index.ts +338 -0
  30. package/src/lib/types.ts +98 -0
  31. package/src/services/index.ts +2 -0
  32. package/src/services/ios/mobile-image-mounter/index.ts +525 -0
  33. package/src/services.ts +18 -0
@@ -0,0 +1,122 @@
1
+ import type { MobileImageMounterService as MobileImageMounterServiceInterface, PlistDictionary } from '../../../lib/types.js';
2
+ import { BaseService } from '../base-service.js';
3
+ /**
4
+ * Base interface for service responses
5
+ */
6
+ interface BaseResponse {
7
+ Status?: string;
8
+ Error?: string;
9
+ DetailedError?: string;
10
+ }
11
+ /**
12
+ * Interface for image-related responses
13
+ */
14
+ export interface ImageResponse extends BaseResponse {
15
+ ImagePresent?: boolean;
16
+ ImageSignature?: Buffer[] | Buffer;
17
+ }
18
+ /**
19
+ * MobileImageMounterService provides an API to:
20
+ * - Mount Developer Disk Images on iOS devices
21
+ * - Lookup mounted images and their signatures
22
+ * - Check if personalized images are mounted
23
+ * - Unmount images when needed
24
+ */
25
+ declare class MobileImageMounterService extends BaseService implements MobileImageMounterServiceInterface {
26
+ static readonly RSD_SERVICE_NAME = "com.apple.mobile.mobile_image_mounter.shim.remote";
27
+ private static readonly FILE_TYPE_IMAGE;
28
+ private static readonly FILE_TYPE_BUILD_MANIFEST;
29
+ private static readonly FILE_TYPE_TRUST_CACHE;
30
+ private static readonly IMAGE_TYPE;
31
+ private static readonly MOUNT_PATH;
32
+ private static readonly UPLOAD_IMAGE_TIMEOUT;
33
+ private connection;
34
+ constructor(address: [string, number]);
35
+ /**
36
+ * Clean up resources when service is no longer needed
37
+ */
38
+ cleanup(): Promise<void>;
39
+ /**
40
+ * Lookup mounted images by type
41
+ * @param imageType Type of image to lookup (defaults to 'Personalized')
42
+ * @returns Array of signatures of mounted images
43
+ */
44
+ lookup(imageType?: string): Promise<Buffer[]>;
45
+ /**
46
+ * Check if personalized image is mounted
47
+ * @returns True if personalized image is mounted
48
+ */
49
+ isPersonalizedImageMounted(): Promise<boolean>;
50
+ /**
51
+ * Mount personalized image for device (iOS >= 17)
52
+ * @param imageFilePath Path to the image file (.dmg)
53
+ * @param buildManifestFilePath Path to the build manifest file (.plist)
54
+ * @param trustCacheFilePath Path to the trust cache file (.trustcache)
55
+ * @param infoPlist Optional info plist dictionary
56
+ */
57
+ mount(imageFilePath: string, buildManifestFilePath: string, trustCacheFilePath: string, infoPlist?: PlistDictionary): Promise<void>;
58
+ /**
59
+ * Unmount image from device
60
+ * @param mountPath Mount path to unmount (defaults to '/System/Developer')
61
+ */
62
+ unmountImage(mountPath?: string): Promise<void>;
63
+ /**
64
+ * Query developer mode status (iOS 16+)
65
+ * @returns True if developer mode is enabled (defaults to true for older iOS)
66
+ */
67
+ queryDeveloperModeStatus(): Promise<boolean>;
68
+ /**
69
+ * Query personalization nonce for personalized images
70
+ * @param personalizedImageType Optional personalized image type
71
+ * @returns Personalization nonce as Buffer
72
+ */
73
+ queryNonce(personalizedImageType?: string): Promise<Buffer>;
74
+ /**
75
+ * Query personalization identifiers from the device
76
+ * @returns Personalization identifiers dictionary
77
+ */
78
+ queryPersonalizationIdentifiers(): Promise<PlistDictionary>;
79
+ /**
80
+ * Copy devices info (only for mounted images)
81
+ * @returns List of mounted devices
82
+ */
83
+ copyDevices(): Promise<any[]>;
84
+ /**
85
+ * Query personalization manifest from device
86
+ * @param imageType The image type
87
+ * @param signature The image signature/hash
88
+ * @returns Personalization manifest as Buffer
89
+ */
90
+ queryPersonalizationManifest(imageType: string, signature: Buffer): Promise<Buffer>;
91
+ /**
92
+ * Upload image to device
93
+ * @param imageType The image type
94
+ * @param image The image data
95
+ * @param signature The image signature/manifest
96
+ * @param timeout Optional timeout for upload operation (defaults to 20000ms)
97
+ */
98
+ uploadImage(imageType: string, image: Buffer, signature: Buffer, timeout?: number): Promise<void>;
99
+ /**
100
+ * Mount image on device
101
+ * @param imageType The image type
102
+ * @param signature The image signature/manifest
103
+ * @param extras Additional parameters for mounting
104
+ */
105
+ mountImage(imageType: string, signature: Buffer, extras?: Record<string, any>): Promise<void>;
106
+ private sendRequest;
107
+ /**
108
+ * Calculate hash of a buffer asynchronously
109
+ * @param buffer The buffer to hash
110
+ * @returns Promise resolving to the hash digest
111
+ */
112
+ private hashLargeBufferAsync;
113
+ private getOrRetrieveManifestFromTSS;
114
+ private isConnectionDestroyed;
115
+ private connectToMobileImageMounterService;
116
+ private closeConnection;
117
+ private checkIfError;
118
+ private assertIsFile;
119
+ }
120
+ export default MobileImageMounterService;
121
+ export { MobileImageMounterService };
122
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/services/ios/mobile-image-mounter/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,yBAAyB,IAAI,kCAAkC,EAC/D,eAAe,EAChB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIjD;;GAEG;AACH,UAAU,YAAY;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;CACpC;AAED;;;;;;GAMG;AACH,cAAM,yBACJ,SAAQ,WACR,YAAW,kCAAkC;IAE7C,MAAM,CAAC,QAAQ,CAAC,gBAAgB,uDACsB;IAGtD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAW;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAAoB;IACpE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAiB;IAC9D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAkB;IACpD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAuB;IACzD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAGrD,OAAO,CAAC,UAAU,CAAkC;gBAExC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;IAIrC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;;;OAIG;IACG,MAAM,CACV,SAAS,SAAuC,GAC/C,OAAO,CAAC,MAAM,EAAE,CAAC;IAUpB;;;OAGG;IACG,0BAA0B,IAAI,OAAO,CAAC,OAAO,CAAC;IAQpD;;;;;;OAMG;IACG,KAAK,CACT,aAAa,EAAE,MAAM,EACrB,qBAAqB,EAAE,MAAM,EAC7B,kBAAkB,EAAE,MAAM,EAC1B,SAAS,CAAC,EAAE,eAAe,GAC1B,OAAO,CAAC,IAAI,CAAC;IAgEhB;;;OAGG;IACG,YAAY,CAChB,SAAS,SAAuC,GAC/C,OAAO,CAAC,IAAI,CAAC;IAsBhB;;;OAGG;IACG,wBAAwB,IAAI,OAAO,CAAC,OAAO,CAAC;IAYlD;;;;OAIG;IACG,UAAU,CAAC,qBAAqB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBjE;;;OAGG;IACG,+BAA+B,IAAI,OAAO,CAAC,eAAe,CAAC;IAQjE;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAKnC;;;;;OAKG;IACG,4BAA4B,CAChC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IAgClB;;;;;;OAMG;IACG,WAAW,CACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,SAAiD,GACvD,OAAO,CAAC,IAAI,CAAC;IAiChB;;;;;OAKG;IACG,UAAU,CACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC3B,OAAO,CAAC,IAAI,CAAC;YA4BF,WAAW;IAczB;;;;OAIG;YACW,oBAAoB;YAmBpB,4BAA4B;IAyC1C,OAAO,CAAC,qBAAqB;YASf,kCAAkC;IAchD,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,YAAY;YAMN,YAAY;CAiB3B;AAED,eAAe,yBAAyB,CAAC;AACzC,OAAO,EAAE,yBAAyB,EAAE,CAAC"}
@@ -0,0 +1,363 @@
1
+ import { logger } from '@appium/support';
2
+ import { createHash } from 'crypto';
3
+ import { Stats, promises as fs } from 'fs';
4
+ import { performance } from 'perf_hooks';
5
+ import { Readable } from 'stream';
6
+ import { parseXmlPlist } from '../../../lib/plist/index.js';
7
+ import { getManifestFromTSS } from '../../../lib/tss/index.js';
8
+ import { ServiceConnection } from '../../../service-connection.js';
9
+ import { BaseService } from '../base-service.js';
10
+ const log = logger.getLogger('MobileImageMounterService');
11
+ /**
12
+ * MobileImageMounterService provides an API to:
13
+ * - Mount Developer Disk Images on iOS devices
14
+ * - Lookup mounted images and their signatures
15
+ * - Check if personalized images are mounted
16
+ * - Unmount images when needed
17
+ */
18
+ class MobileImageMounterService extends BaseService {
19
+ static RSD_SERVICE_NAME = 'com.apple.mobile.mobile_image_mounter.shim.remote';
20
+ // Constants
21
+ static FILE_TYPE_IMAGE = 'image';
22
+ static FILE_TYPE_BUILD_MANIFEST = 'build_manifest';
23
+ static FILE_TYPE_TRUST_CACHE = 'trust_cache';
24
+ static IMAGE_TYPE = 'Personalized';
25
+ static MOUNT_PATH = '/System/Developer';
26
+ static UPLOAD_IMAGE_TIMEOUT = 20000;
27
+ // Connection cache
28
+ connection = null;
29
+ constructor(address) {
30
+ super(address);
31
+ }
32
+ /**
33
+ * Clean up resources when service is no longer needed
34
+ */
35
+ async cleanup() {
36
+ this.closeConnection();
37
+ }
38
+ /**
39
+ * Lookup mounted images by type
40
+ * @param imageType Type of image to lookup (defaults to 'Personalized')
41
+ * @returns Array of signatures of mounted images
42
+ */
43
+ async lookup(imageType = MobileImageMounterService.IMAGE_TYPE) {
44
+ const response = (await this.sendRequest({
45
+ Command: 'LookupImage',
46
+ ImageType: imageType,
47
+ }));
48
+ const signatures = response.ImageSignature || [];
49
+ return signatures.filter(Buffer.isBuffer);
50
+ }
51
+ /**
52
+ * Check if personalized image is mounted
53
+ * @returns True if personalized image is mounted
54
+ */
55
+ async isPersonalizedImageMounted() {
56
+ try {
57
+ return (await this.lookup()).length > 0;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ /**
64
+ * Mount personalized image for device (iOS >= 17)
65
+ * @param imageFilePath Path to the image file (.dmg)
66
+ * @param buildManifestFilePath Path to the build manifest file (.plist)
67
+ * @param trustCacheFilePath Path to the trust cache file (.trustcache)
68
+ * @param infoPlist Optional info plist dictionary
69
+ */
70
+ async mount(imageFilePath, buildManifestFilePath, trustCacheFilePath, infoPlist) {
71
+ if (await this.isPersonalizedImageMounted()) {
72
+ log.info('Personalized image is already mounted');
73
+ return;
74
+ }
75
+ const start = performance.now();
76
+ // Validate files and read content
77
+ await Promise.all([
78
+ this.assertIsFile(imageFilePath, MobileImageMounterService.FILE_TYPE_IMAGE),
79
+ this.assertIsFile(buildManifestFilePath, MobileImageMounterService.FILE_TYPE_BUILD_MANIFEST),
80
+ this.assertIsFile(trustCacheFilePath, MobileImageMounterService.FILE_TYPE_TRUST_CACHE),
81
+ ]);
82
+ const [image, trustCache, buildManifestContent] = await Promise.all([
83
+ fs.readFile(imageFilePath),
84
+ fs.readFile(trustCacheFilePath),
85
+ fs.readFile(buildManifestFilePath, 'utf8'),
86
+ ]);
87
+ const buildManifest = parseXmlPlist(buildManifestContent);
88
+ const manifest = await this.getOrRetrieveManifestFromTSS(image, buildManifest);
89
+ await this.uploadImage(MobileImageMounterService.IMAGE_TYPE, image, manifest);
90
+ const extras = {
91
+ ImageTrustCache: trustCache,
92
+ };
93
+ if (infoPlist) {
94
+ extras.ImageInfoPlist = infoPlist;
95
+ }
96
+ await this.mountImage(MobileImageMounterService.IMAGE_TYPE, manifest, extras);
97
+ const end = performance.now();
98
+ log.info(`Successfully mounted personalized image in ${(end - start).toFixed(2)} ms`);
99
+ }
100
+ /**
101
+ * Unmount image from device
102
+ * @param mountPath Mount path to unmount (defaults to '/System/Developer')
103
+ */
104
+ async unmountImage(mountPath = MobileImageMounterService.MOUNT_PATH) {
105
+ const response = (await this.sendRequest({
106
+ Command: 'UnmountImage',
107
+ MountPath: mountPath,
108
+ }));
109
+ if (response.Error === 'UnknownCommand') {
110
+ throw new Error('Unmount command is not supported on this iOS version');
111
+ }
112
+ if (response.DetailedError?.includes('There is no matching entry')) {
113
+ throw new Error(`No mounted image found at path: ${mountPath}`);
114
+ }
115
+ if (response.Error === 'InternalError') {
116
+ throw new Error(`Internal error occurred while unmounting: ${JSON.stringify(response)}`);
117
+ }
118
+ this.checkIfError(response);
119
+ log.info(`Successfully unmounted image from ${mountPath}`);
120
+ }
121
+ /**
122
+ * Query developer mode status (iOS 16+)
123
+ * @returns True if developer mode is enabled (defaults to true for older iOS)
124
+ */
125
+ async queryDeveloperModeStatus() {
126
+ try {
127
+ const response = await this.sendRequest({
128
+ Command: 'QueryDeveloperModeStatus',
129
+ });
130
+ this.checkIfError(response);
131
+ return Boolean(response.DeveloperModeStatus);
132
+ }
133
+ catch {
134
+ return true; // Default for older iOS versions
135
+ }
136
+ }
137
+ /**
138
+ * Query personalization nonce for personalized images
139
+ * @param personalizedImageType Optional personalized image type
140
+ * @returns Personalization nonce as Buffer
141
+ */
142
+ async queryNonce(personalizedImageType) {
143
+ const request = { Command: 'QueryNonce' };
144
+ if (personalizedImageType) {
145
+ request.PersonalizedImageType = personalizedImageType;
146
+ }
147
+ const response = await this.sendRequest(request);
148
+ this.checkIfError(response);
149
+ const nonce = response.PersonalizationNonce;
150
+ if (!Buffer.isBuffer(nonce)) {
151
+ throw new Error('Invalid nonce received from device');
152
+ }
153
+ return nonce;
154
+ }
155
+ /**
156
+ * Query personalization identifiers from the device
157
+ * @returns Personalization identifiers dictionary
158
+ */
159
+ async queryPersonalizationIdentifiers() {
160
+ const response = await this.sendRequest({
161
+ Command: 'QueryPersonalizationIdentifiers',
162
+ });
163
+ this.checkIfError(response);
164
+ return response.PersonalizationIdentifiers;
165
+ }
166
+ /**
167
+ * Copy devices info (only for mounted images)
168
+ * @returns List of mounted devices
169
+ */
170
+ async copyDevices() {
171
+ const response = await this.sendRequest({ Command: 'CopyDevices' });
172
+ return response.EntryList || [];
173
+ }
174
+ /**
175
+ * Query personalization manifest from device
176
+ * @param imageType The image type
177
+ * @param signature The image signature/hash
178
+ * @returns Personalization manifest as Buffer
179
+ */
180
+ async queryPersonalizationManifest(imageType, signature) {
181
+ try {
182
+ const response = await this.sendRequest({
183
+ Command: 'QueryPersonalizationManifest',
184
+ PersonalizedImageType: imageType,
185
+ ImageType: imageType,
186
+ ImageSignature: signature,
187
+ });
188
+ this.checkIfError(response);
189
+ const manifest = response.ImageSignature;
190
+ if (!manifest || !Buffer.isBuffer(manifest)) {
191
+ throw new Error('MissingManifestError: Personalization manifest not found on device');
192
+ }
193
+ return manifest;
194
+ }
195
+ catch (error) {
196
+ if (error instanceof Error &&
197
+ error.message.includes('MissingManifestError')) {
198
+ throw error;
199
+ }
200
+ throw new Error('MissingManifestError: Personalization manifest not found on device');
201
+ }
202
+ }
203
+ /**
204
+ * Upload image to device
205
+ * @param imageType The image type
206
+ * @param image The image data
207
+ * @param signature The image signature/manifest
208
+ * @param timeout Optional timeout for upload operation (defaults to 20000ms)
209
+ */
210
+ async uploadImage(imageType, image, signature, timeout = MobileImageMounterService.UPLOAD_IMAGE_TIMEOUT) {
211
+ const receiveBytesResult = (await this.sendRequest({
212
+ Command: 'ReceiveBytes',
213
+ ImageType: imageType,
214
+ ImageSize: image.length,
215
+ ImageSignature: signature,
216
+ }));
217
+ this.checkIfError(receiveBytesResult);
218
+ if (receiveBytesResult.Status !== 'ReceiveBytesAck') {
219
+ throw new Error(`Unexpected return from mobile_image_mounter: ${JSON.stringify(receiveBytesResult)}`);
220
+ }
221
+ const conn = await this.connectToMobileImageMounterService();
222
+ const socket = conn.getSocket();
223
+ await new Promise((resolve, reject) => {
224
+ socket.write(image, (error) => error ? reject(error) : resolve());
225
+ });
226
+ const uploadResult = await conn.receive(timeout);
227
+ if (uploadResult.Status !== 'Complete') {
228
+ throw new Error(`Image upload failed: ${JSON.stringify(uploadResult)}`);
229
+ }
230
+ log.debug('Image uploaded successfully');
231
+ }
232
+ /**
233
+ * Mount image on device
234
+ * @param imageType The image type
235
+ * @param signature The image signature/manifest
236
+ * @param extras Additional parameters for mounting
237
+ */
238
+ async mountImage(imageType, signature, extras) {
239
+ const request = {
240
+ Command: 'MountImage',
241
+ ImageType: imageType,
242
+ ImageSignature: signature,
243
+ ...extras,
244
+ };
245
+ const response = (await this.sendRequest(request));
246
+ if (response.DetailedError?.includes('is already mounted')) {
247
+ log.info('Image was already mounted');
248
+ return;
249
+ }
250
+ if (response.DetailedError?.includes('Developer mode is not enabled')) {
251
+ throw new Error('Developer mode is not enabled on this device');
252
+ }
253
+ this.checkIfError(response);
254
+ if (response.Status !== 'Complete') {
255
+ throw new Error(`Mount image failed: ${JSON.stringify(response)}`);
256
+ }
257
+ log.debug('Image mounted successfully');
258
+ }
259
+ async sendRequest(request, timeout) {
260
+ const isNewConnection = !this.connection || this.isConnectionDestroyed();
261
+ const conn = await this.connectToMobileImageMounterService();
262
+ const res = await conn.sendPlistRequest(request, timeout);
263
+ if (isNewConnection && res?.Request === 'StartService') {
264
+ return await conn.receive();
265
+ }
266
+ return res;
267
+ }
268
+ /**
269
+ * Calculate hash of a buffer asynchronously
270
+ * @param buffer The buffer to hash
271
+ * @returns Promise resolving to the hash digest
272
+ */
273
+ async hashLargeBufferAsync(buffer) {
274
+ return new Promise((resolve, reject) => {
275
+ const hash = createHash('sha384');
276
+ const stream = Readable.from(buffer);
277
+ stream.on('data', (chunk) => {
278
+ hash.update(chunk);
279
+ });
280
+ stream.on('end', () => {
281
+ resolve(hash.digest());
282
+ });
283
+ stream.on('error', (err) => {
284
+ reject(err);
285
+ });
286
+ });
287
+ }
288
+ async getOrRetrieveManifestFromTSS(image, buildManifest) {
289
+ try {
290
+ const imageHash = await this.hashLargeBufferAsync(image);
291
+ const manifest = await this.queryPersonalizationManifest('DeveloperDiskImage', imageHash);
292
+ log.debug('Successfully retrieved existing personalization manifest from device');
293
+ return manifest;
294
+ }
295
+ catch (error) {
296
+ if (error.message?.includes('MissingManifestError')) {
297
+ log.debug('Personalization manifest not found on device, using TSS...');
298
+ const identifiers = await this.queryPersonalizationIdentifiers();
299
+ const ecid = identifiers.UniqueChipID;
300
+ if (!ecid) {
301
+ throw new Error('Could not retrieve device ECID from personalization identifiers');
302
+ }
303
+ const manifest = await getManifestFromTSS(ecid, buildManifest, () => this.queryPersonalizationIdentifiers(), (type) => this.queryNonce(type));
304
+ log.debug('Successfully generated manifest from TSS');
305
+ return manifest;
306
+ }
307
+ throw error;
308
+ }
309
+ }
310
+ isConnectionDestroyed() {
311
+ try {
312
+ const socket = this.connection.getSocket();
313
+ return !socket || socket.destroyed;
314
+ }
315
+ catch {
316
+ return true;
317
+ }
318
+ }
319
+ async connectToMobileImageMounterService() {
320
+ if (this.connection && !this.isConnectionDestroyed()) {
321
+ return this.connection;
322
+ }
323
+ const newConnection = await this.startLockdownService({
324
+ serviceName: MobileImageMounterService.RSD_SERVICE_NAME,
325
+ port: this.address[1].toString(),
326
+ });
327
+ this.connection = newConnection;
328
+ return newConnection;
329
+ }
330
+ closeConnection() {
331
+ if (this.connection) {
332
+ try {
333
+ this.connection.close();
334
+ }
335
+ catch {
336
+ // Ignore close errors
337
+ }
338
+ this.connection = null;
339
+ }
340
+ }
341
+ checkIfError(response) {
342
+ if (response.Error) {
343
+ throw new Error(response.Error);
344
+ }
345
+ }
346
+ async assertIsFile(filePath, fileType) {
347
+ try {
348
+ const fileStat = await fs.stat(filePath);
349
+ if (!fileStat.isFile()) {
350
+ throw new Error(`Expected ${fileType} file, got non-file: ${filePath}`);
351
+ }
352
+ return fileStat;
353
+ }
354
+ catch (error) {
355
+ if (error.code === 'ENOENT') {
356
+ throw new Error(`${fileType} file not found: ${filePath}`);
357
+ }
358
+ throw error;
359
+ }
360
+ }
361
+ }
362
+ export default MobileImageMounterService;
363
+ export { MobileImageMounterService };
@@ -1,7 +1,8 @@
1
1
  import { RemoteXpcConnection } from './lib/remote-xpc/remote-xpc-connection.js';
2
- import type { DiagnosticsServiceWithConnection, NotificationProxyServiceWithConnection, SyslogService as SyslogServiceType } from './lib/types.js';
2
+ import type { DiagnosticsServiceWithConnection, MobileImageMounterServiceWithConnection, NotificationProxyServiceWithConnection, SyslogService as SyslogServiceType } from './lib/types.js';
3
3
  export declare function startDiagnosticsService(udid: string): Promise<DiagnosticsServiceWithConnection>;
4
4
  export declare function startNotificationProxyService(udid: string): Promise<NotificationProxyServiceWithConnection>;
5
+ export declare function startMobileImageMounterService(udid: string): Promise<MobileImageMounterServiceWithConnection>;
5
6
  export declare function startSyslogService(udid: string): Promise<SyslogServiceType>;
6
7
  export declare function createRemoteXPCConnection(udid: string): Promise<{
7
8
  remoteXPC: RemoteXpcConnection;
@@ -1 +1 @@
1
- {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/services.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAGhF,OAAO,KAAK,EACV,gCAAgC,EAChC,sCAAsC,EACtC,aAAa,IAAI,iBAAiB,EACnC,MAAM,gBAAgB,CAAC;AAQxB,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,sCAAsC,CAAC,CAYjD;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAG5B;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,MAAM;;;;;;;;GAO3D"}
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/services.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAGhF,OAAO,KAAK,EACV,gCAAgC,EAChC,uCAAuC,EACvC,sCAAsC,EACtC,aAAa,IAAI,iBAAiB,EACnC,MAAM,gBAAgB,CAAC;AASxB,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,sCAAsC,CAAC,CAYjD;AAED,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,uCAAuC,CAAC,CAYlD;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAG5B;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,MAAM;;;;;;;;GAO3D"}
@@ -3,6 +3,7 @@ import { RemoteXpcConnection } from './lib/remote-xpc/remote-xpc-connection.js';
3
3
  import { TunnelManager } from './lib/tunnel/index.js';
4
4
  import { TunnelApiClient } from './lib/tunnel/tunnel-api-client.js';
5
5
  import DiagnosticsService from './services/ios/diagnostic-service/index.js';
6
+ import MobileImageMounterService from './services/ios/mobile-image-mounter/index.js';
6
7
  import { NotificationProxyService } from './services/ios/notification-proxy/index.js';
7
8
  import SyslogService from './services/ios/syslog-service/index.js';
8
9
  const APPIUM_XCUITEST_DRIVER_NAME = 'appium-xcuitest-driver';
@@ -29,6 +30,17 @@ export async function startNotificationProxyService(udid) {
29
30
  ]),
30
31
  };
31
32
  }
33
+ export async function startMobileImageMounterService(udid) {
34
+ const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
35
+ const mobileImageMounterService = remoteXPC.findService(MobileImageMounterService.RSD_SERVICE_NAME);
36
+ return {
37
+ remoteXPC: remoteXPC,
38
+ mobileImageMounterService: new MobileImageMounterService([
39
+ tunnelConnection.host,
40
+ parseInt(mobileImageMounterService.port, 10),
41
+ ]),
42
+ };
43
+ }
32
44
  export async function startSyslogService(udid) {
33
45
  const { tunnelConnection } = await createRemoteXPCConnection(udid);
34
46
  return new SyslogService([tunnelConnection.host, tunnelConnection.port]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-remotexpc",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "main": "build/src/index.js",
5
5
  "types": "build/src/index.d.ts",
6
6
  "type": "module",
@@ -29,6 +29,7 @@
29
29
  "test:pair-record": "mocha test/integration/read-pair-record-test.ts --exit --timeout 1m",
30
30
  "test:diagnostics": "mocha test/integration/diagnostics-test.ts --exit --timeout 1m",
31
31
  "test:notification": "mocha test/integration/notification-proxy-test.ts --exit --timeout 1m",
32
+ "test:image-mounter": "mocha test/integration/mobile-image-mounter-test.ts --exit --timeout 1m",
32
33
  "test:unit": "mocha 'test/unit/**/*.ts' --exit --timeout 2m",
33
34
  "test:tunnel-creation": "sudo tsx scripts/test-tunnel-creation.ts",
34
35
  "test:tunnel-creation:lsof": "sudo tsx scripts/test-tunnel-creation.ts --keep-open"
@@ -70,8 +71,9 @@
70
71
  "@appium/support": "^7.0.0-rc.1",
71
72
  "@types/node": "^24.0.10",
72
73
  "@xmldom/xmldom": "^0.9.8",
73
- "npm-run-all2": "^8.0.4",
74
- "appium-ios-tuntap": "^0.x"
74
+ "appium-ios-tuntap": "^0.x",
75
+ "axios": "^1.12.0",
76
+ "npm-run-all2": "^8.0.4"
75
77
  },
76
78
  "files": [
77
79
  "src",
package/src/index.ts CHANGED
@@ -14,12 +14,14 @@ import { startCoreDeviceProxy } from './services/ios/tunnel-service/index.js';
14
14
 
15
15
  export type {
16
16
  DiagnosticsService,
17
+ MobileImageMounterService,
17
18
  SyslogService,
18
19
  SocketInfo,
19
20
  TunnelResult,
20
21
  TunnelRegistry,
21
22
  TunnelRegistryEntry,
22
23
  DiagnosticsServiceWithConnection,
24
+ MobileImageMounterServiceWithConnection,
23
25
  } from './lib/types.js';
24
26
  export {
25
27
  createUsbmux,
@@ -59,7 +59,11 @@ class BinaryPlistCreator {
59
59
  const objectData: Buffer[] = [];
60
60
 
61
61
  for (const value of this._objectTable) {
62
- objectOffsets.push(this._calculateObjectDataLength(objectData));
62
+ // Calculate offset including the header length
63
+ objectOffsets.push(
64
+ BPLIST_MAGIC_AND_VERSION.length +
65
+ this._calculateObjectDataLength(objectData),
66
+ );
63
67
  objectData.push(this._createObjectData(value));
64
68
  }
65
69
 
@@ -341,11 +345,22 @@ class BinaryPlistCreator {
341
345
  // Check if string can be ASCII
342
346
  // eslint-disable-next-line no-control-regex
343
347
  const isAscii = /^[\x00-\x7F]*$/.test(value);
344
- const stringBuffer = isAscii
345
- ? Buffer.from(value, 'ascii')
346
- : Buffer.from(value, 'utf16le');
347
348
 
348
- // Fixed the typo here - using stringBuffer.length instead of value.length for Unicode strings
349
+ let stringBuffer: Buffer;
350
+ if (isAscii) {
351
+ stringBuffer = Buffer.from(value, 'ascii');
352
+ } else {
353
+ // Unicode strings should be stored as UTF-16BE in binary plists
354
+ const utf16leBuffer = Buffer.from(value, 'utf16le');
355
+ stringBuffer = Buffer.alloc(utf16leBuffer.length);
356
+
357
+ // Convert UTF-16LE to UTF-16BE
358
+ for (let i = 0; i < utf16leBuffer.length; i += 2) {
359
+ stringBuffer[i] = utf16leBuffer[i + 1]; // High byte
360
+ stringBuffer[i + 1] = utf16leBuffer[i]; // Low byte
361
+ }
362
+ }
363
+
349
364
  const length = isAscii ? value.length : stringBuffer.length / 2;
350
365
  let header: Buffer;
351
366
 
@@ -270,15 +270,21 @@ class BinaryPlistParser {
270
270
  * @returns The parsed string
271
271
  */
272
272
  private _parseUnicodeString(startOffset: number, objLength: number): string {
273
- // Unicode strings are stored as UTF-16BE
274
- const utf16Buffer = Buffer.alloc(objLength * 2);
275
- for (let j = 0; j < objLength; j++) {
276
- utf16Buffer.writeUInt16BE(
277
- this._buffer.readUInt16BE(startOffset + j * 2),
278
- j * 2,
279
- );
273
+ // Unicode strings are stored as UTF-16BE in binary plists
274
+ const bytesToRead = objLength * 2;
275
+ const stringBuffer = this._buffer.slice(
276
+ startOffset,
277
+ startOffset + bytesToRead,
278
+ );
279
+
280
+ // Convert UTF-16BE to UTF-16LE for proper decoding
281
+ const utf16leBuffer = Buffer.alloc(bytesToRead);
282
+ for (let i = 0; i < bytesToRead; i += 2) {
283
+ utf16leBuffer[i] = stringBuffer[i + 1]; // Low byte
284
+ utf16leBuffer[i + 1] = stringBuffer[i]; // High byte
280
285
  }
281
- return utf16Buffer.toString('utf16le', 0, objLength * 2);
286
+
287
+ return utf16leBuffer.toString('utf16le');
282
288
  }
283
289
 
284
290
  /**
@@ -476,8 +482,10 @@ class BinaryPlistParser {
476
482
  obj.startOffset + j * this._objectRefSize,
477
483
  );
478
484
  const refValue = this._objectTable[refIdx];
479
- // Ensure we're not adding a TempObject to the array
480
- if (!this._isTempObject(refValue)) {
485
+ // Handle TempObjects correctly - they should be resolved by the time we get here
486
+ if (this._isTempObject(refValue)) {
487
+ array.push(refValue.value);
488
+ } else {
481
489
  array.push(refValue);
482
490
  }
483
491
  }
@@ -511,8 +519,10 @@ class BinaryPlistParser {
511
519
  );
512
520
  }
513
521
 
514
- // Ensure we're not adding a TempObject to the dictionary
515
- if (!this._isTempObject(value)) {
522
+ // Handle TempObjects correctly - they should be resolved by the time we get here
523
+ if (this._isTempObject(value)) {
524
+ dict[key] = value.value;
525
+ } else {
516
526
  dict[key] = value;
517
527
  }
518
528
  }