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.
- package/.github/dependabot.yml +38 -0
- package/.github/workflows/format-check.yml +43 -0
- package/.github/workflows/lint-and-build.yml +40 -0
- package/.github/workflows/pr-title.yml +16 -0
- package/.github/workflows/publish.js.yml +42 -0
- package/.github/workflows/test-validation.yml +40 -0
- package/.mocharc.json +8 -0
- package/.prettierignore +3 -0
- package/.prettierrc +17 -0
- package/.releaserc +37 -0
- package/CHANGELOG.md +63 -0
- package/LICENSE +201 -0
- package/README.md +178 -0
- package/assets/images/ios-arch.png +0 -0
- package/eslint.config.js +45 -0
- package/package.json +78 -0
- package/scripts/test-tunnel-creation.ts +378 -0
- package/src/base-plist-service.ts +83 -0
- package/src/base-socket-service.ts +55 -0
- package/src/index.ts +34 -0
- package/src/lib/apple-tv/constants.ts +83 -0
- package/src/lib/apple-tv/errors.ts +31 -0
- package/src/lib/apple-tv/tlv/decoder.ts +68 -0
- package/src/lib/apple-tv/tlv/encoder.ts +33 -0
- package/src/lib/apple-tv/tlv/index.ts +6 -0
- package/src/lib/apple-tv/tlv/pairing-tlv.ts +31 -0
- package/src/lib/apple-tv/types.ts +58 -0
- package/src/lib/apple-tv/utils/buffer-utils.ts +90 -0
- package/src/lib/apple-tv/utils/index.ts +2 -0
- package/src/lib/apple-tv/utils/uuid-generator.ts +43 -0
- package/src/lib/lockdown/index.ts +468 -0
- package/src/lib/pair-record/index.ts +8 -0
- package/src/lib/pair-record/pair-record.ts +133 -0
- package/src/lib/plist/binary-plist-creator.ts +571 -0
- package/src/lib/plist/binary-plist-parser.ts +587 -0
- package/src/lib/plist/constants.ts +53 -0
- package/src/lib/plist/index.ts +54 -0
- package/src/lib/plist/length-based-splitter.ts +326 -0
- package/src/lib/plist/plist-creator.ts +42 -0
- package/src/lib/plist/plist-decoder.ts +135 -0
- package/src/lib/plist/plist-encoder.ts +36 -0
- package/src/lib/plist/plist-parser.ts +144 -0
- package/src/lib/plist/plist-service.ts +231 -0
- package/src/lib/plist/unified-plist-creator.ts +19 -0
- package/src/lib/plist/unified-plist-parser.ts +25 -0
- package/src/lib/plist/utils.ts +376 -0
- package/src/lib/remote-xpc/constants.ts +22 -0
- package/src/lib/remote-xpc/handshake-frames.ts +377 -0
- package/src/lib/remote-xpc/handshake.ts +152 -0
- package/src/lib/remote-xpc/remote-xpc-connection.ts +461 -0
- package/src/lib/remote-xpc/xpc-protocol.ts +412 -0
- package/src/lib/tunnel/index.ts +253 -0
- package/src/lib/tunnel/packet-stream-client.ts +185 -0
- package/src/lib/tunnel/packet-stream-server.ts +133 -0
- package/src/lib/tunnel/tunnel-api-client.ts +234 -0
- package/src/lib/tunnel/tunnel-registry-server.ts +410 -0
- package/src/lib/types.ts +291 -0
- package/src/lib/usbmux/index.ts +630 -0
- package/src/lib/usbmux/usbmux-decoder.ts +66 -0
- package/src/lib/usbmux/usbmux-encoder.ts +55 -0
- package/src/service-connection.ts +79 -0
- package/src/services/index.ts +15 -0
- package/src/services/ios/base-service.ts +81 -0
- package/src/services/ios/diagnostic-service/index.ts +241 -0
- package/src/services/ios/diagnostic-service/keys.ts +770 -0
- package/src/services/ios/syslog-service/index.ts +387 -0
- package/src/services/ios/tunnel-service/index.ts +88 -0
- package/src/services.ts +81 -0
- package/test/integration/diagnostics-test.ts +44 -0
- package/test/integration/read-pair-record-test.ts +39 -0
- package/test/integration/tunnel-test.ts +104 -0
- package/test/unit/apple-tv/tlv/decoder.spec.ts +144 -0
- package/test/unit/apple-tv/tlv/encoder.spec.ts +91 -0
- package/test/unit/apple-tv/tlv/pairing-tlv.spec.ts +101 -0
- package/test/unit/apple-tv/tlv/tlv-integration.spec.ts +146 -0
- package/test/unit/apple-tv/utils/buffer-utils.spec.ts +74 -0
- package/test/unit/apple-tv/utils/uuid-generator.spec.ts +39 -0
- package/test/unit/fixtures/index.ts +88 -0
- package/test/unit/fixtures/usbmuxconnectmessage.bin +0 -0
- package/test/unit/fixtures/usbmuxlistdevicemessage.bin +0 -0
- package/test/unit/plist/error-handling.spec.ts +101 -0
- package/test/unit/plist/fixtures/sample.binary.plist +0 -0
- package/test/unit/plist/fixtures/sample.xml.plist +38 -0
- package/test/unit/plist/plist-parser.spec.ts +283 -0
- package/test/unit/plist/plist.spec.ts +205 -0
- package/test/unit/plist/tag-position-handling.spec.ts +90 -0
- package/test/unit/plist/unified-plist-parser.spec.ts +227 -0
- package/test/unit/plist/utils.spec.ts +249 -0
- package/test/unit/plist/xml-cleaning.spec.ts +60 -0
- package/test/unit/tunnel/tunnel-registry-server.spec.ts +194 -0
- package/test/unit/usbmux/usbmux-specs.ts +71 -0
- 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
|
+
}
|