diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 9e8432814..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: 2 -updates: - - package-ecosystem: github-actions - directory: / - target-branch: main - schedule: - interval: weekly - open-pull-requests-limit: 5 - groups: - github-actions-minor-patch: - patterns: - - '*' - update-types: - - minor - - patch - commit-message: - prefix: deps(actions) - rebase-strategy: auto diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 3f6f983f6..000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,48 +0,0 @@ -## 变更说明 - - - -## 变更类型 - -- [ ] 自定义域名规则 -- [ ] 自定义 IP 规则 -- [ ] 上游来源或基线 -- [ ] 构建、解析或测试逻辑 -- [ ] 文档或治理 - -## 来源与人工许可评审 - -- 来源 URL / 官方说明: -- 是否为提交者原创: -- 第三方许可证、条款、授权或“未知”: -- [ ] 已核对 `NOTICE`、`THIRD_PARTY_NOTICES.md` 及相关配置/网络输入 -- [ ] 未把公开 URL、官方来源、`trust`、摘要或 CI 通过当作再分发许可 -- [ ] 维护者已人工确认许可处置;若尚未确认,本 PR 不合并、不发布相关内容 - -> 自动化不解析许可文档,也不会因“未知”状态自动阻断。 - -## 规则与产物 - -- [ ] 新文件名只包含小写字母、数字和连字符 -- [ ] 已按新增名称检查的实际范围审查同类型五平台目标,并补充人工检查 -- [ ] 已说明五平台产物和降级行为变化 -- [ ] 修改 `emby-cn` / `emby` 时同步核对三条精确冲突关系并验证前者优先 -- [ ] 已考虑 `sources/custom/ip` 可不存在或为空 -- [ ] 未提交 `.output/`、`.tmp/`、`.bin/`、凭据或本机缓存 - -## 审计信息 - -- [ ] 已说明 `upstream-summary` / 结构清单的预期变化,并未称其为完整来源追溯记录 -- [ ] 涉及 Fake-IP 时已确认源仍为 `sources/custom/domain/fakeip-filter.list`、由自定义构建生成,且未引入第三方 URL、同步步骤或预编译下载 -- [ ] 已考虑自定义 `fakeip-filter` 不在主上游摘要中 -- [ ] 已知 `build-summary.json` 由成功构建事务生成并受 manifest 绑定,独立自定义构建不生成 - -## 验证 - -- [ ] `make validate` -- [ ] `make preflight` -- [ ] 涉及二进制时运行 `make build-custom` -- [ ] 输出行为变化时更新夹具和测试 -- [ ] 没有绕过产物守卫(artifact guard)或发布树检查 - -测试结果、环境或未运行原因: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 37ee509ad..b4074ba86 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -28,76 +28,5 @@ jobs: - name: Prepare scripts run: find scripts -type f \( -name '*.sh' -o -name '*.py' \) -exec chmod +x {} + - - name: Run preflight validation and text build - run: REQUIRE_SHELLCHECK=1 make preflight - - release-candidate: - if: github.base_ref == 'main' - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: '3.11' - - - name: Select release candidate scope - id: scope - env: - EVENT_NAME: pull_request - BEFORE_SHA: ${{ github.event.pull_request.base.sha }} - CURRENT_SHA: ${{ github.sha }} - run: ./scripts/commands/select-build-scope.sh - - - name: Show selected release candidate scope - run: echo "${{ steps.scope.outputs.scope }} - ${{ steps.scope.outputs.reason }}" - - - name: Prepare scripts - if: steps.scope.outputs.scope != 'none' - run: find scripts -type f \( -name '*.sh' -o -name '*.py' \) -exec chmod +x {} + - - - name: Resolve tool versions - if: steps.scope.outputs.scope != 'none' - id: core_versions - run: bash ./scripts/commands/resolve-tool-versions.sh - - - name: Restore verified tool cache - if: steps.scope.outputs.scope != 'none' - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: .bin - key: rules-tools-v3-${{ steps.core_versions.outputs.sing_box_version }}-${{ steps.core_versions.outputs.mihomo_version }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('config/tools-lock.json') }} - - - name: Build and verify release candidate - if: steps.scope.outputs.scope != 'none' - env: - RULES_BUILD_SCOPE: ${{ steps.scope.outputs.scope }} - RULES_CONFLICT_BASE_SHA: ${{ steps.scope.outputs.base_sha }} - ARTIFACT_GENERATION_ID: pr-${{ github.event.pull_request.number }}-${{ github.run_attempt }} - ARTIFACT_BUILD_ID: ${{ github.run_id }} - ARTIFACT_SOURCE_SHA: ${{ github.sha }} - run: ./scripts/commands/build-artifacts-transaction.sh - - - name: Show release candidate summary - if: steps.scope.outputs.scope != 'none' - run: | - cat .output/build-summary.json - if [ -f .output/upstream-summary.json ]; then - cat .output/upstream-summary.json - fi - - - name: Upload failure diagnostics - if: failure() && steps.scope.outputs.scope != 'none' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-candidate-diagnostics-${{ github.run_id }}-${{ github.run_attempt }} - path: .artifacts/diagnostics/ - include-hidden-files: true - if-no-files-found: warn - retention-days: 7 + - name: Quick check + run: REQUIRE_SHELLCHECK=1 make lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c85b0c640..0ba09719d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,96 +1,37 @@ -# 贡献指南 +# 贡献说明 -本文件规定提交与评审要求。开发命令见 [开发指南](docs/DEVELOPMENT.md),实现边界见 [仓库结构](docs/STRUCTURE.md)。 - -## 开始之前 - -日常变更从临时分支向 `main` 提交 Pull Request,验证通过并合并后发布。提交第三方规则、数据或派生内容时,提供原始来源及可核验的许可、条款或授权;无法确认时明确写“未知”,并由维护者在合并和发布前人工决定处置。 - -许可审查不是自动检查。CI 不解析 `NOTICE` 或 `THIRD_PARTY_NOTICES.md`,也不会因“未知”状态自动失败。公开 URL、官方来源、`trust` 分类和 CI 通过都不能替代人工许可评审。 +本仓库以个人使用为主,主要维护自定义规则并整合上游数据。提交规则时保持内容清晰、可生成即可,不设置复杂审批流程。 ## 自定义规则 -域名规则位于 `sources/custom/domain/*.list`: +- 域名规则:`sources/custom/domain/*.list` +- IP 规则:`sources/custom/ip/*.list` +- 文件名使用小写字母、数字和连字符。 +- 规则类型和值保持规范格式,例如: ```text DOMAIN,api.example.com DOMAIN-SUFFIX,example.com -DOMAIN-KEYWORD,example -DOMAIN-REGEX,^(.+\.)?example\.com$ -``` - -IP 规则约定放在 `sources/custom/ip/*.list`: - -```text -IP-CIDR,1.2.3.0/24 +IP-CIDR,192.0.2.0/24 IP-CIDR6,2001:db8::/32 ``` -IP 目录可不存在或为空。文件名只使用小写字母、数字和连字符。 - -新增名称检查有明确边界:`build-custom.sh` 只对相对构建基准新加入的自定义源执行冲突判断,并只检查同一 `domain` 或 `ip` 类型在五个平台 `.output/` 中的目标路径。既有自定义源修改不经过同一新增名称判断,domain 与 ip 可同名。提交者仍应人工检查目标发布分支,避免语义混淆。 - -### `emby-cn` 与 `emby` - -两份规则存在三条经审核的精确覆盖关系,逐条记录在 `config/custom-rule-conflicts.json`。采用首条命中的客户端必须先加载 `emby-cn`,再加载 `emby`。修改时说明重叠变化;失效、重复或只指定文件对而不指定双方规则的宽泛豁免会被拒绝。 - -## 质量要求 - -`make lint` 使用与构建相同的严格域名解析器及严格 IP 解析器,要求规则类型和值已经是规范形式,并在所有自定义文件合并后检查 domain 精确项/后缀覆盖和 IP 重复/包含。除精确审核的关系外,跨文件冲突与同文件冲突都会失败。`build-custom.sh` 会在创建构建目录、检查已有产物或准备工具前先执行该校验。不要通过删除测试、扩大豁免、降低阈值或跳过守卫掩盖原因。 +不同文件可以有意重叠,以便分别绑定不同策略;同一文件内不应保留重复或被更宽规则覆盖的条目。使用首条命中的客户端时,细分规则应放在宽泛规则之前,例如先加载 `emby-cn`,再加载 `emby`。 -配置校验只验证结构和实现约束,例如支持的枚举、HTTPS URL、阈值及必需项目;配置通过不代表许可确认。修改 `config/upstreams.json` 时必须同步更新 `THIRD_PARTY_NOTICES.md`。 +## 修改与验证 -## 审计信息的使用 - -完整主上游同步生成 `.output/upstream-summary.json` 和域名 `rule-manifest.json`。前者记录健康结果与输入摘要,但不覆盖本仓库维护的自定义源(包括 `sources/custom/domain/fakeip-filter.list`)、完整转换链或 HTTP 响应身份,不能称为完整来源追溯记录。`fakeip-filter` 与其他自定义规则一起生成各平台产物,不得引入独立预编译文件或下载步骤。 - -成功的 `build-artifacts-transaction.sh` 在产物守卫之后、manifest 之前生成 `.output/build-summary.json`;manifest 绑定其文件摘要与嵌入内容。独立运行 `make build-custom*` 不生成该文件。所有摘要都不是许可证明。 - -## 本地验证 +日常修改可直接提交,也可通过 Pull Request 合并。建议至少运行: ```bash make validate -make preflight ``` -涉及二进制格式或编译器时再运行: +需要预览自定义文本产物时运行: ```bash -make build-custom +make build-custom-text ``` -`make preflight` 只组合 `make validate` 与文本自定义构建,不执行完整同步、产物守卫(artifact guard)或发布。本地缺少 ShellCheck 时可能跳过,CI 则设置 `REQUIRE_SHELLCHECK=1`。 - -完整支持环境与工具下载边界见 [开发指南](docs/DEVELOPMENT.md)。不要提交 `.output/`、`.tmp/`、`.artifacts/`、`.bin/`、凭据或本机缓存。 - -## 修改实现 - -修改生成逻辑时: - -- 为有意输出变化更新 `tests/fixtures/`; -- 新增或调整 `scripts/tests/test-*.sh`; -- 运行适用的验证和构建命令; -- 说明五平台产物及降级行为变化。 - -测试运行器会自动发现 `test-*.sh`,无需为此修改 Makefile。 - -## Pull Request 评审清单 - -- 说明目的、范围和用户可见影响; -- 提供来源及许可、条款、授权或“未知”状态; -- 由维护者人工确认第三方内容是否可合并与发布; -- 说明预期的平台产物和审计摘要变化; -- 记录已运行命令与结果; -- 涉及 `emby-cn` / `emby` 时确认细分规则优先; -- 不绕过严格校验、产物守卫(artifact guard)或发布树检查。 - -## 文档导航 +涉及完整上游同步或二进制格式时,由 `main` 分支的发布工作流统一构建。不要提交 `.output/`、`.tmp/`、`.artifacts/`、`.bin/`、凭据或本机缓存。 -- [`README.md`](README.md):用户入口、平台示例和关键边界 -- [`CONTRIBUTING.md`](CONTRIBUTING.md):贡献规则与人工评审清单 -- [`docs/README.md`](docs/README.md):文档职责与阅读路径 -- [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md):环境、命令和开发流程 -- [`docs/STRUCTURE.md`](docs/STRUCTURE.md):构建、产物、守卫和发布结构 -- [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md):常见失败与定位步骤 -- [`SECURITY.md`](SECURITY.md):安全支持范围和私密报告 -- [`NOTICE`](NOTICE) / [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md):许可范围与第三方状态 +新增上游时,在 `config/upstreams.json` 中填写来源和健康阈值,并按实际情况更新第三方来源说明。仓库代码使用 MIT 许可证;上游数据仍遵循各自条款。 diff --git a/Makefile b/Makefile index c91954e68..9b4b9cca3 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,11 @@ REQUIRE_SHELLCHECK ?= 0 SHELL_SCRIPTS := $(shell find scripts -type f -name '*.sh' | sort) PYTHON_TOOLS := $(shell find scripts/tools -type f -name '*.py' | sort) -.PHONY: help check-runtime lint lint-shell lint-python lint-config lint-rules test validate preflight build-custom build-custom-text clean +.PHONY: help check check-runtime lint lint-shell lint-python lint-config lint-rules test validate preflight build-custom build-custom-text clean help: @echo "Available targets:" + @echo " make check Quick syntax, config, and rule checks" @echo " make check-runtime Verify the supported Bash and Python runtimes" @echo " make lint Run shell, Python, and custom rule lint checks" @echo " make test Run all repository test scripts" @@ -17,6 +18,8 @@ help: @echo " make build-custom-text Build custom text artifacts without downloading binary compilers" @echo " make clean Remove generated artifacts and temporary files" +check: lint + check-runtime: @./scripts/commands/check-runtime.sh diff --git a/README.md b/README.md index 939c926e6..033b937a7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Repository code license

