extract-strings.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. // 提取 .jsc 里的所有可读字符串 (ASCII + UTF-16LE),按文件输出到 jsc-extract/strings/<name>.md
  2. const fs = require('fs');
  3. const path = require('path');
  4. const ROOT = path.resolve(__dirname, '..', 'src-app');
  5. const OUT_DIR = path.resolve(__dirname, 'strings');
  6. fs.mkdirSync(OUT_DIR, { recursive: true });
  7. function extractAscii(buf, minLen = 4) {
  8. const out = [];
  9. let start = -1;
  10. for (let i = 0; i < buf.length; i++) {
  11. const b = buf[i];
  12. const printable = (b >= 0x20 && b < 0x7f) || b === 0x09 || b === 0x0a || b === 0x0d;
  13. if (printable) {
  14. if (start === -1) start = i;
  15. } else {
  16. if (start !== -1 && i - start >= minLen) {
  17. out.push({ off: start, len: i - start, text: buf.subarray(start, i).toString('ascii') });
  18. }
  19. start = -1;
  20. }
  21. }
  22. if (start !== -1 && buf.length - start >= minLen) {
  23. out.push({ off: start, len: buf.length - start, text: buf.subarray(start).toString('ascii') });
  24. }
  25. return out;
  26. }
  27. function extractUtf16LE(buf, minChars = 3) {
  28. const out = [];
  29. let start = -1, chars = 0;
  30. for (let i = 0; i + 1 < buf.length; i += 2) {
  31. const code = buf.readUInt16LE(i);
  32. const printable = (code >= 0x20 && code < 0xd800) || code === 0x09 || code === 0x0a || code === 0x0d;
  33. if (printable) {
  34. if (start === -1) { start = i; chars = 0; }
  35. chars++;
  36. } else {
  37. if (start !== -1 && chars >= minChars) {
  38. out.push({ off: start, len: chars * 2, text: buf.subarray(start, i).toString('utf16le') });
  39. }
  40. start = -1;
  41. }
  42. }
  43. if (start !== -1 && chars >= minChars) {
  44. out.push({ off: start, len: chars * 2, text: buf.subarray(start).toString('utf16le') });
  45. }
  46. return out;
  47. }
  48. function categorize(text) {
  49. if (/^https?:\/\//.test(text)) return 'url';
  50. if (/^[a-z0-9-]+\.(com|cn|net|org|io|xyz|top|cc|app)/.test(text)) return 'domain';
  51. if (/^[a-zA-Z_$][\w$]*$/.test(text) && text.length < 60) return 'ident';
  52. if (text.length > 80) return 'text';
  53. if (/^[A-Z_0-9]+$/.test(text)) return 'const';
  54. return 'str';
  55. }
  56. function isInteresting(text) {
  57. // 过滤掉纯随机/重复字符串
  58. if (/^(\W)\1+$/.test(text)) return false;
  59. if (/^[0-9]+$/.test(text)) return false;
  60. if (text.length < 3) return false;
  61. // 过滤太像密码哈希的
  62. return true;
  63. }
  64. function processFile(file) {
  65. const rel = path.relative(ROOT, file);
  66. const buf = fs.readFileSync(file);
  67. const ascii = extractAscii(buf);
  68. const utf16 = extractUtf16LE(buf);
  69. // 合并去重
  70. const seen = new Set();
  71. const items = [];
  72. for (const s of [...ascii, ...utf16]) {
  73. if (!isInteresting(s.text)) continue;
  74. if (seen.has(s.text)) continue;
  75. seen.add(s.text);
  76. items.push({ ...s, cat: categorize(s.text) });
  77. }
  78. // 按 offset 排序
  79. items.sort((a, b) => a.off - b.off);
  80. // 输出 md
  81. let md = `# 字符串提取: ${rel}\n\n`;
  82. md += `文件大小: ${buf.length} 字节\n`;
  83. md += `提取到: ${items.length} 条字符串 (ASCII + UTF-16LE, 长度 >= 3-4)\n\n`;
  84. md += `## URL / 域名 (高价值)\n\n`;
  85. for (const it of items.filter(i => i.cat === 'url' || i.cat === 'domain')) {
  86. md += `- \`${it.text}\` *(0x${it.off.toString(16)}, ${it.len}B)*\n`;
  87. }
  88. md += `\n## 标识符 (变量/方法/属性名)\n\n`;
  89. const idents = items.filter(i => i.cat === 'ident');
  90. if (idents.length) {
  91. // 一次打印多列
  92. const cols = 4;
  93. for (let i = 0; i < idents.length; i += cols) {
  94. const row = idents.slice(i, i + cols).map(x => `\`${x.text}\``).join(' | ');
  95. md += row + '\n';
  96. }
  97. }
  98. md += `\n## 常量 (大写)\n\n`;
  99. for (const it of items.filter(i => i.cat === 'const')) {
  100. md += `- \`${it.text}\`\n`;
  101. }
  102. md += `\n## 其它文本字符串\n\n`;
  103. for (const it of items.filter(i => i.cat === 'str' || i.cat === 'text')) {
  104. md += `- \`${it.text.length > 200 ? it.text.slice(0, 200) + '…' : it.text}\`\n`;
  105. }
  106. return md;
  107. }
  108. function walk(dir) {
  109. const out = [];
  110. for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
  111. const full = path.join(dir, entry.name);
  112. if (entry.isDirectory()) {
  113. if (entry.name === 'node_modules') continue;
  114. out.push(...walk(full));
  115. } else if (entry.name.endsWith('.jsc')) {
  116. out.push(full);
  117. }
  118. }
  119. return out;
  120. }
  121. const files = walk(ROOT);
  122. console.log(`找到 ${files.length} 个 .jsc:`);
  123. for (const f of files) {
  124. const rel = path.relative(ROOT, f);
  125. const outName = rel.replace(/[\/\\]/g, '__').replace(/\.jsc$/, '.md');
  126. const outFile = path.join(OUT_DIR, outName);
  127. const md = processFile(f);
  128. fs.writeFileSync(outFile, md, 'utf-8');
  129. console.log(` ${rel} -> ${path.basename(outFile)} (${md.length} 字符)`);
  130. }
  131. console.log(`\n所有输出在: ${OUT_DIR}`);