diff --git a/Build/build-apple-cdn.ts b/Build/build-apple-cdn.ts index 2fc3c5af..3e33fa3e 100644 --- a/Build/build-apple-cdn.ts +++ b/Build/build-apple-cdn.ts @@ -1,11 +1,9 @@ -// @ts-check -import { createRuleset } from './lib/create-file'; import { parseFelixDnsmasq } from './lib/parse-dnsmasq'; import { task } from './trace'; import { SHARED_DESCRIPTION } from './lib/constants'; import { createMemoizedPromise } from './lib/memo-promise'; import { TTL, deserializeArray, fsFetchCache, serializeArray, createCacheKey } from './lib/cache-filesystem'; -import { output } from './lib/misc'; +import { DomainsetOutput } from './lib/create-file-new'; const cacheKey = createCacheKey(__filename); @@ -23,24 +21,16 @@ export const getAppleCdnDomainsPromise = createMemoizedPromise(() => fsFetchCach export const buildAppleCdn = task(require.main === module, __filename)(async (span) => { const res: string[] = await span.traceChildPromise('get apple cdn domains', getAppleCdnDomainsPromise()); - const description = [ - ...SHARED_DESCRIPTION, - '', - 'This file contains Apple\'s domains using their China mainland CDN servers.', - '', - 'Data from:', - ' - https://github.com/felixonmars/dnsmasq-china-list' - ]; - - const domainset = res.map(i => `.${i}`); - - return createRuleset( - span, - 'Sukka\'s Ruleset - Apple CDN', - description, - new Date(), - domainset, - 'domainset', - output('apple_cdn', 'domainset') - ); + return new DomainsetOutput(span, 'apple_cdn') + .withTitle('Sukka\'s Ruleset - Apple CDN') + .withDescription([ + ...SHARED_DESCRIPTION, + '', + 'This file contains Apple\'s domains using their China mainland CDN servers.', + '', + 'Data from:', + ' - https://github.com/felixonmars/dnsmasq-china-list' + ]) + .bulkAddDomainSuffix(res) + .write(); }); diff --git a/Build/build-cdn-download-conf.ts b/Build/build-cdn-download-conf.ts index 47f11a80..45a59be7 100644 --- a/Build/build-cdn-download-conf.ts +++ b/Build/build-cdn-download-conf.ts @@ -1,16 +1,13 @@ import path from 'node:path'; -import { createRuleset } from './lib/create-file'; import { readFileIntoProcessedArray } from './lib/fetch-text-by-line'; import { createTrie } from './lib/trie'; import { task } from './trace'; import { SHARED_DESCRIPTION } from './lib/constants'; import { getPublicSuffixListTextPromise } from './lib/download-publicsuffixlist'; -import { domainsetDeduper } from './lib/domain-deduper'; import { appendArrayInPlace } from './lib/append-array-in-place'; -import { sortDomains } from './lib/stable-sort-domain'; -import { output } from './lib/misc'; import { SOURCE_DIR } from './constants/dir'; import { processLine } from './lib/process-line'; +import { DomainsetOutput } from './lib/create-file-new'; const getS3OSSDomainsPromise = (async (): Promise => { const trie = createTrie( @@ -77,31 +74,24 @@ export const buildCdnDownloadConf = task(require.main === module, __filename)(as appendArrayInPlace(downloadDomainSet, steamDomainSet); return Promise.all([ - createRuleset( - span, - 'Sukka\'s Ruleset - CDN Domains', - [ + new DomainsetOutput(span, 'cdn') + .withTitle('Sukka\'s Ruleset - CDN Domains') + .withDescription([ ...SHARED_DESCRIPTION, '', 'This file contains object storage and static assets CDN domains.' - ], - new Date(), - sortDomains(domainsetDeduper(cdnDomainsList)), - 'domainset', - output('cdn', 'domainset') - ), - createRuleset( - span, - 'Sukka\'s Ruleset - Large Files Hosting Domains', - [ + ]) + .addFromDomainset(cdnDomainsList) + .write(), + + new DomainsetOutput(span, 'download') + .withTitle('Sukka\'s Ruleset - Large Files Hosting Domains') + .withDescription([ ...SHARED_DESCRIPTION, '', 'This file contains domains for software updating & large file hosting.' - ], - new Date(), - sortDomains(domainsetDeduper(downloadDomainSet)), - 'domainset', - output('download', 'domainset') - ) + ]) + .addFromDomainset(downloadDomainSet) + .write() ]); }); diff --git a/Build/build-chn-cidr.ts b/Build/build-chn-cidr.ts index d4619349..1e39cd0b 100644 --- a/Build/build-chn-cidr.ts +++ b/Build/build-chn-cidr.ts @@ -1,5 +1,4 @@ import { fetchRemoteTextByLine } from './lib/fetch-text-by-line'; -import { createRuleset } from './lib/create-file'; import { processLineFromReadline } from './lib/process-line'; import { task } from './trace'; @@ -7,7 +6,7 @@ import { exclude } from 'fast-cidr-tools'; import { createMemoizedPromise } from './lib/memo-promise'; import { CN_CIDR_NOT_INCLUDED_IN_CHNROUTE, NON_CN_CIDR_INCLUDED_IN_CHNROUTE } from './constants/cidr'; import { appendArrayInPlace } from './lib/append-array-in-place'; -import { output } from './lib/misc'; +import { IPListOutput } from './lib/create-file-new'; export const getChnCidrPromise = createMemoizedPromise(async () => { const cidr4 = await processLineFromReadline(await fetchRemoteTextByLine('https://raw.githubusercontent.com/misakaio/chnroutes2/master/chnroutes.txt')); @@ -28,31 +27,22 @@ export const buildChnCidr = task(require.main === module, __filename)(async (spa '' ]; - // Can not use createRuleset here, as Clash support advanced ipset syntax return Promise.all([ - createRuleset( - span, - 'Sukka\'s Ruleset - Mainland China IPv4 CIDR', - [ + new IPListOutput(span, 'china_ip', false) + .withTitle('Sukka\'s Ruleset - Mainland China IPv4 CIDR') + .withDescription([ ...description, 'Data from https://misaka.io (misakaio @ GitHub)' - ], - new Date(), - filteredCidr4, - 'ipcidr', - output('china_ip', 'ip') - ), - createRuleset( - span, - 'Sukka\'s Ruleset - Mainland China IPv6 CIDR', - [ + ]) + .bulkAddCIDR4(filteredCidr4) + .write(), + new IPListOutput(span, 'china_ip_ipv6', false) + .withTitle('Sukka\'s Ruleset - Mainland China IPv6 CIDR') + .withDescription([ ...description, 'Data from https://github.com/gaoyifan/china-operator-ip' - ], - new Date(), - cidr6, - 'ipcidr6', - output('china_ip_ipv6', 'ip') - ) + ]) + .bulkAddCIDR6(cidr6) + .write() ]); }); diff --git a/Build/build-common.ts b/Build/build-common.ts index 0945323c..3539afd4 100644 --- a/Build/build-common.ts +++ b/Build/build-common.ts @@ -4,13 +4,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 { domainsetDeduper } from './lib/domain-deduper'; 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'; const MAGIC_COMMAND_SKIP = '# $ custom_build_script'; const MAGIC_COMMAND_TITLE = '# $ meta_title '; @@ -113,10 +113,8 @@ function transformDomainset(parentSpan: Span, sourcePath: string, relativePath: const res = await processFile(span, sourcePath); if (res === $skip) return; - const clashFileBasename = relativePath.slice(0, -path.extname(relativePath).length); - + const id = path.basename(relativePath).slice(0, -path.extname(relativePath).length); const [title, descriptions, lines] = res; - const deduped = domainsetDeduper(lines); let description: string[]; if (descriptions.length) { @@ -127,19 +125,11 @@ function transformDomainset(parentSpan: Span, sourcePath: string, relativePath: description = SHARED_DESCRIPTION; } - return createRuleset( - span, - title, - description, - new Date(), - deduped, - 'domainset', - [ - path.resolve(OUTPUT_SURGE_DIR, relativePath), - path.resolve(OUTPUT_CLASH_DIR, `${clashFileBasename}.txt`), - path.resolve(OUTPUT_SINGBOX_DIR, `${clashFileBasename}.json`) - ] - ); + return new DomainsetOutput(span, id) + .withTitle(title) + .withDescription(description) + .addFromDomainset(lines) + .write(); } ); } diff --git a/Build/build-speedtest-domainset.ts b/Build/build-speedtest-domainset.ts index 67b6cb61..fd6013c4 100644 --- a/Build/build-speedtest-domainset.ts +++ b/Build/build-speedtest-domainset.ts @@ -1,6 +1,4 @@ import path from 'node:path'; -import { createRuleset } from './lib/create-file'; -import { sortDomains } from './lib/stable-sort-domain'; import { Sema } from 'async-sema'; import { getHostname } from 'tldts'; @@ -10,8 +8,132 @@ import { SHARED_DESCRIPTION } from './lib/constants'; import { readFileIntoProcessedArray } from './lib/fetch-text-by-line'; import { TTL, deserializeArray, fsFetchCache, serializeArray, createCacheKey } from './lib/cache-filesystem'; -import { createTrie } from './lib/trie'; -import { output } from './lib/misc'; +import { DomainsetOutput } from './lib/create-file-new'; +import { OUTPUT_SURGE_DIR } from './constants/dir'; + +const KEYWORDS = [ + 'Hong Kong', + 'Taiwan', + 'China Telecom', + 'China Mobile', + 'China Unicom', + 'Japan', + 'Tokyo', + 'Singapore', + 'Korea', + 'Seoul', + 'Canada', + 'Toronto', + 'Montreal', + 'Los Ang', + 'San Jos', + 'Seattle', + 'New York', + 'Dallas', + 'Miami', + 'Berlin', + 'Frankfurt', + 'London', + 'Paris', + 'Amsterdam', + 'Moscow', + 'Australia', + 'Sydney', + 'Brazil', + 'Turkey' +]; + +const PREDEFINE_DOMAINS = [ + // speedtest.net + '.speedtest.net', + '.speedtestcustom.com', + '.ooklaserver.net', + '.speed.misaka.one', + '.speedtest.rt.ru', + '.speedtest.aptg.com.tw', + '.speedtest.gslnetworks.com', + '.speedtest.jsinfo.net', + '.speedtest.i3d.net', + '.speedtestkorea.com', + '.speedtest.telus.com', + '.speedtest.telstra.net', + '.speedtest.clouvider.net', + '.speedtest.idv.tw', + '.speedtest.frontier.com', + '.speedtest.orange.fr', + '.speedtest.centurylink.net', + '.srvr.bell.ca', + '.speedtest.contabo.net', + 'speedtest.hk.chinamobile.com', + 'speedtestbb.hk.chinamobile.com', + '.hizinitestet.com', + '.linknetspeedtest.net.br', + 'speedtest.rit.edu', + 'speedtest.ropa.de', + 'speedtest.sits.su', + 'speedtest.tigo.cr', + 'speedtest.upp.com', + '.speedtest.pni.tw', + '.speed.pfm.gg', + '.speedtest.faelix.net', + '.speedtest.labixe.net', + '.speedtest.warian.net', + '.speedtest.starhub.com', + '.speedtest.gibir.net.tr', + '.speedtest.ozarksgo.net', + '.speedtest.exetel.com.au', + '.speedtest.sbcglobal.net', + '.speedtest.leaptel.com.au', + '.speedtest.windstream.net', + '.speedtest.vodafone.com.au', + '.speedtest.rascom.ru', + '.speedtest.dchost.com', + '.speedtest.highnet.com', + '.speedtest.seattle.wa.limewave.net', + '.speedtest.optitel.com.au', + '.speednet.net.tr', + '.speedtest.angolacables.co.ao', + '.ookla-speedtest.fsr.com', + '.speedtest.comnet.com.tr', + '.speedtest.gslnetworks.com.au', + '.test.gslnetworks.com.au', + '.speedtest.gslnetworks.com', + '.speedtestunonet.com.br', + '.speedtest.alagas.net', + 'speedtest.surfshark.com', + '.speedtest.aarnet.net.au', + '.ookla.rcp.net', + '.ookla-speedtests.e2ro.com', + '.speedtest.com.sg', + '.ookla.ddnsgeek.com', + '.speedtest.pni.tw', + '.speedtest.cmcnetworks.net', + '.speedtestwnet.com.br', + // Cloudflare + '.speed.cloudflare.com', + // Wi-Fi Man + '.wifiman.com', + '.wifiman.me', + '.wifiman.ubncloud.com', + // Fast.com + '.fast.com', + // MacPaw + 'speedtest.macpaw.com', + // speedtestmaster + '.netspeedtestmaster.com', + // Google Search Result of "speedtest", powered by this + '.measurement-lab.org', + '.measurementlab.net', + // Google Fiber legacy speedtest site (new fiber speedtest use speedtestcustom.com) + '.speed.googlefiber.net', + // librespeed + '.backend.librespeed.org', + // Apple, + 'mensura.cdn-apple.com', // From netQuality command + // OpenSpeedtest + 'open.cachefly.net' // This is also used for openspeedtest server download + +]; const s = new Sema(2); const cacheKey = createCacheKey(__filename); @@ -85,170 +207,24 @@ const querySpeedtestApi = async (keyword: string): Promise> }; export const buildSpeedtestDomainSet = task(require.main === module, __filename)(async (span) => { - const domainTrie = createTrie( - [ - // speedtest.net - '.speedtest.net', - '.speedtestcustom.com', - '.ooklaserver.net', - '.speed.misaka.one', - '.speedtest.rt.ru', - '.speedtest.aptg.com.tw', - '.speedtest.gslnetworks.com', - '.speedtest.jsinfo.net', - '.speedtest.i3d.net', - '.speedtestkorea.com', - '.speedtest.telus.com', - '.speedtest.telstra.net', - '.speedtest.clouvider.net', - '.speedtest.idv.tw', - '.speedtest.frontier.com', - '.speedtest.orange.fr', - '.speedtest.centurylink.net', - '.srvr.bell.ca', - '.speedtest.contabo.net', - 'speedtest.hk.chinamobile.com', - 'speedtestbb.hk.chinamobile.com', - '.hizinitestet.com', - '.linknetspeedtest.net.br', - 'speedtest.rit.edu', - 'speedtest.ropa.de', - 'speedtest.sits.su', - 'speedtest.tigo.cr', - 'speedtest.upp.com', - '.speedtest.pni.tw', - '.speed.pfm.gg', - '.speedtest.faelix.net', - '.speedtest.labixe.net', - '.speedtest.warian.net', - '.speedtest.starhub.com', - '.speedtest.gibir.net.tr', - '.speedtest.ozarksgo.net', - '.speedtest.exetel.com.au', - '.speedtest.sbcglobal.net', - '.speedtest.leaptel.com.au', - '.speedtest.windstream.net', - '.speedtest.vodafone.com.au', - '.speedtest.rascom.ru', - '.speedtest.dchost.com', - '.speedtest.highnet.com', - '.speedtest.seattle.wa.limewave.net', - '.speedtest.optitel.com.au', - '.speednet.net.tr', - '.speedtest.angolacables.co.ao', - '.ookla-speedtest.fsr.com', - '.speedtest.comnet.com.tr', - '.speedtest.gslnetworks.com.au', - '.test.gslnetworks.com.au', - '.speedtest.gslnetworks.com', - '.speedtestunonet.com.br', - '.speedtest.alagas.net', - 'speedtest.surfshark.com', - '.speedtest.aarnet.net.au', - '.ookla.rcp.net', - '.ookla-speedtests.e2ro.com', - '.speedtest.com.sg', - '.ookla.ddnsgeek.com', - '.speedtest.pni.tw', - '.speedtest.cmcnetworks.net', - '.speedtestwnet.com.br', - // Cloudflare - '.speed.cloudflare.com', - // Wi-Fi Man - '.wifiman.com', - '.wifiman.me', - '.wifiman.ubncloud.com', - // Fast.com - '.fast.com', - // MacPaw - 'speedtest.macpaw.com', - // speedtestmaster - '.netspeedtestmaster.com', - // Google Search Result of "speedtest", powered by this - '.measurement-lab.org', - '.measurementlab.net', - // Google Fiber legacy speedtest site (new fiber speedtest use speedtestcustom.com) - '.speed.googlefiber.net', - // librespeed - '.backend.librespeed.org', - // Apple, - 'mensura.cdn-apple.com', // From netQuality command - // OpenSpeedtest - 'open.cachefly.net' // This is also used for openspeedtest server download - ], - true - ); + const output = new DomainsetOutput(span, 'speedtest') + .withTitle('Sukka\'s Ruleset - Speedtest Domains') + .withDescription([ + ...SHARED_DESCRIPTION, + '', + 'This file contains common speedtest endpoints.' + ]) + .addFromDomainset(PREDEFINE_DOMAINS) + .addFromDomainset(await readFileIntoProcessedArray(path.resolve(OUTPUT_SURGE_DIR, 'domainset/speedtest.conf'))); - await span.traceChildAsync( - 'fetch previous speedtest domainset', - async () => { - try { - ( - await readFileIntoProcessedArray(path.resolve(__dirname, '../List/domainset/speedtest.conf')) - ) .forEach(line => { - const hn = getHostname(line, { detectIp: false, validateHostname: true }); - if (hn) { - domainTrie.add(hn); - } - }); - } catch { } - } - ); - - await Promise.all([ - 'Hong Kong', - 'Taiwan', - 'China Telecom', - 'China Mobile', - 'China Unicom', - 'Japan', - 'Tokyo', - 'Singapore', - 'Korea', - 'Seoul', - 'Canada', - 'Toronto', - 'Montreal', - 'Los Ang', - 'San Jos', - 'Seattle', - 'New York', - 'Dallas', - 'Miami', - 'Berlin', - 'Frankfurt', - 'London', - 'Paris', - 'Amsterdam', - 'Moscow', - 'Australia', - 'Sydney', - 'Brazil', - 'Turkey' - ].map((keyword) => span.traceChildAsync( + await Promise.all(KEYWORDS.map((keyword) => span.traceChildAsync( `fetch speedtest endpoints: ${keyword}`, () => querySpeedtestApi(keyword) ).then(hostnameGroup => hostnameGroup.forEach(hostname => { if (hostname) { - domainTrie.add(hostname); + output.addDomain(hostname); } })))); - const deduped = span.traceChildSync('sort result', () => sortDomains(domainTrie.dump())); - - const description = [ - ...SHARED_DESCRIPTION, - '', - 'This file contains common speedtest endpoints.' - ]; - - return createRuleset( - span, - 'Sukka\'s Ruleset - Speedtest Domains', - description, - new Date(), - deduped, - 'domainset', - output('speedtest', 'domainset') - ); + return output.write(); }); diff --git a/Build/lib/create-file-new.ts b/Build/lib/create-file-new.ts new file mode 100644 index 00000000..955cf9c4 --- /dev/null +++ b/Build/lib/create-file-new.ts @@ -0,0 +1,249 @@ +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 { sortDomains } from './stable-sort-domain'; +import { createTrie } from './trie'; +import { invariant } from 'foxact/invariant'; +import { OUTPUT_CLASH_DIR, OUTPUT_SINGBOX_DIR, OUTPUT_SURGE_DIR } from '../constants/dir'; +import stringify from 'json-stringify-pretty-compact'; +import { appendArrayInPlace } from './append-array-in-place'; + +abstract class RuleOutput { + protected domainTrie = createTrie(null, true); + protected domainKeywords = new Set(); + protected domainWildcard = new Set(); + protected ipcidr = new Set(); + protected ipcidrNoResolve = new Set(); + protected ipcidr6 = new Set(); + protected ipcidr6NoResolve = new Set(); + protected otherRules = new Set(); + protected abstract type: 'domainset' | 'non_ip' | 'ip'; + + protected pendingPromise = Promise.resolve(); + + static jsonToLines(this: void, json: unknown): string[] { + return stringify(json).split('\n'); + } + + constructor( + protected readonly span: Span, + protected readonly id: string + ) {} + + protected title: string | null = null; + withTitle(title: string) { + this.title = title; + return this; + } + + protected description: string[] | readonly string[] | null = null; + withDescription(description: string[] | readonly string[]) { + this.description = description; + return this; + } + + protected date = new Date(); + withDate(date: Date) { + this.date = date; + return this; + } + + protected apexDomainMap: Map | null = null; + protected subDomainMap: Map | null = null; + withDomainMap(apexDomainMap: Map, subDomainMap: Map) { + this.apexDomainMap = apexDomainMap; + this.subDomainMap = subDomainMap; + return this; + } + + addDomain(domain: string) { + this.domainTrie.add(domain); + return this; + } + + addDomainSuffix(domain: string) { + this.domainTrie.add(domain[0] === '.' ? domain : '.' + domain); + return this; + } + + bulkAddDomainSuffix(domains: string[]) { + for (let i = 0, len = domains.length; i < len; i++) { + this.addDomainSuffix(domains[i]); + } + return this; + } + + addDomainKeyword(keyword: string) { + this.domainKeywords.add(keyword); + return this; + } + + addDomainWildcard(wildcard: string) { + this.domainWildcard.add(wildcard); + return this; + } + + private async addFromDomainsetPromise(source: AsyncIterable | Iterable | string[]) { + for await (const line of source) { + if (line[0] === '.') { + this.addDomainSuffix(line); + } else { + this.addDomain(line); + } + } + } + + addFromDomainset(source: AsyncIterable | Iterable | string[]) { + this.pendingPromise = this.pendingPromise.then(() => this.addFromDomainsetPromise(source)); + return this; + } + + async addFromRuleset(source: AsyncIterable | Iterable) { + for await (const line of source) { + const [type, value, arg] = line.split(','); + switch (type) { + case 'DOMAIN': + this.addDomain(value); + break; + case 'DOMAIN-SUFFIX': + this.addDomainSuffix(value); + break; + case 'DOMAIN-KEYWORD': + this.addDomainKeyword(value); + break; + case 'DOMAIN-WILDCARD': + this.addDomainWildcard(value); + break; + case 'IP-CIDR': + (arg === 'no-resolve' ? this.ipcidrNoResolve : this.ipcidr).add(value); + break; + case 'IP-CIDR6': + (arg === 'no-resolve' ? this.ipcidr6NoResolve : this.ipcidr6).add(value); + break; + default: + this.otherRules.add(line); + break; + } + } + return this; + } + + bulkAddCIDR4(cidr: string[]) { + for (let i = 0, len = cidr.length; i < len; i++) { + this.ipcidr.add(cidr[i]); + } + return this; + } + + bulkAddCIDR6(cidr: string[]) { + for (let i = 0, len = cidr.length; i < len; i++) { + this.ipcidr6.add(cidr[i]); + } + return this; + } + + abstract write(): Promise; +} + +export class DomainsetOutput extends RuleOutput { + protected type = 'domainset' as const; + + async write() { + await this.pendingPromise; + + invariant(this.title, 'Missing title'); + invariant(this.description, 'Missing description'); + + const sorted = sortDomains(this.domainTrie.dump(), this.apexDomainMap, this.subDomainMap); + sorted.push('this_ruleset_is_made_by_sukkaw.ruleset.skk.moe'); + + const surge = sorted; + const clash = surgeDomainsetToClashDomainset(sorted); + const singbox = RuleOutput.jsonToLines(surgeDomainsetToSingbox(sorted)); + + 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') + ) + ]); + } +} + +export class IPListOutput extends RuleOutput { + protected type = 'ip' as const; + + constructor(span: Span, id: string, private readonly clashUseRule = true) { + super(span, id); + } + + async write() { + await this.pendingPromise; + + invariant(this.title, 'Missing title'); + invariant(this.description, 'Missing description'); + + const sorted4 = Array.from(this.ipcidr); + const sorted6 = Array.from(this.ipcidr6); + const merged = appendArrayInPlace(appendArrayInPlace([], sorted4), sorted6); + + const surge = sorted4.map(i => `IP-CIDR,${i}`); + appendArrayInPlace(surge, sorted6.map(i => `IP-CIDR6,${i}`)); + surge.push('DOMAIN,this_ruleset_is_made_by_sukkaw.ruleset.skk.moe'); + + const clash = this.clashUseRule ? surge : merged; + const singbox = RuleOutput.jsonToLines(ipCidrListToSingbox(merged)); + + 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 9d557d61..166843fb 100644 --- a/Build/lib/create-file.ts +++ b/Build/lib/create-file.ts @@ -7,7 +7,7 @@ import fs from 'node:fs'; import { fastStringArrayJoin, writeFile } from './misc'; import { readFileByLine } from './fetch-text-by-line'; import stringify from 'json-stringify-pretty-compact'; -import { ipCidrListToSingbox, surgeDomainsetToSingbox, surgeRulesetToSingbox } from './singbox'; +import { surgeDomainsetToSingbox, surgeRulesetToSingbox } from './singbox'; import { createTrie } from './trie'; import { pack, unpackFirst, unpackSecond } from './bitwise'; import { asyncWriteToStream } from './async-write-to-stream'; @@ -23,8 +23,7 @@ export const fileEqual = async (linesA: string[], source: AsyncIterable) if (index > linesA.length - 1) { if (index === linesA.length && lineB === '') { - index--; - continue; + return true; } // The file becomes smaller return false; @@ -51,7 +50,7 @@ export const fileEqual = async (linesA: string[], source: AsyncIterable) } } - if (index !== linesA.length - 1) { + if (index < linesA.length - 1) { // The file becomes larger return false; } @@ -96,7 +95,7 @@ export async function compareAndWriteFile(span: Span, linesA: string[], filePath }); } -const withBannerArray = (title: string, description: string[] | readonly string[], date: Date, content: string[]) => { +export const withBannerArray = (title: string, description: string[] | readonly string[], date: Date, content: string[]) => { return [ '#########################################', `# ${title}`, @@ -191,7 +190,7 @@ const MARK = 'this_ruleset_is_made_by_sukkaw.ruleset.skk.moe'; export const createRuleset = ( parentSpan: Span, title: string, description: string[] | readonly string[], date: Date, content: string[], - type: 'ruleset' | 'domainset' | 'ipcidr' | 'ipcidr6', + type: 'ruleset' | 'domainset', [surgePath, clashPath, singBoxPath, _clashMrsPath]: readonly [ surgePath: string, clashPath: string, @@ -210,12 +209,6 @@ export const createRuleset = ( case 'ruleset': _surgeContent = [`DOMAIN,${MARK}`, ...processRuleSet(content)]; break; - case 'ipcidr': - _surgeContent = [`DOMAIN,${MARK}`, ...processRuleSet(content.map(i => `IP-CIDR,${i}`))]; - break; - case 'ipcidr6': - _surgeContent = [`DOMAIN,${MARK}`, ...processRuleSet(content.map(i => `IP-CIDR6,${i}`))]; - break; default: throw new TypeError(`Unknown type: ${type}`); } @@ -232,10 +225,6 @@ export const createRuleset = ( case 'ruleset': _clashContent = [`DOMAIN,${MARK}`, ...surgeRulesetToClashClassicalTextRuleset(processRuleSet(content))]; break; - case 'ipcidr': - case 'ipcidr6': - _clashContent = content; - break; default: throw new TypeError(`Unknown type: ${type}`); } @@ -250,10 +239,6 @@ export const createRuleset = ( case 'ruleset': _singBoxContent = surgeRulesetToSingbox([`DOMAIN,${MARK}`, ...processRuleSet(content)]); break; - case 'ipcidr': - case 'ipcidr6': - _singBoxContent = ipCidrListToSingbox(content); - break; default: throw new TypeError(`Unknown type: ${type}`); }