titanium-apk-recover 2.2.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1126 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ TiRecover: () => TiRecover,
34
+ UnsupportedEncryptionError: () => UnsupportedEncryptionError,
35
+ buildInfo: () => buildInfo,
36
+ buildReconstruct: () => reconstruct,
37
+ buildTiappXml: () => buildTiappXml,
38
+ decodeJavaInt: () => decodeJavaInt,
39
+ decryptCloakAsset: () => decryptCloakAsset,
40
+ decryptRange: () => decryptRange,
41
+ decryptRanges: () => decryptRanges,
42
+ deriveCloakKey: () => deriveCloakKey,
43
+ detectAlloy: () => detectAlloy,
44
+ extractByteArrayFields: () => extractByteArrayFields,
45
+ extractCloakSalt: () => extractCloakSalt,
46
+ extractRanges: () => extractRanges,
47
+ extractStringChunks: () => extractStringChunks,
48
+ instructionWidth: () => instructionWidth,
49
+ isProbablyText: () => isProbablyText,
50
+ parseAssetBuffer: () => parseAssetBuffer,
51
+ parseBinaryManifest: () => parseBinaryManifest,
52
+ parseManifest: () => parseManifest,
53
+ parseRanges: () => parseRanges,
54
+ pickCloakKey: () => pickCloakKey,
55
+ readAssetCrypt: () => readAssetCrypt,
56
+ recover: () => recover
57
+ });
58
+ module.exports = __toCommonJS(src_exports);
59
+
60
+ // node_modules/tsup/assets/cjs_shims.js
61
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
62
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
63
+
64
+ // src/index.ts
65
+ var import_node_path4 = __toESM(require("path"), 1);
66
+ var import_promises6 = require("fs/promises");
67
+
68
+ // src/apk.ts
69
+ var import_node_path2 = __toESM(require("path"), 1);
70
+ var import_promises4 = require("fs/promises");
71
+
72
+ // src/zip.ts
73
+ var import_promises = require("fs/promises");
74
+ var import_fflate = require("fflate");
75
+ async function readApkEntries(apkPath, filter) {
76
+ const buf = await (0, import_promises.readFile)(apkPath);
77
+ const u8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
78
+ return await new Promise((resolve, reject) => {
79
+ (0, import_fflate.unzip)(u8, { filter: filter ?? (() => true) }, (err, data) => {
80
+ if (err) reject(err);
81
+ else resolve(data);
82
+ });
83
+ });
84
+ }
85
+ function isDexEntry(name) {
86
+ return /^classes\d*\.dex$/.test(name);
87
+ }
88
+
89
+ // src/manifest.ts
90
+ var import_promises3 = require("fs/promises");
91
+ var import_node_path = __toESM(require("path"), 1);
92
+ var import_node_module = require("module");
93
+ var import_cheerio = require("cheerio");
94
+
95
+ // src/fs-utils.ts
96
+ var import_promises2 = require("fs/promises");
97
+ async function fileExists(filePath) {
98
+ try {
99
+ return (await (0, import_promises2.stat)(filePath)).isFile();
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+ async function dirExists(dirPath) {
105
+ try {
106
+ return (await (0, import_promises2.stat)(dirPath)).isDirectory();
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ // src/manifest.ts
113
+ var require2 = (0, import_node_module.createRequire)(importMetaUrl);
114
+ var BinaryXmlParser = require2(
115
+ "adbkit-apkreader/lib/apkreader/parser/binaryxml"
116
+ );
117
+ var ANDROID_NS = "http://schemas.android.com/apk/res/android";
118
+ function isBinaryManifest(bytes) {
119
+ return bytes.length >= 4 && bytes[0] === 3 && bytes[1] === 0;
120
+ }
121
+ function attr(node, name) {
122
+ return node.attributes?.find((a) => a.name === name);
123
+ }
124
+ function attrString(node, name) {
125
+ const a = attr(node, name);
126
+ if (!a) return void 0;
127
+ if (a.value != null) return a.value;
128
+ const tv = a.typedValue?.value;
129
+ return tv == null ? void 0 : String(tv);
130
+ }
131
+ function parseBinaryManifest(bytes, dir) {
132
+ const doc = new BinaryXmlParser(bytes).parse();
133
+ const application = doc.childNodes?.find((n) => n.nodeName === "application");
134
+ const info = {
135
+ package: attrString(doc, "package"),
136
+ versionCode: attrString(doc, "versionCode"),
137
+ versionName: attrString(doc, "versionName"),
138
+ appName: application ? attrString(application, "label") : void 0,
139
+ dir
140
+ };
141
+ const xml = `<?xml version="1.0" encoding="utf-8"?>
142
+ ${serialize(doc)}
143
+ `;
144
+ return { info, xml };
145
+ }
146
+ function parseManifest(xml, dir) {
147
+ const $ = (0, import_cheerio.load)(xml, { xml: true });
148
+ return {
149
+ package: $("manifest[package]").attr("package"),
150
+ versionCode: $("manifest").attr("android:versionCode"),
151
+ versionName: $("manifest").attr("android:versionName"),
152
+ appName: $("manifest application").attr("android:label"),
153
+ dir
154
+ };
155
+ }
156
+ async function readManifest(apkDir) {
157
+ const manifestPath = import_node_path.default.join(apkDir, "AndroidManifest.xml");
158
+ if (!await fileExists(manifestPath)) return null;
159
+ const bytes = await (0, import_promises3.readFile)(manifestPath);
160
+ if (isBinaryManifest(bytes)) {
161
+ return parseBinaryManifest(bytes, apkDir).info;
162
+ }
163
+ return parseManifest(bytes.toString("utf8"), apkDir);
164
+ }
165
+ function prefixFor(namespaceURI) {
166
+ if (!namespaceURI) return "";
167
+ return namespaceURI === ANDROID_NS ? "android:" : "";
168
+ }
169
+ function escapeXml(value) {
170
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
171
+ }
172
+ function serialize(node, depth = 0) {
173
+ const indent = " ".repeat(depth);
174
+ const attrs = [];
175
+ if (depth === 0) attrs.push(`xmlns:android="${ANDROID_NS}"`);
176
+ for (const a of node.attributes ?? []) {
177
+ const raw = a.value ?? (a.typedValue?.value == null ? "" : String(a.typedValue.value));
178
+ attrs.push(`${prefixFor(a.namespaceURI)}${a.name}="${escapeXml(raw)}"`);
179
+ }
180
+ const attrText = attrs.length ? " " + attrs.join(" ") : "";
181
+ const children = (node.childNodes ?? []).filter((c) => c.nodeType === 1);
182
+ if (children.length === 0) {
183
+ return `${indent}<${node.nodeName}${attrText} />`;
184
+ }
185
+ const inner = children.map((c) => serialize(c, depth + 1)).join("\n");
186
+ return `${indent}<${node.nodeName}${attrText}>
187
+ ${inner}
188
+ ${indent}</${node.nodeName}>`;
189
+ }
190
+
191
+ // src/apk.ts
192
+ var ASSETS_PREFIX = "assets/Resources/";
193
+ var CLOAK_LIB_SUFFIX = "/libti.cloak.so";
194
+ function isCloakLib(name) {
195
+ return name.startsWith("lib/") && name.endsWith(CLOAK_LIB_SUFFIX);
196
+ }
197
+ async function unpackApk(apkPath, tmpDir, debug = false) {
198
+ if (!await fileExists(apkPath)) {
199
+ throw new Error(`The given APK file doesn't exist: ${apkPath}`);
200
+ }
201
+ if (debug) console.log("preparing -> reading APK entries");
202
+ const entries = await readApkEntries(
203
+ apkPath,
204
+ (f) => f.name === "AndroidManifest.xml" || isDexEntry(f.name) || f.name.startsWith(ASSETS_PREFIX) || isCloakLib(f.name)
205
+ );
206
+ const apkDir = import_node_path2.default.join(process.cwd(), tmpDir) + import_node_path2.default.sep;
207
+ await (0, import_promises4.mkdir)(apkDir, { recursive: true });
208
+ const dexBuffers = Object.keys(entries).filter(isDexEntry).sort().map((name) => entries[name]);
209
+ const cloakLibs = Object.keys(entries).filter(isCloakLib).sort().map((name) => Buffer.from(entries[name]));
210
+ let manifest = null;
211
+ const manifestBytes = entries["AndroidManifest.xml"];
212
+ if (manifestBytes) {
213
+ const { info, xml } = parseBinaryManifest(Buffer.from(manifestBytes), apkDir);
214
+ manifest = info;
215
+ await (0, import_promises4.writeFile)(import_node_path2.default.join(apkDir, "AndroidManifest.xml"), xml);
216
+ }
217
+ for (const [name, data] of Object.entries(entries)) {
218
+ if (!name.startsWith(ASSETS_PREFIX) || name.endsWith("/")) continue;
219
+ const dest = import_node_path2.default.join(apkDir, name);
220
+ await (0, import_promises4.mkdir)(import_node_path2.default.dirname(dest), { recursive: true });
221
+ await (0, import_promises4.writeFile)(dest, Buffer.from(data));
222
+ }
223
+ if (debug) console.log("preparing -> ready");
224
+ return { apkDir, manifest, dexBuffers, cloakLibs };
225
+ }
226
+
227
+ // src/decrypt.ts
228
+ var import_node_crypto = require("crypto");
229
+ function decodeJavaInt(literal) {
230
+ let s = literal.trim();
231
+ let sign = 1;
232
+ if (s.startsWith("+")) {
233
+ s = s.slice(1);
234
+ } else if (s.startsWith("-")) {
235
+ sign = -1;
236
+ s = s.slice(1);
237
+ }
238
+ let value;
239
+ if (s.startsWith("0x") || s.startsWith("0X")) {
240
+ value = parseInt(s.slice(2), 16);
241
+ } else if (s.startsWith("#")) {
242
+ value = parseInt(s.slice(1), 16);
243
+ } else if (s.length > 1 && s.startsWith("0")) {
244
+ value = parseInt(s.slice(1), 8);
245
+ } else {
246
+ value = parseInt(s, 10);
247
+ }
248
+ return sign * value;
249
+ }
250
+ function parseRanges(javaContent) {
251
+ const ranges = {};
252
+ const re = /hashMap\.put\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*new Range\(\s*([^,]+?)\s*,\s*([^)]+?)\s*\)\s*\)/g;
253
+ let match;
254
+ while ((match = re.exec(javaContent)) !== null) {
255
+ const file = match[1];
256
+ const offset = match[2];
257
+ const length = match[3];
258
+ if (file === void 0 || offset === void 0 || length === void 0) continue;
259
+ ranges[file] = {
260
+ offset: decodeJavaInt(offset),
261
+ bytes: decodeJavaInt(length)
262
+ };
263
+ }
264
+ return ranges;
265
+ }
266
+ function parseAssetBuffer(smaliContent) {
267
+ const lines = smaliContent.split(/\r?\n/);
268
+ let started = false;
269
+ let titaniumVersion = "unknown";
270
+ let bufferLen = -1;
271
+ const chunks = [];
272
+ for (const line of lines) {
273
+ if (line.indexOf("private static initAssetsBytes()Ljava/nio/CharBuffer") !== -1) {
274
+ started = true;
275
+ continue;
276
+ }
277
+ if (!started) continue;
278
+ if (line.indexOf("const v0, ") !== -1) {
279
+ titaniumVersion = "<5";
280
+ bufferLen = decodeJavaInt(line.split("const v0, ").join("").trim());
281
+ } else if (line.indexOf("const/16 v0, ") !== -1) {
282
+ titaniumVersion = "5.x";
283
+ bufferLen = decodeJavaInt(line.split("const/16 v0, ").join("").trim());
284
+ } else if (line.indexOf("const-string v1") !== -1) {
285
+ let content = line.split('const-string v1, "').join("").trim();
286
+ content = content.slice(0, -1);
287
+ chunks.push(content);
288
+ } else if (line.indexOf("rewind()Ljava/nio/Buffer;") !== -1) {
289
+ break;
290
+ }
291
+ }
292
+ return { titaniumVersion, bufferLen, escaped: chunks.join("") };
293
+ }
294
+ function detectAlloy(files) {
295
+ return Object.keys(files).some(
296
+ (name) => name === "alloy.js" || name.startsWith("alloy/") || name.includes("/alloy/") || name === "_app_props_.json"
297
+ );
298
+ }
299
+ var KEY_LEN = 16;
300
+ function decryptRange(blob, offset, length) {
301
+ for (const total of [blob.length - 1, blob.length]) {
302
+ if (total - KEY_LEN < 0) continue;
303
+ const key = blob.subarray(total - KEY_LEN, total);
304
+ for (const start of [offset, offset - 1]) {
305
+ if (start < 0 || start + length > blob.length) continue;
306
+ try {
307
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-128-ecb", key, null);
308
+ decipher.setAutoPadding(true);
309
+ const out = Buffer.concat([
310
+ decipher.update(blob.subarray(start, start + length)),
311
+ decipher.final()
312
+ ]);
313
+ const text = out.toString("utf8");
314
+ if (text !== "") return text;
315
+ } catch {
316
+ }
317
+ }
318
+ }
319
+ return null;
320
+ }
321
+ function decryptRanges(blob, ranges, debug = false) {
322
+ const files = {};
323
+ let totalBytes = 0;
324
+ for (const range of ranges) {
325
+ const content = decryptRange(blob, range.offset, range.bytes);
326
+ if (content === null) continue;
327
+ files[range.file] = { offset: range.offset, bytes: range.bytes, content };
328
+ totalBytes += range.bytes;
329
+ if (debug) console.log(`file:${range.file}, decrypted !`);
330
+ }
331
+ return { files, totalBytes };
332
+ }
333
+
334
+ // src/dex.ts
335
+ var import_libdex_ts = require("libdex-ts");
336
+ var WIDTHS = buildWidthTable();
337
+ function buildWidthTable() {
338
+ const w = new Uint8Array(256).fill(1);
339
+ const set = (from, to, units) => {
340
+ for (let op = from; op <= to; op++) w[op] = units;
341
+ };
342
+ set(2, 3, 2);
343
+ w[3] = 3;
344
+ set(5, 6, 2);
345
+ w[6] = 3;
346
+ set(8, 9, 2);
347
+ w[9] = 3;
348
+ w[19] = 2;
349
+ w[20] = 3;
350
+ w[21] = 2;
351
+ w[22] = 2;
352
+ w[23] = 3;
353
+ w[24] = 5;
354
+ w[25] = 2;
355
+ w[26] = 2;
356
+ w[27] = 3;
357
+ w[28] = 2;
358
+ w[31] = 2;
359
+ w[32] = 2;
360
+ w[34] = 2;
361
+ w[35] = 2;
362
+ w[36] = 3;
363
+ w[37] = 3;
364
+ w[38] = 3;
365
+ w[41] = 2;
366
+ w[42] = 3;
367
+ w[43] = 3;
368
+ w[44] = 3;
369
+ set(45, 49, 2);
370
+ set(50, 55, 2);
371
+ set(56, 61, 2);
372
+ set(68, 81, 2);
373
+ set(82, 95, 2);
374
+ set(96, 109, 2);
375
+ set(110, 114, 3);
376
+ set(116, 120, 3);
377
+ set(144, 175, 2);
378
+ set(208, 215, 2);
379
+ set(216, 226, 2);
380
+ w[250] = 4;
381
+ w[251] = 4;
382
+ w[252] = 3;
383
+ w[253] = 3;
384
+ w[254] = 2;
385
+ w[255] = 2;
386
+ return w;
387
+ }
388
+ function instructionWidth(insns, idx) {
389
+ const unit = insns[idx] ?? 0;
390
+ const op = unit & 255;
391
+ if (op === 0 && unit !== 0) {
392
+ switch (unit) {
393
+ case 256: {
394
+ const size = insns[idx + 1] ?? 0;
395
+ return size * 2 + 4;
396
+ }
397
+ case 512: {
398
+ const size = insns[idx + 1] ?? 0;
399
+ return size * 4 + 2;
400
+ }
401
+ case 768: {
402
+ const elementWidth = insns[idx + 1] ?? 0;
403
+ const size = (insns[idx + 2] ?? 0) + (insns[idx + 3] ?? 0) * 65536;
404
+ return 4 + Math.ceil(size * elementWidth / 2);
405
+ }
406
+ default:
407
+ return 1;
408
+ }
409
+ }
410
+ return WIDTHS[op] ?? 1;
411
+ }
412
+ var s16 = (v) => v > 32767 ? v - 65536 : v;
413
+ function extractStringChunks(dex, insns) {
414
+ const chunks = [];
415
+ let idx = 0;
416
+ while (idx < insns.length) {
417
+ const unit = insns[idx] ?? 0;
418
+ const op = unit & 255;
419
+ if (op === 26) {
420
+ chunks.push(dex.getStringById(insns[idx + 1] ?? 0));
421
+ } else if (op === 27) {
422
+ chunks.push(dex.getStringById((insns[idx + 1] ?? 0) + (insns[idx + 2] ?? 0) * 65536));
423
+ }
424
+ idx += instructionWidth(insns, idx);
425
+ }
426
+ return chunks;
427
+ }
428
+ function extractRanges(dex, insns) {
429
+ const regInt = /* @__PURE__ */ new Map();
430
+ const regStr = /* @__PURE__ */ new Map();
431
+ const pending = /* @__PURE__ */ new Map();
432
+ const ranges = [];
433
+ const resolveInvoke = (methodIdx, args) => {
434
+ const mid = dex.getMethodId(methodIdx);
435
+ const cls = dex.getTypeDescriptorByIdx(mid.classIdx);
436
+ const name = dex.getStringById(mid.nameIdx);
437
+ if (name === "<init>" && /[/$]Range;$/.test(cls) && args.length >= 3) {
438
+ const offset = regInt.get(args[1]);
439
+ const length = regInt.get(args[2]);
440
+ if (offset !== void 0 && length !== void 0) {
441
+ pending.set(args[0], { offset, length });
442
+ }
443
+ } else if (name === "put" && args.length >= 3) {
444
+ const key = regStr.get(args[1]);
445
+ const range = pending.get(args[2]);
446
+ if (key !== void 0 && range) {
447
+ ranges.push({ file: key, offset: range.offset, bytes: range.length });
448
+ }
449
+ }
450
+ };
451
+ let idx = 0;
452
+ while (idx < insns.length) {
453
+ const unit = insns[idx] ?? 0;
454
+ const op = unit & 255;
455
+ switch (op) {
456
+ case 18: {
457
+ const a = unit >> 8 & 15;
458
+ let v = unit >> 12 & 15;
459
+ if (v > 7) v -= 16;
460
+ regInt.set(a, v);
461
+ break;
462
+ }
463
+ case 19: {
464
+ regInt.set(unit >> 8 & 255, s16(insns[idx + 1] ?? 0));
465
+ break;
466
+ }
467
+ case 20: {
468
+ const v = (insns[idx + 1] ?? 0) | (insns[idx + 2] ?? 0) << 16 | 0;
469
+ regInt.set(unit >> 8 & 255, v);
470
+ break;
471
+ }
472
+ case 21: {
473
+ regInt.set(unit >> 8 & 255, (insns[idx + 1] ?? 0) << 16 | 0);
474
+ break;
475
+ }
476
+ case 26: {
477
+ regStr.set(unit >> 8 & 255, dex.getStringById(insns[idx + 1] ?? 0));
478
+ break;
479
+ }
480
+ case 27: {
481
+ regStr.set(
482
+ unit >> 8 & 255,
483
+ dex.getStringById((insns[idx + 1] ?? 0) + (insns[idx + 2] ?? 0) * 65536)
484
+ );
485
+ break;
486
+ }
487
+ case 110:
488
+ case 111:
489
+ case 112:
490
+ case 113:
491
+ case 114: {
492
+ const a = unit >> 12 & 15;
493
+ const g = unit >> 8 & 15;
494
+ const methodIdx = insns[idx + 1] ?? 0;
495
+ const regsWord = insns[idx + 2] ?? 0;
496
+ const args = [
497
+ regsWord & 15,
498
+ regsWord >> 4 & 15,
499
+ regsWord >> 8 & 15,
500
+ regsWord >> 12 & 15,
501
+ g
502
+ ].slice(0, a);
503
+ resolveInvoke(methodIdx, args);
504
+ break;
505
+ }
506
+ case 116:
507
+ case 117:
508
+ case 118:
509
+ case 119:
510
+ case 120: {
511
+ const count = unit >> 8 & 255;
512
+ const methodIdx = insns[idx + 1] ?? 0;
513
+ const first = insns[idx + 2] ?? 0;
514
+ const args = [];
515
+ for (let i = 0; i < count; i++) args.push(first + i);
516
+ resolveInvoke(methodIdx, args);
517
+ break;
518
+ }
519
+ }
520
+ idx += instructionWidth(insns, idx);
521
+ }
522
+ return ranges;
523
+ }
524
+ function detectVersion(insns) {
525
+ let idx = 0;
526
+ while (idx < insns.length) {
527
+ const op = (insns[idx] ?? 0) & 255;
528
+ if (op === 20) return "<5";
529
+ if (op === 19) return "5.x";
530
+ if (op === 26 || op === 27) break;
531
+ idx += instructionWidth(insns, idx);
532
+ }
533
+ return "unknown";
534
+ }
535
+ var s32 = (lo, hi) => lo | hi << 16 | 0;
536
+ function readFillArrayBytes(insns, payloadIdx) {
537
+ if (payloadIdx < 0 || (insns[payloadIdx] ?? 0) !== 768) return null;
538
+ const elementWidth = insns[payloadIdx + 1] ?? 0;
539
+ if (elementWidth !== 1) return null;
540
+ const size = (insns[payloadIdx + 2] ?? 0) + (insns[payloadIdx + 3] ?? 0) * 65536;
541
+ const out = Buffer.alloc(size);
542
+ const dataStart = payloadIdx + 4;
543
+ for (let j = 0; j < size; j++) {
544
+ const unit = insns[dataStart + (j >> 1)] ?? 0;
545
+ out[j] = (j & 1) === 1 ? unit >> 8 & 255 : unit & 255;
546
+ }
547
+ return out;
548
+ }
549
+ function extractByteArrayFields(dex, insns) {
550
+ const pending = /* @__PURE__ */ new Map();
551
+ const fields = {};
552
+ let idx = 0;
553
+ while (idx < insns.length) {
554
+ const unit = insns[idx] ?? 0;
555
+ const op = unit & 255;
556
+ if (op === 38) {
557
+ const reg = unit >> 8 & 255;
558
+ const off = s32(insns[idx + 1] ?? 0, insns[idx + 2] ?? 0);
559
+ const bytes = readFillArrayBytes(insns, idx + off);
560
+ if (bytes) pending.set(reg, bytes);
561
+ } else if (op === 105) {
562
+ const reg = unit >> 8 & 255;
563
+ const bytes = pending.get(reg);
564
+ if (bytes) {
565
+ const fieldId = dex.getFieldId(insns[idx + 1] ?? 0);
566
+ fields[dex.getStringById(fieldId.nameIdx)] = bytes;
567
+ }
568
+ }
569
+ idx += instructionWidth(insns, idx);
570
+ }
571
+ return fields;
572
+ }
573
+ function extractCloakSalt(dex, classDef) {
574
+ const clinit = methodCode(dex, classDef, "<clinit>");
575
+ if (!clinit) return null;
576
+ const fields = extractByteArrayFields(dex, clinit.insns);
577
+ if (fields.salt) return fields.salt;
578
+ const sixteen = Object.values(fields).filter((b) => b.length === 16);
579
+ return sixteen.length === 1 ? sixteen[0] : null;
580
+ }
581
+ function findAssetCryptClassDef(dex, packageHint) {
582
+ if (packageHint) {
583
+ const descriptor = `L${packageHint.split(".").join("/")}/AssetCryptImpl;`;
584
+ const byHint = dex.getClassDefByDescriptor(descriptor);
585
+ if (byHint) return byHint;
586
+ }
587
+ for (let i = 0; i < dex.header.classDefsSize; i++) {
588
+ const classDef = dex.getClassDef(i);
589
+ const descriptor = dex.getTypeDescriptorByIdx(classDef.classIdx);
590
+ if (descriptor.endsWith("/AssetCryptImpl;")) return classDef;
591
+ }
592
+ return null;
593
+ }
594
+ function methodCode(dex, classDef, methodName) {
595
+ const classData = dex.getClassData(classDef);
596
+ for (const list of [classData.directMethods, classData.virtualMethods]) {
597
+ let methodIdx = 0;
598
+ for (const method of list) {
599
+ methodIdx += method.methodIdx;
600
+ const name = dex.getStringById(dex.getMethodId(methodIdx).nameIdx);
601
+ if (name === methodName) return dex.getDexCode(method);
602
+ }
603
+ }
604
+ return null;
605
+ }
606
+ function readAssetCrypt(dexBuffers, packageHint) {
607
+ for (const bytes of dexBuffers) {
608
+ let dex;
609
+ try {
610
+ dex = new import_libdex_ts.DexFile(bytes);
611
+ } catch {
612
+ continue;
613
+ }
614
+ const classDef = findAssetCryptClassDef(dex, packageHint);
615
+ if (!classDef) continue;
616
+ const bytesCode = methodCode(dex, classDef, "initAssetsBytes");
617
+ if (!bytesCode) return { kind: "newscheme", salt: extractCloakSalt(dex, classDef) };
618
+ const assetsCode = methodCode(dex, classDef, "initAssets");
619
+ const chunks = extractStringChunks(dex, bytesCode.insns);
620
+ const blob = Buffer.from(chunks.join(""), "latin1");
621
+ const ranges = assetsCode ? extractRanges(dex, assetsCode.insns) : [];
622
+ const titaniumVersion = detectVersion(bytesCode.insns);
623
+ return { kind: "classic", data: { blob, ranges, titaniumVersion } };
624
+ }
625
+ return { kind: "none" };
626
+ }
627
+
628
+ // src/cloak.ts
629
+ var import_node_crypto2 = require("crypto");
630
+ var import_node_zlib = require("zlib");
631
+ var KEY_LEN2 = 16;
632
+ var BLOCK_BASE = 8200;
633
+ function deriveCloakKey(so, salt) {
634
+ if (salt.length !== KEY_LEN2) return null;
635
+ try {
636
+ if (so.length < BLOCK_BASE + 64) return null;
637
+ const randomOffset = so.readUInt8(BLOCK_BASE + 62);
638
+ const xor = Buffer.concat([
639
+ so.subarray(BLOCK_BASE + 1, BLOCK_BASE + 5),
640
+ so.subarray(BLOCK_BASE + randomOffset, BLOCK_BASE + randomOffset + 4),
641
+ so.subarray(BLOCK_BASE + 15, BLOCK_BASE + 15 + 4),
642
+ so.subarray(BLOCK_BASE + 30, BLOCK_BASE + 30 + 4)
643
+ ]);
644
+ if (xor.length < KEY_LEN2) return null;
645
+ const key = Buffer.alloc(KEY_LEN2);
646
+ for (let i = 0; i < KEY_LEN2; i++) key[i] = (salt[i] ?? 0) ^ (xor[i] ?? 0);
647
+ return key;
648
+ } catch {
649
+ return null;
650
+ }
651
+ }
652
+ function decryptCloakAsset(bin, key, salt) {
653
+ if (key.length !== KEY_LEN2 || salt.length !== KEY_LEN2) return null;
654
+ try {
655
+ const decipher = (0, import_node_crypto2.createDecipheriv)("aes-128-cbc", key, salt);
656
+ decipher.setAutoPadding(true);
657
+ let out = Buffer.concat([decipher.update(bin), decipher.final()]);
658
+ if (out.length >= 2 && out[0] === 31 && out[1] === 139) {
659
+ try {
660
+ out = (0, import_node_zlib.gunzipSync)(out);
661
+ } catch {
662
+ }
663
+ }
664
+ return out;
665
+ } catch {
666
+ return null;
667
+ }
668
+ }
669
+ function isProbablyText(buf) {
670
+ if (buf.length === 0) return false;
671
+ const sample = buf.subarray(0, Math.min(buf.length, 512));
672
+ let control = 0;
673
+ for (const b of sample) {
674
+ if (b === 9 || b === 10 || b === 13) continue;
675
+ if (b < 32 || b === 127) control++;
676
+ }
677
+ return control / sample.length < 0.1;
678
+ }
679
+ function pickCloakKey(cloakLibs, salt, sampleBin) {
680
+ for (const so of cloakLibs) {
681
+ const key = deriveCloakKey(so, salt);
682
+ if (!key) continue;
683
+ const decrypted = decryptCloakAsset(sampleBin, key, salt);
684
+ if (decrypted && isProbablyText(decrypted)) return key;
685
+ }
686
+ return null;
687
+ }
688
+
689
+ // src/info.ts
690
+ function buildInfo(input) {
691
+ const { manifest, memorySource, meta, developmentMode } = input;
692
+ const files = Object.entries(memorySource).map(([name, file]) => ({
693
+ name,
694
+ bytes: file.bytes
695
+ }));
696
+ const totalBytes = meta ? meta.totalBytes : files.reduce((sum, f) => sum + (f.bytes || 0), 0);
697
+ return {
698
+ package: manifest?.package,
699
+ versionCode: manifest?.versionCode,
700
+ versionName: manifest?.versionName,
701
+ appName: manifest?.appName,
702
+ dir: manifest?.dir,
703
+ developmentMode,
704
+ titaniumVersion: meta?.titaniumVersion ?? "unknown",
705
+ alloy: meta?.alloy ?? detectAlloy(memorySource),
706
+ files,
707
+ totalBytes
708
+ };
709
+ }
710
+
711
+ // src/reconstruct.ts
712
+ var import_node_crypto3 = require("crypto");
713
+ var RESOURCES_PREFIX = "Resources/";
714
+ function reconstruct(memorySource, info) {
715
+ const remapped = {};
716
+ for (const [relPath, file] of Object.entries(memorySource)) {
717
+ const normalized = relPath.replace(/^[/\\]+/, "").split("\\").join("/");
718
+ const key = normalized.startsWith(RESOURCES_PREFIX) ? normalized : RESOURCES_PREFIX + normalized;
719
+ remapped[key] = file;
720
+ }
721
+ const tiappXml = buildTiappXml(info);
722
+ const tiappFile = {
723
+ offset: 0,
724
+ bytes: Buffer.byteLength(tiappXml, "utf8"),
725
+ content: tiappXml
726
+ };
727
+ remapped["tiapp.xml"] = tiappFile;
728
+ return { memorySource: remapped, tiappXml, alloy: info.alloy, restructured: true };
729
+ }
730
+ function buildTiappXml(info) {
731
+ const id = escapeXml2(info.package || "com.recovered.app");
732
+ const name = escapeXml2(info.appName || info.package || "RecoveredApp");
733
+ const version = escapeXml2(info.versionName || "1.0.0");
734
+ const guid = (0, import_node_crypto3.randomUUID)();
735
+ const alloyNote = info.alloy ? "\n <!-- Reconstructed from an Alloy build: files under Resources/ are the\n compiled output. Original app/ sources (views/styles) are not\n recoverable from a distribution APK. -->" : "";
736
+ return `<?xml version="1.0" encoding="UTF-8"?>
737
+ <!-- Reconstructed by ti_recover. Review before building. -->
738
+ <ti:app xmlns:ti="http://ti.appcelerator.org">${alloyNote}
739
+ <id>${id}</id>
740
+ <name>${name}</name>
741
+ <version>${version}</version>
742
+ <guid>${guid}</guid>
743
+ <deployment-targets>
744
+ <target device="android">true</target>
745
+ </deployment-targets>
746
+ <android xmlns:android="http://schemas.android.com/apk/res/android"/>
747
+ </ti:app>
748
+ `;
749
+ }
750
+ function escapeXml2(value) {
751
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
752
+ }
753
+
754
+ // src/write.ts
755
+ var import_node_path3 = __toESM(require("path"), 1);
756
+ var import_promises5 = require("fs/promises");
757
+ var import_js_beautify = __toESM(require("js-beautify"), 1);
758
+ function prettyCode(code) {
759
+ return import_js_beautify.default.js(code, { indent_with_tabs: true });
760
+ }
761
+ async function writeToDisk(memorySource, outDir) {
762
+ const written = [];
763
+ await (0, import_promises5.mkdir)(outDir, { recursive: true });
764
+ for (const [relPath, file] of Object.entries(memorySource)) {
765
+ const target = import_node_path3.default.join(outDir, relPath);
766
+ await (0, import_promises5.mkdir)(import_node_path3.default.dirname(target), { recursive: true });
767
+ let data = file.content;
768
+ if (typeof data === "string" && relPath.endsWith(".js")) {
769
+ data = prettyCode(data);
770
+ }
771
+ await (0, import_promises5.writeFile)(target, data);
772
+ const bytes = typeof data === "string" ? Buffer.byteLength(data, "utf8") : data.length;
773
+ written.push({ name: relPath, bytes });
774
+ }
775
+ return written;
776
+ }
777
+ async function copyAssets(apkDir, outDir, restructured = false) {
778
+ const manifestSrc = import_node_path3.default.join(apkDir, "AndroidManifest.xml");
779
+ if (await fileExists(manifestSrc)) {
780
+ await (0, import_promises5.mkdir)(outDir, { recursive: true });
781
+ await (0, import_promises5.copyFile)(manifestSrc, import_node_path3.default.join(outDir, "AndroidManifest.xml"));
782
+ }
783
+ const resourcesSrc = import_node_path3.default.join(apkDir, "assets", "Resources");
784
+ if (await dirExists(resourcesSrc)) {
785
+ const dest = restructured ? import_node_path3.default.join(outDir, "Resources") : outDir;
786
+ await (0, import_promises5.mkdir)(dest, { recursive: true });
787
+ await (0, import_promises5.cp)(resourcesSrc, dest, {
788
+ recursive: true,
789
+ filter: (src) => !src.endsWith(".bin")
790
+ });
791
+ }
792
+ }
793
+
794
+ // src/index.ts
795
+ var UnsupportedEncryptionError = class extends Error {
796
+ constructor(reason) {
797
+ super(
798
+ "This APK uses Titanium's newer ti.cloak asset encryption (.bin assets). " + (reason ?? "Its AES key could not be derived from the bundled native library, so the sources could not be recovered.")
799
+ );
800
+ this.name = "UnsupportedEncryptionError";
801
+ }
802
+ };
803
+ var DEV_SOURCE_EXTENSIONS = [".js", ".json", ".xml", ".tss", ".rjss", ".jss", ".css"];
804
+ var DEFAULT_CONFIG = {
805
+ tmpDir: "_tmp",
806
+ debug: false
807
+ };
808
+ var TiRecover = class {
809
+ cwd = process.cwd();
810
+ config;
811
+ apkDir = "";
812
+ tmpUsed = false;
813
+ manifest = null;
814
+ dexBuffers = [];
815
+ cloakLibs = [];
816
+ assetCrypt = { kind: "none" };
817
+ developmentMode = false;
818
+ tested = false;
819
+ isTitanium = false;
820
+ memorySource = {};
821
+ meta;
822
+ restructured = false;
823
+ constructor(config = {}) {
824
+ this.config = { ...DEFAULT_CONFIG, ...config };
825
+ }
826
+ /** Absolute output directory (resolved against cwd for relative paths). */
827
+ get outDir() {
828
+ const out = this.config.outDir ?? "";
829
+ return import_node_path4.default.isAbsolute(out) ? out : import_node_path4.default.resolve(this.cwd, out);
830
+ }
831
+ log(message) {
832
+ if (this.config.debug) console.log(message);
833
+ }
834
+ /**
835
+ * Prepares the working directory. If `apkDir` is supplied it is used directly
836
+ * (reading any `classes*.dex` and manifest found there); otherwise the
837
+ * configured `apk` is unzipped into `tmpDir`.
838
+ */
839
+ async init() {
840
+ if (this.config.apkDir) {
841
+ this.apkDir = this.config.apkDir.endsWith(import_node_path4.default.sep) ? this.config.apkDir : this.config.apkDir + import_node_path4.default.sep;
842
+ this.dexBuffers = await readDexBuffersFromDir(this.apkDir);
843
+ this.cloakLibs = await readCloakLibsFromDir(this.apkDir);
844
+ this.manifest = await readManifest(this.apkDir);
845
+ return;
846
+ }
847
+ if (!this.config.apk) {
848
+ throw new Error("init() requires either 'apk' or 'apkDir' in the config.");
849
+ }
850
+ const apkResolved = import_node_path4.default.isAbsolute(this.config.apk) ? this.config.apk : import_node_path4.default.resolve(this.cwd, this.config.apk);
851
+ const result = await unpackApk(apkResolved, this.config.tmpDir, this.config.debug);
852
+ this.apkDir = result.apkDir;
853
+ this.manifest = result.manifest;
854
+ this.dexBuffers = result.dexBuffers;
855
+ this.cloakLibs = result.cloakLibs;
856
+ this.tmpUsed = true;
857
+ }
858
+ /**
859
+ * Returns `true` if the prepared APK was built with Titanium (in either
860
+ * distribution or development mode), populating internal detection state.
861
+ */
862
+ async test() {
863
+ if (!this.apkDir) {
864
+ throw new Error("Call init() before test().");
865
+ }
866
+ if (!this.manifest) {
867
+ this.manifest = await readManifest(this.apkDir);
868
+ }
869
+ this.tested = true;
870
+ this.assetCrypt = readAssetCrypt(this.dexBuffers, this.manifest?.package);
871
+ if (this.assetCrypt.kind !== "none") {
872
+ this.developmentMode = false;
873
+ this.isTitanium = true;
874
+ return true;
875
+ }
876
+ const appDev = import_node_path4.default.join(this.apkDir, "assets", "Resources", "app.js");
877
+ if (await fileExists(appDev)) {
878
+ this.developmentMode = true;
879
+ this.isTitanium = true;
880
+ return true;
881
+ }
882
+ this.isTitanium = false;
883
+ return false;
884
+ }
885
+ /**
886
+ * Extracts recovered sources into memory. For distribution builds this
887
+ * decrypts the AES asset blob (from data lifted out of the DEX); for
888
+ * development builds it reads the plain `assets/Resources` tree.
889
+ */
890
+ async extract() {
891
+ if (!this.tested) await this.test();
892
+ if (!this.isTitanium) {
893
+ throw new Error("The given APK was not created using Appcelerator Titanium.");
894
+ }
895
+ if (!this.developmentMode) {
896
+ if (this.assetCrypt.kind === "newscheme") {
897
+ const files2 = await this.recoverCloak(this.assetCrypt.salt);
898
+ if (!files2) throw new UnsupportedEncryptionError();
899
+ this.memorySource = files2;
900
+ return files2;
901
+ }
902
+ if (this.assetCrypt.kind !== "classic") {
903
+ throw new Error("No recoverable Titanium asset data was found in the APK.");
904
+ }
905
+ const { blob, ranges, titaniumVersion } = this.assetCrypt.data;
906
+ const { files, totalBytes } = decryptRanges(blob, ranges, this.config.debug);
907
+ this.memorySource = files;
908
+ this.meta = { totalBytes, titaniumVersion, alloy: detectAlloy(files) };
909
+ return files;
910
+ }
911
+ const baseAssets = import_node_path4.default.join(this.apkDir, "assets", "Resources");
912
+ const relFiles = await readSourceFiles(baseAssets);
913
+ const source = {};
914
+ for (const rel of relFiles) {
915
+ const content = await (0, import_promises6.readFile)(import_node_path4.default.join(baseAssets, rel));
916
+ source[rel.split(import_node_path4.default.sep).join("/")] = {
917
+ offset: 0,
918
+ bytes: content.length,
919
+ content
920
+ };
921
+ }
922
+ this.memorySource = source;
923
+ return source;
924
+ }
925
+ /**
926
+ * Attempts ti.cloak (.bin) recovery: derive the AES key from a bundled
927
+ * `libti.cloak.so` + the `salt` lifted from `AssetCryptImpl`, then AES-128-CBC
928
+ * decrypt every `Resources/*.bin` asset. Returns the recovered sources, or
929
+ * `null` when the salt/native lib is missing or no key validates.
930
+ */
931
+ async recoverCloak(salt) {
932
+ if (!salt) {
933
+ this.log("ti.cloak-> no salt found in AssetCryptImpl; cannot derive key");
934
+ return null;
935
+ }
936
+ if (this.cloakLibs.length === 0) {
937
+ this.log("ti.cloak-> no libti.cloak.so bundled in the APK; cannot derive key");
938
+ return null;
939
+ }
940
+ const baseAssets = import_node_path4.default.join(this.apkDir, "assets", "Resources");
941
+ const bins = await readCloakBinFiles(baseAssets);
942
+ if (bins.length === 0) {
943
+ this.log("ti.cloak-> no .bin assets found under assets/Resources");
944
+ return null;
945
+ }
946
+ const sample = bins.find((b) => b.rel.endsWith(".js")) ?? bins[0];
947
+ const key = pickCloakKey(this.cloakLibs, salt, await (0, import_promises6.readFile)(sample.abs));
948
+ if (!key) {
949
+ this.log("ti.cloak-> could not derive a valid AES key from libti.cloak.so");
950
+ return null;
951
+ }
952
+ const source = {};
953
+ let totalBytes = 0;
954
+ for (const bin of bins) {
955
+ const decrypted = decryptCloakAsset(await (0, import_promises6.readFile)(bin.abs), key, salt);
956
+ if (!decrypted) continue;
957
+ const isText = DEV_SOURCE_EXTENSIONS.includes(import_node_path4.default.extname(bin.rel).toLowerCase());
958
+ source[bin.rel] = {
959
+ offset: 0,
960
+ bytes: decrypted.length,
961
+ content: isText ? decrypted.toString("utf8") : decrypted
962
+ };
963
+ totalBytes += decrypted.length;
964
+ this.log(`ti.cloak-> decrypted ${bin.rel}`);
965
+ }
966
+ if (Object.keys(source).length === 0) return null;
967
+ this.meta = { totalBytes, titaniumVersion: "unknown", alloy: detectAlloy(source) };
968
+ return source;
969
+ }
970
+ /**
971
+ * Returns Titanium metadata for the current APK. Must be called after
972
+ * {@link extract} to include the recovered file list.
973
+ */
974
+ async info() {
975
+ if (!this.manifest && this.apkDir) {
976
+ this.manifest = await readManifest(this.apkDir);
977
+ }
978
+ return buildInfo({
979
+ manifest: this.manifest,
980
+ memorySource: this.memorySource,
981
+ meta: this.meta,
982
+ developmentMode: this.developmentMode
983
+ });
984
+ }
985
+ /**
986
+ * Rebuilds the in-memory sources into a Titanium-openable project structure
987
+ * (everything under `Resources/`, plus a synthesised `tiapp.xml`).
988
+ */
989
+ async reconstruct() {
990
+ if (Object.keys(this.memorySource).length === 0) {
991
+ throw new Error("Call extract() before reconstruct().");
992
+ }
993
+ const info = await this.info();
994
+ const result = reconstruct(this.memorySource, info);
995
+ this.memorySource = result.memorySource;
996
+ this.restructured = result.restructured;
997
+ return this.memorySource;
998
+ }
999
+ /** Writes the in-memory sources to the configured `outDir`. */
1000
+ async writeToDisk() {
1001
+ if (!this.config.outDir) {
1002
+ throw new Error("writeToDisk() requires 'outDir' in the config.");
1003
+ }
1004
+ if (Object.keys(this.memorySource).length === 0) {
1005
+ throw new Error("Call extract() before writeToDisk().");
1006
+ }
1007
+ const written = await writeToDisk(this.memorySource, this.outDir);
1008
+ for (const file of written) {
1009
+ const size = file.bytes > 1e3 ? `${Math.round(file.bytes / 1024)} KB` : `${file.bytes} Bytes`;
1010
+ this.log(`writeToDisk-> file ${file.name} written (${size}).`);
1011
+ }
1012
+ return written;
1013
+ }
1014
+ /** Copies the APK's static assets and manifest into `outDir`. */
1015
+ async copyAssets() {
1016
+ if (!this.config.outDir) {
1017
+ throw new Error("copyAssets() requires 'outDir' in the config.");
1018
+ }
1019
+ await copyAssets(this.apkDir, this.outDir, this.restructured);
1020
+ }
1021
+ /** Removes the temporary directory created during {@link init}. */
1022
+ async clean() {
1023
+ if (!this.tmpUsed || !this.apkDir) return;
1024
+ try {
1025
+ await (0, import_promises6.rm)(this.apkDir, { recursive: true, force: true });
1026
+ this.log("clean->ok");
1027
+ } catch {
1028
+ }
1029
+ }
1030
+ };
1031
+ async function recover(options) {
1032
+ const { reconstruct: reconstruct2 = false, clean = true, ...config } = options;
1033
+ const ti = new TiRecover(config);
1034
+ try {
1035
+ await ti.init();
1036
+ if (!await ti.test()) {
1037
+ return { recovered: false };
1038
+ }
1039
+ await ti.extract();
1040
+ const info = await ti.info();
1041
+ if (reconstruct2) {
1042
+ await ti.reconstruct();
1043
+ }
1044
+ const files = await ti.writeToDisk();
1045
+ await ti.copyAssets();
1046
+ return { recovered: true, info, files };
1047
+ } finally {
1048
+ if (clean) await ti.clean();
1049
+ }
1050
+ }
1051
+ async function readDexBuffersFromDir(dir) {
1052
+ if (!await dirExists(dir)) return [];
1053
+ const names = (await (0, import_promises6.readdir)(dir)).filter(isDexEntry).sort();
1054
+ const buffers = [];
1055
+ for (const name of names) {
1056
+ buffers.push(await (0, import_promises6.readFile)(import_node_path4.default.join(dir, name)));
1057
+ }
1058
+ return buffers;
1059
+ }
1060
+ async function readCloakLibsFromDir(dir) {
1061
+ const libDir = import_node_path4.default.join(dir, "lib");
1062
+ if (!await dirExists(libDir)) return [];
1063
+ const entries = await (0, import_promises6.readdir)(libDir, { recursive: true, withFileTypes: true });
1064
+ const libs = [];
1065
+ for (const entry of entries) {
1066
+ if (!entry.isFile() || entry.name !== "libti.cloak.so") continue;
1067
+ const parent = entry.parentPath ?? entry.path ?? libDir;
1068
+ libs.push(await (0, import_promises6.readFile)(import_node_path4.default.join(parent, entry.name)));
1069
+ }
1070
+ return libs;
1071
+ }
1072
+ async function readCloakBinFiles(base) {
1073
+ if (!await dirExists(base)) return [];
1074
+ const entries = await (0, import_promises6.readdir)(base, { recursive: true, withFileTypes: true });
1075
+ const bins = [];
1076
+ for (const entry of entries) {
1077
+ if (!entry.isFile() || !entry.name.endsWith(".bin")) continue;
1078
+ const parent = entry.parentPath ?? entry.path ?? base;
1079
+ const abs = import_node_path4.default.join(parent, entry.name);
1080
+ const rel = import_node_path4.default.relative(base, abs).split(import_node_path4.default.sep).join("/").replace(/\.bin$/, "");
1081
+ bins.push({ rel, abs });
1082
+ }
1083
+ return bins;
1084
+ }
1085
+ async function readSourceFiles(base) {
1086
+ if (!await dirExists(base)) return [];
1087
+ const entries = await (0, import_promises6.readdir)(base, { recursive: true, withFileTypes: true });
1088
+ const files = [];
1089
+ for (const entry of entries) {
1090
+ if (!entry.isFile()) continue;
1091
+ const ext = import_node_path4.default.extname(entry.name).toLowerCase();
1092
+ if (!DEV_SOURCE_EXTENSIONS.includes(ext)) continue;
1093
+ const parentPath = entry.parentPath ?? entry.path ?? base;
1094
+ const abs = import_node_path4.default.join(parentPath, entry.name);
1095
+ files.push(import_node_path4.default.relative(base, abs));
1096
+ }
1097
+ return files;
1098
+ }
1099
+ // Annotate the CommonJS export names for ESM import in node:
1100
+ 0 && (module.exports = {
1101
+ TiRecover,
1102
+ UnsupportedEncryptionError,
1103
+ buildInfo,
1104
+ buildReconstruct,
1105
+ buildTiappXml,
1106
+ decodeJavaInt,
1107
+ decryptCloakAsset,
1108
+ decryptRange,
1109
+ decryptRanges,
1110
+ deriveCloakKey,
1111
+ detectAlloy,
1112
+ extractByteArrayFields,
1113
+ extractCloakSalt,
1114
+ extractRanges,
1115
+ extractStringChunks,
1116
+ instructionWidth,
1117
+ isProbablyText,
1118
+ parseAssetBuffer,
1119
+ parseBinaryManifest,
1120
+ parseManifest,
1121
+ parseRanges,
1122
+ pickCloakKey,
1123
+ readAssetCrypt,
1124
+ recover
1125
+ });
1126
+ //# sourceMappingURL=index.cjs.map