#!/usr/bin/env node import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; interface CliOptions { anchorsPath: string; endAnchor: string; sourcePath: string; startAnchor: string; } interface AnchorCatalog { sourceAnchors?: unknown; } const dirname = path.dirname(fileURLToPath(import.meta.url)); const workspaceRoot = path.resolve( process.env.KT_WORKSPACE_ROOT || path.join(dirname, '../../..'), ); /** * @param value 原始路径,可为绝对路径或相对 KT 工作区根目录。 * @returns 解析后的绝对路径。 */ function resolveWorkspacePath(value: string): string { return path.isAbsolute(value) ? value : path.resolve(workspaceRoot, value); } /** * @param argv 传入脚本的命令行参数。 * @returns 反混淆 anchor 校验配置。 */ function parseArgs(argv: string[]): CliOptions { const args = new Map(); for (let index = 0; index < argv.length; index += 1) { const item = argv[index]; if (!item.startsWith('--')) continue; const [key, inlineValue] = item.slice(2).split('=', 2); if (inlineValue !== undefined) { args.set(key, inlineValue); continue; } const next = argv[index + 1]; if (next && !next.startsWith('--')) { args.set(key, next); index += 1; } } if (args.has('help') || args.has('h')) { printHelp(); process.exit(0); } const sourcePath = args.get('source'); const anchorsPath = args.get('anchors') || args.get('anchors-json'); const startAnchor = args.get('start'); const endAnchor = args.get('end'); if (!sourcePath || !anchorsPath || !startAnchor || !endAnchor) { printHelp(); throw new Error('缺少必要参数:--source、--anchors、--start、--end'); } return { anchorsPath: resolveWorkspacePath(anchorsPath), endAnchor, sourcePath: resolveWorkspacePath(sourcePath), startAnchor, }; } /** * 输出脚本帮助文本。 */ function printHelp(): void { console.log(`KT 反混淆 sourceAnchors 校验 用法: pnpm run validate-deobfuscation-anchors -- --source Vue/kt-blog-web/public/live2d/wordpress-moc/live2d.min.js --anchors .kt-workspace/deobfuscate/.../renames-matrix44-method-aliases.json --start "function ac()" --end "function aD()" 参数: --source 源 min.js 文件,绝对路径或相对 KT 根目录 --anchors 包含 sourceAnchors 数组的 JSON 文件 --start 源码切片起始锚点 --end 源码切片结束锚点 `); } /** * @param rawCatalog 原始 JSON 文本。 * @param anchorsPath JSON 文件路径,用于错误说明。 * @returns sourceAnchors 字符串数组。 */ function readSourceAnchors(rawCatalog: string, anchorsPath: string): string[] { const catalog = JSON.parse(rawCatalog) as AnchorCatalog; if (!Array.isArray(catalog.sourceAnchors)) { throw new Error(`${anchorsPath} 缺少 sourceAnchors 数组`); } const invalidAnchors = catalog.sourceAnchors.filter((anchor) => typeof anchor !== 'string'); if (invalidAnchors.length > 0) { throw new Error(`${anchorsPath} 的 sourceAnchors 只能包含字符串`); } return catalog.sourceAnchors as string[]; } /** * @param source 源码文本。 * @param startAnchor 切片起始锚点。 * @param endAnchor 切片结束锚点。 * @returns 起止锚点之间的源码切片。 */ function sliceSourceByAnchors(source: string, startAnchor: string, endAnchor: string): string { const startIndex = source.indexOf(startAnchor); if (startIndex < 0) { throw new Error(`源文件中找不到起始锚点:${startAnchor}`); } const endIndex = source.indexOf(endAnchor, startIndex); if (endIndex <= startIndex) { throw new Error(`源文件中找不到结束锚点:${endAnchor}`); } return source.slice(startIndex, endIndex); } /** * @param sourceSlice 源码切片。 * @param anchors 需要逐项匹配的 sourceAnchors。 * @returns 未能在源码切片中找到的 anchors。 */ function findMissingAnchors(sourceSlice: string, anchors: string[]): string[] { return anchors.filter((anchor) => !sourceSlice.includes(anchor)); } /** * 校验 sourceAnchors 是否都存在于指定源码切片。 */ function main(): void { const options = parseArgs(process.argv.slice(2)); const source = readFileSync(options.sourcePath, 'utf-8'); const rawCatalog = readFileSync(options.anchorsPath, 'utf-8'); const sourceSlice = sliceSourceByAnchors(source, options.startAnchor, options.endAnchor); const sourceAnchors = readSourceAnchors(rawCatalog, options.anchorsPath); const missingAnchors = findMissingAnchors(sourceSlice, sourceAnchors); if (missingAnchors.length > 0) { throw new Error(`sourceAnchors 未命中源码切片:${missingAnchors.join('; ')}`); } console.log( JSON.stringify( { anchors: sourceAnchors.length, anchorsPath: options.anchorsPath, ok: true, sourcePath: options.sourcePath, }, null, 2, ), ); } try { main(); } catch (error) { console.error(error instanceof Error ? error.message : error); process.exit(1); }