@hot-updater/protocol 1.1.3 → 1.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1090 @@
1
+ import { l as canonicalizeAppVersion, n as isUUIDv7, o as encodeChannelKey } from "./uuid-kZvLA0Ly.mjs";
2
+ //#region src/nativeArtifact/byteSource.ts
3
+ /** A `Blob`, such as Node's `fs.openAsBlob`, read by slices. */
4
+ const blobSource = (blob) => ({
5
+ size: blob.size,
6
+ async read(offset, length) {
7
+ return new Uint8Array(await blob.slice(offset, Math.min(blob.size, offset + length)).arrayBuffer());
8
+ }
9
+ });
10
+ /** Why an artifact can't be read, or can't serve as a native build. */
11
+ var NativeArtifactError = class extends Error {
12
+ code;
13
+ name = "NativeArtifactError";
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.code = code;
17
+ }
18
+ };
19
+ //#endregion
20
+ //#region src/nativeArtifact/androidManifest.ts
21
+ const corrupt$1 = () => new NativeArtifactError("UNREADABLE", "The artifact's AndroidManifest.xml is corrupt.");
22
+ const view$3 = (bytes) => new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
23
+ const RES_STRING_POOL_TYPE = 1;
24
+ const RES_XML_TYPE = 3;
25
+ const RES_XML_START_ELEMENT_TYPE = 258;
26
+ const RES_XML_END_ELEMENT_TYPE = 259;
27
+ const RES_XML_RESOURCE_MAP_TYPE = 384;
28
+ const UTF8_FLAG = 256;
29
+ const NO_INDEX = 4294967295;
30
+ /** A `ResXMLTree_attrExt`, and a `ResXMLTree_attribute`, in bytes. */
31
+ const ELEMENT_EXT_BYTES = 20;
32
+ const ATTRIBUTE_BYTES = 20;
33
+ /** How deep a manifest's elements nest: an AndroidManifest.xml, a few levels. */
34
+ const MAX_ELEMENT_DEPTH = 64;
35
+ const TYPE_REFERENCE = 1;
36
+ const TYPE_STRING = 3;
37
+ const TYPE_INT_DEC = 16;
38
+ const TYPE_INT_HEX = 17;
39
+ const TYPE_INT_BOOLEAN = 18;
40
+ /**
41
+ * How many bytes a manifest's strings decode to in all. A pool's offsets can
42
+ * point many indexes, or overlapping ranges, at one long string, so the
43
+ * manifest's own size bounds nothing.
44
+ */
45
+ const DECODED_STRING_BYTES = 16777216;
46
+ /**
47
+ * A `ResStringPool` chunk's strings, decoded when an index is read: each
48
+ * offset once, against one budget, within the chunk.
49
+ */
50
+ const readStringPool = (bytes, start, size) => {
51
+ const data = view$3(bytes);
52
+ const end = start + size;
53
+ if (size < 28) throw corrupt$1();
54
+ const count = data.getUint32(start + 8, true);
55
+ const flags = data.getUint32(start + 16, true);
56
+ const stringsStart = start + data.getUint32(start + 20, true);
57
+ if (start + 28 + count * 4 > end || stringsStart > end) throw corrupt$1();
58
+ const utf8 = (flags & UTF8_FLAG) !== 0;
59
+ const decoder = new TextDecoder(utf8 ? "utf-8" : "utf-16le");
60
+ const decoded = /* @__PURE__ */ new Map();
61
+ let budget = DECODED_STRING_BYTES;
62
+ return (index) => {
63
+ if (index === NO_INDEX || index >= count) return "";
64
+ const offset = data.getUint32(start + 28 + index * 4, true);
65
+ const known = decoded.get(offset);
66
+ if (known !== void 0) return known;
67
+ let at = stringsStart + offset;
68
+ let byteLength;
69
+ if (at + 4 > end) throw corrupt$1();
70
+ if (utf8) {
71
+ at += bytes[at] & 128 ? 2 : 1;
72
+ let length = bytes[at];
73
+ if (length & 128) {
74
+ length = (length & 127) << 8 | bytes[at + 1];
75
+ at += 2;
76
+ } else at += 1;
77
+ byteLength = length;
78
+ } else {
79
+ let length = data.getUint16(at, true);
80
+ if (length & 32768) {
81
+ length = (length & 32767) << 16 | data.getUint16(at + 2, true);
82
+ at += 4;
83
+ } else at += 2;
84
+ byteLength = length * 2;
85
+ }
86
+ budget -= byteLength;
87
+ if (at + byteLength > end || budget < 0) throw corrupt$1();
88
+ const text = decoder.decode(bytes.subarray(at, at + byteLength));
89
+ decoded.set(offset, text);
90
+ return text;
91
+ };
92
+ };
93
+ const parseBinaryManifest = (bytes) => {
94
+ const data = view$3(bytes);
95
+ if (bytes.byteLength < 8 || data.getUint16(0, true) !== RES_XML_TYPE) throw corrupt$1();
96
+ let stringAt = () => "";
97
+ let resourceIds = [];
98
+ const stack = [];
99
+ let root = null;
100
+ for (let at = data.getUint16(2, true); at + 8 <= bytes.byteLength;) {
101
+ const type = data.getUint16(at, true);
102
+ const headerSize = data.getUint16(at + 2, true);
103
+ const size = data.getUint32(at + 4, true);
104
+ if (size < 8 || at + size > bytes.byteLength) throw corrupt$1();
105
+ if (type === RES_STRING_POOL_TYPE) stringAt = readStringPool(bytes, at, size);
106
+ else if (type === RES_XML_RESOURCE_MAP_TYPE) {
107
+ resourceIds = [];
108
+ for (let i = at + headerSize; i + 4 <= at + size; i += 4) resourceIds.push(data.getUint32(i, true));
109
+ } else if (type === RES_XML_START_ELEMENT_TYPE) {
110
+ const ext = at + headerSize;
111
+ const attributeStart = data.getUint16(ext + 8, true);
112
+ const attributeSize = data.getUint16(ext + 10, true);
113
+ const attributeCount = data.getUint16(ext + 12, true);
114
+ if (headerSize < 16 || ext + ELEMENT_EXT_BYTES > at + size || attributeStart < ELEMENT_EXT_BYTES || attributeSize < ATTRIBUTE_BYTES || ext + attributeStart + attributeCount * attributeSize > at + size || stack.length >= MAX_ELEMENT_DEPTH) throw corrupt$1();
115
+ const name = stringAt(data.getUint32(ext + 4, true));
116
+ const attributes = [];
117
+ for (let i = 0; i < attributeCount; i += 1) {
118
+ const attr = ext + attributeStart + i * attributeSize;
119
+ const nameIndex = data.getUint32(attr + 4, true);
120
+ const raw = data.getUint32(attr + 8, true);
121
+ const dataType = bytes[attr + 15];
122
+ const value = data.getUint32(attr + 16, true);
123
+ attributes.push({
124
+ name: stringAt(nameIndex),
125
+ resourceId: resourceIds[nameIndex] ?? 0,
126
+ value: dataType === TYPE_STRING ? {
127
+ type: "string",
128
+ value: stringAt(raw === NO_INDEX ? value : raw)
129
+ } : dataType === TYPE_INT_BOOLEAN ? {
130
+ type: "boolean",
131
+ value: value !== 0
132
+ } : dataType === TYPE_INT_DEC || dataType === TYPE_INT_HEX ? {
133
+ type: "int",
134
+ value: value | 0
135
+ } : dataType === TYPE_REFERENCE ? {
136
+ type: "reference",
137
+ id: value
138
+ } : { type: "other" }
139
+ });
140
+ }
141
+ stack.push({
142
+ name,
143
+ attributes,
144
+ children: []
145
+ });
146
+ } else if (type === RES_XML_END_ELEMENT_TYPE) {
147
+ const element = stack.pop();
148
+ if (element === void 0) throw corrupt$1();
149
+ const parent = stack.at(-1);
150
+ if (parent === void 0) root ??= element;
151
+ else parent.children.push(element);
152
+ }
153
+ at += size;
154
+ }
155
+ if (root === null) throw corrupt$1();
156
+ return root;
157
+ };
158
+ /** A minimal protobuf reader: fields of one message, by number. */
159
+ const readMessage = (bytes) => {
160
+ const fields = [];
161
+ let at = 0;
162
+ const varint = () => {
163
+ let value = 0;
164
+ let multiplier = 1;
165
+ for (;;) {
166
+ if (at >= bytes.byteLength) throw corrupt$1();
167
+ const byte = bytes[at++];
168
+ value += (byte & 127) * multiplier;
169
+ if ((byte & 128) === 0) return value;
170
+ multiplier *= 128;
171
+ if (multiplier > 2 ** 63) throw corrupt$1();
172
+ }
173
+ };
174
+ while (at < bytes.byteLength) {
175
+ const key = varint();
176
+ const field = Math.floor(key / 8);
177
+ const wireType = key % 8;
178
+ if (wireType === 0) fields.push({
179
+ field,
180
+ wireType,
181
+ value: varint()
182
+ });
183
+ else if (wireType === 2) {
184
+ const length = varint();
185
+ if (at + length > bytes.byteLength) throw corrupt$1();
186
+ fields.push({
187
+ field,
188
+ wireType,
189
+ value: bytes.subarray(at, at + length)
190
+ });
191
+ at += length;
192
+ } else if (wireType === 5) at += 4;
193
+ else if (wireType === 1) at += 8;
194
+ else throw corrupt$1();
195
+ }
196
+ return fields;
197
+ };
198
+ const decoder = new TextDecoder();
199
+ const bytesOf = (fields, field) => fields.filter((f) => f.field === field && f.value instanceof Uint8Array).map((f) => f.value);
200
+ const stringOf = (fields, field) => {
201
+ const [value] = bytesOf(fields, field);
202
+ return value === void 0 ? "" : decoder.decode(value);
203
+ };
204
+ const numberOf = (fields, field) => {
205
+ const found = fields.find((f) => f.field === field && f.wireType === 0);
206
+ return found === void 0 ? void 0 : found.value;
207
+ };
208
+ /** An `Item` of aapt2's Resources.proto, as an attribute's compiled value. */
209
+ const protoItemValue = (item, raw) => {
210
+ const fields = readMessage(item);
211
+ const [reference] = bytesOf(fields, 1);
212
+ if (reference !== void 0) {
213
+ const ref = readMessage(reference);
214
+ return {
215
+ type: "reference",
216
+ id: numberOf(ref, 2) ?? 0,
217
+ name: stringOf(ref, 3) || void 0
218
+ };
219
+ }
220
+ const [str] = bytesOf(fields, 2);
221
+ if (str !== void 0) return {
222
+ type: "string",
223
+ value: stringOf(readMessage(str), 1)
224
+ };
225
+ const [primitive] = bytesOf(fields, 7);
226
+ if (primitive !== void 0) {
227
+ const prim = readMessage(primitive);
228
+ const boolean = numberOf(prim, 8);
229
+ if (boolean !== void 0) return {
230
+ type: "boolean",
231
+ value: boolean !== 0
232
+ };
233
+ const int = numberOf(prim, 6) ?? numberOf(prim, 7);
234
+ if (int !== void 0) return {
235
+ type: "int",
236
+ value: int | 0
237
+ };
238
+ if (raw !== "") return {
239
+ type: "string",
240
+ value: raw
241
+ };
242
+ }
243
+ return null;
244
+ };
245
+ /** An `XmlElement` of aapt2's Resources.proto. */
246
+ const protoElement = (bytes, depth = 0) => {
247
+ if (depth >= MAX_ELEMENT_DEPTH) throw corrupt$1();
248
+ const fields = readMessage(bytes);
249
+ const attributes = bytesOf(fields, 4).map((attribute) => {
250
+ const attr = readMessage(attribute);
251
+ const raw = stringOf(attr, 3);
252
+ const [item] = bytesOf(attr, 6);
253
+ return {
254
+ name: stringOf(attr, 2),
255
+ resourceId: numberOf(attr, 5) ?? 0,
256
+ value: (item && protoItemValue(item, raw)) ?? {
257
+ type: "string",
258
+ value: raw
259
+ }
260
+ };
261
+ });
262
+ const children = bytesOf(fields, 5).flatMap((node) => {
263
+ const [element] = bytesOf(readMessage(node), 1);
264
+ return element === void 0 ? [] : [protoElement(element, depth + 1)];
265
+ });
266
+ return {
267
+ name: stringOf(fields, 3),
268
+ attributes,
269
+ children
270
+ };
271
+ };
272
+ /**
273
+ * An `.aab` module's `manifest/AndroidManifest.xml`, an `XmlNode` of
274
+ * aapt2's Resources.proto.
275
+ */
276
+ const parseProtoManifest = (bytes) => {
277
+ const [element] = bytesOf(readMessage(bytes), 1);
278
+ if (element === void 0) throw corrupt$1();
279
+ return protoElement(element);
280
+ };
281
+ const VERSION_CODE = 16843291;
282
+ const VERSION_NAME = 16843292;
283
+ const NAME = 16842755;
284
+ const VALUE = 16842788;
285
+ const RESOURCE = 16842789;
286
+ const attributeOf = (element, resourceId, name) => element.attributes.find((attribute) => attribute.resourceId !== 0 ? attribute.resourceId === resourceId : attribute.name === name)?.value ?? null;
287
+ /** The values of a parsed manifest that import reads. */
288
+ const readAndroidManifest = (manifest) => {
289
+ if (manifest.name !== "manifest") throw corrupt$1();
290
+ const packageValue = manifest.attributes.find((attribute) => attribute.name === "package")?.value;
291
+ const versionCode = attributeOf(manifest, VERSION_CODE, "versionCode");
292
+ const application = manifest.children.find(({ name }) => name === "application");
293
+ const metaData = /* @__PURE__ */ new Map();
294
+ for (const element of application?.children ?? []) {
295
+ if (element.name !== "meta-data") continue;
296
+ const name = attributeOf(element, NAME, "name");
297
+ const value = attributeOf(element, VALUE, "value") ?? attributeOf(element, RESOURCE, "resource");
298
+ if (name?.type === "string" && value !== null) metaData.set(name.value, value);
299
+ }
300
+ return {
301
+ packageName: packageValue?.type === "string" ? packageValue.value : null,
302
+ versionName: attributeOf(manifest, VERSION_NAME, "versionName"),
303
+ versionCode: versionCode?.type === "int" ? versionCode.value : null,
304
+ metaData
305
+ };
306
+ };
307
+ //#endregion
308
+ //#region src/nativeArtifact/plist.ts
309
+ /** How deep a plist nests: an Info.plist nests a few levels. */
310
+ const MAX_DEPTH = 64;
311
+ const corrupt = () => new NativeArtifactError("UNREADABLE", "The app's Info.plist is corrupt.");
312
+ const view$2 = (bytes) => new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
313
+ /** A big-endian unsigned integer of `size` bytes. */
314
+ const uintAt = (bytes, offset, size) => {
315
+ let value = 0;
316
+ for (let i = 0; i < size; i += 1) value = value * 256 + bytes[offset + i];
317
+ return value;
318
+ };
319
+ /** A binary property list (`bplist00`), as Xcode writes an app's Info.plist. */
320
+ const parseBinaryPlist = (bytes) => {
321
+ if (bytes.byteLength < 40) throw corrupt();
322
+ const trailer = bytes.byteLength - 32;
323
+ const offsetSize = bytes[trailer + 6];
324
+ const refSize = bytes[trailer + 7];
325
+ const objectCount = uintAt(bytes, trailer + 8, 8);
326
+ const top = uintAt(bytes, trailer + 16, 8);
327
+ const tableOffset = uintAt(bytes, trailer + 24, 8);
328
+ if (offsetSize < 1 || offsetSize > 8 || refSize < 1 || refSize > 8 || objectCount < 1 || top >= objectCount || tableOffset < 8 || tableOffset + objectCount * offsetSize > trailer) throw corrupt();
329
+ const data = view$2(bytes);
330
+ const seen = /* @__PURE__ */ new Set();
331
+ const parsed = /* @__PURE__ */ new Map();
332
+ let budget = 2 * bytes.byteLength + 4096;
333
+ const spend = (units) => {
334
+ budget -= units;
335
+ if (budget < 0) throw corrupt();
336
+ };
337
+ const parse = (ref) => {
338
+ if (ref >= objectCount) throw corrupt();
339
+ const offset = uintAt(bytes, tableOffset + ref * offsetSize, offsetSize);
340
+ if (offset < 8 || offset >= tableOffset) throw corrupt();
341
+ const done = parsed.get(offset);
342
+ if (done !== void 0) return done;
343
+ if (seen.has(offset) || seen.size > MAX_DEPTH) throw corrupt();
344
+ const value = parseObject(offset);
345
+ parsed.set(offset, value);
346
+ return value;
347
+ };
348
+ const parseObject = (offset) => {
349
+ const marker = bytes[offset];
350
+ const kind = marker >> 4;
351
+ const info = marker & 15;
352
+ /** `count` units of `unit` bytes from `start`, which must fit the objects. */
353
+ const fits = (start, count, unit) => {
354
+ if (start + count * unit > tableOffset) throw corrupt();
355
+ };
356
+ /**
357
+ * A length in the marker, or in the integer object after it, and where
358
+ * its `unit`-byte items start, all of which must fit the objects.
359
+ */
360
+ const lengthAt = (unit) => {
361
+ let length = info;
362
+ let start = offset + 1;
363
+ if (info === 15) {
364
+ const sizeMarker = bytes[offset + 1];
365
+ if (sizeMarker >> 4 !== 1 || (sizeMarker & 15) > 3) throw corrupt();
366
+ const size = 1 << (sizeMarker & 15);
367
+ fits(offset + 2, size, 1);
368
+ length = uintAt(bytes, offset + 2, size);
369
+ start = offset + 2 + size;
370
+ }
371
+ fits(start, length, unit);
372
+ spend(length + 1);
373
+ return [length, start];
374
+ };
375
+ switch (kind) {
376
+ case 0: return info === 9 ? true : info === 8 ? false : null;
377
+ case 1: {
378
+ if (info > 3) throw corrupt();
379
+ const size = 1 << info;
380
+ fits(offset + 1, size, 1);
381
+ return size === 8 ? Number(data.getBigInt64(offset + 1, false)) : uintAt(bytes, offset + 1, size);
382
+ }
383
+ case 2:
384
+ if (info !== 2 && info !== 3) throw corrupt();
385
+ fits(offset + 1, 1 << info, 1);
386
+ return info === 2 ? data.getFloat32(offset + 1, false) : data.getFloat64(offset + 1, false);
387
+ case 3:
388
+ fits(offset + 1, 8, 1);
389
+ return data.getFloat64(offset + 1, false);
390
+ case 4: {
391
+ const [length, start] = lengthAt(1);
392
+ return bytes.slice(start, start + length);
393
+ }
394
+ case 5: {
395
+ const [length, start] = lengthAt(1);
396
+ return new TextDecoder("latin1").decode(bytes.subarray(start, start + length));
397
+ }
398
+ case 6: {
399
+ const [length, start] = lengthAt(2);
400
+ let text = "";
401
+ for (let i = 0; i < length; i += 1) text += String.fromCharCode(data.getUint16(start + i * 2, false));
402
+ return text;
403
+ }
404
+ case 8:
405
+ if (info > 7) throw corrupt();
406
+ fits(offset + 1, info + 1, 1);
407
+ return uintAt(bytes, offset + 1, info + 1);
408
+ case 10:
409
+ case 12: {
410
+ const [length, start] = lengthAt(refSize);
411
+ seen.add(offset);
412
+ const items = [];
413
+ for (let i = 0; i < length; i += 1) items.push(parse(uintAt(bytes, start + i * refSize, refSize)));
414
+ seen.delete(offset);
415
+ return items;
416
+ }
417
+ case 13: {
418
+ const [length, start] = lengthAt(2 * refSize);
419
+ seen.add(offset);
420
+ const dict = Object.create(null);
421
+ for (let i = 0; i < length; i += 1) {
422
+ const key = parse(uintAt(bytes, start + i * refSize, refSize));
423
+ const value = parse(uintAt(bytes, start + (length + i) * refSize, refSize));
424
+ if (typeof key !== "string") throw corrupt();
425
+ dict[key] = value;
426
+ }
427
+ seen.delete(offset);
428
+ return dict;
429
+ }
430
+ default: throw corrupt();
431
+ }
432
+ };
433
+ return parse(top);
434
+ };
435
+ const ENTITIES = {
436
+ amp: "&",
437
+ lt: "<",
438
+ gt: ">",
439
+ quot: "\"",
440
+ apos: "'"
441
+ };
442
+ const decodeEntities = (text) => text.replace(/&(#x[0-9a-f]+|#\d+|\w+);/giu, (whole, entity) => {
443
+ if (entity[0] === "#") {
444
+ const code = entity[1] === "x" || entity[1] === "X" ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
445
+ return String.fromCodePoint(code);
446
+ }
447
+ return ENTITIES[entity] ?? whole;
448
+ });
449
+ /** An XML property list, as some tools leave an app's Info.plist. */
450
+ const parseXmlPlist = (text) => {
451
+ const tokens = text.replace(/<!--[\s\S]*?-->/gu, "").replace(/<\?[\s\S]*?\?>/gu, "").replace(/<!DOCTYPE[^>]*>/giu, "").split(/(<[^>]+>)/u);
452
+ let i = 0;
453
+ const nextTag = () => {
454
+ while (i < tokens.length) {
455
+ const token = tokens[i++];
456
+ if (token.startsWith("<")) return token;
457
+ if (token.trim() !== "") throw corrupt();
458
+ }
459
+ throw corrupt();
460
+ };
461
+ const textUntil = (close) => {
462
+ let value = "";
463
+ while (i < tokens.length && tokens[i] !== close) value += tokens[i++];
464
+ if (tokens[i++] !== close) throw corrupt();
465
+ return decodeEntities(value);
466
+ };
467
+ let depth = 0;
468
+ const parseValue = (tag) => {
469
+ if (depth > MAX_DEPTH) throw corrupt();
470
+ const name = /^<\s*([\w]+)/u.exec(tag)?.[1];
471
+ const empty = tag.endsWith("/>");
472
+ switch (name) {
473
+ case "string": return empty ? "" : textUntil("</string>");
474
+ case "integer": return Number(textUntil("</integer>"));
475
+ case "real": return Number(textUntil("</real>"));
476
+ case "true": return true;
477
+ case "false": return false;
478
+ case "date": return textUntil("</date>");
479
+ case "data": return textUntil("</data>").replace(/\s+/gu, "");
480
+ case "array": {
481
+ const items = [];
482
+ if (empty) return items;
483
+ depth += 1;
484
+ for (let next = nextTag(); next !== "</array>"; next = nextTag()) items.push(parseValue(next));
485
+ depth -= 1;
486
+ return items;
487
+ }
488
+ case "dict": {
489
+ const dict = Object.create(null);
490
+ if (empty) return dict;
491
+ depth += 1;
492
+ for (let next = nextTag(); next !== "</dict>"; next = nextTag()) {
493
+ if (next !== "<key>") throw corrupt();
494
+ const key = textUntil("</key>");
495
+ dict[key] = parseValue(nextTag());
496
+ }
497
+ depth -= 1;
498
+ return dict;
499
+ }
500
+ default: throw corrupt();
501
+ }
502
+ };
503
+ let tag = nextTag();
504
+ if (/^<plist\b/u.test(tag)) tag = nextTag();
505
+ return parseValue(tag);
506
+ };
507
+ /** A property list, binary or XML, as a JavaScript value. */
508
+ const parsePlist = (bytes) => {
509
+ if (new TextDecoder().decode(bytes.subarray(0, 8)) === "bplist00") return parseBinaryPlist(bytes);
510
+ return parseXmlPlist(new TextDecoder().decode(bytes));
511
+ };
512
+ //#endregion
513
+ //#region src/nativeArtifact/strings.ts
514
+ const view$1 = (bytes) => new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
515
+ /** Bytes that are all ASCII, as text; null otherwise. */
516
+ const asciiOf = (bytes) => {
517
+ let text = "";
518
+ for (const byte of bytes) {
519
+ if (byte === 0 || byte >= 128) return null;
520
+ text += String.fromCharCode(byte);
521
+ }
522
+ return text;
523
+ };
524
+ /**
525
+ * The string constants of a dex file with one of `lengths`, from its string
526
+ * pool: every string literal the app's code holds, each one whole. R8
527
+ * renames and inlines classes, but never drops a literal the code returns,
528
+ * so the pool still holds a value the native SDK compiled in.
529
+ */
530
+ const readDexStrings = (dex, lengths) => {
531
+ const data = view$1(dex);
532
+ if (dex.byteLength < 112 || asciiOf(dex.subarray(0, 3)) !== "dex" || dex[3] !== 10) throw new NativeArtifactError("UNREADABLE", "A dex file is corrupt.");
533
+ const count = data.getUint32(56, true);
534
+ const idsOffset = data.getUint32(60, true);
535
+ if (idsOffset + count * 4 > dex.byteLength) throw new NativeArtifactError("UNREADABLE", "A dex file is corrupt.");
536
+ const strings = [];
537
+ const read = /* @__PURE__ */ new Set();
538
+ for (let i = 0; i < count; i += 1) {
539
+ let offset = data.getUint32(idsOffset + i * 4, true);
540
+ if (read.has(offset)) continue;
541
+ read.add(offset);
542
+ let utf16Length = 0;
543
+ for (let shift = 0; offset < dex.byteLength; shift += 7) {
544
+ const byte = dex[offset++];
545
+ utf16Length |= (byte & 127) << shift;
546
+ if ((byte & 128) === 0 || shift > 28) break;
547
+ }
548
+ if (!lengths.has(utf16Length)) continue;
549
+ const end = offset + utf16Length;
550
+ if (end >= dex.byteLength || dex[end] !== 0) continue;
551
+ const text = asciiOf(dex.subarray(offset, end));
552
+ if (text !== null) strings.push(text);
553
+ }
554
+ return strings;
555
+ };
556
+ const MH_MAGIC = 4277009102;
557
+ const MH_MAGIC_64 = 4277009103;
558
+ const FAT_MAGIC = 3405691582;
559
+ const FAT_MAGIC_64 = 3405691583;
560
+ const LC_SEGMENT = 1;
561
+ const LC_SEGMENT_64 = 25;
562
+ const CPU_TYPE_ARM64 = 16777228;
563
+ const nameAt = (bytes, offset) => {
564
+ let name = "";
565
+ for (let i = offset; i < offset + 16 && bytes[i] !== 0; i += 1) name += String.fromCharCode(bytes[i]);
566
+ return name;
567
+ };
568
+ /** The slice of a universal binary to read: arm64's, else the first. */
569
+ const sliceOf = async (read) => {
570
+ const head = await read(0, 4096);
571
+ if (head.byteLength < 8) return null;
572
+ const data = view$1(head);
573
+ const magic = data.getUint32(0, false);
574
+ if (magic !== FAT_MAGIC && magic !== FAT_MAGIC_64) return 0;
575
+ const count = data.getUint32(4, false);
576
+ const size = magic === FAT_MAGIC ? 20 : 32;
577
+ let first = null;
578
+ for (let i = 0; i < count && 8 + (i + 1) * size <= head.byteLength; i += 1) {
579
+ const at = 8 + i * size;
580
+ const offset = magic === FAT_MAGIC ? data.getUint32(at + 8, false) : Number(data.getBigUint64(at + 8, false));
581
+ if (data.getUint32(at, false) === CPU_TYPE_ARM64) return offset;
582
+ first ??= offset;
583
+ }
584
+ return first;
585
+ };
586
+ /**
587
+ * The C string literals of a Mach-O binary with one of `lengths`, from its
588
+ * `__TEXT,__cstring` section, each one whole: the literals an Objective-C or
589
+ * Swift source compiled in. A universal binary is read by its arm64 slice.
590
+ * A file that is no Mach-O binary holds none.
591
+ */
592
+ const readMachOStrings = async (read, lengths) => {
593
+ const base = await sliceOf(read);
594
+ if (base === null) return [];
595
+ const header = await read(base, base + 32);
596
+ if (header.byteLength < 28) return [];
597
+ const data = view$1(header);
598
+ const magic = data.getUint32(0, true);
599
+ if (magic !== MH_MAGIC && magic !== MH_MAGIC_64) return [];
600
+ const is64 = magic === MH_MAGIC_64;
601
+ const headerSize = is64 ? 32 : 28;
602
+ const commandCount = data.getUint32(16, true);
603
+ const commandsSize = data.getUint32(20, true);
604
+ const commands = await read(base + headerSize, base + headerSize + commandsSize);
605
+ const cmds = view$1(commands);
606
+ let section = null;
607
+ for (let i = 0, at = 0; i < commandCount && at + 8 <= commands.byteLength && section === null; i += 1) {
608
+ const cmd = cmds.getUint32(at, true);
609
+ const cmdSize = cmds.getUint32(at + 4, true);
610
+ if (cmdSize < 8) break;
611
+ if ((cmd === LC_SEGMENT_64 || cmd === LC_SEGMENT) && nameAt(commands, at + 8) === "__TEXT") {
612
+ const sectionCount = cmds.getUint32(at + (is64 ? 64 : 48), true);
613
+ const first = at + (is64 ? 72 : 56);
614
+ const sectionSize = is64 ? 80 : 68;
615
+ for (let s = 0; s < sectionCount; s += 1) {
616
+ const sec = first + s * sectionSize;
617
+ if (sec + sectionSize > commands.byteLength) break;
618
+ if (nameAt(commands, sec) !== "__cstring") continue;
619
+ section = is64 ? {
620
+ size: Number(cmds.getBigUint64(sec + 40, true)),
621
+ offset: cmds.getUint32(sec + 48, true)
622
+ } : {
623
+ size: cmds.getUint32(sec + 36, true),
624
+ offset: cmds.getUint32(sec + 40, true)
625
+ };
626
+ break;
627
+ }
628
+ }
629
+ at += cmdSize;
630
+ }
631
+ if (section === null || section.size === 0) return [];
632
+ const cstrings = await read(base + section.offset, base + section.offset + section.size);
633
+ const strings = [];
634
+ for (let start = 0; start < cstrings.byteLength;) {
635
+ let end = cstrings.indexOf(0, start);
636
+ if (end === -1) end = cstrings.byteLength;
637
+ if (lengths.has(end - start)) {
638
+ const text = asciiOf(cstrings.subarray(start, end));
639
+ if (text !== null) strings.push(text);
640
+ }
641
+ start = end + 1;
642
+ }
643
+ return strings;
644
+ };
645
+ //#endregion
646
+ //#region src/nativeArtifact/zip.ts
647
+ const EOCD_SIGNATURE = 101010256;
648
+ const ZIP64_EOCD_LOCATOR_SIGNATURE = 117853008;
649
+ const ZIP64_EOCD_SIGNATURE = 101075792;
650
+ const CENTRAL_HEADER_SIGNATURE = 33639248;
651
+ const LOCAL_HEADER_SIGNATURE = 67324752;
652
+ const ZIP64_EXTRA_ID = 1;
653
+ const UINT32_MAX = 4294967295;
654
+ /** The end-of-central-directory record and the longest comment after it. */
655
+ const EOCD_SEARCH_LENGTH = 65557;
656
+ /** How much compressed data one read takes. */
657
+ const READ_CHUNK = 1048576;
658
+ const MiB$1 = 1048576;
659
+ /**
660
+ * What an archive may hold before it is refused as no artifact: an upload is
661
+ * untrusted, so a crafted directory or entry (a zip bomb) stops here, not
662
+ * after filling memory.
663
+ */
664
+ const ZIP_LIMITS = {
665
+ /** The central directory. */
666
+ directoryBytes: 64 * MiB$1,
667
+ entries: 5e5,
668
+ /** An entry streamed out whole, such as the built-in bundle. */
669
+ entryBytes: 1024 * MiB$1
670
+ };
671
+ const UNIX_HOST = 3;
672
+ const S_IFMT = 61440;
673
+ const S_IFLNK = 40960;
674
+ const view = (bytes) => new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
675
+ const notZip = () => new NativeArtifactError("UNREADABLE", "The file is not a zip archive, as an .ipa, .aab or .apk is.");
676
+ const u64 = (data, offset) => Number(data.getBigUint64(offset, true));
677
+ /** The central directory's offset and size, from its end record (ZIP64 too). */
678
+ const centralDirectoryOf = async (source) => {
679
+ const start = Math.max(0, source.size - EOCD_SEARCH_LENGTH);
680
+ const tail = await source.read(start, source.size - start);
681
+ const data = view(tail);
682
+ for (let i = tail.length - 22; i >= 0; i -= 1) {
683
+ if (data.getUint32(i, true) !== EOCD_SIGNATURE) continue;
684
+ let size = data.getUint32(i + 12, true);
685
+ let offset = data.getUint32(i + 16, true);
686
+ if (offset === UINT32_MAX || size === UINT32_MAX) {
687
+ const locator = i - 20;
688
+ if (locator < 0 || data.getUint32(locator, true) !== ZIP64_EOCD_LOCATOR_SIGNATURE) throw notZip();
689
+ const record = view(await source.read(u64(data, locator + 8), 56));
690
+ if (record.byteLength < 56 || record.getUint32(0, true) !== ZIP64_EOCD_SIGNATURE) throw notZip();
691
+ size = u64(record, 40);
692
+ offset = u64(record, 48);
693
+ }
694
+ if (offset + size > source.size) throw notZip();
695
+ return {
696
+ offset,
697
+ size
698
+ };
699
+ }
700
+ throw notZip();
701
+ };
702
+ /** Every entry of a zip archive, reading only its central directory. */
703
+ const readZipDirectory = async (source) => {
704
+ const { offset, size } = await centralDirectoryOf(source);
705
+ if (size > ZIP_LIMITS.directoryBytes) throw new NativeArtifactError("UNREADABLE", "The archive's directory is larger than any artifact's.");
706
+ const directory = await source.read(offset, size);
707
+ const data = view(directory);
708
+ const decoder = new TextDecoder();
709
+ const entries = [];
710
+ for (let i = 0; i + 46 <= directory.length;) {
711
+ if (data.getUint32(i, true) !== CENTRAL_HEADER_SIGNATURE) break;
712
+ if (entries.length >= ZIP_LIMITS.entries) throw new NativeArtifactError("UNREADABLE", "The archive holds more entries than any artifact.");
713
+ const madeBy = data.getUint16(i + 4, true);
714
+ const externalAttributes = data.getUint32(i + 38, true);
715
+ const method = data.getUint16(i + 10, true);
716
+ let compressedSize = data.getUint32(i + 20, true);
717
+ let uncompressedSize = data.getUint32(i + 24, true);
718
+ const nameLength = data.getUint16(i + 28, true);
719
+ const extraLength = data.getUint16(i + 30, true);
720
+ const commentLength = data.getUint16(i + 32, true);
721
+ let localHeaderOffset = data.getUint32(i + 42, true);
722
+ const nameStart = i + 46;
723
+ const name = decoder.decode(directory.subarray(nameStart, nameStart + nameLength));
724
+ const extraStart = nameStart + nameLength;
725
+ for (let j = extraStart; j + 4 <= extraStart + extraLength;) {
726
+ const id = data.getUint16(j, true);
727
+ const length = data.getUint16(j + 2, true);
728
+ if (id === ZIP64_EXTRA_ID) {
729
+ let k = j + 4;
730
+ if (uncompressedSize === UINT32_MAX) {
731
+ uncompressedSize = u64(data, k);
732
+ k += 8;
733
+ }
734
+ if (compressedSize === UINT32_MAX) {
735
+ compressedSize = u64(data, k);
736
+ k += 8;
737
+ }
738
+ if (localHeaderOffset === UINT32_MAX) localHeaderOffset = u64(data, k);
739
+ }
740
+ j += 4 + length;
741
+ }
742
+ const segments = name.split("/");
743
+ entries.push({
744
+ name,
745
+ unsafe: name.startsWith("/") || /^[A-Za-z]:/u.test(name) || name.includes("\\") || name.includes("\0") || segments.includes(".."),
746
+ symlink: madeBy >> 8 === UNIX_HOST && (externalAttributes >>> 16 & S_IFMT) === S_IFLNK,
747
+ method,
748
+ compressedSize,
749
+ uncompressedSize,
750
+ localHeaderOffset
751
+ });
752
+ i = extraStart + extraLength + commentLength;
753
+ }
754
+ return entries;
755
+ };
756
+ /**
757
+ * The one entry an artifact's reader takes by `name`: a symbolic link, or a
758
+ * name the archive holds twice, which could hand the reader other bytes
759
+ * than an installer reads, is refused. Unsafe names match nothing.
760
+ */
761
+ const findZipEntry = (entries, match) => {
762
+ const test = typeof match === "string" ? (name) => name === match : match;
763
+ const found = entries.filter(({ name, unsafe }) => !unsafe && test(name));
764
+ for (const entry of found) assertReadable(entry);
765
+ if (typeof match === "string" && found.length > 1) throw new NativeArtifactError("UNREADABLE", `The archive holds ${match} more than once.`);
766
+ return found[0];
767
+ };
768
+ /** Every safe entry `match` takes, each a file the reader may read. */
769
+ const filterZipEntries = (entries, match) => {
770
+ const found = entries.filter(({ name, unsafe }) => !unsafe && match(name));
771
+ for (const entry of found) assertReadable(entry);
772
+ return found;
773
+ };
774
+ const assertReadable = (entry) => {
775
+ if (entry.symlink) throw new NativeArtifactError("UNREADABLE", `The archive's ${entry.name} is a symbolic link, not a file.`);
776
+ };
777
+ const tooLarge = (entry, limit) => new NativeArtifactError("UNREADABLE", `The archive's ${entry.name} is larger than ${Math.round(limit / MiB$1)} MB, more than any artifact's.`);
778
+ /** Where an entry's data starts, past its local header. */
779
+ const dataOffsetOf = async (source, entry) => {
780
+ const local = view(await source.read(entry.localHeaderOffset, 30));
781
+ if (local.byteLength < 30 || local.getUint32(0, true) !== LOCAL_HEADER_SIGNATURE) throw new NativeArtifactError("UNREADABLE", `The archive's ${entry.name} is corrupt.`);
782
+ return entry.localHeaderOffset + 30 + local.getUint16(26, true) + local.getUint16(28, true);
783
+ };
784
+ /**
785
+ * An entry's uncompressed bytes as a stream, read from the source a chunk at
786
+ * a time, so a large entry is never held whole.
787
+ */
788
+ const openZipEntry = async (source, entry, { maxBytes = ZIP_LIMITS.entryBytes } = {}) => {
789
+ assertReadable(entry);
790
+ if (entry.unsafe) throw new NativeArtifactError("UNREADABLE", `The archive's entry ${JSON.stringify(entry.name)} has a name no artifact holds.`);
791
+ if (entry.uncompressedSize > maxBytes) throw tooLarge(entry, maxBytes);
792
+ if (entry.method !== 0 && entry.method !== 8) throw new NativeArtifactError("UNREADABLE", `The archive stores ${entry.name} with an unsupported compression method (${entry.method}).`);
793
+ const start = await dataOffsetOf(source, entry);
794
+ const end = start + entry.compressedSize;
795
+ let position = start;
796
+ const compressed = new ReadableStream({ async pull(controller) {
797
+ if (position >= end) {
798
+ controller.close();
799
+ return;
800
+ }
801
+ const chunk = await source.read(position, Math.min(READ_CHUNK, end - position));
802
+ if (chunk.byteLength === 0) {
803
+ controller.error(new NativeArtifactError("UNREADABLE", `The archive ends inside ${entry.name}.`));
804
+ return;
805
+ }
806
+ position += chunk.byteLength;
807
+ controller.enqueue(chunk);
808
+ } });
809
+ const inflated = entry.method === 0 ? compressed : compressed.pipeThrough(new DecompressionStream("deflate-raw"));
810
+ const limit = Math.min(entry.uncompressedSize, maxBytes);
811
+ let produced = 0;
812
+ return inflated.pipeThrough(new TransformStream({ transform(chunk, controller) {
813
+ produced += chunk.byteLength;
814
+ if (produced > limit) {
815
+ controller.error(new NativeArtifactError("UNREADABLE", `The archive's ${entry.name} inflates past the size it declares.`));
816
+ return;
817
+ }
818
+ controller.enqueue(chunk);
819
+ } }));
820
+ };
821
+ /**
822
+ * The uncompressed bytes in `[start, end)` of an entry, which stops
823
+ * inflating once it has them: a Mach-O header without the rest of the
824
+ * binary.
825
+ */
826
+ const readZipEntryRange = async (source, entry, start, end, budget) => {
827
+ const length = Math.max(0, Math.min(end, entry.uncompressedSize) - start);
828
+ if (budget !== void 0 && start + length > budget.remaining) throw new NativeArtifactError("UNREADABLE", "The artifact inflates to more than any artifact's.");
829
+ const out = new Uint8Array(length);
830
+ if (length === 0) return out;
831
+ const reader = (await openZipEntry(source, entry)).getReader();
832
+ let position = 0;
833
+ let written = 0;
834
+ try {
835
+ while (written < length) {
836
+ if (budget !== void 0 && Date.now() > budget.deadline) throw new NativeArtifactError("UNREADABLE", "Reading the artifact took too long.");
837
+ const { done, value } = await reader.read();
838
+ if (done) break;
839
+ if (budget !== void 0) budget.remaining -= value.byteLength;
840
+ const from = Math.max(start - position, 0);
841
+ const to = Math.min(start + length - position, value.byteLength);
842
+ if (from < to) {
843
+ out.set(value.subarray(from, to), position + from - start);
844
+ written += to - from;
845
+ }
846
+ position += value.byteLength;
847
+ }
848
+ } catch (error) {
849
+ throw error instanceof NativeArtifactError ? error : new NativeArtifactError("UNREADABLE", `The archive's ${entry.name} can't be decompressed: ${error instanceof Error ? error.message : String(error)}.`);
850
+ } finally {
851
+ await reader.cancel().catch(() => {});
852
+ }
853
+ return written === length ? out : out.subarray(0, written);
854
+ };
855
+ /** An entry's uncompressed bytes, refused past `maxBytes`. */
856
+ const readZipEntry = (source, entry, maxBytes, budget) => {
857
+ if (entry.uncompressedSize > maxBytes) throw tooLarge(entry, maxBytes);
858
+ return readZipEntryRange(source, entry, 0, entry.uncompressedSize, budget);
859
+ };
860
+ //#endregion
861
+ //#region src/nativeArtifact/readNativeArtifact.ts
862
+ const MiB = 1048576;
863
+ /** The most a manifest or an Info.plist holds. */
864
+ const METADATA_BYTES = 16 * MiB;
865
+ /** The most one dex file holds. */
866
+ const DEX_BYTES = 256 * MiB;
867
+ /** The most reading one artifact inflates, across every entry it reads. */
868
+ const READ_BUDGET_BYTES = 2048 * MiB;
869
+ /** How long reading one artifact may take. */
870
+ const DEFAULT_TIMEOUT_MS = 12e4;
871
+ /** Where each kind of artifact packs its built-in bundle. */
872
+ const builtInBundlePathOf = (kind, appDirectory = kind === "app" ? "<App>.app/" : "Payload/<App>.app/") => kind === "aab" ? "base/assets/index.android.bundle" : kind === "apk" ? "assets/index.android.bundle" : `${appDirectory}main.jsbundle`;
873
+ /** The kind of artifact a file name names, or null. */
874
+ const nativeArtifactKindOf = (fileName) => {
875
+ if (/\.app\.zip$/iu.test(fileName)) return "app";
876
+ const extension = /\.([^./\\]+)$/u.exec(fileName)?.[1]?.toLowerCase();
877
+ return extension === "ipa" || extension === "aab" || extension === "apk" ? extension : null;
878
+ };
879
+ const UUID_LENGTH = 36;
880
+ const SHA256_HEX = /^[0-9a-f]{64}$/u;
881
+ const LENGTHS = /* @__PURE__ */ new Set([UUID_LENGTH, 64]);
882
+ const unique = (values) => [...new Set(values)].sort();
883
+ const candidatesOf = (strings) => {
884
+ const all = [...strings];
885
+ return {
886
+ minBundleIds: unique(all.filter(isUUIDv7)),
887
+ fingerprints: unique(all.filter((value) => SHA256_HEX.test(value)))
888
+ };
889
+ };
890
+ const literalOf = (value, what) => {
891
+ if (value === null || value === void 0) return null;
892
+ if (value.type === "string") return value.value;
893
+ if (value.type === "int") return String(value.value);
894
+ return { unreadable: value.type === "reference" ? `${what} is a resource reference (${value.name ?? `@0x${value.id.toString(16)}`}), not a literal` : `${what} is not a string` };
895
+ };
896
+ /**
897
+ * The channel an Android app reports from its `com.hotupdater.CHANNEL`
898
+ * meta-data, as the SDK reads it (`NativeConfigUtils`): the value as a
899
+ * string, and none when it is empty, which the SDK reports as `production`.
900
+ */
901
+ const androidChannelOf = (value) => {
902
+ if (value === void 0) return null;
903
+ if (value.type === "boolean") return String(value.value);
904
+ const channel = literalOf(value, "The com.hotupdater.CHANNEL meta-data");
905
+ return channel === "" ? null : channel;
906
+ };
907
+ const readAndroid = async (source, kind, entries, budget) => {
908
+ const manifestEntry = findZipEntry(entries, kind === "aab" ? "base/manifest/AndroidManifest.xml" : "AndroidManifest.xml");
909
+ if (manifestEntry === void 0) throw new NativeArtifactError("UNREADABLE", `The ${kind === "aab" ? ".aab has no base/manifest/AndroidManifest.xml" : ".apk has no AndroidManifest.xml"}.`);
910
+ const manifestBytes = await readZipEntry(source, manifestEntry, METADATA_BYTES, budget);
911
+ const manifest = readAndroidManifest(kind === "aab" ? parseProtoManifest(manifestBytes) : parseBinaryManifest(manifestBytes));
912
+ const strings = [];
913
+ const dexes = filterZipEntries(entries, (name) => kind === "aab" ? /^base\/dex\/classes\d*\.dex$/u.test(name) : /^classes\d*\.dex$/u.test(name));
914
+ for (const entry of dexes) strings.push(...readDexStrings(await readZipEntry(source, entry, DEX_BYTES, budget), LENGTHS));
915
+ const { minBundleIds, fingerprints } = candidatesOf(strings);
916
+ const bundle = findZipEntry(entries, builtInBundlePathOf(kind));
917
+ return {
918
+ platform: "android",
919
+ kind,
920
+ appId: manifest.packageName,
921
+ appVersion: literalOf(manifest.versionName, "versionName"),
922
+ buildNumber: manifest.versionCode === null ? null : String(manifest.versionCode),
923
+ channel: androidChannelOf(manifest.metaData.get("com.hotupdater.CHANNEL")),
924
+ minBundleIdCandidates: minBundleIds,
925
+ fingerprintCandidates: fingerprints,
926
+ builtInBundle: bundle === void 0 ? null : {
927
+ path: bundle.name,
928
+ byteSize: bundle.uncompressedSize
929
+ }
930
+ };
931
+ };
932
+ const stringValue = (value) => typeof value === "string" && value !== "" ? value : null;
933
+ const readIos = async (source, kind, entries, budget) => {
934
+ const infoPlist = kind === "ipa" ? /^Payload\/[^/]+\.app\/Info\.plist$/u : /^[^/]+\.app\/Info\.plist$/u;
935
+ const plistEntry = findZipEntry(entries, (name) => infoPlist.test(name));
936
+ if (plistEntry === void 0) throw new NativeArtifactError("UNREADABLE", kind === "ipa" ? "The .ipa has no Payload/<App>.app/Info.plist." : "The .app.zip has no <App>.app/Info.plist at its root.");
937
+ const appDirectory = plistEntry.name.slice(0, -10);
938
+ const plist = parsePlist(await readZipEntry(source, plistEntry, METADATA_BYTES, budget));
939
+ if (plist === null || typeof plist !== "object" || Array.isArray(plist)) throw new NativeArtifactError("UNREADABLE", "The app's Info.plist is not a dictionary.");
940
+ const info = plist;
941
+ const machO = (entry) => readMachOStrings((start, end) => readZipEntryRange(source, entry, start, end, budget), LENGTHS);
942
+ const frameworks = filterZipEntries(entries, (name) => {
943
+ const match = new RegExp(`^${appDirectory.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}Frameworks/([^/]+)\\.framework/([^/]+)$`, "u").exec(name);
944
+ return match !== null && match[1] === match[2];
945
+ });
946
+ const sdk = frameworks.find(({ name }) => name.endsWith("/HotUpdater"));
947
+ const executable = stringValue(info["CFBundleExecutable"]);
948
+ const executableEntry = executable === null ? void 0 : findZipEntry(entries, `${appDirectory}${executable}`);
949
+ let strings = sdk === void 0 ? [] : await machO(sdk);
950
+ if (!strings.some(isUUIDv7) && executableEntry !== void 0) strings = await machO(executableEntry);
951
+ if (!strings.some(isUUIDv7)) {
952
+ for (const framework of frameworks) if (framework !== sdk) strings.push(...await machO(framework));
953
+ }
954
+ const { minBundleIds, fingerprints } = candidatesOf(strings);
955
+ const bundle = findZipEntry(entries, builtInBundlePathOf(kind, appDirectory));
956
+ const channel = info["HOT_UPDATER_CHANNEL"];
957
+ return {
958
+ platform: "ios",
959
+ kind,
960
+ appId: stringValue(info["CFBundleIdentifier"]),
961
+ appVersion: stringValue(info["CFBundleShortVersionString"]),
962
+ buildNumber: stringValue(info["CFBundleVersion"]),
963
+ channel: channel === void 0 || channel === "" ? null : typeof channel === "string" ? channel : { unreadable: "HOT_UPDATER_CHANNEL is not a string" },
964
+ minBundleIdCandidates: minBundleIds,
965
+ fingerprintCandidates: fingerprints,
966
+ builtInBundle: bundle === void 0 ? null : {
967
+ path: bundle.name,
968
+ byteSize: bundle.uncompressedSize
969
+ }
970
+ };
971
+ };
972
+ /**
973
+ * Reads a store artifact (`.ipa`, `.aab` or `.apk`) by its file name's
974
+ * extension: what its app reports at runtime, and its built-in bundle. It
975
+ * reads the archive's directory, the manifest or Info.plist, and the app's
976
+ * code, but not the built-in bundle, so a large artifact is never held
977
+ * whole. Whether the result can be imported is `checkNativeArtifact`'s.
978
+ */
979
+ const readNativeArtifact = async (source, fileName, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) => {
980
+ const kind = nativeArtifactKindOf(fileName);
981
+ if (kind === null) throw new NativeArtifactError("UNSUPPORTED_FILE", `${fileName} is not an .ipa, .aab, .apk or a simulator .app. Import the artifact of a release build.`);
982
+ const budget = {
983
+ remaining: READ_BUDGET_BYTES,
984
+ deadline: Date.now() + timeoutMs
985
+ };
986
+ const timed = {
987
+ size: source.size,
988
+ read(offset, length) {
989
+ if (Date.now() > budget.deadline) return Promise.reject(new NativeArtifactError("UNREADABLE", "Reading the artifact took too long."));
990
+ return source.read(offset, length);
991
+ }
992
+ };
993
+ try {
994
+ const entries = await readZipDirectory(timed);
995
+ return await (kind === "ipa" || kind === "app" ? readIos(timed, kind, entries, budget) : readAndroid(timed, kind, entries, budget));
996
+ } catch (error) {
997
+ if (error instanceof NativeArtifactError) throw error;
998
+ throw new NativeArtifactError("UNREADABLE", `The artifact is malformed: ${error instanceof Error ? error.message : String(error)}.`);
999
+ }
1000
+ };
1001
+ //#endregion
1002
+ //#region src/nativeArtifact/checkNativeArtifact.ts
1003
+ /** The channel an app without a default channel reports. */
1004
+ const DEFAULT_CHANNEL = "production";
1005
+ const isUnreadable = (value) => typeof value === "object" && value !== null && "unreadable" in value;
1006
+ /**
1007
+ * Whether a read artifact can be imported as a native build, whose built-in
1008
+ * bundle OTAs patch from, and what it registers as. It refuses an artifact
1009
+ * that ships no built-in bundle, such as a debug build, which loads its
1010
+ * JavaScript from Metro, one built without the native SDK's build step (no MIN_BUNDLE_ID), and
1011
+ * one the project's update strategy can't place: no fingerprint under the
1012
+ * fingerprint strategy, or no app version a device compares under the
1013
+ * appVersion strategy. With `appIds`, the identifiers the project's
1014
+ * `nativeBuild` schemes name, it warns about an artifact of another app.
1015
+ */
1016
+ const checkNativeArtifact = (info, { updateStrategy, appIds = [], channel: channelOverride }) => {
1017
+ const problems = [];
1018
+ const warnings = [];
1019
+ const refuse = (code, message) => problems.push({
1020
+ code,
1021
+ message
1022
+ });
1023
+ if (info.builtInBundle === null) refuse("NO_BUILT_IN_BUNDLE", `The .${info.kind} has no ${builtInBundlePathOf(info.kind)}, so it ships no built-in bundle to patch from. A debug build loads its JavaScript from Metro, and a release build packs it there unless the project renames its bundle. Import a release build.`);
1024
+ const [minBundleId, ...otherMinBundleIds] = info.minBundleIdCandidates;
1025
+ if (minBundleId === void 0) refuse("NO_MIN_BUNDLE_ID", "The app's code holds no MIN_BUNDLE_ID: it was built without @hot-updater/react-native, or with a version of it that doesn't compile MIN_BUNDLE_ID in. Rebuild it with the @hot-updater/react-native this project uses.");
1026
+ else if (otherMinBundleIds.length > 0) refuse("AMBIGUOUS_MIN_BUNDLE_ID", `The app's code holds several UUIDv7 strings (${info.minBundleIdCandidates.join(", ")}), so which is MIN_BUNDLE_ID can't be told.`);
1027
+ let fingerprintHash = null;
1028
+ let appVersion = null;
1029
+ if (updateStrategy === "fingerprint") {
1030
+ const [fingerprint, ...others] = info.fingerprintCandidates;
1031
+ if (fingerprint === void 0) refuse("NO_FINGERPRINT", "The app's code holds no fingerprint, but hot-updater.config.ts updates by fingerprint: it was built without fingerprint.json, or with a @hot-updater/react-native that doesn't compile the fingerprint in, so its app can't check for fingerprint updates.");
1032
+ else if (others.length > 0) refuse("AMBIGUOUS_FINGERPRINT", `The app's code holds several SHA-256 strings (${info.fingerprintCandidates.join(", ")}), so which is its fingerprint can't be told.`);
1033
+ else fingerprintHash = fingerprint;
1034
+ } else if (info.appVersion === null) refuse("NO_APP_VERSION", `The app has no ${info.platform === "ios" ? "CFBundleShortVersionString" : "versionName"}, which the appVersion strategy updates by.`);
1035
+ else if (isUnreadable(info.appVersion)) refuse("NO_APP_VERSION", `The app's ${info.appVersion.unreadable}.`);
1036
+ else if (canonicalizeAppVersion(info.appVersion) === null) refuse("INVALID_SCOPE", `The app version ${info.appVersion} holds no version a device compares, such as 1.4.0, so no appVersion update targets it.`);
1037
+ else appVersion = info.appVersion;
1038
+ let channel = DEFAULT_CHANNEL;
1039
+ if (channelOverride !== void 0) {
1040
+ channel = channelOverride;
1041
+ try {
1042
+ encodeChannelKey(channel);
1043
+ } catch (error) {
1044
+ refuse("INVALID_SCOPE", `The channel ${JSON.stringify(channel)} is no channel a release can target: ${error instanceof Error ? error.message : String(error)}.`);
1045
+ }
1046
+ } else if (isUnreadable(info.channel)) refuse("INVALID_SCOPE", `${info.channel.unreadable}, so the channel its app reports can't be read: name it with --channel.`);
1047
+ else if (info.channel !== null) {
1048
+ channel = info.channel;
1049
+ try {
1050
+ encodeChannelKey(channel);
1051
+ } catch (error) {
1052
+ refuse("INVALID_SCOPE", `The app's channel ${JSON.stringify(channel)} is no channel a release can target: ${error instanceof Error ? error.message : String(error)}.`);
1053
+ }
1054
+ }
1055
+ if (appIds.length > 0 && info.appId !== null && !appIds.includes(info.appId)) warnings.push({
1056
+ code: "APP_ID_MISMATCH",
1057
+ message: `The app is ${info.appId}, which no ${info.platform} scheme in nativeBuild names (${appIds.join(", ")}): check that it is this project's build.`
1058
+ });
1059
+ if (problems.length > 0 || minBundleId === void 0) return {
1060
+ ok: false,
1061
+ problems,
1062
+ warnings
1063
+ };
1064
+ return {
1065
+ ok: true,
1066
+ warnings,
1067
+ build: {
1068
+ platform: info.platform,
1069
+ kind: info.kind,
1070
+ appId: info.appId,
1071
+ buildNumber: info.buildNumber,
1072
+ minBundleId,
1073
+ channel,
1074
+ fingerprintHash,
1075
+ appVersion,
1076
+ builtInBundlePath: info.builtInBundle.path
1077
+ }
1078
+ };
1079
+ };
1080
+ /**
1081
+ * The refusal of an artifact whose built-in bundle is already registered:
1082
+ * the registry takes a MIN_BUNDLE_ID once.
1083
+ */
1084
+ const alreadyImportedProblem = (minBundleId, nativeBuildId) => ({
1085
+ code: "ALREADY_IMPORTED",
1086
+ ...nativeBuildId === null ? {} : { nativeBuildId },
1087
+ message: nativeBuildId === null ? `The built-in bundle ${minBundleId} is already registered, as a bundle that is no native build's.` : `This build is already registered as native build ${nativeBuildId} (built-in bundle ${minBundleId}).`
1088
+ });
1089
+ //#endregion
1090
+ export { NativeArtifactError, alreadyImportedProblem, blobSource, checkNativeArtifact, findZipEntry, openZipEntry, readNativeArtifact, readZipDirectory };