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,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary Property List (bplist) Parser
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to parse binary property lists (bplists)
|
|
5
|
+
* commonly used in Apple's iOS and macOS systems.
|
|
6
|
+
*/
|
|
7
|
+
import { logger } from '@appium/support';
|
|
8
|
+
|
|
9
|
+
import type { PlistArray, PlistDictionary, PlistValue } from '../types.js';
|
|
10
|
+
import {
|
|
11
|
+
APPLE_EPOCH_OFFSET,
|
|
12
|
+
BPLIST_MAGIC_AND_VERSION,
|
|
13
|
+
BPLIST_TRAILER_SIZE,
|
|
14
|
+
BPLIST_TYPE,
|
|
15
|
+
} from './constants.js';
|
|
16
|
+
|
|
17
|
+
const log = logger.getLogger('Plist');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Represents a temporary object during binary plist parsing
|
|
21
|
+
*/
|
|
22
|
+
interface TempObject {
|
|
23
|
+
type: 'array' | 'dict';
|
|
24
|
+
objLength: number;
|
|
25
|
+
startOffset: number;
|
|
26
|
+
value: PlistArray | PlistDictionary;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Type for the object table during parsing
|
|
31
|
+
*/
|
|
32
|
+
type ObjectTableItem = PlistValue | TempObject;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Class for parsing binary property lists
|
|
36
|
+
*/
|
|
37
|
+
class BinaryPlistParser {
|
|
38
|
+
private _buffer: Buffer;
|
|
39
|
+
private _offsetSize: number;
|
|
40
|
+
private _objectRefSize: number;
|
|
41
|
+
private _numObjects: number;
|
|
42
|
+
private _topObject: number;
|
|
43
|
+
private _offsetTableOffset: number;
|
|
44
|
+
private readonly _objectTable: ObjectTableItem[];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new BinaryPlistParser
|
|
48
|
+
* @param buffer - The binary plist data as a Buffer
|
|
49
|
+
*/
|
|
50
|
+
constructor(buffer: Buffer) {
|
|
51
|
+
this._buffer = buffer;
|
|
52
|
+
this._objectTable = [];
|
|
53
|
+
|
|
54
|
+
// Initialize with default values, will be set in parseTrailer
|
|
55
|
+
this._offsetSize = 0;
|
|
56
|
+
this._objectRefSize = 0;
|
|
57
|
+
this._numObjects = 0;
|
|
58
|
+
this._topObject = 0;
|
|
59
|
+
this._offsetTableOffset = 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parses the binary plist
|
|
64
|
+
* @returns The parsed JavaScript object
|
|
65
|
+
*/
|
|
66
|
+
parse(): PlistValue {
|
|
67
|
+
this._validateHeader();
|
|
68
|
+
this._parseTrailer();
|
|
69
|
+
this._parseObjects();
|
|
70
|
+
this._resolveReferences();
|
|
71
|
+
return this._handleTopObject();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validates the binary plist header
|
|
76
|
+
* @throws Error if the buffer is not a valid binary plist
|
|
77
|
+
*/
|
|
78
|
+
private _validateHeader(): void {
|
|
79
|
+
if (
|
|
80
|
+
this._buffer.length < 8 ||
|
|
81
|
+
!this._buffer.slice(0, 8).equals(BPLIST_MAGIC_AND_VERSION)
|
|
82
|
+
) {
|
|
83
|
+
throw new Error('Not a binary plist. Expected bplist00 magic.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parses the trailer section of the binary plist
|
|
89
|
+
* @throws Error if the buffer is too small to contain a trailer
|
|
90
|
+
*/
|
|
91
|
+
private _parseTrailer(): void {
|
|
92
|
+
if (this._buffer.length < BPLIST_TRAILER_SIZE) {
|
|
93
|
+
throw new Error('Binary plist is too small to contain a trailer.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const trailer = this._buffer.slice(
|
|
97
|
+
this._buffer.length - BPLIST_TRAILER_SIZE,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Extract trailer information
|
|
101
|
+
this._offsetSize = trailer.readUInt8(6);
|
|
102
|
+
this._objectRefSize = trailer.readUInt8(7);
|
|
103
|
+
this._numObjects = Number(trailer.readBigUInt64BE(8));
|
|
104
|
+
this._topObject = Number(trailer.readBigUInt64BE(16));
|
|
105
|
+
this._offsetTableOffset = Number(trailer.readBigUInt64BE(24));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reads an object reference from the buffer
|
|
110
|
+
* @param offset - The offset to read from
|
|
111
|
+
* @returns The object reference index
|
|
112
|
+
*/
|
|
113
|
+
/**
|
|
114
|
+
* Helper method to read multi-byte integers safely, handling potential overflow
|
|
115
|
+
* @param startOffset - The offset to start reading from
|
|
116
|
+
* @param byteCount - The number of bytes to read
|
|
117
|
+
* @param valueName - Name of the value type for error messages
|
|
118
|
+
* @returns The parsed integer value
|
|
119
|
+
*/
|
|
120
|
+
private _readMultiByteInteger(
|
|
121
|
+
startOffset: number,
|
|
122
|
+
byteCount: number,
|
|
123
|
+
valueName: string,
|
|
124
|
+
): number {
|
|
125
|
+
// Use BigInt for calculations if byteCount is large enough to potentially overflow
|
|
126
|
+
if (byteCount > 6) {
|
|
127
|
+
// 6 bytes = 48 bits, safely under MAX_SAFE_INTEGER
|
|
128
|
+
let result = 0n;
|
|
129
|
+
for (let i = 0; i < byteCount; i++) {
|
|
130
|
+
result =
|
|
131
|
+
(result << 8n) | BigInt(this._buffer.readUInt8(startOffset + i));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if the value exceeds MAX_SAFE_INTEGER
|
|
135
|
+
if (result > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`${valueName} value ${result} exceeds MAX_SAFE_INTEGER. Cannot safely convert to number.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Safe to convert to number without precision loss
|
|
142
|
+
return Number(result);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Use regular number arithmetic for smaller values
|
|
146
|
+
let result = 0;
|
|
147
|
+
for (let i = 0; i < byteCount; i++) {
|
|
148
|
+
result = (result << 8) | this._buffer.readUInt8(startOffset + i);
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private _readObjectRef(offset: number): number {
|
|
154
|
+
return this._readMultiByteInteger(
|
|
155
|
+
offset,
|
|
156
|
+
this._objectRefSize,
|
|
157
|
+
'Object reference',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Reads an offset from the offset table
|
|
163
|
+
* @param index - The index in the offset table
|
|
164
|
+
* @returns The offset value
|
|
165
|
+
*/
|
|
166
|
+
private _readOffset(index: number): number {
|
|
167
|
+
const offsetStart = this._offsetTableOffset + index * this._offsetSize;
|
|
168
|
+
return this._readMultiByteInteger(offsetStart, this._offsetSize, 'Offset');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parses an integer value from the buffer
|
|
173
|
+
* @param startOffset - The offset to start reading from
|
|
174
|
+
* @param intByteCount - The number of bytes to read
|
|
175
|
+
* @returns The parsed integer value (number or bigint)
|
|
176
|
+
*/
|
|
177
|
+
private _parseIntegerValue(
|
|
178
|
+
startOffset: number,
|
|
179
|
+
intByteCount: number,
|
|
180
|
+
): number | bigint {
|
|
181
|
+
// Handle different integer sizes
|
|
182
|
+
switch (intByteCount) {
|
|
183
|
+
case 1:
|
|
184
|
+
return this._buffer.readInt8(startOffset);
|
|
185
|
+
case 2:
|
|
186
|
+
return this._buffer.readInt16BE(startOffset);
|
|
187
|
+
case 4:
|
|
188
|
+
return this._buffer.readInt32BE(startOffset);
|
|
189
|
+
case 8: {
|
|
190
|
+
// For 64-bit integers, we need to handle potential precision loss
|
|
191
|
+
const bigInt = this._buffer.readBigInt64BE(startOffset);
|
|
192
|
+
const intValue = Number(bigInt);
|
|
193
|
+
|
|
194
|
+
// Check if conversion to Number caused precision loss
|
|
195
|
+
if (BigInt(intValue) !== bigInt) {
|
|
196
|
+
log.warn(
|
|
197
|
+
'Precision loss when converting 64-bit integer to Number. Returning BigInt value.',
|
|
198
|
+
);
|
|
199
|
+
return bigInt; // Return the BigInt directly to avoid precision loss
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return intValue; // Return as number if no precision loss
|
|
203
|
+
}
|
|
204
|
+
default:
|
|
205
|
+
throw new TypeError(
|
|
206
|
+
`Unexpected integer byte count: ${intByteCount}. Cannot parse integer value.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parses a real (floating point) value from the buffer
|
|
213
|
+
* @param startOffset - The offset to start reading from
|
|
214
|
+
* @param floatByteCount - The number of bytes to read
|
|
215
|
+
* @returns The parsed floating point value
|
|
216
|
+
*/
|
|
217
|
+
private _parseRealValue(startOffset: number, floatByteCount: number): number {
|
|
218
|
+
switch (floatByteCount) {
|
|
219
|
+
case 4:
|
|
220
|
+
return this._buffer.readFloatBE(startOffset);
|
|
221
|
+
case 8:
|
|
222
|
+
return this._buffer.readDoubleBE(startOffset);
|
|
223
|
+
default:
|
|
224
|
+
throw new TypeError(
|
|
225
|
+
`Unexpected float byte count: ${floatByteCount}. Cannot parse real value.`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Parses a date value from the buffer
|
|
232
|
+
* @param startOffset - The offset to start reading from
|
|
233
|
+
* @returns The parsed Date object
|
|
234
|
+
*/
|
|
235
|
+
private _parseDateValue(startOffset: number): Date {
|
|
236
|
+
// Date is stored as a float, seconds since 2001-01-01
|
|
237
|
+
const timestamp = this._buffer.readDoubleBE(startOffset);
|
|
238
|
+
// Convert Apple epoch (2001-01-01) to Unix epoch (1970-01-01)
|
|
239
|
+
return new Date((timestamp + APPLE_EPOCH_OFFSET) * 1000);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Parses a data value from the buffer
|
|
244
|
+
* @param startOffset - The offset to start reading from
|
|
245
|
+
* @param objLength - The length of the data
|
|
246
|
+
* @returns The parsed Buffer
|
|
247
|
+
*/
|
|
248
|
+
private _parseDataValue(startOffset: number, objLength: number): Buffer {
|
|
249
|
+
return Buffer.from(
|
|
250
|
+
this._buffer.slice(startOffset, startOffset + objLength),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Parses an ASCII string from the buffer
|
|
256
|
+
* @param startOffset - The offset to start reading from
|
|
257
|
+
* @param objLength - The length of the string
|
|
258
|
+
* @returns The parsed string
|
|
259
|
+
*/
|
|
260
|
+
private _parseAsciiString(startOffset: number, objLength: number): string {
|
|
261
|
+
return this._buffer
|
|
262
|
+
.slice(startOffset, startOffset + objLength)
|
|
263
|
+
.toString('ascii');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Parses a Unicode string from the buffer
|
|
268
|
+
* @param startOffset - The offset to start reading from
|
|
269
|
+
* @param objLength - The length of the string in characters
|
|
270
|
+
* @returns The parsed string
|
|
271
|
+
*/
|
|
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
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return utf16Buffer.toString('utf16le', 0, objLength * 2);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Parses a UID value from the buffer
|
|
286
|
+
* @param startOffset - The offset to start reading from
|
|
287
|
+
* @param uidByteCount - The number of bytes to read
|
|
288
|
+
* @returns The parsed UID value
|
|
289
|
+
*/
|
|
290
|
+
private _parseUidValue(startOffset: number, uidByteCount: number): number {
|
|
291
|
+
return this._readMultiByteInteger(startOffset, uidByteCount, 'UID');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Parses all objects in the binary plist
|
|
296
|
+
*/
|
|
297
|
+
private _parseObjects(): void {
|
|
298
|
+
for (let i = 0; i < this._numObjects; i++) {
|
|
299
|
+
const objOffset = this._readOffset(i);
|
|
300
|
+
const objType = this._buffer.readUInt8(objOffset) & 0xf0;
|
|
301
|
+
const objInfo = this._buffer.readUInt8(objOffset) & 0x0f;
|
|
302
|
+
|
|
303
|
+
let objLength = objInfo;
|
|
304
|
+
let startOffset = objOffset + 1;
|
|
305
|
+
|
|
306
|
+
// For objects with length > 15, the actual length follows
|
|
307
|
+
if (objInfo === 0x0f) {
|
|
308
|
+
const intType = this._buffer.readUInt8(startOffset) & 0xf0;
|
|
309
|
+
if (intType !== BPLIST_TYPE.INT) {
|
|
310
|
+
throw new TypeError(
|
|
311
|
+
`Expected integer type for length at offset ${startOffset}`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const intInfo = this._buffer.readUInt8(startOffset) & 0x0f;
|
|
316
|
+
startOffset++;
|
|
317
|
+
|
|
318
|
+
// Read the length based on the integer size
|
|
319
|
+
const intByteCount = 1 << intInfo;
|
|
320
|
+
objLength = this._readMultiByteInteger(
|
|
321
|
+
startOffset,
|
|
322
|
+
intByteCount,
|
|
323
|
+
'Object length',
|
|
324
|
+
);
|
|
325
|
+
startOffset += intByteCount;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Parse the object based on its type
|
|
329
|
+
this._objectTable[i] = this._parseObjectByType(
|
|
330
|
+
objType,
|
|
331
|
+
objInfo,
|
|
332
|
+
startOffset,
|
|
333
|
+
objLength,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Parses an object based on its type
|
|
340
|
+
* @param objType - The object type
|
|
341
|
+
* @param objInfo - The object info
|
|
342
|
+
* @param startOffset - The start offset
|
|
343
|
+
* @param objLength - The object length
|
|
344
|
+
* @returns The parsed object
|
|
345
|
+
*/
|
|
346
|
+
private _parseObjectByType(
|
|
347
|
+
objType: number,
|
|
348
|
+
objInfo: number,
|
|
349
|
+
startOffset: number,
|
|
350
|
+
objLength: number,
|
|
351
|
+
): PlistValue | TempObject {
|
|
352
|
+
switch (objType) {
|
|
353
|
+
case BPLIST_TYPE.NULL:
|
|
354
|
+
return this._parseNullType(objInfo);
|
|
355
|
+
|
|
356
|
+
case BPLIST_TYPE.INT:
|
|
357
|
+
return this._parseIntegerValue(startOffset, 1 << objInfo);
|
|
358
|
+
|
|
359
|
+
case BPLIST_TYPE.REAL:
|
|
360
|
+
return this._parseRealValue(startOffset, 1 << objInfo);
|
|
361
|
+
|
|
362
|
+
case BPLIST_TYPE.DATE:
|
|
363
|
+
return this._parseDateValue(startOffset);
|
|
364
|
+
|
|
365
|
+
case BPLIST_TYPE.DATA:
|
|
366
|
+
return this._parseDataValue(startOffset, objLength);
|
|
367
|
+
|
|
368
|
+
case BPLIST_TYPE.STRING_ASCII:
|
|
369
|
+
return this._parseAsciiString(startOffset, objLength);
|
|
370
|
+
|
|
371
|
+
case BPLIST_TYPE.STRING_UNICODE:
|
|
372
|
+
return this._parseUnicodeString(startOffset, objLength);
|
|
373
|
+
|
|
374
|
+
case BPLIST_TYPE.UID:
|
|
375
|
+
return this._parseUidValue(startOffset, objInfo + 1);
|
|
376
|
+
|
|
377
|
+
case BPLIST_TYPE.ARRAY:
|
|
378
|
+
return this._createTempArray(objLength, startOffset);
|
|
379
|
+
|
|
380
|
+
case BPLIST_TYPE.DICT:
|
|
381
|
+
return this._createTempDict(objLength, startOffset);
|
|
382
|
+
|
|
383
|
+
default:
|
|
384
|
+
throw new TypeError(
|
|
385
|
+
`Unsupported binary plist object type: ${objType.toString(16)}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Parses a null type object
|
|
392
|
+
* @param objInfo - The object info
|
|
393
|
+
* @returns The parsed value (null, false, or true)
|
|
394
|
+
*/
|
|
395
|
+
private _parseNullType(objInfo: number): PlistValue {
|
|
396
|
+
switch (objInfo) {
|
|
397
|
+
case 0x00:
|
|
398
|
+
return null;
|
|
399
|
+
case 0x08:
|
|
400
|
+
return false;
|
|
401
|
+
case 0x09:
|
|
402
|
+
return true;
|
|
403
|
+
case 0x0f:
|
|
404
|
+
return null; // fill byte
|
|
405
|
+
default:
|
|
406
|
+
throw new TypeError(
|
|
407
|
+
`Unexpected null type object info: 0x${objInfo.toString(16)}. Cannot parse null value.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Creates a temporary array object
|
|
414
|
+
* @param objLength - The array length
|
|
415
|
+
* @param startOffset - The start offset
|
|
416
|
+
* @returns The temporary array object
|
|
417
|
+
*/
|
|
418
|
+
private _createTempArray(objLength: number, startOffset: number): TempObject {
|
|
419
|
+
return {
|
|
420
|
+
type: 'array',
|
|
421
|
+
objLength,
|
|
422
|
+
startOffset,
|
|
423
|
+
value: [] as PlistArray,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Creates a temporary dictionary object
|
|
429
|
+
* @param objLength - The dictionary length
|
|
430
|
+
* @param startOffset - The start offset
|
|
431
|
+
* @returns The temporary dictionary object
|
|
432
|
+
*/
|
|
433
|
+
private _createTempDict(objLength: number, startOffset: number): TempObject {
|
|
434
|
+
return {
|
|
435
|
+
type: 'dict',
|
|
436
|
+
objLength,
|
|
437
|
+
startOffset,
|
|
438
|
+
value: {} as PlistDictionary,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Type guard to check if an object is a TempObject
|
|
444
|
+
* @param obj - The object to check
|
|
445
|
+
* @returns True if the object is a TempObject
|
|
446
|
+
*/
|
|
447
|
+
private _isTempObject(obj: ObjectTableItem): obj is TempObject {
|
|
448
|
+
return typeof obj === 'object' && obj !== null && 'type' in obj;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Resolves references for arrays and dictionaries
|
|
453
|
+
*/
|
|
454
|
+
private _resolveReferences(): void {
|
|
455
|
+
for (let i = 0; i < this._numObjects; i++) {
|
|
456
|
+
const obj = this._objectTable[i];
|
|
457
|
+
if (this._isTempObject(obj)) {
|
|
458
|
+
if (obj.type === 'array') {
|
|
459
|
+
this._resolveArrayReferences(obj, i);
|
|
460
|
+
} else if (obj.type === 'dict') {
|
|
461
|
+
this._resolveDictReferences(obj, i);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Resolves references for an array
|
|
469
|
+
* @param obj - The temporary array object
|
|
470
|
+
* @param index - The index in the object table
|
|
471
|
+
*/
|
|
472
|
+
private _resolveArrayReferences(obj: TempObject, index: number): void {
|
|
473
|
+
const array = obj.value as PlistArray;
|
|
474
|
+
for (let j = 0; j < obj.objLength; j++) {
|
|
475
|
+
const refIdx = this._readObjectRef(
|
|
476
|
+
obj.startOffset + j * this._objectRefSize,
|
|
477
|
+
);
|
|
478
|
+
const refValue = this._objectTable[refIdx];
|
|
479
|
+
// Ensure we're not adding a TempObject to the array
|
|
480
|
+
if (!this._isTempObject(refValue)) {
|
|
481
|
+
array.push(refValue);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
this._objectTable[index] = array;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Resolves references for a dictionary
|
|
489
|
+
* @param obj - The temporary dictionary object
|
|
490
|
+
* @param index - The index in the object table
|
|
491
|
+
*/
|
|
492
|
+
private _resolveDictReferences(obj: TempObject, index: number): void {
|
|
493
|
+
const dict = obj.value as PlistDictionary;
|
|
494
|
+
const keyCount = obj.objLength;
|
|
495
|
+
|
|
496
|
+
// Keys are stored first, followed by values
|
|
497
|
+
for (let j = 0; j < keyCount; j++) {
|
|
498
|
+
const keyRef = this._readObjectRef(
|
|
499
|
+
obj.startOffset + j * this._objectRefSize,
|
|
500
|
+
);
|
|
501
|
+
const valueRef = this._readObjectRef(
|
|
502
|
+
obj.startOffset + (keyCount + j) * this._objectRefSize,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const key = this._objectTable[keyRef];
|
|
506
|
+
const value = this._objectTable[valueRef];
|
|
507
|
+
|
|
508
|
+
if (typeof key !== 'string') {
|
|
509
|
+
throw new TypeError(
|
|
510
|
+
`Dictionary key must be a string, got ${typeof key}`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Ensure we're not adding a TempObject to the dictionary
|
|
515
|
+
if (!this._isTempObject(value)) {
|
|
516
|
+
dict[key] = value;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
this._objectTable[index] = dict;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Handles special case for the top object
|
|
524
|
+
* @returns The final parsed value
|
|
525
|
+
*/
|
|
526
|
+
private _handleTopObject(): PlistValue {
|
|
527
|
+
// If the top object is an empty object but we have key-value pairs in the array format,
|
|
528
|
+
// convert it to a proper object
|
|
529
|
+
if (
|
|
530
|
+
this._topObject === 0 &&
|
|
531
|
+
this._objectTable[0] &&
|
|
532
|
+
typeof this._objectTable[0] === 'object' &&
|
|
533
|
+
!this._isTempObject(this._objectTable[0]) &&
|
|
534
|
+
Object.keys(this._objectTable[0] as object).length === 0 &&
|
|
535
|
+
this._objectTable.length > 1
|
|
536
|
+
) {
|
|
537
|
+
return this._convertArrayToDict();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Ensure the top object is a PlistValue and not a TempObject
|
|
541
|
+
const topValue = this._objectTable[this._topObject];
|
|
542
|
+
if (this._isTempObject(topValue)) {
|
|
543
|
+
return topValue.value;
|
|
544
|
+
}
|
|
545
|
+
return topValue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Converts an array format to a dictionary
|
|
550
|
+
* @returns The converted dictionary
|
|
551
|
+
*/
|
|
552
|
+
private _convertArrayToDict(): PlistDictionary {
|
|
553
|
+
const result: PlistDictionary = {};
|
|
554
|
+
// Process the array in key-value pairs
|
|
555
|
+
for (let i = 1; i < this._objectTable.length; i += 2) {
|
|
556
|
+
const key = this._objectTable[i];
|
|
557
|
+
if (i + 1 < this._objectTable.length && typeof key === 'string') {
|
|
558
|
+
const value = this._objectTable[i + 1];
|
|
559
|
+
if (!this._isTempObject(value)) {
|
|
560
|
+
result[key] = value;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Parses a binary plist buffer into a JavaScript object
|
|
570
|
+
* @param buffer - The binary plist data as a Buffer
|
|
571
|
+
* @returns The parsed JavaScript object
|
|
572
|
+
*/
|
|
573
|
+
export function parseBinaryPlist(buffer: Buffer): PlistValue {
|
|
574
|
+
const parser = new BinaryPlistParser(buffer);
|
|
575
|
+
return parser.parse();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Determines if a buffer is a binary plist
|
|
580
|
+
* @param buffer - The buffer to check
|
|
581
|
+
* @returns True if the buffer is a binary plist
|
|
582
|
+
*/
|
|
583
|
+
export function isBinaryPlist(buffer: Buffer): boolean {
|
|
584
|
+
return (
|
|
585
|
+
buffer.length >= 8 && buffer.slice(0, 8).equals(BPLIST_MAGIC_AND_VERSION)
|
|
586
|
+
);
|
|
587
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common constants for plist operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Constants for binary plist format
|
|
6
|
+
export const BPLIST_MAGIC = 'bplist';
|
|
7
|
+
export const BPLIST_VERSION = '00';
|
|
8
|
+
export const BPLIST_MAGIC_AND_VERSION = Buffer.from(
|
|
9
|
+
`${BPLIST_MAGIC}${BPLIST_VERSION}`,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
// Binary plist header constants
|
|
13
|
+
export const BINARY_PLIST_MAGIC = 'bplist00';
|
|
14
|
+
export const IBINARY_PLIST_MAGIC = 'Ibplist00';
|
|
15
|
+
export const BINARY_PLIST_HEADER_LENGTH = 9;
|
|
16
|
+
|
|
17
|
+
// XML plist constants
|
|
18
|
+
export const XML_DECLARATION = '<?xml';
|
|
19
|
+
export const PLIST_CLOSING_TAG = '</plist>';
|
|
20
|
+
|
|
21
|
+
// Length field constants
|
|
22
|
+
export const LENGTH_FIELD_1_BYTE = 1;
|
|
23
|
+
export const LENGTH_FIELD_2_BYTES = 2;
|
|
24
|
+
export const LENGTH_FIELD_4_BYTES = 4;
|
|
25
|
+
export const LENGTH_FIELD_8_BYTES = 8;
|
|
26
|
+
export const UINT32_HIGH_MULTIPLIER = 0x100000000;
|
|
27
|
+
|
|
28
|
+
// Encoding constants
|
|
29
|
+
export const UTF8_ENCODING = 'utf8';
|
|
30
|
+
|
|
31
|
+
// Apple epoch offset (seconds between Unix epoch 1970-01-01 and Apple epoch 2001-01-01)
|
|
32
|
+
export const APPLE_EPOCH_OFFSET = 978307200;
|
|
33
|
+
|
|
34
|
+
// Binary plist trailer size (last 32 bytes of the file)
|
|
35
|
+
export const BPLIST_TRAILER_SIZE = 32;
|
|
36
|
+
|
|
37
|
+
// Object types in binary plist
|
|
38
|
+
export const BPLIST_TYPE = {
|
|
39
|
+
NULL: 0x00,
|
|
40
|
+
FALSE: 0x08,
|
|
41
|
+
TRUE: 0x09,
|
|
42
|
+
FILL: 0x0f,
|
|
43
|
+
INT: 0x10,
|
|
44
|
+
REAL: 0x20,
|
|
45
|
+
DATE: 0x30,
|
|
46
|
+
DATA: 0x40,
|
|
47
|
+
STRING_ASCII: 0x50,
|
|
48
|
+
STRING_UNICODE: 0x60,
|
|
49
|
+
UID: 0x80,
|
|
50
|
+
ARRAY: 0xa0,
|
|
51
|
+
SET: 0xc0,
|
|
52
|
+
DICT: 0xd0,
|
|
53
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Export all components for easy imports
|
|
2
|
+
import { createBinaryPlist } from './binary-plist-creator.js';
|
|
3
|
+
import { isBinaryPlist, parseBinaryPlist } from './binary-plist-parser.js';
|
|
4
|
+
import {
|
|
5
|
+
APPLE_EPOCH_OFFSET,
|
|
6
|
+
BPLIST_MAGIC_AND_VERSION,
|
|
7
|
+
BPLIST_TYPE,
|
|
8
|
+
UTF8_ENCODING,
|
|
9
|
+
} from './constants.js';
|
|
10
|
+
import { LengthBasedSplitter } from './length-based-splitter.js';
|
|
11
|
+
import { createPlist as createXmlPlist } from './plist-creator.js';
|
|
12
|
+
import { PlistServiceDecoder } from './plist-decoder.js';
|
|
13
|
+
import { PlistServiceEncoder } from './plist-encoder.js';
|
|
14
|
+
import { parsePlist as parseXmlPlist } from './plist-parser.js';
|
|
15
|
+
import { PlistService } from './plist-service.js';
|
|
16
|
+
import { createPlist } from './unified-plist-creator.js';
|
|
17
|
+
import { parsePlist } from './unified-plist-parser.js';
|
|
18
|
+
import {
|
|
19
|
+
ensureString,
|
|
20
|
+
escapeXml,
|
|
21
|
+
findFirstReplacementCharacter,
|
|
22
|
+
fixMultipleXmlDeclarations,
|
|
23
|
+
hasUnicodeReplacementCharacter,
|
|
24
|
+
isValidXml,
|
|
25
|
+
isXmlPlistContent,
|
|
26
|
+
trimBeforeXmlDeclaration,
|
|
27
|
+
} from './utils.js';
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
createPlist,
|
|
31
|
+
createXmlPlist,
|
|
32
|
+
createBinaryPlist,
|
|
33
|
+
LengthBasedSplitter,
|
|
34
|
+
parsePlist,
|
|
35
|
+
parseXmlPlist,
|
|
36
|
+
parseBinaryPlist,
|
|
37
|
+
isBinaryPlist,
|
|
38
|
+
PlistService,
|
|
39
|
+
PlistServiceDecoder,
|
|
40
|
+
PlistServiceEncoder,
|
|
41
|
+
APPLE_EPOCH_OFFSET,
|
|
42
|
+
BPLIST_MAGIC_AND_VERSION,
|
|
43
|
+
BPLIST_TYPE,
|
|
44
|
+
UTF8_ENCODING,
|
|
45
|
+
// Utility functions
|
|
46
|
+
hasUnicodeReplacementCharacter,
|
|
47
|
+
findFirstReplacementCharacter,
|
|
48
|
+
ensureString,
|
|
49
|
+
trimBeforeXmlDeclaration,
|
|
50
|
+
fixMultipleXmlDeclarations,
|
|
51
|
+
isValidXml,
|
|
52
|
+
escapeXml,
|
|
53
|
+
isXmlPlistContent,
|
|
54
|
+
};
|