Browse Source

Refactor: new output

SukkaW 1 year ago
parent
commit
eb2023b9aa
4 changed files with 113 additions and 37 deletions
  1. 6 11
      Build/build-cloudmounter-rules.ts
  2. 11 17
      Build/build-common.ts
  3. 94 7
      Build/lib/create-file-new.ts
  4. 2 2
      Build/lib/create-file.ts

+ 6 - 11
Build/build-cloudmounter-rules.ts

@@ -1,8 +1,7 @@
 import { DOMAINS, PROCESS_NAMES } from '../Source/non_ip/cloudmounter';
 import { SHARED_DESCRIPTION } from './lib/constants';
-import { createRuleset } from './lib/create-file';
 import { task } from './trace';
-import { output } from './lib/misc';
+import { RulesetOutput } from './lib/create-file-new';
 
 export const buildCloudMounterRules = task(require.main === module, __filename)(async (span) => {
   // AND,((SRC-IP,192.168.1.110), (DOMAIN, example.com))
@@ -21,13 +20,9 @@ export const buildCloudMounterRules = task(require.main === module, __filename)(
 
   const description = SHARED_DESCRIPTION;
 
-  return createRuleset(
-    span,
-    'Sukka\'s Ruleset - CloudMounter / RaiDrive',
-    description,
-    new Date(),
-    results,
-    'ruleset',
-    output('cloudmounter', 'non_ip')
-  );
+  return new RulesetOutput(span, 'cloudmounter', 'non_ip')
+    .withTitle('Sukka\'s Ruleset - CloudMounter / RaiDrive')
+    .withDescription(description)
+    .addFromRuleset(results)
+    .write();
 });

+ 11 - 17
Build/build-common.ts

@@ -3,14 +3,13 @@
 import * as path from 'node:path';
 import { readFileByLine } from './lib/fetch-text-by-line';
 import { processLine } from './lib/process-line';
-import { createRuleset } from './lib/create-file';
 import type { Span } from './trace';
 import { task } from './trace';
 import { SHARED_DESCRIPTION } from './lib/constants';
 import { fdir as Fdir } from 'fdir';
 import { appendArrayInPlace } from './lib/append-array-in-place';
-import { OUTPUT_CLASH_DIR, OUTPUT_SINGBOX_DIR, OUTPUT_SURGE_DIR, SOURCE_DIR } from './constants/dir';
-import { DomainsetOutput } from './lib/create-file-new';
+import { SOURCE_DIR } from './constants/dir';
+import { DomainsetOutput, RulesetOutput } from './lib/create-file-new';
 
 const MAGIC_COMMAND_SKIP = '# $ custom_build_script';
 const MAGIC_COMMAND_TITLE = '# $ meta_title ';
@@ -144,7 +143,10 @@ async function transformRuleset(parentSpan: Span, sourcePath: string, relativePa
       const res = await processFile(span, sourcePath);
       if (res === $skip) return;
 
-      const clashFileBasename = relativePath.slice(0, -path.extname(relativePath).length);
+      const [type, id] = relativePath.slice(0, -path.extname(relativePath).length).split(path.sep);
+      if (type !== 'ip' && type !== 'non_ip') {
+        throw new TypeError(`Invalid type: ${type}`);
+      }
 
       const [title, descriptions, lines] = res;
 
@@ -157,18 +159,10 @@ async function transformRuleset(parentSpan: Span, sourcePath: string, relativePa
         description = SHARED_DESCRIPTION;
       }
 
-      return createRuleset(
-        span,
-        title,
-        description,
-        new Date(),
-        lines,
-        'ruleset',
-        [
-          path.resolve(OUTPUT_SURGE_DIR, relativePath),
-          path.resolve(OUTPUT_CLASH_DIR, `${clashFileBasename}.txt`),
-          path.resolve(OUTPUT_SINGBOX_DIR, `${clashFileBasename}.json`)
-        ]
-      );
+      return new RulesetOutput(span, id, type)
+        .withTitle(title)
+        .withDescription(description)
+        .addFromRuleset(lines)
+        .write();
     });
 }

+ 94 - 7
Build/lib/create-file-new.ts

@@ -1,9 +1,9 @@
 import path from 'node:path';
 
 import type { Span } from '../trace';
-import { surgeDomainsetToClashDomainset } from './clash';
-import { compareAndWriteFile, withBannerArray } from './create-file';
-import { ipCidrListToSingbox, surgeDomainsetToSingbox } from './singbox';
+import { surgeDomainsetToClashDomainset, surgeRulesetToClashClassicalTextRuleset } from './clash';
+import { compareAndWriteFile, defaultSortTypeOrder, sortTypeOrder, withBannerArray } from './create-file';
+import { ipCidrListToSingbox, surgeDomainsetToSingbox, surgeRulesetToSingbox } from './singbox';
 import { sortDomains } from './stable-sort-domain';
 import { createTrie } from './trie';
 import { invariant } from 'foxact/invariant';
