create-file.ts 7.1 KB

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