appium-ios-remotexpc 2.2.4 → 2.3.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 (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/src/index.d.ts +1 -0
  3. package/build/src/index.d.ts.map +1 -1
  4. package/build/src/index.js.map +1 -1
  5. package/build/src/services/ios/afc/codec.d.ts +5 -0
  6. package/build/src/services/ios/afc/codec.d.ts.map +1 -1
  7. package/build/src/services/ios/afc/codec.js +10 -0
  8. package/build/src/services/ios/afc/codec.js.map +1 -1
  9. package/build/src/services/ios/installation-proxy/index.d.ts.map +1 -1
  10. package/build/src/services/ios/installation-proxy/index.js +1 -24
  11. package/build/src/services/ios/installation-proxy/index.js.map +1 -1
  12. package/build/src/services/ios/zipconduit/constants.d.ts +20 -0
  13. package/build/src/services/ios/zipconduit/constants.d.ts.map +1 -0
  14. package/build/src/services/ios/zipconduit/constants.js +20 -0
  15. package/build/src/services/ios/zipconduit/constants.js.map +1 -0
  16. package/build/src/services/ios/zipconduit/index.d.ts +38 -0
  17. package/build/src/services/ios/zipconduit/index.d.ts.map +1 -0
  18. package/build/src/services/ios/zipconduit/index.js +148 -0
  19. package/build/src/services/ios/zipconduit/index.js.map +1 -0
  20. package/build/src/services/ios/zipconduit/plists.d.ts +46 -0
  21. package/build/src/services/ios/zipconduit/plists.d.ts.map +1 -0
  22. package/build/src/services/ios/zipconduit/plists.js +87 -0
  23. package/build/src/services/ios/zipconduit/plists.js.map +1 -0
  24. package/build/src/services/ios/zipconduit/stream-zip.d.ts +105 -0
  25. package/build/src/services/ios/zipconduit/stream-zip.d.ts.map +1 -0
  26. package/build/src/services/ios/zipconduit/stream-zip.js +883 -0
  27. package/build/src/services/ios/zipconduit/stream-zip.js.map +1 -0
  28. package/build/src/services/ios/zipconduit/zip-reader.d.ts +15 -0
  29. package/build/src/services/ios/zipconduit/zip-reader.d.ts.map +1 -0
  30. package/build/src/services/ios/zipconduit/zip-reader.js +33 -0
  31. package/build/src/services/ios/zipconduit/zip-reader.js.map +1 -0
  32. package/build/src/services/ios/zipconduit/zip-utils.d.ts +27 -0
  33. package/build/src/services/ios/zipconduit/zip-utils.d.ts.map +1 -0
  34. package/build/src/services/ios/zipconduit/zip-utils.js +116 -0
  35. package/build/src/services/ios/zipconduit/zip-utils.js.map +1 -0
  36. package/build/src/services.d.ts +6 -0
  37. package/build/src/services.d.ts.map +1 -1
  38. package/build/src/services.js +14 -0
  39. package/build/src/services.js.map +1 -1
  40. package/package.json +2 -1
  41. package/src/index.ts +6 -0
  42. package/src/services/ios/afc/codec.ts +15 -0
  43. package/src/services/ios/installation-proxy/index.ts +1 -42
  44. package/src/services/ios/zipconduit/constants.ts +30 -0
  45. package/src/services/ios/zipconduit/index.ts +242 -0
  46. package/src/services/ios/zipconduit/plists.ts +139 -0
  47. package/src/services/ios/zipconduit/stream-zip.ts +1082 -0
  48. package/src/services/ios/zipconduit/zip-reader.ts +48 -0
  49. package/src/services/ios/zipconduit/zip-utils.ts +185 -0
  50. package/src/services.ts +19 -0
@@ -0,0 +1,30 @@
1
+ /** Observed Xcode directory permission value (0o755). */
2
+ export const STD_DIR_PERM = 16877;
3
+
4
+ /** Observed Xcode file permission value (0o644). */
5
+ export const STD_FILE_PERM = -32348;
6
+
7
+ export const METAINF_FILE_NAME = 'com.apple.ZipMetadata.plist';
8
+
9
+ /** Fake central directory signature sent after the streaming payload. */
10
+ export const CENTRAL_DIRECTORY_HEADER = Buffer.from([0x50, 0x4b, 0x01, 0x02]);
11
+
12
+ /**
13
+ * Fixed 32-byte UT extra field observed in Xcode zip_conduit traffic.
14
+ * https://commons.apache.org/proper/commons-compress/apidocs/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestamp.html
15
+ */
16
+ export const ZIP_EXTRA_BYTES = Buffer.from(
17
+ '55540D0007F3A2EC60F6A2EC60F3A2EC6075780B000104F50100000414000000',
18
+ 'hex',
19
+ );
20
+
21
+ export const ZIP_LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
22
+ export const ZIP_HEADER_LAST_MODIFIED_TIME = 0xbdef;
23
+ export const ZIP_HEADER_LAST_MODIFIED_DATE = 0x52ec;
24
+
25
+ export const COPY_BUFFER_SIZE = 32 * 1024;
26
+
27
+ /** Socket write size while streaming entry payloads (match AFC push chunking). */
28
+ export const TRANSFER_CHUNK_SIZE = 1024 * 1024;
29
+
30
+ export const DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
@@ -0,0 +1,242 @@
1
+ import fsp from 'node:fs/promises';
2
+ import type net from 'node:net';
3
+
4
+ import { getLogger } from '../../../lib/logger.js';
5
+ import type { PlistDictionary } from '../../../lib/types.js';
6
+ import {
7
+ cleanupServiceSocket,
8
+ createRawServiceSocket,
9
+ recvOnePlist,
10
+ sendOnePlist,
11
+ writeBufferToSocket,
12
+ } from '../afc/codec.js';
13
+ import {
14
+ CENTRAL_DIRECTORY_HEADER,
15
+ DEFAULT_INSTALL_TIMEOUT_MS,
16
+ } from './constants.js';
17
+ import {
18
+ type ZipConduitProgressUpdate,
19
+ createInitTransfer,
20
+ evaluateProgress,
21
+ } from './plists.js';
22
+ import {
23
+ type IpaZipEntry,
24
+ listZipEntries,
25
+ openZipEntryStream,
26
+ withZipFile,
27
+ } from './zip-reader.js';
28
+ import {
29
+ transferDirectory,
30
+ transferFile,
31
+ transferMetaInfDirectory,
32
+ transferMetaInfFile,
33
+ } from './zip-utils.js';
34
+
35
+ const log = getLogger('ZipConduitService');
36
+
37
+ export type ZipConduitProgressCallback = (
38
+ update: ZipConduitProgressUpdate,
39
+ ) => void;
40
+
41
+ export interface ZipConduitInstallOptions {
42
+ progress?: ZipConduitProgressCallback;
43
+ timeoutMs?: number;
44
+ /** Stop after payload upload; skip waiting for install progress plists. */
45
+ streamOnly?: boolean;
46
+ }
47
+
48
+ export interface ZipConduitStreamStats {
49
+ streamMs: number;
50
+ payloadBytes: number;
51
+ entryCount: number;
52
+ }
53
+
54
+ /**
55
+ * Streaming zip_conduit client for fast IPA installation over RSD.
56
+ */
57
+ export class ZipConduitService {
58
+ static readonly RSD_SERVICE_NAME =
59
+ 'com.apple.streaming_zip_conduit.shim.remote';
60
+
61
+ private socket: net.Socket | null = null;
62
+
63
+ constructor(private readonly address: [string, number]) {}
64
+
65
+ /**
66
+ * Connect to the zip_conduit service and complete the RSD handshake.
67
+ */
68
+ async connect(): Promise<void> {
69
+ if (this.socket) {
70
+ return;
71
+ }
72
+ this.socket = await createRawServiceSocket(
73
+ this.address[0],
74
+ this.address[1],
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Install an IPA or app directory using streaming zip_conduit.
80
+ */
81
+ async install(
82
+ appPath: string,
83
+ options: ZipConduitInstallOptions = {},
84
+ ): Promise<ZipConduitStreamStats | void> {
85
+ await this.connect();
86
+ const socket = this.socket;
87
+ if (!socket) {
88
+ throw new Error('ZipConduitService is not connected');
89
+ }
90
+
91
+ const fileStats = await fsp.stat(appPath);
92
+ if (fileStats.isDirectory()) {
93
+ throw new Error(
94
+ 'Directory install is not supported yet; provide a path to an .ipa file',
95
+ );
96
+ }
97
+
98
+ const streamStats = await this.sendIpaFile(socket, appPath);
99
+ if (options.streamOnly) {
100
+ return streamStats;
101
+ }
102
+ await this.waitForInstallation(socket, options);
103
+ }
104
+
105
+ /**
106
+ * Close the underlying socket.
107
+ */
108
+ close(): void {
109
+ if (!this.socket) {
110
+ return;
111
+ }
112
+ cleanupServiceSocket(this.socket);
113
+ this.socket = null;
114
+ }
115
+
116
+ private async sendIpaFile(
117
+ socket: net.Socket,
118
+ ipaPath: string,
119
+ ): Promise<ZipConduitStreamStats> {
120
+ const streamStart = performance.now();
121
+ let payloadBytes = 0;
122
+
123
+ const init = createInitTransfer(ipaPath);
124
+ const { entries } = await withZipFile(ipaPath, async (zip) => {
125
+ const listed = await listZipEntries(zip);
126
+ const { totalBytes, numFiles } = collectZipStats(listed);
127
+ log.debug(`Sending InitTransfer for ${init.MediaSubdir}`);
128
+ await sendOnePlist(socket, init as unknown as PlistDictionary);
129
+
130
+ await transferMetaInfDirectory(socket);
131
+ await transferMetaInfFile(socket, numFiles, totalBytes);
132
+
133
+ for (const entry of listed) {
134
+ if (isDirectoryEntry(entry)) {
135
+ await transferDirectory(socket, entry.name);
136
+ continue;
137
+ }
138
+
139
+ const stream = await openZipEntryStream(zip, entry);
140
+ try {
141
+ await transferFile(socket, stream, entry.crc, entry.size, entry.name);
142
+ payloadBytes += entry.size;
143
+ } finally {
144
+ stream.destroy();
145
+ }
146
+ }
147
+
148
+ return { entries: listed };
149
+ });
150
+
151
+ log.debug('IPA payload sent, writing central directory marker');
152
+ await writeBufferToSocket(socket, CENTRAL_DIRECTORY_HEADER);
153
+
154
+ const streamMs = performance.now() - streamStart;
155
+ const stats: ZipConduitStreamStats = {
156
+ streamMs,
157
+ payloadBytes,
158
+ entryCount: entries.length,
159
+ };
160
+ log.debug(
161
+ `zip_conduit stream finished in ${formatSeconds(streamMs)} ` +
162
+ `(${formatMiBPerSec(payloadBytes, streamMs)}, ${entries.length} entries)`,
163
+ );
164
+ return stats;
165
+ }
166
+
167
+ private async waitForInstallation(
168
+ socket: net.Socket,
169
+ options: Pick<ZipConduitInstallOptions, 'progress' | 'timeoutMs'>,
170
+ ): Promise<void> {
171
+ const timeoutMs = options.timeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
172
+ const startTime = performance.now();
173
+
174
+ while (performance.now() - startTime <= timeoutMs) {
175
+ const remaining = timeoutMs - (performance.now() - startTime);
176
+ const plist = await recvOnePlistWithTimeout(socket, remaining);
177
+ const { done, percent, status } = evaluateProgress(plist);
178
+ options.progress?.({ percent, status });
179
+ if (done) {
180
+ return;
181
+ }
182
+ }
183
+
184
+ throw new Error(
185
+ `Timed out waiting for zip_conduit installation after ${timeoutMs}ms`,
186
+ );
187
+ }
188
+ }
189
+
190
+ function collectZipStats(entries: IpaZipEntry[]): {
191
+ totalBytes: number;
192
+ numFiles: number;
193
+ } {
194
+ const totalBytes = entries.reduce((sum, entry) => sum + entry.size, 0);
195
+ return { totalBytes, numFiles: entries.length };
196
+ }
197
+
198
+ function isDirectoryEntry(entry: IpaZipEntry): boolean {
199
+ return entry.isDirectory || entry.name.endsWith('/');
200
+ }
201
+
202
+ function formatSeconds(ms: number): string {
203
+ return `${(ms / 1000).toFixed(2)}s`;
204
+ }
205
+
206
+ function formatMiBPerSec(bytes: number, ms: number): string {
207
+ if (ms <= 0) {
208
+ return 'n/a';
209
+ }
210
+ const mibPerSec = bytes / (1024 * 1024) / (ms / 1000);
211
+ return `${mibPerSec.toFixed(2)} MiB/s`;
212
+ }
213
+
214
+ async function recvOnePlistWithTimeout(
215
+ socket: net.Socket,
216
+ timeoutMs: number,
217
+ ): Promise<PlistDictionary> {
218
+ if (timeoutMs <= 0) {
219
+ throw new Error('Timed out waiting for zip_conduit progress update');
220
+ }
221
+
222
+ let timeoutId: NodeJS.Timeout | undefined;
223
+ const timeoutPromise = new Promise<never>((_resolve, reject) => {
224
+ timeoutId = setTimeout(() => {
225
+ reject(
226
+ new Error(
227
+ `Timed out waiting for zip_conduit progress after ${timeoutMs}ms`,
228
+ ),
229
+ );
230
+ }, timeoutMs);
231
+ });
232
+
233
+ try {
234
+ return await Promise.race([recvOnePlist(socket), timeoutPromise]);
235
+ } finally {
236
+ if (timeoutId) {
237
+ clearTimeout(timeoutId);
238
+ }
239
+ }
240
+ }
241
+
242
+ export default ZipConduitService;
@@ -0,0 +1,139 @@
1
+ import path from 'node:path';
2
+
3
+ import type { PlistDictionary } from '../../../lib/types.js';
4
+ import { STD_DIR_PERM, STD_FILE_PERM } from './constants.js';
5
+
6
+ export const SIGNING_ERROR = 'ApplicationVerificationFailed';
7
+
8
+ export interface ZipConduitMetadata {
9
+ StandardDirectoryPerms: number;
10
+ StandardFilePerms: number;
11
+ RecordCount: number;
12
+ TotalUncompressedBytes: number;
13
+ Version: number;
14
+ }
15
+
16
+ export interface InitTransferRequest {
17
+ InstallOptionsDictionary: {
18
+ DisableDeltaTransfer: number;
19
+ InstallDeltaTypeKey: string;
20
+ IsUserInitiated: number;
21
+ PackageType: string;
22
+ PreferWifi: number;
23
+ };
24
+ InstallTransferredDirectory: number;
25
+ MediaSubdir: string;
26
+ UserInitiatedTransfer: number;
27
+ }
28
+
29
+ export interface ZipConduitProgressUpdate {
30
+ percent: number;
31
+ status: string;
32
+ }
33
+
34
+ /**
35
+ * Build the InitTransfer plist payload for zip_conduit.
36
+ * @param fileName Local IPA path used to derive the PublicStaging destination.
37
+ */
38
+ export function createInitTransfer(fileName: string): InitTransferRequest {
39
+ const base = path.basename(fileName);
40
+ return {
41
+ InstallTransferredDirectory: 1,
42
+ UserInitiatedTransfer: 0,
43
+ MediaSubdir: `PublicStaging/${base}`,
44
+ InstallOptionsDictionary: {
45
+ InstallDeltaTypeKey: 'InstallDeltaTypeSparseIPAFiles',
46
+ DisableDeltaTransfer: 1,
47
+ IsUserInitiated: 1,
48
+ PreferWifi: 1,
49
+ PackageType: 'Customer',
50
+ },
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Build ZipMetadata plist values embedded in the streamed archive.
56
+ * @param numFiles Number of entries in the source IPA.
57
+ * @param totalBytes Sum of uncompressed sizes for all IPA entries.
58
+ */
59
+ export function createMetaInfPlist(
60
+ numFiles: number,
61
+ totalBytes: number,
62
+ ): ZipConduitMetadata {
63
+ return {
64
+ RecordCount: 2 + numFiles,
65
+ StandardDirectoryPerms: STD_DIR_PERM,
66
+ StandardFilePerms: STD_FILE_PERM,
67
+ TotalUncompressedBytes: totalBytes,
68
+ Version: 2,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Parse a zip_conduit progress plist into completion state and percentage.
74
+ * @param progressUpdate Progress plist received from the device.
75
+ */
76
+ export function evaluateProgress(progressUpdate: PlistDictionary): {
77
+ done: boolean;
78
+ percent: number;
79
+ status: string;
80
+ } {
81
+ const topStatus = asString(progressUpdate.Status);
82
+ if (topStatus === 'DataComplete') {
83
+ return { done: true, percent: 100, status: topStatus };
84
+ }
85
+
86
+ // The device can report a failure as a top-level Error (e.g. ExtractionFailed)
87
+ // with no InstallProgressDict; surface the real reason instead of a generic
88
+ // "missing InstallProgressDict".
89
+ const topError = asString(progressUpdate.Error);
90
+ if (topError) {
91
+ const topDescription = asString(progressUpdate.ErrorDescription) ?? '';
92
+ throw new Error(
93
+ `Failed installing: '${topError}'${topDescription ? ` errorDescription:'${topDescription}'` : ''}`,
94
+ );
95
+ }
96
+
97
+ const installProgressDict = progressUpdate.InstallProgressDict;
98
+ if (!installProgressDict || typeof installProgressDict !== 'object') {
99
+ throw new Error(
100
+ `Invalid progress update, missing InstallProgressDict: ${JSON.stringify(progressUpdate)}`,
101
+ );
102
+ }
103
+
104
+ const progress = installProgressDict as PlistDictionary;
105
+ const errorMessage = asString(progress.Error);
106
+ if (errorMessage) {
107
+ const description = asString(progress.ErrorDescription) ?? '';
108
+ if (errorMessage === SIGNING_ERROR) {
109
+ throw new Error(
110
+ `App is not properly signed for this device. original error: '${errorMessage}' errorDescription:'${description}'`,
111
+ );
112
+ }
113
+ throw new Error(
114
+ `Failed installing: '${errorMessage}' errorDescription:'${description}'`,
115
+ );
116
+ }
117
+
118
+ const percent = asNumber(progress.PercentComplete) ?? 0;
119
+ const status = asString(progress.Status) ?? '';
120
+ return { done: false, percent, status };
121
+ }
122
+
123
+ function asNumber(value: unknown): number | undefined {
124
+ if (typeof value === 'number' && Number.isFinite(value)) {
125
+ return value;
126
+ }
127
+ if (typeof value === 'bigint') {
128
+ return Number(value);
129
+ }
130
+ if (typeof value === 'string' && value.trim() !== '') {
131
+ const parsed = Number(value);
132
+ return Number.isFinite(parsed) ? parsed : undefined;
133
+ }
134
+ return undefined;
135
+ }
136
+
137
+ function asString(value: unknown): string | undefined {
138
+ return typeof value === 'string' ? value : undefined;
139
+ }