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,231 @@
1
+ import { logger } from '@appium/support';
2
+ import { Socket } from 'net';
3
+ import { TLSSocket } from 'tls';
4
+
5
+ import type { PlistDictionary } from '../types.js';
6
+ import { LengthBasedSplitter } from './length-based-splitter.js';
7
+ import { PlistServiceDecoder } from './plist-decoder.js';
8
+ import { PlistServiceEncoder } from './plist-encoder.js';
9
+
10
+ const log = logger.getLogger('Plist');
11
+ const errorLog = logger.getLogger('PlistError');
12
+
13
+ const config = {
14
+ verboseErrorLogging: false,
15
+ };
16
+
17
+ /**
18
+ * Message type for plist communications
19
+ */
20
+ type PlistMessage = PlistDictionary;
21
+
22
+ /**
23
+ * Options for PlistService
24
+ */
25
+ export interface PlistServiceOptions {
26
+ maxFrameLength?: number;
27
+ }
28
+
29
+ /**
30
+ * Service for communication using plist protocol
31
+ */
32
+ export class PlistService {
33
+ /**
34
+ * Enable verbose error logging
35
+ */
36
+ static enableVerboseErrorLogging(): void {
37
+ config.verboseErrorLogging = true;
38
+ errorLog.debug('Verbose plist error logging enabled');
39
+ }
40
+
41
+ /**
42
+ * Disable verbose error logging
43
+ */
44
+ static disableVerboseErrorLogging(): void {
45
+ config.verboseErrorLogging = false;
46
+ }
47
+
48
+ /**
49
+ * Check if verbose error logging is enabled
50
+ * @returns True if verbose error logging is enabled
51
+ */
52
+ static isVerboseErrorLoggingEnabled(): boolean {
53
+ return config.verboseErrorLogging;
54
+ }
55
+
56
+ /**
57
+ * Gets the underlying socket
58
+ * @returns The socket used by this service
59
+ */
60
+ public getSocket(): Socket | TLSSocket {
61
+ return this._socket;
62
+ }
63
+ private readonly _socket: Socket | TLSSocket;
64
+ private readonly _splitter: LengthBasedSplitter;
65
+ private readonly _decoder: PlistServiceDecoder;
66
+ private _encoder: PlistServiceEncoder;
67
+ private _messageQueue: PlistMessage[];
68
+
69
+ /**
70
+ * Creates a new PlistService instance
71
+ * @param socket The socket to use for communication
72
+ * @param options Configuration options
73
+ */
74
+ constructor(socket: Socket, options: PlistServiceOptions = {}) {
75
+ this._socket = socket;
76
+
77
+ // Set up transformers
78
+ this._splitter = new LengthBasedSplitter({
79
+ maxFrameLength: options.maxFrameLength ?? 100 * 1024 * 1024, // Default to 100MB
80
+ });
81
+ this._decoder = new PlistServiceDecoder();
82
+ this._encoder = new PlistServiceEncoder();
83
+
84
+ // Set up the pipeline
85
+ this.setupPipeline();
86
+
87
+ // Message queue for async receiving
88
+ this._messageQueue = [];
89
+ this._decoder.on('data', (data: PlistMessage) =>
90
+ this._messageQueue.push(data),
91
+ );
92
+
93
+ // Handle errors
94
+ this.setupErrorHandlers();
95
+ }
96
+
97
+ /**
98
+ * Send a plist message and receive a response
99
+ * @param data Message to send
100
+ * @param timeout Response timeout in ms
101
+ * @returns Promise resolving to the received message
102
+ */
103
+ public async sendPlistAndReceive(
104
+ data: PlistMessage,
105
+ timeout = 5000,
106
+ ): Promise<PlistMessage> {
107
+ this.sendPlist(data);
108
+ return this.receivePlist(timeout);
109
+ }
110
+
111
+ /**
112
+ * Send a plist message
113
+ * @param data Message to send
114
+ * @throws Error if data is null or undefined
115
+ */
116
+ public sendPlist(data: PlistMessage): void {
117
+ if (!data) {
118
+ throw new Error('Cannot send null or undefined data');
119
+ }
120
+ this._encoder.write(data);
121
+ }
122
+
123
+ /**
124
+ * Receive a plist message with timeout
125
+ * @param timeout Timeout in ms
126
+ * @returns Promise resolving to the received message
127
+ * @throws Error if timeout is reached before receiving a message
128
+ */
129
+ public async receivePlist(timeout = 5000): Promise<PlistMessage> {
130
+ return new Promise<PlistMessage>((resolve, reject) => {
131
+ // Check if we already have a message
132
+ const message = this._messageQueue.shift();
133
+ if (message) {
134
+ return resolve(message);
135
+ }
136
+
137
+ // Set up a check interval
138
+ const checkInterval = setInterval(() => {
139
+ const message = this._messageQueue.shift();
140
+ if (message) {
141
+ clearInterval(checkInterval);
142
+ clearTimeout(timeoutId);
143
+ resolve(message);
144
+ }
145
+ }, 50);
146
+
147
+ // Set up timeout
148
+ const timeoutId = setTimeout(() => {
149
+ clearInterval(checkInterval);
150
+ reject(
151
+ new Error(`Timed out waiting for plist response after ${timeout}ms`),
152
+ );
153
+ }, timeout);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Close the connection and clean up resources
159
+ */
160
+ public close(): void {
161
+ try {
162
+ // Remove all data listeners to prevent parsing during close
163
+ this._splitter.removeAllListeners();
164
+ this._decoder.removeAllListeners();
165
+
166
+ // Clear the message queue to prevent processing during close
167
+ this._messageQueue = [];
168
+
169
+ // Unpipe the transformers to prevent data flow during close
170
+ try {
171
+ this._socket.unpipe(this._splitter);
172
+ this._splitter.unpipe(this._decoder);
173
+ } catch (unpipeError) {
174
+ log.debug(
175
+ `Non-critical error during unpipe: ${unpipeError instanceof Error ? unpipeError.message : String(unpipeError)}`,
176
+ );
177
+ }
178
+
179
+ // End the socket
180
+ this._socket.end();
181
+ } catch (error) {
182
+ // Log the error but don't rethrow it to ensure cleanup completes
183
+ log.error(
184
+ `Error closing socket: ${error instanceof Error ? error.message : String(error)}`,
185
+ );
186
+
187
+ // If ending fails, destroy the socket
188
+ this._socket.destroy();
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Sets up the data pipeline between socket and transformers
194
+ */
195
+ private setupPipeline(): void {
196
+ this._socket.pipe(this._splitter);
197
+ this._splitter.pipe(this._decoder);
198
+ this._encoder.pipe(this._socket);
199
+ }
200
+
201
+ /**
202
+ * Sets up error handlers for socket and transformers
203
+ */
204
+ private setupErrorHandlers(): void {
205
+ this._socket.on('error', this.handleError.bind(this));
206
+ this._encoder.on('error', this.handleError.bind(this));
207
+ this._decoder.on('error', this.handleError.bind(this));
208
+ this._splitter.on('error', this.handleError.bind(this));
209
+ }
210
+
211
+ /**
212
+ * Handles errors from any component
213
+ * @param error The error that occurred
214
+ */
215
+ private handleError(error: Error): void {
216
+ // Only log detailed errors if verbose logging is enabled
217
+ if (!config.verboseErrorLogging) {
218
+ return;
219
+ }
220
+
221
+ errorLog.debug(`PlistService Error: ${error.message}`);
222
+
223
+ // If this is an XML parsing error, it might be a binary plist
224
+ if (
225
+ error.message.includes('Invalid XML') ||
226
+ error.message.includes('XML parsing')
227
+ ) {
228
+ errorLog.debug('This might be a binary plist with a non-standard format');
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,19 @@
1
+ import type { PlistDictionary } from '../types.js';
2
+ import { createBinaryPlist } from './binary-plist-creator.js';
3
+ import { createPlist as createXmlPlist } from './plist-creator.js';
4
+
5
+ /**
6
+ * Unified plist creator that can create both XML and binary plists
7
+ * @param obj - The JavaScript object to convert to a plist
8
+ * @param binary - Whether to create a binary plist (true) or XML plist (false)
9
+ * @returns The plist data as a string (XML) or Buffer (binary)
10
+ */
11
+ export function createPlist(
12
+ obj: PlistDictionary,
13
+ binary: boolean = false,
14
+ ): string | Buffer {
15
+ if (binary) {
16
+ return createBinaryPlist(obj);
17
+ }
18
+ return createXmlPlist(obj);
19
+ }
@@ -0,0 +1,25 @@
1
+ import type { PlistValue } from '../types.js';
2
+ import { isBinaryPlist, parseBinaryPlist } from './binary-plist-parser.js';
3
+ import { parsePlist as parseXmlPlist } from './plist-parser.js';
4
+ import { ensureString } from './utils.js';
5
+
6
+ /**
7
+ * Unified plist parser that can handle both XML and binary plists
8
+ * @param data - The plist data as a string or Buffer
9
+ * @returns The parsed JavaScript object
10
+ */
11
+ export function parsePlist(data: string | Buffer): PlistValue {
12
+ try {
13
+ // Check if it's a binary plist (only if data is a Buffer)
14
+ if (Buffer.isBuffer(data) && isBinaryPlist(data)) {
15
+ return parseBinaryPlist(data);
16
+ } else {
17
+ // Otherwise, assume it's an XML plist
18
+ return parseXmlPlist(ensureString(data));
19
+ }
20
+ } catch (error) {
21
+ throw new Error(
22
+ `Failed to parse plist: ${error instanceof Error ? error.message : String(error)}`,
23
+ );
24
+ }
25
+ }
@@ -0,0 +1,376 @@
1
+ import { UTF8_ENCODING } from './constants.js';
2
+
3
+ /**
4
+ * Represents a tag position in XML
5
+ */
6
+ export interface TagPosition {
7
+ start: number;
8
+ end: number;
9
+ isOpening: boolean;
10
+ tagName: string;
11
+ }
12
+
13
+ /**
14
+ * Represents the result of finding tags around a position
15
+ */
16
+ export interface TagsAroundPosition {
17
+ beforeTag: TagPosition | null;
18
+ afterTag: TagPosition | null;
19
+ }
20
+
21
+ /**
22
+ * Ensures data is a string for string operations
23
+ *
24
+ * @param data - The data to convert, can be a string or Buffer
25
+ * @returns The data as a string
26
+ */
27
+ export function ensureString(data: string | Buffer): string {
28
+ return typeof data === 'string' ? data : data.toString(UTF8_ENCODING);
29
+ }
30
+
31
+ /**
32
+ * Finds the position of the first Unicode replacement character in the data.
33
+ *
34
+ * @param data - The data to check, can be a string or Buffer
35
+ * @returns The position of the first replacement character, or -1 if not found
36
+ */
37
+ export function findFirstReplacementCharacter(data: string | Buffer): number {
38
+ const strData = ensureString(data);
39
+ return strData.indexOf('�');
40
+ }
41
+
42
+ /**
43
+ * Checks if the provided data contains Unicode replacement characters (�),
44
+ * which might indicate encoding issues.
45
+ *
46
+ * @param data - The data to check, can be a string or Buffer
47
+ * @returns True if replacement characters are found, false otherwise
48
+ */
49
+ export function hasUnicodeReplacementCharacter(data: string | Buffer): boolean {
50
+ const strData = ensureString(data);
51
+
52
+ return strData.includes('�');
53
+ }
54
+
55
+ /**
56
+ * Finds the XML declaration and trims any preceding content
57
+ *
58
+ * @param data - The data to process, can be a string or Buffer
59
+ * @returns The trimmed data as a string
60
+ */
61
+ export function trimBeforeXmlDeclaration(data: string | Buffer): string {
62
+ const strData = ensureString(data);
63
+ const xmlDeclIndex = strData.indexOf('<?xml');
64
+
65
+ if (xmlDeclIndex > 0) {
66
+ return strData.slice(xmlDeclIndex);
67
+ }
68
+
69
+ return strData;
70
+ }
71
+
72
+ /**
73
+ * Checks for multiple XML declarations and fixes the data by keeping only the first one
74
+ *
75
+ * @param data - The data to check and fix, can be a string or Buffer
76
+ * @returns The fixed data as a string, or the original data if no fix was needed
77
+ */
78
+ export function fixMultipleXmlDeclarations(data: string | Buffer): string {
79
+ const strData = ensureString(data);
80
+ const xmlDeclMatches = strData.match(/(<\?xml[^>]*\?>)/g) || [];
81
+ const xmlDeclCount = xmlDeclMatches.length;
82
+
83
+ if (xmlDeclCount > 1) {
84
+ const firstDeclEnd = strData.indexOf('?>') + 2;
85
+ const restOfXml = strData.substring(firstDeclEnd);
86
+ const cleanedRest = restOfXml.replace(/<\?xml[^>]*\?>/g, '');
87
+ return strData.substring(0, firstDeclEnd) + cleanedRest;
88
+ }
89
+
90
+ return strData;
91
+ }
92
+
93
+ /**
94
+ * Removes extra content after the closing plist tag
95
+ *
96
+ * @param data - The data to clean, can be a string or Buffer
97
+ * @returns The cleaned data as a string
98
+ */
99
+ export function removeExtraContentAfterPlist(data: string | Buffer): string {
100
+ const strData = ensureString(data);
101
+
102
+ const closingPlistIndex = strData.lastIndexOf('</plist>');
103
+
104
+ if (closingPlistIndex > 0) {
105
+ return strData.substring(0, closingPlistIndex + 8);
106
+ }
107
+
108
+ return strData;
109
+ }
110
+
111
+ /**
112
+ * Checks if the data is valid XML (contains at least one tag)
113
+ *
114
+ * @param data - The data to check, can be a string or Buffer
115
+ * @returns True if the data is valid XML, false otherwise
116
+ */
117
+ export function isValidXml(data: string | Buffer): boolean {
118
+ const strData = ensureString(data);
119
+ return Boolean(strData) && Boolean(strData.trim()) && strData.includes('<');
120
+ }
121
+
122
+ /**
123
+ * Escapes special XML characters in a string
124
+ *
125
+ * @param str - The string to escape
126
+ * @returns The escaped string
127
+ */
128
+ export function escapeXml(str: string): string {
129
+ return str.replace(/[<>&"']/g, function (c) {
130
+ switch (c) {
131
+ case '<':
132
+ return '&lt;';
133
+ case '>':
134
+ return '&gt;';
135
+ case '&':
136
+ return '&amp;';
137
+ case '"':
138
+ return '&quot;';
139
+ // eslint-disable-next-line quotes -- Prettier uses double quotes here to avoid escaping the single quote character
140
+ case `'`:
141
+ return '&apos;';
142
+ default:
143
+ return c;
144
+ }
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Checks if the data contains XML plist content by detecting XML declaration or plist tags
150
+ *
151
+ * @param data - The data to check, can be a string or Buffer
152
+ * @returns True if the data contains XML plist content, false otherwise
153
+ */
154
+ export function isXmlPlistContent(data: string | Buffer): boolean {
155
+ return (
156
+ data.toString(UTF8_ENCODING).includes('<?xml') ||
157
+ data.toString(UTF8_ENCODING).includes('<plist')
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Parses a tag content to extract tag name and determine if it's an opening tag
163
+ *
164
+ * @param tagContent - The content between < and > in an XML tag
165
+ * @returns An object with tag name and whether it's an opening tag
166
+ */
167
+ function parseTagContent(tagContent: string): {
168
+ tagName: string;
169
+ isOpening: boolean;
170
+ } {
171
+ const isClosing = tagContent.startsWith('/');
172
+ const tagName = isClosing
173
+ ? tagContent.substring(1).trim().split(/\s+/)[0]
174
+ : tagContent.trim().split(/\s+/)[0];
175
+
176
+ return {
177
+ tagName,
178
+ isOpening: !isClosing,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Creates a TagPosition object from tag positions and content
184
+ *
185
+ * @param startPos - Start position of the tag
186
+ * @param endPos - End position of the tag
187
+ * @param tagContent - Content between < and > in the tag
188
+ * @returns A TagPosition object
189
+ */
190
+ function createTagPosition(
191
+ startPos: number,
192
+ endPos: number,
193
+ tagContent: string,
194
+ ): TagPosition {
195
+ const { tagName, isOpening } = parseTagContent(tagContent);
196
+
197
+ return {
198
+ start: startPos,
199
+ end: endPos + 1,
200
+ isOpening,
201
+ tagName,
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Finds the tag before a specific position in XML
207
+ *
208
+ * @param xmlString - The XML string to search
209
+ * @param position - The position to search before
210
+ * @returns The tag position or null if not found
211
+ */
212
+ function findTagBefore(
213
+ xmlString: string,
214
+ position: number,
215
+ ): TagPosition | null {
216
+ const tagEndPos = xmlString.lastIndexOf('>', position);
217
+ if (tagEndPos < 0) {
218
+ return null;
219
+ }
220
+
221
+ const tagStartPos = xmlString.lastIndexOf('<', tagEndPos);
222
+ if (tagStartPos < 0) {
223
+ return null;
224
+ }
225
+
226
+ const tagContent = xmlString.substring(tagStartPos + 1, tagEndPos);
227
+ return createTagPosition(tagStartPos, tagEndPos, tagContent);
228
+ }
229
+
230
+ /**
231
+ * Finds the tag after a specific position in XML
232
+ *
233
+ * @param xmlString - The XML string to search
234
+ * @param position - The position to search after
235
+ * @returns The tag position or null if not found
236
+ */
237
+ function findTagAfter(xmlString: string, position: number): TagPosition | null {
238
+ const tagStartPos = xmlString.indexOf('<', position);
239
+ if (tagStartPos < 0) {
240
+ return null;
241
+ }
242
+
243
+ const tagEndPos = xmlString.indexOf('>', tagStartPos);
244
+ if (tagEndPos < 0) {
245
+ return null;
246
+ }
247
+
248
+ const tagContent = xmlString.substring(tagStartPos + 1, tagEndPos);
249
+ return createTagPosition(tagStartPos, tagEndPos, tagContent);
250
+ }
251
+
252
+ /**
253
+ * Finds XML tags around a specific position
254
+ *
255
+ * @param xmlString - The XML string to search
256
+ * @param position - The position to search around
257
+ * @returns An object with the nearest tags before and after the position
258
+ */
259
+ export function findTagsAroundPosition(
260
+ xmlString: string,
261
+ position: number,
262
+ ): TagsAroundPosition {
263
+ return {
264
+ beforeTag: findTagBefore(xmlString, position),
265
+ afterTag: findTagAfter(xmlString, position),
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Removes content between two positions in an XML string
271
+ *
272
+ * @param xmlString - The XML string to modify
273
+ * @param startPos - The start position to remove from
274
+ * @param endPos - The end position to remove to
275
+ * @returns The modified XML string
276
+ */
277
+ function removeContentBetween(
278
+ xmlString: string,
279
+ startPos: number,
280
+ endPos: number,
281
+ ): string {
282
+ return xmlString.substring(0, startPos) + xmlString.substring(endPos);
283
+ }
284
+
285
+ /**
286
+ * Handles the case where a replacement character is between complete tags
287
+ *
288
+ * @param xmlString - The XML string to clean
289
+ * @param beforeTag - The tag before the replacement character
290
+ * @param afterTag - The tag after the replacement character
291
+ * @returns The cleaned XML string
292
+ */
293
+ function cleanBetweenTags(
294
+ xmlString: string,
295
+ beforeTag: TagPosition,
296
+ afterTag: TagPosition,
297
+ ): string {
298
+ return removeContentBetween(xmlString, beforeTag.end, afterTag.start);
299
+ }
300
+
301
+ /**
302
+ * Handles the case where a replacement character is inside a tag
303
+ *
304
+ * @param xmlString - The XML string to clean
305
+ * @param beforeTag - The tag containing the replacement character
306
+ * @param afterTag - The tag after the replacement character
307
+ * @returns The cleaned XML string or null if can't be cleaned
308
+ */
309
+ function cleanInsideTag(
310
+ xmlString: string,
311
+ beforeTag: TagPosition,
312
+ afterTag: TagPosition,
313
+ ): string | null {
314
+ const prevCompleteTag = xmlString.lastIndexOf('>', beforeTag.start);
315
+ if (prevCompleteTag < 0) {
316
+ return null;
317
+ }
318
+
319
+ return removeContentBetween(xmlString, prevCompleteTag + 1, afterTag.start);
320
+ }
321
+
322
+ /**
323
+ * Fallback cleaning method when tags aren't available on both sides
324
+ *
325
+ * @param xmlString - The XML string to clean
326
+ * @returns The cleaned XML string
327
+ */
328
+ function fallbackCleaning(xmlString: string): string {
329
+ const xmlDeclIndex = xmlString.indexOf('<?xml');
330
+ if (xmlDeclIndex > 0) {
331
+ return xmlString.slice(xmlDeclIndex);
332
+ }
333
+
334
+ const plistTagIndex = xmlString.indexOf('<plist');
335
+ if (plistTagIndex > 0) {
336
+ return xmlString.slice(plistTagIndex);
337
+ }
338
+
339
+ const anyTagIndex = xmlString.indexOf('<');
340
+ if (anyTagIndex > 0) {
341
+ return xmlString.slice(anyTagIndex);
342
+ }
343
+
344
+ return xmlString;
345
+ }
346
+
347
+ /**
348
+ * Intelligently cleans XML with Unicode replacement characters
349
+ *
350
+ * @param xmlString - The XML string to clean
351
+ * @param badCharPos - The position of the replacement character
352
+ * @returns The cleaned XML string
353
+ */
354
+ export function cleanXmlWithReplacementChar(
355
+ xmlString: string,
356
+ badCharPos: number,
357
+ ): string {
358
+ const { beforeTag, afterTag } = findTagsAroundPosition(xmlString, badCharPos);
359
+
360
+ if (!beforeTag || !afterTag) {
361
+ return fallbackCleaning(xmlString);
362
+ }
363
+
364
+ if (beforeTag.end <= badCharPos && badCharPos < afterTag.start) {
365
+ return cleanBetweenTags(xmlString, beforeTag, afterTag);
366
+ }
367
+
368
+ if (beforeTag.start <= badCharPos && badCharPos < beforeTag.end) {
369
+ const cleaned = cleanInsideTag(xmlString, beforeTag, afterTag);
370
+ if (cleaned) {
371
+ return cleaned;
372
+ }
373
+ }
374
+
375
+ return removeContentBetween(xmlString, beforeTag.start, afterTag.start);
376
+ }
@@ -0,0 +1,22 @@
1
+ // HTTP/2 Constants
2
+ export const Http2Constants = {
3
+ HTTP2_MAGIC: Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n', 'ascii'),
4
+ FRAME_HEADER_SIZE: 9,
5
+ ROOT_CHANNEL: 1,
6
+ REPLY_CHANNEL: 3,
7
+ FLAG_END_HEADERS: 0x4,
8
+ FLAG_ACK: 0x1,
9
+ DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS: 100,
10
+ DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: 1048576,
11
+ DEFAULT_WIN_SIZE_INCR: 983041,
12
+ SETTINGS_MAX_CONCURRENT_STREAMS: 0x03,
13
+ SETTINGS_INITIAL_WINDOW_SIZE: 0x04,
14
+ } as const;
15
+
16
+ // XPC Constants
17
+ export const XpcConstants = {
18
+ XPC_FLAGS_INIT_HANDSHAKE: 0x00400000,
19
+ XPC_FLAGS_ALWAYS_SET: 0x00000001,
20
+ XPC_FLAGS_DATA_PRESENT: 0x00000100,
21
+ XPC_FLAGS_WANTING_REPLY: 0x00010000,
22
+ } as const;