-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
157 additions
and
140 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import fs from 'node:fs/promises' | ||
import { existsSync as fileExistsSync } from 'node:fs' | ||
import path from 'node:path' | ||
import { fileURLToPath } from 'node:url' | ||
import Readline from 'node:readline/promises' | ||
import debug from 'debug' | ||
import { stringify as yamlStringify } from 'yaml' | ||
|
||
const debugLogger = debug('v2ray-2-clash-rule:convetor') | ||
const __dirname = path.dirname(fileURLToPath(import.meta.url)) | ||
function isFullDomainRule(rule: string) { | ||
return rule.startsWith('full:') | ||
} | ||
function isKeywordRule(rule: string) { | ||
return rule.startsWith('keyword:') | ||
} | ||
function isRegexDomainRule(rule: string) { | ||
return rule.startsWith('regexp:') | ||
} | ||
|
||
function isIncludeRule(rule: string) { | ||
return rule.startsWith('include:') | ||
} | ||
|
||
function isInvalidRule(rule?: string) { | ||
return !rule || !rule.trim() || rule.startsWith('#') || rule.includes('@') | ||
} | ||
|
||
interface V2RayRules { | ||
fullDomain: string[] | ||
subdomain: string[] | ||
keyword: string[] | ||
} | ||
|
||
interface ClashRule { | ||
payload: string[] | ||
type: 'classic' | 'domain' | ||
} | ||
|
||
async function parseV2rayRuleFile(v2rayRuleFilePath: string) { | ||
const openedFile = await fs.open(path.resolve(v2rayRuleFilePath), 'r') | ||
const fileReadStream = openedFile.createReadStream() | ||
const rl = Readline.createInterface({ | ||
input: fileReadStream, | ||
}) | ||
const result: V2RayRules = { | ||
fullDomain: Array<string>(), | ||
subdomain: Array<string>(), | ||
keyword: Array<string>(), | ||
} | ||
for await (let rule of rl) { | ||
const commentIndex = rule.indexOf('#') | ||
if (commentIndex >= 0) | ||
rule = rule.slice(0, commentIndex).trim() | ||
|
||
if (isInvalidRule(rule)) { | ||
debugLogger('ignore invalid rule: ', rule) | ||
continue | ||
} | ||
else if (isFullDomainRule(rule)) { | ||
result.fullDomain.push(rule.slice(5).trim()) | ||
} | ||
else if (isKeywordRule(rule)) { | ||
result.keyword.push(rule.slice(8).trim()) | ||
} | ||
else if (isRegexDomainRule(rule)) { | ||
debugLogger('ignore regex rule: ', rule) | ||
} | ||
else if (isIncludeRule(rule)) { | ||
const includeFilePath = path.resolve(path.dirname(v2rayRuleFilePath), rule.slice(8).trim()) | ||
const includeResult = await parseV2rayRuleFile(includeFilePath) | ||
result.fullDomain = result.fullDomain.concat(includeResult.fullDomain) | ||
result.subdomain = result.subdomain.concat(includeResult.subdomain) | ||
result.keyword = result.keyword.concat(includeResult.keyword) | ||
} | ||
else { | ||
// Subdomain begins with `domain:`, followed by a valid domain name. The prefix `domain:` may be omitted. | ||
if (rule.startsWith('domain:')) | ||
result.subdomain.push(rule.slice(7).trim()) | ||
else | ||
result.subdomain.push(rule.trim()) | ||
} | ||
} | ||
fileReadStream.close() | ||
return result | ||
} | ||
|
||
function convertV2rayRuleToClashRule(v2rayRule: V2RayRules): ClashRule { | ||
const result: ClashRule = { | ||
payload: Array<string>(), | ||
type: 'domain', | ||
} | ||
const domainSuffixRule = Array<string>() | ||
const domainRule = Array<string>() | ||
const domainKeywordRule = Array<string>() | ||
const ruleType = domainKeywordRule.length > 0 ? 'classic' : 'domain' | ||
result.type = ruleType | ||
if (ruleType === 'classic') { | ||
for (const fullDomain of v2rayRule.fullDomain) | ||
domainRule.push(`DOMAIN,${fullDomain}`) | ||
|
||
for (const subdomain of v2rayRule.subdomain) | ||
domainSuffixRule.push(`DOMAIN-SUFFIX,${subdomain}`) | ||
|
||
for (const keyword of v2rayRule.keyword) | ||
domainKeywordRule.push(`DOMAIN-KEYWORD,${keyword}`) | ||
} | ||
else { | ||
for (const fullDomain of v2rayRule.fullDomain) | ||
domainRule.push(fullDomain) | ||
for (const subdomain of v2rayRule.subdomain) | ||
domainSuffixRule.push(`.${subdomain}`) | ||
} | ||
result.payload = result.payload.concat(domainSuffixRule) | ||
result.payload = result.payload.concat(domainRule) | ||
result.payload = result.payload.concat(domainKeywordRule) | ||
|
||
return result | ||
} | ||
|
||
export async function generateRuleList(domainDataDir: string, rulesDistPath: string) { | ||
const dataListFiles = await fs.readdir(domainDataDir, { withFileTypes: true }) | ||
|
||
if (!fileExistsSync(rulesDistPath)) | ||
await fs.mkdir(rulesDistPath, { recursive: true }) | ||
|
||
for (const file of dataListFiles) { | ||
if (file.isFile()) { | ||
const v2rayRules = await parseV2rayRuleFile(path.join(file.path, file.name)) | ||
const clashRules = convertV2rayRuleToClashRule(v2rayRules) | ||
const yamlObj = { | ||
payload: Array<string>(), | ||
} | ||
yamlObj.payload = clashRules.payload | ||
const yamlFile = await fs.open(path.join(rulesDistPath, `${file.name}.yaml`), 'w') | ||
const writeStream = yamlFile.createWriteStream() | ||
writeStream.write('# Generated by v2ray2clashrule\n') | ||
if (clashRules.type === 'domain') | ||
writeStream.write('# type: domain\n') | ||
else | ||
writeStream.write('# type: classic\n') | ||
writeStream.write(yamlStringify(yamlObj)) | ||
writeStream.end() | ||
writeStream.close() | ||
} | ||
} | ||
} | ||
|
||
export default { | ||
generateRuleList, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,152 +1,18 @@ | ||
import fs from 'node:fs/promises' | ||
import { existsSync as fileExistsSync } from 'node:fs' | ||
import path from 'node:path' | ||
import { fileURLToPath } from 'node:url' | ||
import Readline from 'node:readline/promises' | ||
import debug from 'debug' | ||
import { stringify as yamlStringify } from 'yaml' | ||
|
||
import { generateRuleList } from './convetor' | ||
import { generateRulePage } from './page' | ||
|
||
const debugLogger = debug('v2ray-2-clash-rule') | ||
const __dirname = path.dirname(fileURLToPath(import.meta.url)) | ||
const dataPath = path.resolve(__dirname, '../data') | ||
const rulesDistPath = path.resolve(__dirname, '../dist/rules') | ||
const pageDistPath = path.resolve(__dirname, '../dist/public') | ||
function isFullDomainRule(rule: string) { | ||
return rule.startsWith('full:') | ||
} | ||
function isKeywordRule(rule: string) { | ||
return rule.startsWith('keyword:') | ||
} | ||
function isRegexDomainRule(rule: string) { | ||
return rule.startsWith('regexp:') | ||
} | ||
|
||
function isIncludeRule(rule: string) { | ||
return rule.startsWith('include:') | ||
} | ||
|
||
function isInvalidRule(rule?: string) { | ||
return !rule || !rule.trim() || rule.startsWith('#') || rule.includes('@') | ||
} | ||
|
||
interface V2RayRules { | ||
fullDomain: string[] | ||
subdomain: string[] | ||
keyword: string[] | ||
} | ||
|
||
interface ClashRule { | ||
payload: string[] | ||
type: 'classic' | 'domain' | ||
} | ||
|
||
async function parseV2rayRuleFile(fileFullPath: string) { | ||
const openedFile = await fs.open(path.resolve(fileFullPath), 'r') | ||
const fileReadStream = openedFile.createReadStream() | ||
const rl = Readline.createInterface({ | ||
input: fileReadStream, | ||
}) | ||
const result: V2RayRules = { | ||
fullDomain: Array<string>(), | ||
subdomain: Array<string>(), | ||
keyword: Array<string>(), | ||
} | ||
for await (let rule of rl) { | ||
const commentIndex = rule.indexOf('#') | ||
if (commentIndex >= 0) | ||
rule = rule.slice(0, commentIndex).trim() | ||
const v2rayRuleDataPath = path.resolve(__dirname, '../data') | ||
|
||
if (isInvalidRule(rule)) { | ||
debugLogger('ignore invalid rule: ', rule) | ||
continue | ||
} | ||
else if (isFullDomainRule(rule)) { | ||
result.fullDomain.push(rule.slice(5).trim()) | ||
} | ||
else if (isKeywordRule(rule)) { | ||
result.keyword.push(rule.slice(8).trim()) | ||
} | ||
else if (isRegexDomainRule(rule)) { | ||
debugLogger('ignore regex rule: ', rule) | ||
} | ||
else if (isIncludeRule(rule)) { | ||
const includeFilePath = path.resolve(dataPath, rule.slice(8).trim()) | ||
const includeResult = await parseV2rayRuleFile(includeFilePath) | ||
result.fullDomain = result.fullDomain.concat(includeResult.fullDomain) | ||
result.subdomain = result.subdomain.concat(includeResult.subdomain) | ||
result.keyword = result.keyword.concat(includeResult.keyword) | ||
} | ||
else { | ||
// Subdomain begins with `domain:`, followed by a valid domain name. The prefix `domain:` may be omitted. | ||
if (rule.startsWith('domain:')) | ||
result.subdomain.push(rule.slice(7).trim()) | ||
else | ||
result.subdomain.push(rule.trim()) | ||
} | ||
} | ||
fileReadStream.close() | ||
return result | ||
} | ||
|
||
function convertV2rayRuleToClashRule(v2rayRule: V2RayRules): ClashRule { | ||
const result: ClashRule = { | ||
payload: Array<string>(), | ||
type: 'domain', | ||
} | ||
const domainSuffixRule = Array<string>() | ||
const domainRule = Array<string>() | ||
const domainKeywordRule = Array<string>() | ||
const ruleType = domainKeywordRule.length > 0 ? 'classic' : 'domain' | ||
result.type = ruleType | ||
if (ruleType === 'classic') { | ||
for (const fullDomain of v2rayRule.fullDomain) | ||
domainRule.push(`DOMAIN,${fullDomain}`) | ||
|
||
for (const subdomain of v2rayRule.subdomain) | ||
domainSuffixRule.push(`DOMAIN-SUFFIX,${subdomain}`) | ||
|
||
for (const keyword of v2rayRule.keyword) | ||
domainKeywordRule.push(`DOMAIN-KEYWORD,${keyword}`) | ||
} | ||
else { | ||
for (const fullDomain of v2rayRule.fullDomain) | ||
domainRule.push(fullDomain) | ||
for (const subdomain of v2rayRule.subdomain) | ||
domainSuffixRule.push(`.${subdomain}`) | ||
} | ||
result.payload = result.payload.concat(domainSuffixRule) | ||
result.payload = result.payload.concat(domainRule) | ||
result.payload = result.payload.concat(domainKeywordRule) | ||
|
||
return result | ||
} | ||
|
||
const dataListFiles = await fs.readdir(dataPath, { withFileTypes: true }) | ||
|
||
if (!fileExistsSync(rulesDistPath)) | ||
await fs.mkdir(rulesDistPath, { recursive: true }) | ||
const pageDistPath = path.resolve(__dirname, '../dist/public') | ||
const rulesDistPath = path.resolve(__dirname, '../dist/rules') | ||
|
||
for (const file of dataListFiles) { | ||
if (file.isFile()) { | ||
const v2rayRules = await parseV2rayRuleFile(path.join(file.path, file.name)) | ||
const clashRules = convertV2rayRuleToClashRule(v2rayRules) | ||
const yamlObj = { | ||
payload: Array<string>(), | ||
} | ||
yamlObj.payload = clashRules.payload | ||
const yamlFile = await fs.open(path.join(rulesDistPath, `${file.name}.yaml`), 'w') | ||
const writeStream = yamlFile.createWriteStream() | ||
writeStream.write('# Generated by v2ray2clashrule\n') | ||
if (clashRules.type === 'domain') | ||
writeStream.write('# type: domain\n') | ||
else | ||
writeStream.write('# type: classic\n') | ||
writeStream.write(yamlStringify(yamlObj)) | ||
writeStream.end() | ||
writeStream.close() | ||
} | ||
} | ||
await generateRuleList(v2rayRuleDataPath, rulesDistPath) | ||
debugLogger('generate rule list done') | ||
await generateRulePage(rulesDistPath, pageDistPath) | ||
debugLogger('generate rule page done') |