appium-ios-remotexpc 0.0.1

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 (92) hide show
  1. package/.github/dependabot.yml +38 -0
  2. package/.github/workflows/format-check.yml +43 -0
  3. package/.github/workflows/lint-and-build.yml +40 -0
  4. package/.github/workflows/pr-title.yml +16 -0
  5. package/.github/workflows/publish.js.yml +42 -0
  6. package/.github/workflows/test-validation.yml +40 -0
  7. package/.mocharc.json +8 -0
  8. package/.prettierignore +3 -0
  9. package/.prettierrc +17 -0
  10. package/.releaserc +37 -0
  11. package/CHANGELOG.md +63 -0
  12. package/LICENSE +201 -0
  13. package/README.md +178 -0
  14. package/assets/images/ios-arch.png +0 -0
  15. package/eslint.config.js +45 -0
  16. package/package.json +78 -0
  17. package/scripts/test-tunnel-creation.ts +378 -0
  18. package/src/base-plist-service.ts +83 -0
  19. package/src/base-socket-service.ts +55 -0
  20. package/src/index.ts +34 -0
  21. package/src/lib/apple-tv/constants.ts +83 -0
  22. package/src/lib/apple-tv/errors.ts +31 -0
  23. package/src/lib/apple-tv/tlv/decoder.ts +68 -0
  24. package/src/lib/apple-tv/tlv/encoder.ts +33 -0
  25. package/src/lib/apple-tv/tlv/index.ts +6 -0
  26. package/src/lib/apple-tv/tlv/pairing-tlv.ts +31 -0
  27. package/src/lib/apple-tv/types.ts +58 -0
  28. package/src/lib/apple-tv/utils/buffer-utils.ts +90 -0
  29. package/src/lib/apple-tv/utils/index.ts +2 -0
  30. package/src/lib/apple-tv/utils/uuid-generator.ts +43 -0
  31. package/src/lib/lockdown/index.ts +468 -0
  32. package/src/lib/pair-record/index.ts +8 -0
  33. package/src/lib/pair-record/pair-record.ts +133 -0
  34. package/src/lib/plist/binary-plist-creator.ts +571 -0
  35. package/src/lib/plist/binary-plist-parser.ts +587 -0
  36. package/src/lib/plist/constants.ts +53 -0
  37. package/src/lib/plist/index.ts +54 -0
  38. package/src/lib/plist/length-based-splitter.ts +326 -0
  39. package/src/lib/plist/plist-creator.ts +42 -0
  40. package/src/lib/plist/plist-decoder.ts +135 -0
  41. package/src/lib/plist/plist-encoder.ts +36 -0
  42. package/src/lib/plist/plist-parser.ts +144 -0
  43. package/src/lib/plist/plist-service.ts +231 -0
  44. package/src/lib/plist/unified-plist-creator.ts +19 -0
  45. package/src/lib/plist/unified-plist-parser.ts +25 -0
  46. package/src/lib/plist/utils.ts +376 -0
  47. package/src/lib/remote-xpc/constants.ts +22 -0
  48. package/src/lib/remote-xpc/handshake-frames.ts +377 -0
  49. package/src/lib/remote-xpc/handshake.ts +152 -0
  50. package/src/lib/remote-xpc/remote-xpc-connection.ts +461 -0
  51. package/src/lib/remote-xpc/xpc-protocol.ts +412 -0
  52. package/src/lib/tunnel/index.ts +253 -0
  53. package/src/lib/tunnel/packet-stream-client.ts +185 -0
  54. package/src/lib/tunnel/packet-stream-server.ts +133 -0
  55. package/src/lib/tunnel/tunnel-api-client.ts +234 -0
  56. package/src/lib/tunnel/tunnel-registry-server.ts +410 -0
  57. package/src/lib/types.ts +291 -0
  58. package/src/lib/usbmux/index.ts +630 -0
  59. package/src/lib/usbmux/usbmux-decoder.ts +66 -0
  60. package/src/lib/usbmux/usbmux-encoder.ts +55 -0
  61. package/src/service-connection.ts +79 -0
  62. package/src/services/index.ts +15 -0
  63. package/src/services/ios/base-service.ts +81 -0
  64. package/src/services/ios/diagnostic-service/index.ts +241 -0
  65. package/src/services/ios/diagnostic-service/keys.ts +770 -0
  66. package/src/services/ios/syslog-service/index.ts +387 -0
  67. package/src/services/ios/tunnel-service/index.ts +88 -0
  68. package/src/services.ts +81 -0
  69. package/test/integration/diagnostics-test.ts +44 -0
  70. package/test/integration/read-pair-record-test.ts +39 -0
  71. package/test/integration/tunnel-test.ts +104 -0
  72. package/test/unit/apple-tv/tlv/decoder.spec.ts +144 -0
  73. package/test/unit/apple-tv/tlv/encoder.spec.ts +91 -0
  74. package/test/unit/apple-tv/tlv/pairing-tlv.spec.ts +101 -0
  75. package/test/unit/apple-tv/tlv/tlv-integration.spec.ts +146 -0
  76. package/test/unit/apple-tv/utils/buffer-utils.spec.ts +74 -0
  77. package/test/unit/apple-tv/utils/uuid-generator.spec.ts +39 -0
  78. package/test/unit/fixtures/index.ts +88 -0
  79. package/test/unit/fixtures/usbmuxconnectmessage.bin +0 -0
  80. package/test/unit/fixtures/usbmuxlistdevicemessage.bin +0 -0
  81. package/test/unit/plist/error-handling.spec.ts +101 -0
  82. package/test/unit/plist/fixtures/sample.binary.plist +0 -0
  83. package/test/unit/plist/fixtures/sample.xml.plist +38 -0
  84. package/test/unit/plist/plist-parser.spec.ts +283 -0
  85. package/test/unit/plist/plist.spec.ts +205 -0
  86. package/test/unit/plist/tag-position-handling.spec.ts +90 -0
  87. package/test/unit/plist/unified-plist-parser.spec.ts +227 -0
  88. package/test/unit/plist/utils.spec.ts +249 -0
  89. package/test/unit/plist/xml-cleaning.spec.ts +60 -0
  90. package/test/unit/tunnel/tunnel-registry-server.spec.ts +194 -0
  91. package/test/unit/usbmux/usbmux-specs.ts +71 -0
  92. package/tsconfig.json +36 -0
