create-file.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. // @ts-check
  2. import { surgeDomainsetToClashDomainset, surgeRulesetToClashClassicalTextRuleset } from './clash';
  3. import picocolors from 'picocolors';
  4. import type { Span } from '../trace';
  5. import path from 'node:path';
  6. import fs from 'node:fs';
  7. import { fastStringArrayJoin, writeFile } from './misc';
  8. import { readFileByLine } from './fetch-text-by-line';
  9. import stringify from 'json-stringify-pretty-compact';
  10. import { ipCidrListToSingbox, surgeDomainsetToSingbox, surgeRulesetToSingbox } from './singbox';
  11. import { createTrie } from './trie';
  12. import { pack, unpack } from './bitwise';
  13. export async function compareAndWriteFile(span: Span, linesA: string[], filePath: string) {
  14. let isEqual = true;
  15. const linesALen = linesA.length;
  16. if (!fs.existsSync(filePath)) {
  17. console.log(`${filePath} does not exists, writing...`);
  18. isEqual = false;
  19. } else if (linesALen === 0) {
  20. console.log(`Nothing to write to ${filePath}...`);
  21. isEqual = false;
  22. } else {
  23. isEqual = await span.traceChildAsync(`comparing ${filePath}`, async () => {
  24. let index = 0;
  25. for await (const lineB of readFileByLine(filePath)) {
  26. const lineA = linesA[index] as string | undefined;
  27. index++;
  28. if (lineA == null) {
  29. // The file becomes smaller
  30. return false;
  31. }
  32. if (lineA[0] === '#' && lineB[0] === '#') {
  33. continue;
  34. }
  35. if (
  36. lineA[0] === '/'
  37. && lineA[1] === '/'
  38. && lineB[0] === '/'
  39. && lineB[1] === '/'
  40. && lineA[3] === '#'
  41. && lineB[3] === '#'
  42. ) {
  43. continue;
  44. }
  45. if (lineA !== lineB) {
  46. return false;
  47. }
  48. }
  49. if (index !== linesALen) {
  50. // The file becomes larger
  51. return false;
  52. }
  53. return true;
  54. });
  55. }
  56. if (isEqual) {
  57. console.log(picocolors.dim(`same content, bail out writing: ${filePath}`));
  58. return;
  59. }
  60. await span.traceChildAsync(`writing ${filePath}`, async () => {
  61. // if (linesALen < 10000) {
  62. return writeFile(filePath, fastStringArrayJoin(linesA, '\n') + '\n');
  63. // }
  64. // const writer = file.writer();
  65. // for (let i = 0; i < linesALen; i++) {
  66. // writer.write(linesA[i]);
  67. // writer.write('\n');
  68. // }
  69. // return writer.end();
  70. });
  71. }
  72. const withBannerArray = (title: string, description: string[] | readonly string[], date: Date, content: string[]) => {
  73. return [
  74. '#########################################',
  75. `# ${title}`,
  76. `# Last Updated: ${date.toISOString()}`,
  77. `# Size: ${content.length}`,
  78. ...description.map(line => (line ? `# ${line}` : '#')),
  79. '#########################################',
  80. ...content,
  81. '################## EOF ##################'
  82. ];
  83. };
  84. const defaultSortTypeOrder = Symbol('defaultSortTypeOrder');
  85. const sortTypeOrder: Record<string | typeof defaultSortTypeOrder, number> = {
  86. DOMAIN: 1,
  87. 'DOMAIN-SUFFIX': 2,
  88. 'DOMAIN-KEYWORD': 10,
  89. // experimental domain wildcard support
  90. 'DOMAIN-WILDCARD': 20,
  91. 'DOMAIN-REGEX': 21,
  92. 'USER-AGENT': 30,
  93. 'PROCESS-NAME': 40,
  94. [defaultSortTypeOrder]: 50, // default sort order for unknown type
  95. 'URL-REGEX': 100,
  96. AND: 300,
  97. OR: 300,
  98. 'IP-CIDR': 400,
  99. 'IP-CIDR6': 400
  100. };
  101. const flagDomain = 1 << 2;
  102. const flagDomainSuffix = 1 << 3;
  103. // dedupe and sort based on rule type
  104. const processRuleSet = (ruleSet: string[]) => {
  105. const trie = createTrie<number>(null, true);
  106. const sortMap: Array<[value: number, weight: number]> = [];
  107. for (let i = 0, len = ruleSet.length; i < len; i++) {
  108. const line = ruleSet[i];
  109. const [type, value] = line.split(',');
  110. let extraWeight = 0;
  111. switch (type) {
  112. case 'DOMAIN':
  113. trie.add(value, pack(i, flagDomain));
  114. break;
  115. case 'DOMAIN-SUFFIX':
  116. trie.add('.' + value, pack(i, flagDomainSuffix));
  117. break;
  118. case 'URL-REGEX':
  119. if (value.includes('.+') || value.includes('.*')) {
  120. extraWeight += 10;
  121. }
  122. if (value.includes('|')) {
  123. extraWeight += 1;
  124. }
  125. sortMap.push([i, sortTypeOrder[type] + extraWeight]);
  126. break;
  127. case null:
  128. sortMap.push([i, 10]);
  129. break;
  130. default:
  131. if (type in sortTypeOrder) {
  132. sortMap.push([i, sortTypeOrder[type]]);
  133. } else {
  134. sortMap.push([i, sortTypeOrder[defaultSortTypeOrder]]);
  135. }
  136. }
  137. }
  138. const dumped = trie.dumpWithMeta();
  139. for (let i = 0, len = dumped.length; i < len; i++) {
  140. const [originalIndex, flag] = unpack(dumped[i][1]);
  141. const type = flag === flagDomain ? 'DOMAIN' : 'DOMAIN-SUFFIX';
  142. sortMap.push([originalIndex, sortTypeOrder[type]]);
  143. }
  144. return sortMap
  145. .sort((a, b) => a[1] - b[1])
  146. .map(c => ruleSet[c[0]]);
  147. };
  148. const MARK = 'this_ruleset_is_made_by_sukkaw.ruleset.skk.moe';
  149. export const createRuleset = (
  150. parentSpan: Span,
  151. title: string, description: string[] | readonly string[], date: Date, content: string[],
  152. type: 'ruleset' | 'domainset' | 'ipcidr' | 'ipcidr6',
  153. [surgePath, clashPath, singBoxPath, _clashMrsPath]: readonly [
  154. surgePath: string,
  155. clashPath: string,
  156. singBoxPath: string,
  157. _clashMrsPath?: string
  158. ]
  159. ) => parentSpan.traceChild(`create ruleset: ${path.basename(surgePath, path.extname(surgePath))}`).traceAsyncFn(async (childSpan) => {
  160. content = processRuleSet(content);
  161. const surgeContent = childSpan.traceChildSync('process surge ruleset', () => {
  162. let _surgeContent;
  163. switch (type) {
  164. case 'domainset':
  165. _surgeContent = [MARK, ...content];
  166. break;
  167. case 'ruleset':
  168. _surgeContent = [`DOMAIN,${MARK}`, ...content];
  169. break;
  170. case 'ipcidr':
  171. _surgeContent = [`DOMAIN,${MARK}`, ...content.map(i => `IP-CIDR,${i}`)];
  172. break;
  173. case 'ipcidr6':
  174. _surgeContent = [`DOMAIN,${MARK}`, ...content.map(i => `IP-CIDR6,${i}`)];
  175. break;
  176. default:
  177. throw new TypeError(`Unknown type: ${type}`);
  178. }
  179. return withBannerArray(title, description, date, _surgeContent);
  180. });
  181. const clashContent = childSpan.traceChildSync('convert incoming ruleset to clash', () => {
  182. let _clashContent;
  183. switch (type) {
  184. case 'domainset':
  185. _clashContent = [MARK, ...surgeDomainsetToClashDomainset(content)];
  186. break;
  187. case 'ruleset':
  188. _clashContent = [`DOMAIN,${MARK}`, ...surgeRulesetToClashClassicalTextRuleset(content)];
  189. break;
  190. case 'ipcidr':
  191. case 'ipcidr6':
  192. _clashContent = content;
  193. break;
  194. default:
  195. throw new TypeError(`Unknown type: ${type}`);
  196. }
  197. return withBannerArray(title, description, date, _clashContent);
  198. });
  199. const singboxContent = childSpan.traceChildSync('convert incoming ruleset to singbox', () => {
  200. let _singBoxContent;
  201. switch (type) {
  202. case 'domainset':
  203. _singBoxContent = surgeDomainsetToSingbox([MARK, ...content]);
  204. break;
  205. case 'ruleset':
  206. _singBoxContent = surgeRulesetToSingbox([`DOMAIN,${MARK}`, ...content]);
  207. break;
  208. case 'ipcidr':
  209. case 'ipcidr6':
  210. _singBoxContent = ipCidrListToSingbox(content);
  211. break;
  212. default:
  213. throw new TypeError(`Unknown type: ${type}`);
  214. }
  215. return stringify(_singBoxContent).split('\n');
  216. });
  217. await Promise.all([
  218. compareAndWriteFile(childSpan, surgeContent, surgePath),
  219. compareAndWriteFile(childSpan, clashContent, clashPath),
  220. compareAndWriteFile(childSpan, singboxContent, singBoxPath)
  221. ]);
  222. // if (clashMrsPath) {
  223. // if (type === 'domainset') {
  224. // await childSpan.traceChildAsync('clash meta mrs domain ' + clashMrsPath, async () => {
  225. // await fs.promises.mkdir(path.dirname(clashMrsPath), { recursive: true });
  226. // await convertClashMetaMrs(
  227. // 'domain', 'text', clashPath, clashMrsPath
  228. // );
  229. // });
  230. // }
  231. // }
  232. });