create-file.ts 5.6 KB

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