create-file.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. // @ts-check
  2. import { readFileByLine } from './fetch-text-by-line';
  3. import { surgeDomainsetToClashDomainset, surgeRulesetToClashClassicalTextRuleset } from './clash';
  4. import picocolors from 'picocolors';
  5. import type { Span } from '../trace';
  6. import path from 'path';
  7. import { sort } from './timsort';
  8. export async function compareAndWriteFile(span: Span, linesA: string[], filePath: string) {
  9. let isEqual = true;
  10. const file = Bun.file(filePath);
  11. const linesALen = linesA.length;
  12. if (!(await file.exists())) {
  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. /* The `isEqual` variable is used to determine whether the content of a file is equal to the
  20. provided lines or not. It is initially set to `true`, and then it is updated based on different
  21. conditions: */
  22. isEqual = await span.traceChildAsync(`comparing ${filePath}`, async () => {
  23. let index = 0;
  24. for await (const lineB of readFileByLine(file)) {
  25. const lineA = linesA[index] as string | undefined;
  26. index++;
  27. if (lineA == null) {
  28. // The file becomes smaller
  29. return false;
  30. }
  31. if (lineA[0] === '#' && lineB[0] === '#') {
  32. continue;
  33. }
  34. if (
  35. lineA[0] === '/'
  36. && lineA[1] === '/'
  37. && lineB[0] === '/'
  38. && lineB[1] === '/'
  39. && lineA[3] === '#'
  40. && lineB[3] === '#'
  41. ) {
  42. continue;
  43. }
  44. if (lineA !== lineB) {
  45. return false;
  46. }
  47. }
  48. if (index !== linesALen) {
  49. // The file becomes larger
  50. return false;
  51. }
  52. return true;
  53. });
  54. }
  55. if (isEqual) {
  56. console.log(picocolors.dim(`same content, bail out writing: ${filePath}`));
  57. return;
  58. }
  59. await span.traceChildAsync(`writing ${filePath}`, async () => {
  60. // if (linesALen < 10000) {
  61. return Bun.write(file, `${linesA.join('\n')}\n`);
  62. // }
  63. // const writer = file.writer();
  64. // for (let i = 0; i < linesALen; i++) {
  65. // writer.write(linesA[i]);
  66. // writer.write('\n');
  67. // }
  68. // return writer.end();
  69. });
  70. }
  71. export const withBannerArray = (title: string, description: string[] | readonly string[], date: Date, content: string[]) => {
  72. return [
  73. '#########################################',
  74. `# ${title}`,
  75. `# Last Updated: ${date.toISOString()}`,
  76. `# Size: ${content.length}`,
  77. ...description.map(line => (line ? `# ${line}` : '#')),
  78. '#########################################',
  79. ...content,
  80. '################## EOF ##################'
  81. ];
  82. };
  83. const collectType = (rule: string) => {
  84. let buf = '';
  85. for (let i = 0, len = rule.length; i < len; i++) {
  86. if (rule[i] === ',') {
  87. return buf;
  88. }
  89. buf += rule[i];
  90. }
  91. return null;
  92. };
  93. const defaultSortTypeOrder = Symbol('defaultSortTypeOrder');
  94. const sortTypeOrder: Record<string | typeof defaultSortTypeOrder, number> = {
  95. DOMAIN: 1,
  96. 'DOMAIN-SUFFIX': 2,
  97. 'DOMAIN-KEYWORD': 10,
  98. // experimental domain wildcard support
  99. 'DOMAIN-WILDCARD': 20,
  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 sort(
  112. ruleSet.map((rule) => {
  113. const type = collectType(rule);
  114. if (!type) {
  115. return [10, rule] as const;
  116. }
  117. if (!(type in sortTypeOrder)) {
  118. return [sortTypeOrder[defaultSortTypeOrder], rule] as const;
  119. }
  120. if (type === 'URL-REGEX') {
  121. let extraWeight = 0;
  122. if (rule.includes('.+') || rule.includes('.*')) {
  123. extraWeight += 10;
  124. }
  125. if (rule.includes('|')) {
  126. extraWeight += 1;
  127. }
  128. return [
  129. sortTypeOrder[type] + extraWeight,
  130. rule
  131. ] as const;
  132. }
  133. return [sortTypeOrder[type], rule] as const;
  134. }),
  135. (a, b) => a[0] - b[0]
  136. ).map(c => c[1]);
  137. };
  138. const MARK = 'this_ruleset_is_made_by_sukkaw.ruleset.skk.moe';
  139. export const createRuleset = (
  140. parentSpan: Span,
  141. title: string, description: string[] | readonly string[], date: Date, content: string[],
  142. type: 'ruleset' | 'domainset', surgePath: string, clashPath: string
  143. ) => parentSpan.traceChild(`create ruleset: ${path.basename(surgePath, path.extname(surgePath))}`).traceAsyncFn((childSpan) => {
  144. const surgeContent = withBannerArray(
  145. title, description, date,
  146. sortRuleSet(type === 'domainset'
  147. ? [MARK, ...content]
  148. : [`DOMAIN,${MARK}`, ...content])
  149. );
  150. const clashContent = childSpan.traceChildSync('convert incoming ruleset to clash', () => {
  151. let _clashContent;
  152. switch (type) {
  153. case 'domainset':
  154. _clashContent = [MARK, ...surgeDomainsetToClashDomainset(content)];
  155. break;
  156. case 'ruleset':
  157. _clashContent = [`DOMAIN,${MARK}`, ...surgeRulesetToClashClassicalTextRuleset(content)];
  158. break;
  159. default:
  160. throw new TypeError(`Unknown type: ${type as any}`);
  161. }
  162. return withBannerArray(title, description, date, _clashContent);
  163. });
  164. return Promise.all([
  165. compareAndWriteFile(childSpan, surgeContent, surgePath),
  166. compareAndWriteFile(childSpan, clashContent, clashPath)
  167. ]);
  168. });