-本仓库从配置的上游和本地维护源生成 Surge、Quantumult X、Egern、sing-box 与 mihomo 规则。`main` 保存构建源码、配置和工作流;可直接使用的文件位于对应平台分支。 +本仓库用于个人维护自定义规则并整合上游数据,生成 Surge、Quantumult X、Egern、sing-box 与 mihomo 规则。`main` 保存源码、配置和工作流;可直接使用的文件位于对应平台分支。 > [!IMPORTANT] > 客户端不要引用 `main`。规则名称和区域分类沿用上游定义,不构成准确性、完整性或适用性保证。第三方内容不因格式转换而自动适用本仓库的 MIT 许可,详见 [`NOTICE`](NOTICE) 与 [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。 @@ -76,14 +76,15 @@ https://raw.githubusercontent.com/KuGouGo/Rules/mihomo/ip/google.mrs 本地命令要求 Bash 5+、Python 3.11+、GNU Make 和 Git。macOS 可使用 Homebrew Bash 与 Python 运行检查和文本构建;需要下载 sing-box 或 mihomo 的二进制构建只支持 lock 文件声明的 Linux 平台。 ```bash -make check-runtime +make check make validate make build-custom-text make preflight make clean ``` -- `make validate`:运行 Shell、Python、配置、自定义规则和测试检查。 +- `make check`:快速检查脚本语法、配置和自定义规则,适合日常改规则。 +- `make validate`:在快速检查基础上运行完整测试。 - `make build-custom-text`:生成自定义文本产物,不下载二进制工具。 - `make preflight`:执行 `make validate` 和自定义文本构建;不执行上游完整同步、二进制构建或发布。 - `make clean`:删除生成产物和临时文件,保留已校验的工具缓存。 @@ -93,9 +94,9 @@ make clean ## 仓库边界 - 长期分支只保留 `main` 与 `surge`、`quanx`、`egern`、`sing-box`、`mihomo` 五个产物分支。 -- 变更通过临时分支向 `main` 提交 Pull Request;PR 始终运行静态检查,并按改动路径选择跳过、custom 或 full 候选构建,临时分支在合并后删除。 +- 日常变更可直接提交,也可通过 Pull Request;PR 只运行基础校验,完整生成和发布由 `main` 工作流负责。 - `main` 在构建相关路径变化、定时任务运行或人工触发时执行发布工作流;产物内容没有变化时不会创建新的产物分支提交。 -- Dependabot 每周向 `main` 集中提交一个 GitHub Actions minor/patch 更新 PR;major 与安全更新保持独立,逐项评估。 +- 不启用自动依赖 PR;GitHub Actions 版本按需手工更新。 - `fakeip-filter` 是本仓库维护的文本源,不下载第三方预编译文件。 - 构建摘要、manifest 和 CI 通过都不是第三方许可证明。 - 规则按现状提供。使用者需自行判断策略、顺序和更新带来的影响,并保留可回退版本。 diff --git a/config/custom-rule-conflicts.json b/config/custom-rule-conflicts.json deleted file mode 100644 index 5ffd3c864..000000000 --- a/config/custom-rule-conflicts.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "version": 1, - "relations": [ - { - "family": "domain", - "covered": { - "file": "emby-cn.list", - "rule": "DOMAIN,cdn.zhezhi.art" - }, - "covering": { - "file": "emby.list", - "rule": "DOMAIN-SUFFIX,zhezhi.art" - } - }, - { - "family": "domain", - "covered": { - "file": "emby.list", - "rule": "DOMAIN,pdtvs.com" - }, - "covering": { - "file": "emby-cn.list", - "rule": "DOMAIN-SUFFIX,pdtvs.com" - } - }, - { - "family": "domain", - "covered": { - "file": "emby-cn.list", - "rule": "DOMAIN,precdns.cn2gias.uk" - }, - "covering": { - "file": "emby.list", - "rule": "DOMAIN-SUFFIX,cn2gias.uk" - } - } - ] -} diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index f2c8548a8..708d229df 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -21,6 +21,7 @@ make check-runtime ```bash make help +make check make lint make test make validate @@ -30,7 +31,8 @@ make preflight make clean ``` -- `make validate`:Shell 语法、可用时的 ShellCheck、Python 编译、配置、自定义规则和测试。 +- `make check` / `make lint`:快速检查 Shell、Python、配置和自定义规则。 +- `make validate`:快速检查后再运行完整测试。 - `make check-runtime`:验证当前 `PATH` 解析到 Bash 5+ 和 Python 3.11+。 - `make build-custom-text`:只生成自定义文本产物,不下载二进制编译器。 - `make build-custom`:生成自定义文本和二进制产物。 @@ -44,16 +46,16 @@ make clean CI 设置 `REQUIRE_SHELLCHECK=1`,本地缺少 ShellCheck 时的跳过不代表 CI 会通过。 -GitHub Actions 使用完整 commit SHA 固定版本,仓库测试拒绝 tag 或非完整 SHA 的 `uses:`。Dependabot 每周把 GitHub Actions 的 minor/patch 更新组合为一个以 `main` 为目标的 PR;major 与安全更新保持独立并逐项评估。合并后的临时分支由 GitHub 自动删除。 +GitHub Actions 使用完整 commit SHA 固定版本,按需手工更新。 ## 开发流程 -1. 从 `main` 创建临时分支,不手工编辑生成目录。 +1. 直接修改 `main` 或使用临时分支,不手工编辑生成目录。 2. 修改自定义源、配置、实现或测试夹具。 -3. 运行 `make preflight` 和适用的完整构建命令。 +3. 日常规则变更运行 `make check`;修改生成逻辑时再运行 `make validate` 或适用的构建命令。 4. 检查差异中没有 `.output/`、`.tmp/`、`.bin/`、凭据或无关格式化。 -5. 通过 Pull Request 合并到 `main`;PR 必须完成预检。候选构建按路径选择范围:仅文档和治理文件为 `none`,仅修改且不删除自定义源为 `custom`,构建脚本、配置、模板、测试或自定义源删除为 `full`。合并后的构建相关变更由 `main` 工作流更新五个平台分支。 -6. 按 [贡献指南](../CONTRIBUTING.md) 说明来源、人工许可评审状态、测试和产物影响。 +5. 可直接提交到 `main`,也可通过 Pull Request 合并。PR 只运行基础校验;完整候选构建不再重复执行。 +6. `main` 的发布工作流按变更范围生成并更新五个平台分支。 ## 自定义规则与名称 @@ -61,7 +63,7 @@ GitHub Actions 使用完整 commit SHA 固定版本,仓库测试拒绝 tag 或 新增名称冲突检查仅针对相对基准提交新加入的自定义源,且 domain 与 ip 分开检查五个平台的当前 `.output/` 目标路径。它不是全仓库名称注册表,也不覆盖既有自定义源修改。 -自定义源在全局范围检查 domain 精确项/后缀覆盖及 IP 重复/包含;类型和值必须已是规范形式。唯一允许的三条 `emby-cn` / `emby` 精确关系记录在 `config/custom-rule-conflicts.json`,失效、重复或文件对级宽泛豁免会失败。首条命中客户端必须先加载 `emby-cn`,再加载 `emby`。自定义构建会在创建构建目录和准备工具前执行该严格校验。 +自定义源会检查类型、值和同一文件内的重复/覆盖关系。不同文件允许有意重叠,以便绑定不同策略,不再维护逐条冲突审批清单。首条命中客户端应先加载细分规则,例如先加载 `emby-cn`,再加载 `emby`。 ## 摘要与许可评审 @@ -78,7 +80,7 @@ GitHub Actions 使用完整 commit SHA 固定版本,仓库测试拒绝 tag 或 ## 文档导航 - [`README.md`](../README.md):用户入口、平台示例和关键边界 -- [`CONTRIBUTING.md`](../CONTRIBUTING.md):贡献规则与人工评审清单 +- [`CONTRIBUTING.md`](../CONTRIBUTING.md):自定义规则格式与修改说明 - [`docs/README.md`](README.md):文档职责与阅读路径 - [`docs/DEVELOPMENT.md`](DEVELOPMENT.md):环境、命令和开发流程 - [`docs/STRUCTURE.md`](STRUCTURE.md):构建、产物、守卫和发布结构 diff --git a/docs/README.md b/docs/README.md index ef284fda3..8be4e47f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ - [开发指南](DEVELOPMENT.md):受支持环境、工具依赖、本地命令和开发流程。 - [仓库结构](STRUCTURE.md):同步、构建、审计文件、守卫和发布分支的真实边界。 - [故障排查](TROUBLESHOOTING.md):按症状定位构建、产物与客户端问题。 -- [贡献指南](../CONTRIBUTING.md):提交格式、来源说明和人工评审要求。 +- [贡献指南](../CONTRIBUTING.md):自定义规则格式和修改说明。 - [`NOTICE`](../NOTICE) 与 [第三方声明](../THIRD_PARTY_NOTICES.md):MIT 适用范围和第三方核对状态。 - [安全政策](../SECURITY.md):安全问题与普通质量问题的分界及私密报告渠道。 @@ -16,7 +16,7 @@ ## 文档导航 - [`README.md`](../README.md):用户入口、平台示例和关键边界 -- [`CONTRIBUTING.md`](../CONTRIBUTING.md):贡献规则与人工评审清单 +- [`CONTRIBUTING.md`](../CONTRIBUTING.md):自定义规则格式与修改说明 - [`docs/README.md`](README.md):文档职责与阅读路径 - [`docs/DEVELOPMENT.md`](DEVELOPMENT.md):环境、命令和开发流程 - [`docs/STRUCTURE.md`](STRUCTURE.md):构建、产物、守卫和发布结构 diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md index 040bd251f..1b0025e84 100644 --- a/docs/STRUCTURE.md +++ b/docs/STRUCTURE.md @@ -25,7 +25,7 @@ `fakeip-filter` 当前源为本仓库维护的 `sources/custom/domain/fakeip-filter.list`,由 `build-custom.sh` 与其他自定义规则一起生成五平台形式,不从网络下载预编译文件。`config/upstreams.json` 覆盖主上游网络输入;工具资产下载另由工具 lock 控制。 -`main` 是唯一长期源码与发布源分支,开发使用合并后删除的临时分支。发布分支为 `surge`、`quanx`、`egern`、`sing-box`、`mihomo`,只允许生成的 `README.md`、`domain/`、`ip/` 及平台对应扩展名。各分支 `README.md` 由 `templates/branch-readmes/` 生成,并直接包含 v2fly/domain-list-community 的完整 MIT 版权与许可通知;因此发布树无需新增独立许可证文件。模板变更属于构建触发路径。 +`main` 是唯一长期源码与发布源分支;日常修改可直接提交或使用临时分支。发布分支为 `surge`、`quanx`、`egern`、`sing-box`、`mihomo`,只允许生成的 `README.md`、`domain/`、`ip/` 及平台对应扩展名。各分支 `README.md` 由 `templates/branch-readmes/` 生成,并直接包含 v2fly/domain-list-community 的完整 MIT 版权与许可通知;因此发布树无需新增独立许可证文件。模板变更属于构建触发路径。 ## 审计文件 @@ -53,7 +53,7 @@ 因此该检查不覆盖既有自定义源修改、未出现在当前 `.output/` 的潜在未来上游名称,也不禁止 domain 与 ip 使用同一名称。它是防覆盖措施,不是全局命名守卫。 -这与源码语义冲突阶段相互独立:严格共享解析器先验证 canonical 类型和值,再把所有 custom domain 文件合并检查精确项/后缀覆盖,把所有 custom IP 文件合并检查重复/包含。`config/custom-rule-conflicts.json` 只能逐条描述真实覆盖关系;失效、重复或文件对级宽泛关系会失败。当前仅列出三条经审核的 `emby-cn` / `emby` 关系。`build-custom.sh` 在创建构建目录、读取已有产物或准备工具前运行该阶段。 +共享解析器先验证 canonical 类型和值,并检查同一自定义文件内的重复或覆盖关系。不同文件允许按策略需要有意重叠。`build-custom.sh` 在生成产物前运行该阶段。 ## 产物守卫(artifact guard)范围 @@ -75,7 +75,7 @@ sing-box 和 mihomo 只按 `config/tools-lock.json` 固定版本、tag commit ## 文档导航 - [`README.md`](../README.md):用户入口、平台示例和关键边界 -- [`CONTRIBUTING.md`](../CONTRIBUTING.md):贡献规则与人工评审清单 +- [`CONTRIBUTING.md`](../CONTRIBUTING.md):自定义规则格式与修改说明 - [`docs/README.md`](README.md):文档职责与阅读路径 - [`docs/DEVELOPMENT.md`](DEVELOPMENT.md):环境、命令和开发流程 - [`docs/STRUCTURE.md`](STRUCTURE.md):构建、产物、守卫和发布结构 diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 5af7d6e9d..793f14976 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -7,7 +7,7 @@ - `shellcheck not found`:本地默认可跳过,CI 强制要求;安装后重试。 - Python 编译失败:确认 `python3 --version` 为 3.11 或更高版本,再修复首个语法错误。 - 配置失败:检查 HTTPS URL、正整数阈值、必需项和支持的枚举。 -- 自定义规则失败:处理非 canonical 类型/值、跨文件或同文件的 domain 精确项/后缀覆盖、IP 重复/包含、正则或 CIDR 规范问题。若涉及豁免,只能在 `config/custom-rule-conflicts.json` 逐条记录真实关系;不要添加文件对级宽泛关系,失效和重复关系也会失败。 +- 自定义规则失败:处理非 canonical 类型/值、同文件内的 domain 精确项/后缀覆盖、IP 重复/包含、正则或 CIDR 规范问题。不同文件之间允许按策略需要重叠。 - 测试失败:运行 `bash scripts/tests/test-*.sh` 中对应的脚本,再检查夹具是否应有意更新。 ## `make check-runtime` 失败 @@ -42,7 +42,7 @@ make build-custom ## `emby-cn` 未按预期命中 -确认 `emby-cn` 位于 `emby` 之前,并绑定不同策略。两份规则当前有三条在 `config/custom-rule-conflicts.json` 逐条记录的精确覆盖关系;反向加载会让宽泛规则先命中。若 lint 报告该配置失效或出现新冲突,应核对具体双方规则,不要扩大为文件对级豁免。 +确认 `emby-cn` 位于 `emby` 之前,并绑定不同策略;反向加载会让宽泛规则先命中。 ## 上游同步或条目异常 @@ -77,7 +77,7 @@ CI 的失败事务会把 `.artifacts/diagnostics/` 上传为保留 7 天的 diag ## 文档导航 - [`README.md`](../README.md):用户入口、平台示例和关键边界 -- [`CONTRIBUTING.md`](../CONTRIBUTING.md):贡献规则与人工评审清单 +- [`CONTRIBUTING.md`](../CONTRIBUTING.md):自定义规则格式与修改说明 - [`docs/README.md`](README.md):文档职责与阅读路径 - [`docs/DEVELOPMENT.md`](DEVELOPMENT.md):环境、命令和开发流程 - [`docs/STRUCTURE.md`](STRUCTURE.md):构建、产物、守卫和发布结构 diff --git a/scripts/commands/build-custom.sh b/scripts/commands/build-custom.sh index 8337d5adf..ac1a9c89b 100755 --- a/scripts/commands/build-custom.sh +++ b/scripts/commands/build-custom.sh @@ -466,7 +466,8 @@ if [ "$TEXT_ONLY_MODE" -ne 1 ]; then # This point is after the last binary compile but before the controlled commit. inject_custom_build_failure late-binary fi -mapfile -t CONTROLLED_ARTIFACTS < <(controlled_artifact_paths) +controlled_artifact_paths > "$TMP_DIR/controlled-artifacts.list" +mapfile -t CONTROLLED_ARTIFACTS < "$TMP_DIR/controlled-artifacts.list" origin_args=(mark-custom "$ARTIFACT_ROOT" "$CUSTOM_DOMAIN_DIR" "$CUSTOM_IP_DIR") if [ "$TEXT_ONLY_MODE" -eq 1 ]; then origin_args+=(--text-only) diff --git a/scripts/commands/lint-custom-rules.sh b/scripts/commands/lint-custom-rules.sh index 0e30606b0..8f209ab62 100755 --- a/scripts/commands/lint-custom-rules.sh +++ b/scripts/commands/lint-custom-rules.sh @@ -6,7 +6,6 @@ cd "$ROOT" python3 "$ROOT/scripts/tools/lint-custom-rules.py" \ --domain-dir "$ROOT/sources/custom/domain" \ - --ip-dir "$ROOT/sources/custom/ip" \ - --conflicts "$ROOT/config/custom-rule-conflicts.json" + --ip-dir "$ROOT/sources/custom/ip" echo "custom rule lint passed" diff --git a/scripts/tests/run.sh b/scripts/tests/run.sh index 9b06c632e..cd4370d86 100755 --- a/scripts/tests/run.sh +++ b/scripts/tests/run.sh @@ -20,9 +20,10 @@ if ! [[ "$TEST_TIMEOUT_SECONDS" =~ ^[0-9]+([.][0-9]+)?$ ]] || [ "$TEST_TIMEOUT_S exit 2 fi -while IFS= read -r test_script; do +for test_script in "$TEST_DIR"/test-*.sh; do + [ -f "$test_script" ] || continue test_scripts+=("$test_script") -done < <(find "$TEST_DIR" -maxdepth 1 -type f -name 'test-*.sh' | sort) +done if [ "${#test_scripts[@]}" -eq 0 ]; then echo "no test scripts found in $TEST_DIR" >&2 diff --git a/scripts/tests/test-custom-rule-quality.sh b/scripts/tests/test-custom-rule-quality.sh index 66244c63b..ff2826b38 100755 --- a/scripts/tests/test-custom-rule-quality.sh +++ b/scripts/tests/test-custom-rule-quality.sh @@ -2,269 +2,63 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -cd "$ROOT" - +TOOL="$ROOT/scripts/tools/lint-custom-rules.py" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT -TOOL="$ROOT/scripts/tools/lint-custom-rules.py" -EMPTY_CONFLICTS="$TMP_DIR/empty-conflicts.json" -printf '%s\n' '{"version":1,"relations":[]}' > "$EMPTY_CONFLICTS" - -assert_lint_passes() { - local label="$1" - local domain_dir="$2" - local ip_dir="$3" - - if ! python3 "$TOOL" --domain-dir "$domain_dir" --ip-dir "$ip_dir" --conflicts "$EMPTY_CONFLICTS" >"$TMP_DIR/${label}.stdout" 2>"$TMP_DIR/${label}.stderr"; then - echo "test failed: expected lint to pass for $label" >&2 - cat "$TMP_DIR/${label}.stderr" >&2 - exit 1 - fi -} - -assert_lint_fails_with() { - local label="$1" - local expected="$2" - local domain_dir="$3" - local ip_dir="$4" - - if python3 "$TOOL" --domain-dir "$domain_dir" --ip-dir "$ip_dir" --conflicts "$EMPTY_CONFLICTS" >"$TMP_DIR/${label}.stdout" 2>"$TMP_DIR/${label}.stderr"; then - echo "test failed: expected lint to fail for $label" >&2 - exit 1 - fi - if ! grep -Fq "$expected" "$TMP_DIR/${label}.stderr"; then - echo "test failed: missing lint message for $label: $expected" >&2 - cat "$TMP_DIR/${label}.stderr" >&2 - exit 1 - fi +run_lint() { + python3 "$TOOL" --domain-dir "$1/domain" --ip-dir "$1/ip" } -make_case_dirs() { - local case_dir="$1" - mkdir -p "$case_dir/domain" "$case_dir/ip" +make_case() { + mkdir -p "$TMP_DIR/$1/domain" "$TMP_DIR/$1/ip" } -make_case_dirs "$TMP_DIR/pass" -cat > "$TMP_DIR/pass/domain/example.list" <<'EOF' +make_case valid +cat > "$TMP_DIR/valid/domain/example.list" <<'EOF' DOMAIN,api.example.com DOMAIN-SUFFIX,example.net DOMAIN-KEYWORD,emby DOMAIN-REGEX,^(.+\.)?example\.org$ EOF -cat > "$TMP_DIR/pass/ip/private.list" <<'EOF' -IP-CIDR,10.0.0.0/8 -IP-CIDR6,fc00::/7 -EOF -assert_lint_passes "pass" "$TMP_DIR/pass/domain" "$TMP_DIR/pass/ip" - -make_case_dirs "$TMP_DIR/fakeip-single-label" -printf '%s\n' 'DOMAIN-SUFFIX,lan' > "$TMP_DIR/fakeip-single-label/domain/fakeip-filter.list" -assert_lint_passes \ - "fakeip-single-label" \ - "$TMP_DIR/fakeip-single-label/domain" \ - "$TMP_DIR/fakeip-single-label/ip" - -make_case_dirs "$TMP_DIR/non-fakeip-single-label" -printf '%s\n' 'DOMAIN-SUFFIX,lan' > "$TMP_DIR/non-fakeip-single-label/domain/example.list" -assert_lint_fails_with \ - "non-fakeip-single-label" \ - "DOMAIN-SUFFIX value is too broad" \ - "$TMP_DIR/non-fakeip-single-label/domain" \ - "$TMP_DIR/non-fakeip-single-label/ip" - -make_case_dirs "$TMP_DIR/domain-coverage" -cat > "$TMP_DIR/domain-coverage/domain/example.list" <<'EOF' -DOMAIN-SUFFIX,example.com -DOMAIN,api.example.com -EOF -assert_lint_fails_with \ - "domain-coverage" \ - "DOMAIN,api.example.com is covered by DOMAIN-SUFFIX,example.com" \ - "$TMP_DIR/domain-coverage/domain" \ - "$TMP_DIR/domain-coverage/ip" - -make_case_dirs "$TMP_DIR/domain-exact-suffix-coverage" -cat > "$TMP_DIR/domain-exact-suffix-coverage/domain/example.list" <<'EOF' -DOMAIN-SUFFIX,example.com -DOMAIN,example.com -EOF -assert_lint_fails_with \ - "domain-exact-suffix-coverage" \ - "DOMAIN,example.com is covered by DOMAIN-SUFFIX,example.com" \ - "$TMP_DIR/domain-exact-suffix-coverage/domain" \ - "$TMP_DIR/domain-exact-suffix-coverage/ip" - -make_case_dirs "$TMP_DIR/domain-coverage-order" -cat > "$TMP_DIR/domain-coverage-order/domain/example.list" <<'EOF' -DOMAIN-SUFFIX,example.com -DOMAIN-SUFFIX,api.example.com -DOMAIN,www.api.example.com -EOF -assert_lint_fails_with \ - "domain-coverage-order" \ - "DOMAIN-SUFFIX,api.example.com is covered by DOMAIN-SUFFIX,example.com" \ - "$TMP_DIR/domain-coverage-order/domain" \ - "$TMP_DIR/domain-coverage-order/ip" - -make_case_dirs "$TMP_DIR/domain-regex" -cat > "$TMP_DIR/domain-regex/domain/example.list" <<'EOF' -DOMAIN-REGEX,[ -EOF -assert_lint_fails_with \ - "domain-regex" \ - "invalid DOMAIN-REGEX pattern" \ - "$TMP_DIR/domain-regex/domain" \ - "$TMP_DIR/domain-regex/ip" - -make_case_dirs "$TMP_DIR/ip-canonical" -cat > "$TMP_DIR/ip-canonical/ip/private.list" <<'EOF' -IP-CIDR,192.168.1.1/24 -EOF -assert_lint_fails_with \ - "ip-canonical" \ - "CIDR must be canonical; use 192.168.1.0/24 instead of 192.168.1.1/24" \ - "$TMP_DIR/ip-canonical/domain" \ - "$TMP_DIR/ip-canonical/ip" - -make_case_dirs "$TMP_DIR/ip-coverage" -cat > "$TMP_DIR/ip-coverage/ip/private.list" <<'EOF' -IP-CIDR,10.0.0.0/8 -IP-CIDR,10.1.0.0/16 -EOF -assert_lint_fails_with \ - "ip-coverage" \ - "IP-CIDR,10.1.0.0/16 is covered by IP-CIDR,10.0.0.0/8" \ - "$TMP_DIR/ip-coverage/domain" \ - "$TMP_DIR/ip-coverage/ip" - -make_case_dirs "$TMP_DIR/ip-coverage-order" -cat > "$TMP_DIR/ip-coverage-order/ip/private.list" <<'EOF' -IP-CIDR,10.0.0.0/8 -IP-CIDR,10.1.0.0/16 -IP-CIDR,10.1.2.0/24 -EOF -assert_lint_fails_with \ - "ip-coverage-order" \ - "IP-CIDR,10.1.0.0/16 is covered by IP-CIDR,10.0.0.0/8" \ - "$TMP_DIR/ip-coverage-order/domain" \ - "$TMP_DIR/ip-coverage-order/ip" - -make_case_dirs "$TMP_DIR/empty-file" -: > "$TMP_DIR/empty-file/domain/empty.list" -assert_lint_fails_with \ - "empty-file" \ - "has no effective rules" \ - "$TMP_DIR/empty-file/domain" \ - "$TMP_DIR/empty-file/ip" - -make_case_dirs "$TMP_DIR/invalid-name" -cat > "$TMP_DIR/invalid-name/domain/Bad_Name.list" <<'EOF' -DOMAIN,api.example.com +cat > "$TMP_DIR/valid/ip/example.list" <<'EOF' +IP-CIDR,192.0.2.0/24 +IP-CIDR6,2001:db8::/32 EOF -assert_lint_fails_with \ - "invalid-name" \ - "invalid custom rule filename" \ - "$TMP_DIR/invalid-name/domain" \ - "$TMP_DIR/invalid-name/ip" +run_lint "$TMP_DIR/valid" >/dev/null -make_case_dirs "$TMP_DIR/canonical-type" -printf '%s\n' 'domain,api.example.com' > "$TMP_DIR/canonical-type/domain/example.list" -assert_lint_fails_with \ - "canonical-type" \ - "rule type must be canonical; use DOMAIN instead of domain" \ - "$TMP_DIR/canonical-type/domain" \ - "$TMP_DIR/canonical-type/ip" - -make_case_dirs "$TMP_DIR/canonical-value" -printf '%s\n' 'DOMAIN,Api.Example.com' > "$TMP_DIR/canonical-value/domain/example.list" -printf '%s\n' 'ip_cidr,192.0.2.0/24' > "$TMP_DIR/canonical-value/ip/example.list" -assert_lint_fails_with \ - "canonical-value" \ - "DOMAIN value must be lowercase: Api.Example.com" \ - "$TMP_DIR/canonical-value/domain" \ - "$TMP_DIR/canonical-value/ip" -if ! grep -Fq "rule type must be canonical; use IP-CIDR instead of ip_cidr" "$TMP_DIR/canonical-value.stderr"; then - echo "test failed: missing strict IP rule type error" >&2 - cat "$TMP_DIR/canonical-value.stderr" >&2 +make_case invalid-domain +printf '%s\n' 'DOMAIN,Api.Example.com' > "$TMP_DIR/invalid-domain/domain/example.list" +if run_lint "$TMP_DIR/invalid-domain" > /dev/null 2>"$TMP_DIR/error"; then + echo "test failed: invalid domain passed" >&2 exit 1 fi +grep -Fq 'DOMAIN value must be lowercase' "$TMP_DIR/error" -make_case_dirs "$TMP_DIR/cross-domain" -printf '%s\n' 'DOMAIN-SUFFIX,example.com' > "$TMP_DIR/cross-domain/domain/base.list" -printf '%s\n' 'DOMAIN,api.example.com' > "$TMP_DIR/cross-domain/domain/specific.list" -assert_lint_fails_with \ - "cross-domain" \ - "DOMAIN,api.example.com is covered by DOMAIN-SUFFIX,example.com" \ - "$TMP_DIR/cross-domain/domain" \ - "$TMP_DIR/cross-domain/ip" - -make_case_dirs "$TMP_DIR/cross-ip" -printf '%s\n' 'IP-CIDR,10.0.0.0/8' > "$TMP_DIR/cross-ip/ip/base.list" -printf '%s\n' 'IP-CIDR,10.1.0.0/16' > "$TMP_DIR/cross-ip/ip/specific.list" -assert_lint_fails_with \ - "cross-ip" \ - "IP-CIDR,10.1.0.0/16 is covered by IP-CIDR,10.0.0.0/8" \ - "$TMP_DIR/cross-ip/domain" \ - "$TMP_DIR/cross-ip/ip" - -make_case_dirs "$TMP_DIR/cross-duplicates" -printf '%s\n' 'DOMAIN,api.example.com' > "$TMP_DIR/cross-duplicates/domain/a.list" -printf '%s\n' 'DOMAIN,api.example.com' > "$TMP_DIR/cross-duplicates/domain/b.list" -printf '%s\n' 'IP-CIDR,192.0.2.0/24' > "$TMP_DIR/cross-duplicates/ip/a.list" -printf '%s\n' 'IP-CIDR,192.0.2.0/24' > "$TMP_DIR/cross-duplicates/ip/b.list" -assert_lint_fails_with \ - "cross-duplicates" \ - "duplicate domain rule; first seen" \ - "$TMP_DIR/cross-duplicates/domain" \ - "$TMP_DIR/cross-duplicates/ip" -if ! grep -Fq "duplicate ip rule; first seen" "$TMP_DIR/cross-duplicates.stderr"; then - echo "test failed: missing cross-file duplicate IP error" >&2 - cat "$TMP_DIR/cross-duplicates.stderr" >&2 +make_case invalid-cidr +printf '%s\n' 'IP-CIDR,192.168.1.1/24' > "$TMP_DIR/invalid-cidr/ip/example.list" +if run_lint "$TMP_DIR/invalid-cidr" > /dev/null 2>"$TMP_DIR/error"; then + echo "test failed: non-canonical CIDR passed" >&2 exit 1 fi +grep -Fq 'CIDR must be canonical' "$TMP_DIR/error" -cat > "$TMP_DIR/cross-domain/allowed.json" <<'EOF' -{"version":1,"relations":[{"family":"domain","covered":{"file":"specific.list","rule":"DOMAIN,api.example.com"},"covering":{"file":"base.list","rule":"DOMAIN-SUFFIX,example.com"}}]} -EOF -python3 "$TOOL" \ - --domain-dir "$TMP_DIR/cross-domain/domain" \ - --ip-dir "$TMP_DIR/cross-domain/ip" \ - --conflicts "$TMP_DIR/cross-domain/allowed.json" >/dev/null - -cat > "$TMP_DIR/cross-domain/stale.json" <<'EOF' -{"version":1,"relations":[{"family":"domain","covered":{"file":"specific.list","rule":"DOMAIN,missing.example.com"},"covering":{"file":"base.list","rule":"DOMAIN-SUFFIX,example.com"}}]} -EOF -if python3 "$TOOL" --domain-dir "$TMP_DIR/cross-domain/domain" --ip-dir "$TMP_DIR/cross-domain/ip" \ - --conflicts "$TMP_DIR/cross-domain/stale.json" > /dev/null 2>"$TMP_DIR/stale.stderr"; then - echo "test failed: stale allowlist should fail" >&2 - exit 1 -fi -grep -Fq "stale conflict allowlist relation" "$TMP_DIR/stale.stderr" - -cat > "$TMP_DIR/cross-domain/duplicate.json" <<'EOF' -{"version":1,"relations":[{"family":"domain","covered":{"file":"specific.list","rule":"DOMAIN,api.example.com"},"covering":{"file":"base.list","rule":"DOMAIN-SUFFIX,example.com"}},{"family":"domain","covered":{"file":"specific.list","rule":"DOMAIN,api.example.com"},"covering":{"file":"base.list","rule":"DOMAIN-SUFFIX,example.com"}}]} +make_case redundant +cat > "$TMP_DIR/redundant/domain/example.list" <<'EOF' +DOMAIN-SUFFIX,example.com +DOMAIN,api.example.com EOF -if python3 "$TOOL" --domain-dir "$TMP_DIR/cross-domain/domain" --ip-dir "$TMP_DIR/cross-domain/ip" \ - --conflicts "$TMP_DIR/cross-domain/duplicate.json" > /dev/null 2>"$TMP_DIR/duplicate.stderr"; then - echo "test failed: duplicate allowlist relation should fail" >&2 +if run_lint "$TMP_DIR/redundant" > /dev/null 2>"$TMP_DIR/error"; then + echo "test failed: within-file redundancy passed" >&2 exit 1 fi -grep -Fq "duplicates an earlier allowlisted relation" "$TMP_DIR/duplicate.stderr" +grep -Fq 'is covered by DOMAIN-SUFFIX,example.com' "$TMP_DIR/error" -cat > "$TMP_DIR/cross-domain/broad.json" <<'EOF' -{"version":1,"relations":[{"family":"domain","covered":{"file":"specific.list"},"covering":{"file":"base.list"}}]} -EOF -if python3 "$TOOL" --domain-dir "$TMP_DIR/cross-domain/domain" --ip-dir "$TMP_DIR/cross-domain/ip" \ - --conflicts "$TMP_DIR/cross-domain/broad.json" > /dev/null 2>"$TMP_DIR/broad.stderr"; then - echo "test failed: broad allowlist should fail" >&2 - exit 1 -fi -grep -Fq "must contain exactly 'file' and 'rule'" "$TMP_DIR/broad.stderr" +make_case intentional-overlap +printf '%s\n' 'DOMAIN-SUFFIX,example.com' > "$TMP_DIR/intentional-overlap/domain/general.list" +printf '%s\n' 'DOMAIN,api.example.com' > "$TMP_DIR/intentional-overlap/domain/specific.list" +run_lint "$TMP_DIR/intentional-overlap" >/dev/null -python3 "$TOOL" \ - --domain-dir "$ROOT/sources/custom/domain" \ - --ip-dir "$ROOT/sources/custom/ip" \ - --conflicts "$ROOT/config/custom-rule-conflicts.json" >/dev/null +run_lint "$ROOT/sources/custom" >/dev/null echo "custom rule quality tests passed" diff --git a/scripts/tools/lint-custom-rules.py b/scripts/tools/lint-custom-rules.py index c97ffe127..55e4efc57 100644 --- a/scripts/tools/lint-custom-rules.py +++ b/scripts/tools/lint-custom-rules.py @@ -2,7 +2,6 @@ from __future__ import annotations import argparse -import json import re import sys from dataclasses import dataclass @@ -115,6 +114,8 @@ def find_domain_conflicts(rules: list[LocatedRule]) -> list[Conflict]: conflicts: list[Conflict] = [] for index, covered in enumerate(rules): for covering in rules[:index]: + if covered.file != covering.file: + continue if covered.text == covering.text: conflicts.append(Conflict("domain", covered, covering)) elif domain_covers(covering, covered): @@ -128,6 +129,8 @@ def find_ip_conflicts(rules: list[LocatedRule]) -> list[Conflict]: conflicts: list[Conflict] = [] for index, right in enumerate(rules): for left in rules[:index]: + if right.file != left.file: + continue if left.network == right.network: conflicts.append(Conflict("ip", right, left)) elif left.network.version == right.network.version and right.network.subnet_of(left.network): @@ -147,103 +150,30 @@ def unique_conflicts(conflicts: list[Conflict]) -> list[Conflict]: return result -def parse_endpoint(value: object, location: str, reporter: Reporter) -> tuple[str, str] | None: - if not isinstance(value, dict) or set(value) != {"file", "rule"}: - reporter.error(f"{location} must contain exactly 'file' and 'rule'") - return None - file = value.get("file") - rule = value.get("rule") - if ( - not isinstance(file, str) - or not file.endswith(".list") - or "/" in file - or "\\" in file - or Path(file).name != file - or not RULE_FILE_NAME_RE.fullmatch(Path(file).stem) - ): - reporter.error(f"{location}.file must be a bare .list filename") - return None - if not isinstance(rule, str) or not rule or rule != rule.strip(): - reporter.error(f"{location}.rule must be a non-empty canonical rule string") - return None - return file, rule - - -def load_allowlist(path: Path, reporter: Reporter) -> set[tuple[str, str, str, str, str]]: - try: - data = json.loads(path.read_text(encoding="utf-8")) - except FileNotFoundError: - reporter.error(f"{path} conflict allowlist does not exist") - return set() - except json.JSONDecodeError as exc: - reporter.error(f"{path} invalid JSON: {exc.msg}") - return set() - if ( - not isinstance(data, dict) - or set(data) != {"version", "relations"} - or type(data.get("version")) is not int - or data.get("version") != 1 - ): - reporter.error(f"{path} must be an object with version 1 and relations only; broad pair-wide allowlists are forbidden") - return set() - relations = data.get("relations") - if not isinstance(relations, list): - reporter.error(f"{path}.relations must be an array") - return set() - - result: set[tuple[str, str, str, str, str]] = set() - for index, item in enumerate(relations): - location = f"{path}.relations[{index}]" - if not isinstance(item, dict) or set(item) != {"family", "covered", "covering"}: - reporter.error(f"{location} must contain exactly family, covered, and covering") - continue - family = item.get("family") - if family not in {"domain", "ip"}: - reporter.error(f"{location}.family must be domain or ip") - continue - covered = parse_endpoint(item.get("covered"), f"{location}.covered", reporter) - covering = parse_endpoint(item.get("covering"), f"{location}.covering", reporter) - if covered is None or covering is None: - continue - key = (family, covered[0], covered[1], covering[0], covering[1]) - if key in result: - reporter.error(f"{location} duplicates an earlier allowlisted relation") - continue - result.add(key) - return result - - -def report_conflicts(conflicts: list[Conflict], allowlist: set[tuple[str, str, str, str, str]], reporter: Reporter) -> None: - actual = {conflict.key for conflict in conflicts} +def report_conflicts(conflicts: list[Conflict], reporter: Reporter) -> None: + # Different files may intentionally overlap because they can be assigned to + # different policies. Only reject redundancy inside one source file. for conflict in conflicts: - if conflict.key in allowlist: + if conflict.covered.file != conflict.covering.file: continue if conflict.covered.text == conflict.covering.text: message = f"duplicate {conflict.family} rule; first seen at {conflict.covering.location}: {conflict.covered.text}" else: message = f"{conflict.covered.text} is covered by {conflict.covering.text} at {conflict.covering.location}" reporter.error(f"{conflict.covered.location} {message}") - for stale in sorted(allowlist - actual): - family, covered_file, covered_rule, covering_file, covering_rule = stale - reporter.error( - f"stale conflict allowlist relation: {family} {covered_file}:{covered_rule} covered by " - f"{covering_file}:{covering_rule}" - ) def main() -> int: - parser = argparse.ArgumentParser(description="Strictly lint all custom rule sources and global conflicts.") + parser = argparse.ArgumentParser(description="Lint custom rule syntax and within-file redundancy.") parser.add_argument("--domain-dir", default=str(ROOT / "sources" / "custom" / "domain")) parser.add_argument("--ip-dir", default=str(ROOT / "sources" / "custom" / "ip")) - parser.add_argument("--conflicts", default=str(ROOT / "config" / "custom-rule-conflicts.json")) args = parser.parse_args() reporter = Reporter() domain_rules = load_domain_rules(Path(args.domain_dir), reporter) ip_rules = load_ip_rules(Path(args.ip_dir), reporter) - allowlist = load_allowlist(Path(args.conflicts), reporter) conflicts = find_domain_conflicts(domain_rules) + find_ip_conflicts(ip_rules) - report_conflicts(conflicts, allowlist, reporter) + report_conflicts(conflicts, reporter) if not reporter.ok: reporter.emit() diff --git a/sources/custom/domain/emby.list b/sources/custom/domain/emby.list index 0cb66c285..cea77655f 100644 --- a/sources/custom/domain/emby.list +++ b/sources/custom/domain/emby.list @@ -1,11 +1,10 @@ DOMAIN,gy.meowfly.de -DOMAIN,pdtvs.com -DOMAIN,emby.younoyes.com DOMAIN,xiangshechulai.zhyu.de +DOMAIN-SUFFIX,pdtvs.com DOMAIN-SUFFIX,mooguu.top DOMAIN-SUFFIX,zhezhi.art -DOMAIN-SUFFIX,sakim.org DOMAIN-SUFFIX,cn2gias.uk DOMAIN-SUFFIX,023168.xyz +DOMAIN-SUFFIX,younoyes.com DOMAIN-SUFFIX,alphatvapp.top DOMAIN-KEYWORD,emby