create-file.ts 5.4 KB

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