117 lines
3.1 KiB
JavaScript
117 lines
3.1 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const zlib = require("zlib");
|
|
|
|
const INPUT =
|
|
"/Users/lanmu/Desktop/ai-chatbot-main/analysis/qiaodou-extract/app.arm64";
|
|
const OUTPUT_ROOT =
|
|
"/Users/lanmu/Desktop/ai-chatbot-main/analysis/qiaodou-extract/extracted";
|
|
const MANIFEST =
|
|
"/Users/lanmu/Desktop/ai-chatbot-main/analysis/qiaodou-extract/manifest.json";
|
|
|
|
const ASSET_REGEX =
|
|
/\/assets\/[A-Za-z0-9 _.@/-]+\.(?:mjs|wasm|webp|png|svg|css|js|ico)(?=[^A-Za-z0-9_.@/-]|$)/g;
|
|
const INDEX_HTML_REGEX = /\/index\.html/g;
|
|
|
|
function collectOffsets(text, regex) {
|
|
const hits = [];
|
|
let match;
|
|
while ((match = regex.exec(text)) !== null) {
|
|
hits.push({
|
|
value: match[0],
|
|
offset: match.index,
|
|
});
|
|
}
|
|
return hits;
|
|
}
|
|
|
|
function uniqueByOffset(items) {
|
|
const seen = new Set();
|
|
return items.filter((item) => {
|
|
if (seen.has(item.offset)) {
|
|
return false;
|
|
}
|
|
seen.add(item.offset);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function ensureDir(filePath) {
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
}
|
|
|
|
function main() {
|
|
const buffer = fs.readFileSync(INPUT);
|
|
const text = buffer.toString("latin1");
|
|
|
|
const assets = uniqueByOffset(collectOffsets(text, ASSET_REGEX)).sort(
|
|
(a, b) => a.offset - b.offset,
|
|
);
|
|
const indexHtmlHits = uniqueByOffset(collectOffsets(text, INDEX_HTML_REGEX)).sort(
|
|
(a, b) => a.offset - b.offset,
|
|
);
|
|
|
|
if (!assets.length) {
|
|
throw new Error("No embedded /assets/* entries were found.");
|
|
}
|
|
|
|
const nextBoundaries = [...assets.map((a) => a.offset), ...indexHtmlHits.map((a) => a.offset)]
|
|
.sort((a, b) => a - b);
|
|
|
|
const manifest = [];
|
|
|
|
for (const asset of assets) {
|
|
const contentStart = asset.offset + Buffer.byteLength(asset.value);
|
|
const nextOffset = nextBoundaries.find((offset) => offset > asset.offset);
|
|
|
|
if (!nextOffset) {
|
|
manifest.push({
|
|
asset: asset.value,
|
|
offset: asset.offset,
|
|
status: "skipped_no_boundary",
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const compressed = buffer.subarray(contentStart, nextOffset);
|
|
|
|
try {
|
|
const decompressed = zlib.brotliDecompressSync(compressed);
|
|
const outputPath = path.join(OUTPUT_ROOT, asset.value.replace(/^\//, ""));
|
|
ensureDir(outputPath);
|
|
fs.writeFileSync(outputPath, decompressed);
|
|
|
|
manifest.push({
|
|
asset: asset.value,
|
|
offset: asset.offset,
|
|
nextOffset,
|
|
compressedBytes: compressed.length,
|
|
decompressedBytes: decompressed.length,
|
|
outputPath,
|
|
status: "ok",
|
|
});
|
|
} catch (error) {
|
|
manifest.push({
|
|
asset: asset.value,
|
|
offset: asset.offset,
|
|
nextOffset,
|
|
compressedBytes: compressed.length,
|
|
status: "decompress_failed",
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
|
|
fs.mkdirSync(OUTPUT_ROOT, { recursive: true });
|
|
fs.writeFileSync(MANIFEST, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
|
|
const ok = manifest.filter((item) => item.status === "ok").length;
|
|
const failed = manifest.filter((item) => item.status !== "ok").length;
|
|
|
|
console.log(`Extracted: ${ok}`);
|
|
console.log(`Failed: ${failed}`);
|
|
console.log(`Manifest: ${MANIFEST}`);
|
|
}
|
|
|
|
main();
|