@@ -0,0 +1,326 @@
1
+ import { logger } from '@appium/support';
2
+ import { Transform, type TransformCallback } from 'stream';
3
+
4
+ import {
5
+ BINARY_PLIST_HEADER_LENGTH,
6
+ BINARY_PLIST_MAGIC,
7
+ IBINARY_PLIST_MAGIC,
8
+ LENGTH_FIELD_1_BYTE,
9
+ LENGTH_FIELD_2_BYTES,
10
+ LENGTH_FIELD_4_BYTES,
11
+ LENGTH_FIELD_8_BYTES,
12
+ PLIST_CLOSING_TAG,
13
+ UINT32_HIGH_MULTIPLIER,
14
+ UTF8_ENCODING,
15
+ XML_DECLARATION,
16
+ } from './constants.js';
17
+ import { isXmlPlistContent } from './utils.js';
18
+
19
+ const log = logger.getLogger('Plist');
20
+
21
+ // Constants
22
+ const DEFAULT_MAX_FRAME_LENGTH = 100 * 1024 * 1024; // 100MB default for large IORegistry responses
23
+ const DEFAULT_LENGTH_FIELD_OFFSET = 0;
24
+ const DEFAULT_LENGTH_FIELD_LENGTH = 4;
25
+ const DEFAULT_LENGTH_ADJUSTMENT = 0;
26
+ const MAX_PREVIEW_LENGTH = 100; // Maximum number of bytes to preview for content type detection
27
+
28
+ /**
29
+ * Options for LengthBasedSplitter
30
+ */
31
+ export interface LengthBasedSplitterOptions {
32
+ readableStream?: NodeJS.ReadableStream;
33
+ littleEndian?: boolean;
34
+ maxFrameLength?: number;
35
+ lengthFieldOffset?: number;
36
+ lengthFieldLength?: number;
37
+ lengthAdjustment?: number;
38
+ }
39
+
40
+ /**
41
+ * Splits incoming data into length-prefixed chunks
42
+ */
43
+ export class LengthBasedSplitter extends Transform {
44
+ private buffer: Buffer;
45
+ private readonly littleEndian: boolean;
46
+ private readonly maxFrameLength: number;
47
+ private readonly lengthFieldOffset: number;
48
+ private readonly lengthFieldLength: number;
49
+ private readonly lengthAdjustment: number;
50
+ private isXmlMode: boolean = false;
51
+
52
+ /**
53
+ * Creates a new LengthBasedSplitter
54
+ * @param options Configuration options
55
+ */
56
+ constructor(options: LengthBasedSplitterOptions = {}) {
57
+ super();
58
+ this.buffer = Buffer.alloc(0);
59
+ this.littleEndian = options.littleEndian ?? false;
60
+ this.maxFrameLength = options.maxFrameLength ?? DEFAULT_MAX_FRAME_LENGTH;
61
+ this.lengthFieldOffset =
62
+ options.lengthFieldOffset ?? DEFAULT_LENGTH_FIELD_OFFSET;
63
+ this.lengthFieldLength =
64
+ options.lengthFieldLength ?? DEFAULT_LENGTH_FIELD_LENGTH;
65
+ this.lengthAdjustment =
66
+ options.lengthAdjustment ?? DEFAULT_LENGTH_ADJUSTMENT;
67
+
68
+ // If readableStream is provided, pipe it to this
69
+ if (options.readableStream) {
70
+ options.readableStream.pipe(this);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Shutdown the splitter and remove all listeners
76
+ */
77
+ shutdown(): void {
78
+ // Reset internal state
79
+ this.buffer = Buffer.alloc(0);
80
+ this.isXmlMode = false;
81
+
82
+ // Remove all listeners
83
+ this.removeAllListeners();
84
+ log.debug('LengthBasedSplitter shutdown complete');
85
+ }
86
+
87
+ _transform(
88
+ chunk: Buffer,
89
+ encoding: BufferEncoding,
90
+ callback: TransformCallback,
91
+ ): void {
92
+ try {
93
+ // Add the new chunk to our buffer
94
+ this.buffer = Buffer.concat([this.buffer, chunk]);
95
+
96
+ // Check if this is XML data or binary plist before doing any other processing
97
+ const bufferString = this.buffer.toString(
98
+ UTF8_ENCODING,
99
+ 0,
100
+ Math.min(MAX_PREVIEW_LENGTH, this.buffer.length),
101
+ );
102
+
103
+ // Check for XML format
104
+ if (isXmlPlistContent(bufferString) || this.isXmlMode) {
105
+ // This is XML data, set XML mode
106
+ this.isXmlMode = true;
107
+ this.processXmlData(callback);
108
+ return;
109
+ }
110
+
111
+ // Check for binary plist format (bplist00 or Ibplist00)
112
+ if (this.buffer.length >= BINARY_PLIST_HEADER_LENGTH) {
113
+ const possibleBplistHeader = this.buffer.toString(
114
+ UTF8_ENCODING,
115
+ 0,
116
+ BINARY_PLIST_HEADER_LENGTH,
117
+ );
118
+
119
+ if (
120
+ possibleBplistHeader === BINARY_PLIST_MAGIC ||
121
+ possibleBplistHeader.includes(BINARY_PLIST_MAGIC)
122
+ ) {
123
+ log.debug('Detected standard binary plist format');
124
+ this.push(this.buffer);
125
+ this.buffer = Buffer.alloc(0);
126
+ return callback();
127
+ }
128
+
129
+ if (
130
+ possibleBplistHeader === IBINARY_PLIST_MAGIC ||
131
+ possibleBplistHeader.includes(IBINARY_PLIST_MAGIC)
132
+ ) {
133
+ log.debug('Detected non-standard Ibplist00 format');
134
+ this.push(this.buffer);
135
+ this.buffer = Buffer.alloc(0);
136
+ return callback();
137
+ }
138
+ }
139
+ // Process as many complete messages as possible for binary data
140
+ this.processBinaryData(callback);
141
+ } catch (err) {
142
+ callback(err as Error);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Process data as XML
148
+ */
149
+ private processXmlData(callback: TransformCallback): void {
150
+ const fullBufferString = this.buffer.toString(UTF8_ENCODING);
151
+
152
+ let startIndex = 0;
153
+ if (!fullBufferString.startsWith(XML_DECLARATION)) {
154
+ const declIndex = fullBufferString.indexOf(XML_DECLARATION);
155
+ if (declIndex >= 0) {
156
+ startIndex = declIndex;
157
+ } else {
158
+ return callback();
159
+ }
160
+ }
161
+
162
+ // Now search for the closing tag in the string starting at startIndex.
163
+ const plistEndIndex = fullBufferString.indexOf(
164
+ PLIST_CLOSING_TAG,
165
+ startIndex,
166
+ );
167
+
168
+ if (plistEndIndex >= 0) {
169
+ const endPos = plistEndIndex + PLIST_CLOSING_TAG.length;
170
+
171
+ const xmlData = this.buffer.slice(0, endPos);
172
+
173
+ // Push the complete XML document downstream.
174
+ this.push(xmlData);
175
+
176
+ // Remove the processed data from the buffer.
177
+ this.buffer = this.buffer.slice(endPos);
178
+
179
+ // If there's remaining data, check if it still looks XML.
180
+ if (this.buffer.length === 0) {
181
+ this.isXmlMode = false;
182
+ } else {
183
+ const remainingData = this.buffer.toString(
184
+ UTF8_ENCODING,
185
+ 0,
186
+ Math.min(MAX_PREVIEW_LENGTH, this.buffer.length),
187
+ );
188
+
189
+ this.isXmlMode = isXmlPlistContent(remainingData);
190
+ }
191
+ }
192
+
193
+ callback();
194
+ }
195
+
196
+ /**
197
+ * Process data as binary with length prefix
198
+ */
199
+ private processBinaryData(callback: TransformCallback): void {
200
+ while (
201
+ this.buffer.length >=
202
+ this.lengthFieldOffset + this.lengthFieldLength
203
+ ) {
204
+ let messageLength: number;
205
+
206
+ // Read the length prefix according to configuration
207
+ if (this.lengthFieldLength === LENGTH_FIELD_4_BYTES) {
208
+ messageLength = this.littleEndian
209
+ ? this.buffer.readUInt32LE(this.lengthFieldOffset)
210
+ : this.buffer.readUInt32BE(this.lengthFieldOffset);
211
+ } else if (this.lengthFieldLength === LENGTH_FIELD_2_BYTES) {
212
+ messageLength = this.littleEndian
213
+ ? this.buffer.readUInt16LE(this.lengthFieldOffset)
214
+ : this.buffer.readUInt16BE(this.lengthFieldOffset);
215
+ } else if (this.lengthFieldLength === LENGTH_FIELD_1_BYTE) {
216
+ messageLength = this.buffer.readUInt8(this.lengthFieldOffset);
217
+ } else if (this.lengthFieldLength === LENGTH_FIELD_8_BYTES) {
218
+ const high = this.littleEndian
219
+ ? this.buffer.readUInt32LE(
220
+ this.lengthFieldOffset + LENGTH_FIELD_4_BYTES,
221
+ )
222
+ : this.buffer.readUInt32BE(this.lengthFieldOffset);
223
+ const low = this.littleEndian
224
+ ? this.buffer.readUInt32LE(this.lengthFieldOffset)
225
+ : this.buffer.readUInt32BE(
226
+ this.lengthFieldOffset + LENGTH_FIELD_4_BYTES,
227
+ );
228
+ messageLength = high * UINT32_HIGH_MULTIPLIER + low;
229
+ } else {
230
+ throw new Error(
231
+ `Unsupported lengthFieldLength: ${this.lengthFieldLength}`,
232
+ );
233
+ }
234
+
235
+ // Apply adjustment
236
+ messageLength += this.lengthAdjustment;
237
+
238
+ // Check if the extracted message length seems suspicious
239
+ if (messageLength < 0 || messageLength > this.maxFrameLength) {
240
+ let alternateLength: number;
241
+ if (this.lengthFieldLength === LENGTH_FIELD_4_BYTES) {
242
+ alternateLength = this.littleEndian
243
+ ? this.buffer.readUInt32BE(this.lengthFieldOffset)
244
+ : this.buffer.readUInt32LE(this.lengthFieldOffset);
245
+
246
+ if (alternateLength > 0 && alternateLength <= this.maxFrameLength) {
247
+ messageLength = alternateLength;
248
+ } else {
249
+ // If length is still invalid, check if this might actually be XML
250
+ const suspiciousData = this.buffer.toString(
251
+ UTF8_ENCODING,
252
+ 0,
253
+ Math.min(MAX_PREVIEW_LENGTH, this.buffer.length),
254
+ );
255
+
256
+ if (isXmlPlistContent(suspiciousData)) {
257
+ this.isXmlMode = true;
258
+ // Process as XML on next iteration
259
+ return callback();
260
+ }
261
+
262
+ // Invalid length - skip one byte and try again
263
+ this.buffer = this.buffer.slice(1);
264
+ continue;
265
+ }
266
+ } else {
267
+ // For non-4-byte length fields, just use the original approach
268
+ // If length is invalid, check if this might actually be XML
269
+ const suspiciousData = this.buffer.toString(
270
+ UTF8_ENCODING,
271
+ 0,
272
+ Math.min(MAX_PREVIEW_LENGTH, this.buffer.length),
273
+ );
274
+
275
+ if (isXmlPlistContent(suspiciousData)) {
276
+ this.isXmlMode = true;
277
+ // Process as XML on next iteration
278
+ return callback();
279
+ }
280
+
281
+ // Invalid length - skip one byte and try again
282
+ this.buffer = this.buffer.slice(1);
283
+ continue;
284
+ }
285
+ }
286
+
287
+ // Total length of frame = lengthFieldOffset + lengthFieldLength + messageLength
288
+ const totalLength =
289
+ this.lengthFieldOffset + this.lengthFieldLength + messageLength;
290
+
291
+ // If we don't have the complete message yet, wait for more data
292
+ if (this.buffer.length < totalLength) {
293
+ break;
294
+ }
295
+
296
+ // Extract the message
297
+ try {
298
+ // Extract the complete message
299
+ const message = this.buffer.slice(0, totalLength);
300
+
301
+ // Check if this message is actually XML
302
+ const messageStart = message.toString(
303
+ UTF8_ENCODING,
304
+ 0,
305
+ Math.min(MAX_PREVIEW_LENGTH, message.length),
306
+ );
307
+
308
+ if (isXmlPlistContent(messageStart)) {
309
+ // Switch to XML mode
310
+ this.isXmlMode = true;
311
+ return callback();
312
+ }
313
+
314
+ // Push the message
315
+ this.push(message);
316
+
317
+ // Remove the processed message from the buffer
318
+ this.buffer = this.buffer.slice(totalLength);
319
+ } catch {
320
+ // move forward by 1 byte and try again
321
+ this.buffer = this.buffer.slice(1);
322
+ }
323
+ }
324
+ callback();
325
+ }
326
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Creates an XML plist string from a JavaScript object
3
+ * @param obj - The JavaScript object to convert
4
+ * @returns - XML plist string
5
+ */
6
+ import type { PlistDictionary, PlistValue } from '../types.js';
7
+ import { escapeXml } from './utils.js';
8
+
9
+ export function createPlist(obj: PlistDictionary): string {
10
+ function convert(value: PlistValue): string {
11
+ if (typeof value === 'number') {
12
+ return `<integer>${value}</integer>`;
13
+ }
14
+ if (typeof value === 'boolean') {
15
+ return value ? '<true/>' : '<false/>';
16
+ }
17
+ if (typeof value === 'string') {
18
+ return `<string>${escapeXml(value)}</string>`;
19
+ }
20
+ if (Array.isArray(value)) {
21
+ return `<array>${value.map((item) => convert(item)).join('')}</array>`;
22
+ }
23
+ if (typeof value === 'object' && value !== null) {
24
+ const entries = Object.entries(value)
25
+ .map(([k, v]) => `<key>${escapeXml(k)}</key>${convert(v)}`)
26
+ .join('');
27
+ return `<dict>${entries}</dict>`;
28
+ }
29
+ return '<string></string>';
30
+ }
31
+
32
+ const body = Object.entries(obj)
33
+ .map(([key, val]) => `<key>${escapeXml(key)}</key>${convert(val)}`)
34
+ .join('');
35
+
36
+ return `<?xml version="1.0" encoding="UTF-8"?>
37
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
38
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
39
+ <plist version="1.0">
40
+ <dict>${body}</dict>
41
+ </plist>`;
42
+ }
@@ -0,0 +1,135 @@
1
+ import { logger } from '@appium/support';
2
+ import { Transform, type TransformCallback } from 'stream';
3
+
4
+ import { UTF8_ENCODING } from './constants.js';
5
+ import { parsePlist } from './plist-parser.js';
6
+ import {
7
+ ensureString,
8
+ findFirstReplacementCharacter,
9
+ fixMultipleXmlDeclarations,
10
+ hasUnicodeReplacementCharacter,
11
+ } from './utils.js';
12
+
13
+ const log = logger.getLogger('Plist');
14
+
15
+ /**
16
+ * Decodes plist format data with length prefix to JavaScript objects
17
+ */
18
+ export class PlistServiceDecoder extends Transform {
19
+ // Static property to store the last decoded result
20
+ static lastDecodedResult: any = null;
21
+ constructor() {
22
+ super({ objectMode: true });
23
+ }
24
+
25
+ _transform(
26
+ data: Buffer,
27
+ encoding: BufferEncoding,
28
+ callback: TransformCallback,
29
+ ): void {
30
+ try {
31
+ // Get the plist data without the 4-byte header
32
+ let plistData = data.slice(4);
33
+
34
+ // Skip empty data
35
+ if (plistData.length === 0) {
36
+ return callback();
37
+ }
38
+
39
+ // Check if this is XML data with potential binary header and trim content before XML declaration
40
+ const dataStr = plistData.toString(
41
+ UTF8_ENCODING,
42
+ 0,
43
+ Math.min(100, plistData.length),
44
+ );
45
+ const xmlIndex = dataStr.indexOf('<?xml');
46
+
47
+ if (xmlIndex > 0) {
48
+ // There's content before the XML declaration, remove it
49
+ log.debug(
50
+ `Found XML declaration at position ${xmlIndex}, trimming preceding content`,
51
+ );
52
+ plistData = plistData.slice(xmlIndex);
53
+ }
54
+
55
+ // Check for multiple XML declarations which can cause parsing errors
56
+ const fullDataStr = ensureString(plistData);
57
+
58
+ // Check for potential corruption indicators and handle them
59
+ if (hasUnicodeReplacementCharacter(plistData)) {
60
+ log.debug(
61
+ 'Detected Unicode replacement characters in plist data, which may indicate encoding issues',
62
+ );
63
+
64
+ // Try to find and clean the corrupted data
65
+ const firstReplacementPos = findFirstReplacementCharacter(fullDataStr);
66
+ if (firstReplacementPos >= 0) {
67
+ log.debug(
68
+ `Found replacement character at position ${firstReplacementPos}, attempting to clean data`,
69
+ );
70
+ }
71
+ }
72
+ const xmlDeclMatches = fullDataStr.match(/(<\?xml[^>]*\?>)/g) || [];
73
+ if (xmlDeclMatches.length > 1) {
74
+ log.debug(
75
+ `Found ${xmlDeclMatches.length} XML declarations, which may cause parsing errors`,
76
+ );
77
+ // Fix multiple XML declarations
78
+ plistData = Buffer.from(fixMultipleXmlDeclarations(plistData));
79
+ }
80
+
81
+ try {
82
+ // Parse the plist
83
+ this._parseAndProcess(plistData, callback);
84
+ } catch (error) {
85
+ // If parsing fails, try to recover by cleaning up the data more aggressively
86
+ const parseError = error as Error;
87
+
88
+ try {
89
+ // Find the first valid XML tag
90
+ const firstTagIndex = fullDataStr.indexOf('<');
91
+ if (firstTagIndex > 0) {
92
+ const cleanedData = plistData.slice(firstTagIndex);
93
+ this._parseAndProcess(cleanedData, callback);
94
+ } else {
95
+ // If we can't find a valid starting point, propagate the original error
96
+ throw parseError;
97
+ }
98
+ } catch (error) {
99
+ // If recovery also fails, propagate the original error
100
+ callback(error as Error);
101
+ }
102
+ }
103
+ } catch (err) {
104
+ callback(err as Error);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Parse plist data and process the result
110
+ *
111
+ * @param data - The plist data to parse
112
+ * @param callback - The transform callback
113
+ */
114
+ private _parseAndProcess(data: Buffer, callback: TransformCallback): void {
115
+ const result = parsePlist(data);
116
+ this._processResult(result, callback);
117
+ }
118
+
119
+ /**
120
+ * Process a successfully parsed result
121
+ * Stores the result in the static property and pushes it to the stream
122
+ *
123
+ * @param result - The parsed plist result
124
+ * @param callback - The transform callback
125
+ */
126
+ private _processResult(result: any, callback: TransformCallback): void {
127
+ // Store the result in the static property for later access
128
+ if (typeof result === 'object' && result !== null) {
129
+ PlistServiceDecoder.lastDecodedResult = result;
130
+ }
131
+
132
+ this.push(result);
133
+ callback();
134
+ }
135
+ }
@@ -0,0 +1,36 @@
1
+ import { Transform, type TransformCallback } from 'stream';
2
+
3
+ import type { PlistDictionary } from '../types.js';
4
+ import { UTF8_ENCODING } from './constants.js';
5
+ import { createPlist } from './plist-creator.js';
6
+
7
+ /**
8
+ * Encodes JavaScript objects to plist format with length prefix
9
+ */
10
+ export class PlistServiceEncoder extends Transform {
11
+ constructor() {
12
+ super({ objectMode: true });
13
+ }
14
+
15
+ _transform(
16
+ data: PlistDictionary,
17
+ encoding: BufferEncoding,
18
+ callback: TransformCallback,
19
+ ): void {
20
+ try {
21
+ // Convert object to plist
22
+ const plist = createPlist(data);
23
+ const plistBuffer = Buffer.from(plist, UTF8_ENCODING);
24
+
25
+ // Create length header (4 bytes, big endian)
26
+ const header = Buffer.alloc(4);
27
+ header.writeUInt32BE(plistBuffer.length, 0);
28
+
29
+ // Send header + plist
30
+ this.push(Buffer.concat([header, plistBuffer]));
31
+ callback();
32
+ } catch (err) {
33
+ callback(err as Error);
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,144 @@
1
+ import { logger } from '@appium/support';
2
+ import { DOMParser, Element, Node } from '@xmldom/xmldom';
3
+
4
+ import type { PlistArray, PlistDictionary, PlistValue } from '../types.js';
5
+ import { PlistService } from './plist-service.js';
6
+ import {
7
+ cleanXmlWithReplacementChar,
8
+ ensureString,
9
+ findFirstReplacementCharacter,
10
+ fixMultipleXmlDeclarations,
11
+ hasUnicodeReplacementCharacter,
12
+ isValidXml,
13
+ removeExtraContentAfterPlist,
14
+ trimBeforeXmlDeclaration,
15
+ } from './utils.js';
16
+
17
+ const errorLog = logger.getLogger('PlistError');
18
+
19
+ /**
20
+ * Parses an XML plist string into a JavaScript object
21
+ *
22
+ * @param xmlData - XML plist data as string or Buffer
23
+ * @returns Parsed JavaScript object
24
+ */
25
+ export function parsePlist(xmlData: string | Buffer): PlistDictionary {
26
+ let xmlStr = ensureString(xmlData);
27
+
28
+ xmlStr = trimBeforeXmlDeclaration(xmlStr);
29
+
30
+ if (hasUnicodeReplacementCharacter(xmlStr)) {
31
+ const badCharPos = findFirstReplacementCharacter(xmlStr);
32
+ xmlStr = cleanXmlWithReplacementChar(xmlStr, badCharPos);
33
+ }
34
+
35
+ if (!isValidXml(xmlStr)) {
36
+ if (PlistService.isVerboseErrorLoggingEnabled()) {
37
+ errorLog.debug(
38
+ `Invalid XML: missing root element - XML content: ${xmlStr.substring(0, 200)}...`,
39
+ );
40
+ }
41
+ throw new Error('Invalid XML: missing root element or malformed XML');
42
+ }
43
+
44
+ xmlStr = fixMultipleXmlDeclarations(xmlStr);
45
+
46
+ xmlStr = removeExtraContentAfterPlist(xmlStr);
47
+
48
+ const parser = new DOMParser({
49
+ errorHandler(level, message) {
50
+ if (level === 'fatalError') {
51
+ throw new Error(`Fatal XML parsing error: ${message}`);
52
+ }
53
+ return true;
54
+ },
55
+ });
56
+
57
+ const doc = parser.parseFromString(xmlStr, 'text/xml');
58
+
59
+ if (!doc) {
60
+ throw new Error('Invalid XML response');
61
+ }
62
+
63
+ const plistElements = doc.getElementsByTagName('plist');
64
+ if (plistElements.length === 0) {
65
+ throw new Error('No plist element found in XML');
66
+ }
67
+
68
+ const rootDict = doc.getElementsByTagName('dict')[0];
69
+ if (!rootDict) {
70
+ return {};
71
+ }
72
+
73
+ return parseDict(rootDict);
74
+
75
+ function parseNode(node: Element): PlistValue {
76
+ if (!node) {
77
+ return null;
78
+ }
79
+
80
+ switch (node.nodeName) {
81
+ case 'dict':
82
+ return parseDict(node);
83
+ case 'array':
84
+ return parseArray(node);
85
+ case 'string':
86
+ return node.textContent || '';
87
+ case 'integer':
88
+ return parseInt(node.textContent || '0', 10);
89
+ case 'real':
90
+ return parseFloat(node.textContent || '0');
91
+ case 'true':
92
+ return true;
93
+ case 'false':
94
+ return false;
95
+ case 'date':
96
+ return new Date(node.textContent || '');
97
+ case 'data':
98
+ if (!node.textContent) {
99
+ return null;
100
+ }
101
+ try {
102
+ return Buffer.from(node.textContent, 'base64');
103
+ } catch {
104
+ return node.textContent;
105
+ }
106
+ default:
107
+ return node.textContent || null;
108
+ }
109
+ }
110
+
111
+ function parseDict(dictNode: Element): PlistDictionary {
112
+ const obj: PlistDictionary = {};
113
+ const keys = dictNode.getElementsByTagName('key');
114
+
115
+ for (let i = 0; i < keys.length; i++) {
116
+ const keyName = keys[i].textContent || '';
117
+ let valueNode = keys[i].nextSibling;
118
+
119
+ while (valueNode && valueNode.nodeType !== Node.ELEMENT_NODE) {
120
+ valueNode = valueNode.nextSibling;
121
+ }
122
+
123
+ if (valueNode) {
124
+ obj[keyName] = parseNode(valueNode as Element);
125
+ }
126
+ }
127
+
128
+ return obj;
129
+ }
130
+
131
+ function parseArray(arrayNode: Element): PlistArray {
132
+ const result: PlistArray = [];
133
+ let childNode = arrayNode.firstChild;
134
+
135
+ while (childNode) {
136
+ if (childNode.nodeType === Node.ELEMENT_NODE) {
137
+ result.push(parseNode(childNode as Element));
138
+ }
139
+ childNode = childNode.nextSibling;
140
+ }
141
+
142
+ return result;
143
+ }
144
+ }