diff --git a/Build/build-cloudmounter-rules.ts b/Build/build-cloudmounter-rules.ts index 2ca88698..a79be03e 100644 --- a/Build/build-cloudmounter-rules.ts +++ b/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(); }); diff --git a/Build/build-common.ts b/Build/build-common.ts index 3539afd4..4c97225b 100644 --- a/Build/build-common.ts +++ b/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(); }); } diff --git a/Build/lib/create-file-new.ts b/Build/lib/create-file-new.ts index 955cf9c4..fe437e31 100644 --- a/Build/lib/create-file-new.ts +++ b/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(); protected ipcidr6 = new Set(); protected ipcidr6NoResolve = new Set(); - protected otherRules = new Set(); + 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 | Iterable) { + private async addFromRulesetPromise(source: AsyncIterable | Iterable) { 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 | Iterable) { + 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') + ) + ]); + } +} diff --git a/Build/lib/create-file.ts b/Build/lib/create-file.ts index 166843fb..8be60c97 100644 --- a/Build/lib/create-file.ts +++ b/Build/lib/create-file.ts @@ -108,8 +108,8 @@ export const withBannerArray = (title: string, description: string[] | readonly ]; }; -const defaultSortTypeOrder = Symbol('defaultSortTypeOrder'); -const sortTypeOrder: Record = { +export const defaultSortTypeOrder = Symbol('defaultSortTypeOrder'); +export const sortTypeOrder: Record = { DOMAIN: 1, 'DOMAIN-SUFFIX': 2, 'DOMAIN-KEYWORD': 10,