create-file.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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. export async function compareAndWriteFile(span: Span, linesA: string[], filePath: string) {
  8. let isEqual = true;
  9. const file = Bun.file(filePath);
  10. const linesALen = linesA.length;
  11. if (!(await file.exists())) {
  12. console.log(`${filePath} does not exists, writing...`);
  13. isEqual = false;
  14. } else if (linesALen === 0) {
  15. console.log(`Nothing to write to ${filePath}...`);
  16. isEqual = false;
  17. } else {
  18. /* The `isEqual` variable is used to determine whether the content of a file is equal to the
  19. provided lines or not. It is initially set to `true`, and then it is updated based on different
  20. conditions: */
  21. isEqual = await span.traceChildAsync(`comparing ${filePath}`, async () => {
  22. let index = 0;
  23. for await (const lineB of readFileByLine(file)) {
  24. const lineA = linesA[index];
  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 Bun.write(file, `${linesA.join('\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. export 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. 'USER-AGENT': 30,
  100. 'PROCESS-NAME': 40,
  101. [defaultSortTypeOrder]: 50, // default sort order for unknown type
  102. AND: 100,
  103. OR: 100,
  104. 'IP-CIDR': 200,
  105. 'IP-CIDR6': 200
  106. };
  107. // sort DOMAIN-SUFFIX and DOMAIN first, then DOMAIN-KEYWORD, then IP-CIDR and IP-CIDR6 if any
  108. export const sortRuleSet = (ruleSet: string[]) => ruleSet
  109. .map((rule) => {
  110. const type = collectType(rule);
  111. return [type ? (type in sortTypeOrder ? sortTypeOrder[type] : sortTypeOrder[defaultSortTypeOrder]) : 10, rule] as const;
  112. })
  113. .sort((a, b) => a[0] - b[0])
  114. .map(c => c[1]);
  115. const MARK = 'this_ruleset_is_made_by_sukkaw.ruleset.skk.moe';
  116. export const createRuleset = (
  117. parentSpan: Span,
  118. title: string, description: string[] | readonly string[], date: Date, content: string[],
  119. type: 'ruleset' | 'domainset', surgePath: string, clashPath: string
  120. ) => parentSpan.traceChild(`create ruleset: ${path.basename(surgePath, path.extname(surgePath))}`).traceAsyncFn((childSpan) => {
  121. const surgeContent = withBannerArray(
  122. title, description, date,
  123. sortRuleSet(type === 'domainset'
  124. ? [MARK, ...content]
  125. : [`DOMAIN,${MARK}`, ...content])
  126. );
  127. const clashContent = childSpan.traceChildSync('convert incoming ruleset to clash', () => {
  128. let _clashContent;
  129. switch (type) {
  130. case 'domainset':
  131. _clashContent = [MARK, ...surgeDomainsetToClashDomainset(content)];
  132. break;
  133. case 'ruleset':
  134. _clashContent = [`DOMAIN,${MARK}`, ...surgeRulesetToClashClassicalTextRuleset(content)];
  135. break;
  136. default:
  137. throw new TypeError(`Unknown type: ${type as any}`);
  138. }
  139. return withBannerArray(title, description, date, _clashContent);
  140. });
  141. return Promise.all([
  142. compareAndWriteFile(childSpan, surgeContent, surgePath),
  143. compareAndWriteFile(childSpan, clashContent, clashPath)
  144. ]);
  145. });