@@ -19,7 +19,7 @@ abstract class RuleOutput {
   protected ipcidrNoResolve = new Set<string>();
   protected ipcidr6 = new Set<string>();
   protected ipcidr6NoResolve = new Set<string>();
-  protected otherRules = new Set<string>();
+  protected otherRules: Array<[raw: string, orderWeight: number]> = [];
   protected abstract type: 'domainset' | 'non_ip' | 'ip';
 
   protected pendingPromise = Promise.resolve();
@@ -101,9 +101,13 @@ abstract class RuleOutput {
     return this;
   }
 
-  async addFromRuleset(source: AsyncIterable<string> | Iterable<string>) {
+  private async addFromRulesetPromise(source: AsyncIterable<string> | Iterable<string>) {
     for await (const line of source) {
-      const [type, value, arg] = line.split(',');
+      const splitted = line.split(',');
+      const type = splitted[0];
+      const value = splitted[1];
+      const arg = splitted[2];
+
       switch (type) {
         case 'DOMAIN':
           this.addDomain(value);
@@ -124,10 +128,14 @@ abstract class RuleOutput {
           (arg === 'no-resolve' ? this.ipcidr6NoResolve : this.ipcidr6).add(value);
           break;
         default:
-          this.otherRules.add(line);
+          this.otherRules.push([line, type in sortTypeOrder ? sortTypeOrder[type] : sortTypeOrder[defaultSortTypeOrder]]);
           break;
       }
     }
+  }
+
+  addFromRuleset(source: AsyncIterable<string> | Iterable<string>) {
+    this.pendingPromise = this.pendingPromise.then(() => this.addFromRulesetPromise(source));
     return this;
   }
 
@@ -162,6 +170,7 @@ export class DomainsetOutput extends RuleOutput {
 
     const surge = sorted;
     const clash = surgeDomainsetToClashDomainset(sorted);
+    // TODO: Implement singbox directly using data
     const singbox = RuleOutput.jsonToLines(surgeDomainsetToSingbox(sorted));
 
     await Promise.all([
@@ -216,6 +225,7 @@ export class IPListOutput extends RuleOutput {
     surge.push('DOMAIN,this_ruleset_is_made_by_sukkaw.ruleset.skk.moe');
 
     const clash = this.clashUseRule ? surge : merged;
+    // TODO: Implement singbox directly using data
     const singbox = RuleOutput.jsonToLines(ipCidrListToSingbox(merged));
 
     await Promise.all([
@@ -247,3 +257,80 @@ export class IPListOutput extends RuleOutput {
     ]);
   }
 }
+
+export class RulesetOutput extends RuleOutput {
+  constructor(span: Span, id: string, protected type: 'non_ip' | 'ip') {
+    super(span, id);
+  }
+
+  async write() {
+    await this.pendingPromise;
+
+    invariant(this.title, 'Missing title');
+    invariant(this.description, 'Missing description');
+
+    const results: string[] = [
+      'DOMAIN,this_ruleset_is_made_by_sukkaw.ruleset.skk.moe'
+    ];
+
+    const sortedDomains = sortDomains(this.domainTrie.dump(), this.apexDomainMap, this.subDomainMap);
+    for (let i = 0, len = sortedDomains.length; i < len; i++) {
+      const domain = sortedDomains[i];
+      if (domain[0] === '.') {
+        results.push(`DOMAIN-SUFFIX,${domain.slice(1)}`);
+      } else {
+        results.push(`DOMAIN,${domain}`);
+      }
+    }
+
+    for (const keyword of this.domainKeywords) {
+      results.push(`DOMAIN-KEYWORD,${keyword}`);
+    }
+    for (const wildcard of this.domainWildcard) {
+      results.push(`DOMAIN-WILDCARD,${wildcard}`);
+    }
+
+    const sortedRules = this.otherRules.sort((a, b) => a[1] - b[1]);
+    for (let i = 0, len = sortedRules.length; i < len; i++) {
+      results.push(sortedRules[i][0]);
+    }
+
+    this.ipcidr.forEach(cidr => results.push(`IP-CIDR,${cidr}`));
+    this.ipcidrNoResolve.forEach(cidr => results.push(`IP-CIDR,${cidr},no-resolve`));
+    this.ipcidr6.forEach(cidr => results.push(`IP-CIDR6,${cidr}`));
+    this.ipcidr6NoResolve.forEach(cidr => results.push(`IP-CIDR6,${cidr},no-resolve`));
+
+    const surge = results;
+    const clash = surgeRulesetToClashClassicalTextRuleset(results);
+    // TODO: Implement singbox directly using data
+    const singbox = RuleOutput.jsonToLines(surgeRulesetToSingbox(results));
+
+    await Promise.all([
+      compareAndWriteFile(
+        this.span,
+        withBannerArray(
+          this.title,
+          this.description,
+          this.date,
+          surge
+        ),
+        path.join(OUTPUT_SURGE_DIR, this.type, this.id + '.conf')
+      ),
+      compareAndWriteFile(
+        this.span,
+        withBannerArray(
+          this.title,
+          this.description,
+          this.date,
+          clash
+        ),
+        path.join(OUTPUT_CLASH_DIR, this.type, this.id + '.txt')
+      ),
+      compareAndWriteFile(
+        this.span,
+        singbox,
+        path.join(OUTPUT_SINGBOX_DIR, this.type, this.id + '.json')
+      )
+    ]);
+  }
+}

+ 2 - 2
Build/lib/create-file.ts

@@ -108,8 +108,8 @@ export const withBannerArray = (title: string, description: string[] | readonly
   ];
 };
 
-const defaultSortTypeOrder = Symbol('defaultSortTypeOrder');
-const sortTypeOrder: Record<string | typeof defaultSortTypeOrder, number> = {
+export const defaultSortTypeOrder = Symbol('defaultSortTypeOrder');
+export const sortTypeOrder: Record<string | typeof defaultSortTypeOrder, number> = {
   DOMAIN: 1,
   'DOMAIN-SUFFIX': 2,
   'DOMAIN-KEYWORD': 10,