[
  {
    "path": ".devcontainer/Dockerfile",
    "content": "# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 11, 17, 11-bullseye, 17-bullseye, 11-buster, 17-buster\nARG VARIANT=21-bullseye\nFROM mcr.microsoft.com/vscode/devcontainers/java:${VARIANT}\n\n# [Option] Install Maven\nARG INSTALL_MAVEN=\"true\"\nARG MAVEN_VERSION=\"3.9.0\"\n# [Option] Install Gradle\nARG INSTALL_GRADLE=\"false\"\nARG GRADLE_VERSION=\"\"\nRUN if [ \"${INSTALL_MAVEN}\" = \"true\" ]; then su vscode -c \"umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \\\"${MAVEN_VERSION}\\\"\"; fi \\\n    && if [ \"${INSTALL_GRADLE}\" = \"true\" ]; then su vscode -c \"umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \\\"${GRADLE_VERSION}\\\"\"; fi\n\n# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10\nARG NODE_VERSION=\"none\"\nRUN if [ \"${NODE_VERSION}\" != \"none\" ]; then su vscode -c \"umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1\"; fi\n\n# [Optional] Uncomment this section to install additional OS packages.\n# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \\\n#     && apt-get -y install --no-install-recommends <your-package-list-here>\n\n# [Optional] Uncomment this line to install global node packages.\n# RUN su vscode -c \"source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>\" 2>&1\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n  \"name\": \"CodeSpace\",\n  \"dockerFile\": \"Dockerfile\",\n  \"extensions\": [\n    \"vscjava.vscode-java-pack\",\n    \"vscjava.vscode-java-debug\",\n    \"vscjava.vscode-java-test\",\n    \"vscjava.vscode-spring-boot-dashboard\",\n    \"vmware.vscode-spring-boot\",\n    \"vscjava.vscode-maven\"\n  ]\n}"
  },
  {
    "path": ".github/workflows/auto-update.yml",
    "content": "name: Update Filters\non:\n  push:\n    paths-ignore:\n      - 'README.md'\n      - 'README_en.md'\n      - '.github/**'\n      - '.devcontainer/**'\n      - 'LICENSE'\n  schedule:\n    - cron: 0 */8 * * *\n  workflow_dispatch:\n    inputs:\n      profile:\n        required: false\n        default: 'prod'\n        type: string\n        description: 'application running profile'\n\n      should-commit:\n        required: false\n        default: true\n        type: boolean\n        description: 'Whether to commit output files'\n\n      release-branch:\n        required: false\n        default: 'release'\n        type: string\n        description: 'Release branch name, if should-commit is false, this parameter will be ignored'\nenv:\n  TZ: Asia/Shanghai\n\njobs:\n  Update_Filters:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      actions: write\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ github.head_ref }}\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v4\n        with:\n          java-version: '21'\n          distribution: 'temurin'\n          cache: maven\n\n      - name: Set default values\n        id: defaults\n        run: |\n          echo \"profile=${{ github.event.inputs.profile || 'prod' }}\" >> $GITHUB_OUTPUT\n          echo \"should-commit=${{ github.event.inputs.should-commit || 'true' }}\" >> $GITHUB_OUTPUT\n          echo \"release-branch=${{ github.event.inputs.release-branch || 'release' }}\" >> $GITHUB_OUTPUT\n\n      - name: Run Jar\n        run:  mvn spring-boot:run -Dspring-boot.run.profiles=${{ steps.defaults.outputs.profile }}\n\n      - name: Archive tracking log\n        if: hashFiles('logs/tracking.list') != ''\n        uses: actions/upload-artifact@v4\n        with:\n          name: tracking-log\n          path: logs/tracking.list\n\n      - name: Clean up\n        if: steps.defaults.outputs.should-commit == 'true'\n        run: |\n          find . -maxdepth 1 ! -name 'rule' ! -name '.' ! -name '.git' -exec rm -rf {} +\n          mv rule/* .\n          rm -rf rule\n\n      - name: Set current date\n        id: date\n        if: steps.defaults.outputs.should-commit == 'true'\n        run: echo \"date=$(date +'%Y-%m-%d %H:%M:%S')\" >> $GITHUB_OUTPUT\n\n      - name: Commit Changes\n        id: commit\n        if: steps.defaults.outputs.should-commit == 'true'\n        uses: stefanzweifel/git-auto-commit-action@v5\n        with:\n          commit_message: 'chore: ✅ auto release at ${{ steps.date.outputs.date }}'\n          branch: ${{ steps.defaults.outputs.release-branch }}\n          skip_dirty_check: true\n          push_options: '--force'\n          create_branch: true\n          skip_fetch: true\n          skip_checkout: true\n          commit_options: '--allow-empty'\n\n      - name: Delete Workflow Runs\n        uses: Mattraks/delete-workflow-runs@v2\n        with:\n          token: ${{secrets.GITHUB_TOKEN}}\n          repository: ${{ github.repository }}\n          retain_days: 1\n          keep_minimum_runs: 1\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n.idea\ntarget\nsrc/test"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 以谶\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# DD-AD\n\n## [***✈️ tg 频道***](https://t.me/DDadsss)</br>[***🐧 QQ群***](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=z4tq1QhIHdGOX6PslCFBqDRBqH6WGfXb&authKey=Inrcu9LZL6G6%2F26qpdxo9WEAw0nQuJ%2FpIqGuKsrX1kOgSVSZRQkyLxqfvKoJDlEB&noverify=0&group_code=666178576)\n\n## 🎯 ***规则订阅***\n\n> AdGuard客户端(软件、扩展)、AdBlock、AdBlockPlus、uBlock Origin 推荐使用：`easylist`</br>\n> AdGuardHome 推荐使用：`dns.txt`</br>\n> AdAway 等其他仅支持 hosts 的工具，推荐使用：`hosts.txt`\n\n| 文件              | 说明                          |        github        |         ghproxy         |         jsdelivr           |\n|-----------------|:----------------------------|:--------------------:|:------------------------:|:-------------------------:|\n| `easylist.txt`  | 完整主规则                       | [订阅][easylist-raw] | [订阅][easylist-ghproxy] | [订阅][easylist-jsdelivr] |\n| `modify.txt`    | 不含 DNS 过滤规则的 `easylist.txt` |  [订阅][modify-raw]  |  [订阅][modify-ghproxy]  |  [订阅][modify-jsdelivr]  |\n| `dns.txt`       | 仅含 DNS 过滤规则的 `easylist.txt` |   [订阅][dns-raw]    |   [订阅][dns-ghproxy]    |   [订阅][dns-jsdelivr]    |\n| `dnsmasq.conf`  | dnsmasq 及其衍生版本              | [订阅][dnsmasq-raw]  | [订阅][dnsmasq-ghproxy]  | [订阅][dnsmasq-jsdelivr]  |\n| `clash.yaml`    | clash 及其衍生版本                |  [订阅][clash-raw]   |  [订阅][clash-ghproxy]   |  [订阅][clash-jsdelivr]   |\n| `smartdns.conf` | smartdns                    | [订阅][smartdns-raw] | [订阅][smartdns-ghproxy] | [订阅][smartdns-jsdelivr] |\n| `hosts`         | 几乎所有操作系统原生支持                |  [订阅][hosts-raw]   |  [订阅][hosts-ghproxy]   |  [订阅][hosts-jsdelivr]   |\n| `DD-AD.txt`   | 本仓库维护的私有规则，以 easylist 形式提供  | [订阅][DD-AD-raw]  | [订阅][DD-AD-ghproxy]  | [订阅][DD-AD-jsdelivr]  |\n\n[easylist-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/easylist.txt\n\n[easylist-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/easylist.txt\n\n[easylist-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/easylist.txt\n\n[modify-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/modify.txt\n\n[modify-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/modify.txt\n\n[modify-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/modify.txt\n\n[dns-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dns.txt\n\n[dns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dns.txt\n\n[dns-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/dns.txt\n\n[dnsmasq-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dnsmasq.conf\n\n[dnsmasq-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dnsmasq.conf\n\n[dnsmasq-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/dnsmasq.conf\n\n[clash-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/clash.yaml\n\n[clash-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/clash.yaml\n\n[clash-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/clash.yaml\n\n[smartdns-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/smartdns.conf\n\n[smartdns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/smartdns.conf\n\n[smartdns-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/smartdns.conf\n\n[hosts-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/hosts\n\n[hosts-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/hosts\n\n[hosts-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/hosts\n\n[DD-AD-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/DD-AD.txt\n\n[DD-AD-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/DD-AD.txt\n\n[DD-AD-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/DD-AD.txt\n\n### 说明\n\n广告过滤规则整合，使用 *[ad-filters-subscriber](https://github.com/fordes123/ad-filters-subscriber)*\n\n1.**针对番茄小说广告添加了规则**\n\n2.**针对七猫小说广告添加了规则**\n\n3.**私人dns：dd.afwfv.cn**\n"
  },
  {
    "path": "config/application-example.yaml",
    "content": "##########################################\n## !!! 示例配置，修改无效 !!!\n## !!! example config, modify invalid !!!\n##########################################\napplication:\n\n  # 规则源配置\n  # ！使用前请删除下方示例配置, 注意缩进\n  input:\n\n    - name: 'Subscription 1'               #必要参数: 规则名称，请勿重复\n      path: 'https://example.org/rule.txt' #必要参数: 规则 url (http/https) 或 本地文件位置 (绝对/相对路径)\n      type: easylist                       #可选参数: 规则类型：easylist (默认)、dnsmasq、clash、smartdns、hosts\n\n    - name: 'Subscription 2'\n      path: 'rule/local.txt'\n      type: hosts\n\n  #输出配置\n  output:\n    #文件头配置，将自动作为注释添加至每个规则文件开始\n    #可使用占位符 ${name}、${type}、${desc} 以及 ${date} (当前日期)、${total} (规则总数)\n    file_header: |\n      ADFS AdBlock ${type}\n      Last Modified: ${date}\n      Total Size: ${total}\n      Homepage: https://github.com/fordes123/ad-filters-subscriber/\n\n    #输出规则文件列表 （！注意缩进，且每个类型只能输出一个文件）\n    files:\n      - name: easylist.txt     #必要参数: 文件名\n        type: easylist         #必要参数: 文件类型: easylist、dnsmasq、clash、smartdns、hosts\n        file_header:           #可选参数: 文件头配置，将自动作为注释添加至每个规则文件开始 (此处优先于 output.file_header)\n        desc: 'ADFS EasyList'  #可选参数: 文件描述，可在file_header中通过 ${} 中使用\n        filter:\n          - basic              #基本规则，不包含任何控制、匹配符号, 可以转换为 hosts\n          - wildcard           #通配规则，仅使用通配符\n          - unknown            #其他规则，如使用了正则、高级修饰符号的规则，这些规则目前无法转换为其他格式\n\n        rule:                  #可选参数: 限定此文件使用的规则源，如果不指定则使用 input 中的所有规则源\n          - Subscription 1\n          - Subscription 2\n\n      - name: dns.txt\n        type: easylist\n        filter:\n          - basic\n          - wildcard\n\n      - name: dnsmasq.txt\n        type: dnsmasq\n\n      - name: clash.yaml\n        type: clash\n\n      - name: smartdns.txt\n        type: smartdns\n\n      - name: hosts.txt\n        type: hosts\n\n\n  # 程序运行配置（可选）\n  # ！删除即使用默认配置\n  config:\n\n    expected_quantity: 2000000  #预期规则数量\n    fault_tolerance: 0.001 #容错率\n    warn_limit: 6 #警告阈值, 原始规则长度小于该值时会输出警告日志\n\n    # 域名检测，启用时将进行解析以验证域名有效性\n    # ！开启此功能可能导致处理时间大幅延长\n    # ！仅支持特定规则，具体原因请查看项目主页说明\n    domain-detect:\n      enable: false   # 是否启用\n      timeout: 2000   # 查询超时（毫秒）\n      concurrency: 10 # 并发查询数\n      cache-ttl-min: 600            # 缓存最小TTL（秒）\n      cache-ttl-max: 86400          # 缓存最大TTL（秒）\n      cache-negative-ttl: 300       # 失败结果缓存TTL（秒）\n      provider:       # 使用的 DNS 服务器，仅支持 IP，多个之间具备优先级; 为空则使用默认DNS\n        - 223.5.5.5\n\n    # 排除指定内容\n    # 在解析结束后，规则内容命中此列表将直接认定为无效\n    exclude:\n      - \"www.example.org\"\n\n    # 通过文件收集程序认定无效的规则，用于排查问题\n    tracking:\n      enable: true\n      path: logs/tracking.list"
  },
  {
    "path": "config/application.yaml",
    "content": "# 参考 `application-example.yaml` 并在处添加订阅规则，请注意缩进\n# Refer to `application-example.yaml` and add subscription rules there，please note the indentation\n\napplication:\n  config:\n    domain-detect:\n      enable: true\n      timeout: 3000\n      concurrency: 128\n      provider:\n        - 1.1.1.1\n        - 8.8.8.8\n\n    exclude:\n      # 屏蔽此域会导致Edge浏览器翻译不可用\n      - \"edge.microsoft.com\"\n      # 屏蔽此域会导致小米天气网络异常\n      - \"metok.sys.miui.com\"\n      # 屏蔽此域会导致10086 App登陆异常\n      - \"client.app.coc.10086.cn\"\n      # 屏蔽此域会导致MSN页面无法正常加载\n      - \"assets.msn.com\"\n\n  rule:\n    remote:\n      - name: AdGuard 基础过滤器\n        path: https://filters.adtidy.org/extension/ublock/filters/2_optimized.txt\n        type: easylist\n\n      - name: AdGuard 移动广告过滤器\n        path: https://filters.adtidy.org/extension/ublock/filters/11_optimized.txt\n        type: easylist\n\n      - name: AdGuard 中文过滤器\n        path: https://filters.adtidy.org/extension/ublock/filters/224_optimized.txt\n        type: easylist\n\n      - name: NoAppDownload\n        path: https://raw.githubusercontent.com/Noyllopa/NoAppDownload/master/NoAppDownload.txt\n        type: easylist\n\n      - name: xndeye web-ad-rule\n        path: https://raw.githubusercontent.com/xndeye/web-ad-rule/master/easylist.txt\n        type: easylist\n\n      - name: xinggsf movie\n        path: https://raw.githubusercontent.com/xinggsf/Adblock-Plus-Rule/master/mv.txt\n\n      - name: cjxlist annoyance\n        path: https://raw.githubusercontent.com/cjx82630/cjxlist/master/cjx-annoyance.txt\n\n      - name: ABP filters\n        path: https://easylist-downloads.adblockplus.org/antiadblockfilters.txt\n\n      - name: ABP 反广告过滤\n        path: https://easylist-downloads.adblockplus.org/abp-filters-anti-cv.txt\n\n      - name: uBlockOrigin privacy\n        path: https://raw.githubusercontent.com/uBlockOrigin/uAssets/refs/heads/master/filters/privacy.txt\n\n\n    local:\n      - name: 'DD-AD'\n        path: 'rule/DD-AD.txt'\n        type: easylist\n\n  output:\n    file_header: |\n      Title: DD-AD of ${type}\n      Last Modified: ${date}\n      Homepage: https://github.com/afwfv/DD-AD\n\n    files:\n      - name: easylist.txt\n        type: easylist\n        filter:\n          - basic\n          - wildcard\n          - unknown\n\n      - name: dns.txt\n        type: easylist\n        file_header: |\n          Title: DD-AD \n          Last Modified: ${date}\n          Homepage: https://github.com/afwfv/DD-AD\n        filter:\n          - basic\n          - wildcard\n\n      - name: modify.txt\n        type: easylist\n        filter:\n          - unknown\n\n      - name: dnsmasq.conf\n        type: dnsmasq\n\n      - name: clash.yaml\n        type: clash\n\n      - name: smartdns.conf\n        type: smartdns\n\n      - name: hosts\n        type: hosts\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <name>ad-filters-subscriber</name>\n    <groupId>org.fordes</groupId>\n    <description>AD Filters Subscriber</description>\n\n    <modelVersion>4.0.0</modelVersion>\n    <artifactId>ad-filters-subscriber</artifactId>\n    <version>1.4.0</version>\n\n    <properties>\n        <java.version>21</java.version>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <spring-boot.version>3.5.7</spring-boot.version>\n        <maven-compiler-plugin.version>3.12.1</maven-compiler-plugin.version>\n    </properties>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-webflux</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-validation</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.github.alexandrnikitin</groupId>\n            <artifactId>bloom-filter_2.13</artifactId>\n            <version>0.13.1</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n    </dependencies>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-dependencies</artifactId>\n                <version>${spring-boot.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven-compiler-plugin.version}</version>\n                <configuration>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                    <encoding>UTF-8</encoding>\n                    <compilerArgs>\n                        <arg>-parameters</arg>\n                    </compilerArgs>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <version>${spring-boot.version}</version>\n                <executions>\n                    <execution>\n                        <id>repackage</id>\n                        <goals>\n                            <goal>repackage</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n        <filters>\n            <filter>src/main/resources/application.yml</filter>\n        </filters>\n        <resources>\n            <resource>\n                <directory>src/main/resources</directory>\n                <filtering>true</filtering>\n            </resource>\n        </resources>\n    </build>\n\n</project>\n"
  },
  {
    "path": "rule/DD-AD.txt",
    "content": "! Title: AFWFV DD-AD list of DD-AD\n! Last Modified: 2025-11-05 00:00:00\n! Homepage: https://github.com/afwfv/DD-AD/\n! Description: 对本仓库上游规则的补充与修正, 不定期更新.\n!-------------白名单---------------\n#图床\n@@||pic.imgdb.cn\n#微信支付\n@@||payapp.weixin.qq.com^\n#夸克网盘\n@@||pan.quark.cn^\n@@||drive.quark.cn^\n#夸克首页\n@@||myquark.cn^\n#小米骚扰拦截 数据库更新\n@@||api.sec.miui.com^\n#小米推送(mipush)\n@@||cn.app.chat.xiaomi.net^\n#个推\n@@||sdk.open.talk.gepush.com^\n#公共CDN\n@@||bootcdn.net^\n#斗鱼\n@@||msg.douyu.com^\n@@||bjfesdk.douyucdn.cn^\n@@||gift.douyucdn.cn^\n#360\n@@||360.cn^\n@@down.360safe.com^\n#酷安\n@@||316.coolapk1s.com^\n@@||api.coolapk.com^\n@@||avatar.coolapk.com^\n@@||umengjmacs.m.taobao.com^\n#抖音网页\n@@||lf3-cdn-tos.bytegoofy.com^\n@@||lf6-cdn-tos.bytegoofy.com^\n@@||lf1-cdn-tos.bytegoofy.com^\n#京东\n@@||m.jingxi.com^\n@@||gia.jd.com^\n#笔趣阁\n@@||www.bqgyy.com^\n#皮皮虾pro\n@@||is.snssdk.com^\n@@||ib.snssdk.com^\n#qq\n@@||c.pc.qq.com^\n#163\n||dl.reg.163.com^\n#放行log文件,减少重复请求造成的发热\n@@||log-api.pangolin-sdk-toutiao-b.com^\n@@||log-api.pangolin-sdk-toutiao.com^\n@@||dig.lianjia.com^\n##豆包\n@@||lf-c-flwb.bytetos.com^\n@@||p3-search.byteimg.com^\n@@||p3-flow-imagex-sign.byteimg.com^\n@@||p6-flow-imagex-sign.byteimg.com^\n@@||p9-flow-imagex-sign.byteimg.com^\n##今日头条\n@@||mssdk3-normal-lf.zijieapi.com^\n@@||www.toutiao.com^\n@@||m.toutiao.com^\n@@||app.toutiao.com^\n@@||lf3-short.ibytedapm.com^\n@@||ttwid.bytedance.com^\n@@||lf3-cdn2-tos.bytescm.com^\n@@||lf*-cdn-tos.bytegoofy.com^\n@@||*ad*.ksyungslb.com^\n@@||p*-img.searchpstatp.com^\n@@||lf*material.searchpstatp.com^\n@@||p*.toutiaoimg.com^\n##百度云盘\n@@||staticsns.cdn.bcebos.com^\n@@||tcbox.baidu.com^\n@@||nsclick.baidu.com^\n##谷歌广告\n||tpc.googlesyndication.com^\n##虎牙直播\n@@||ylog.huya.com^\n##懂车帝\n@@||p1-dcd.byteimg.com^\n@@||p3-dcd.byteimg.com^\n@@||p9-dcd.byteimg.com^\n@@||p3-dcd-sign.byteimg.com^\n@@||p6-dcd-sign.byteimg.com^\n@@||p9-dcd-sign.byteimg.com^\n@@||open.bytedanceapi.com^\n##百度搜索\n@@||ms.bdimg.com^\n@@||feed-image.baidu.com^\n##咪咕视频\n@@||v.miguvideo.com^\n##Appshare\n@@||edu-image.nosdn.127.net^\n##快看视频\n@@||yanxuan.nosdn.127.net^\n##中国移动\n@@||qwhdcdn.cmcc-cs.cn^\n##Omofun\n@@||pic2.zhimg.com^\n@@||lf6-imcloud-file-sign.bytetos.com^\n@@||lf3-imcloud-file-sign.bytetos.com^\n##巴哈姆特動畫瘋\n@@||data.flurry.com^\n##飞猪\n@@||cdn.ynuf.aliapp.org^\n##酷安\n@@||necaptcha.nosdn.127.net^\n##微信\n@@||cm-10-*.getui.com^\n##网易企业邮箱\n@@||mimg.127.net^\n##QQ\n@@||qzonestyle.gtimg.cn^\n@@||tianshu.gtimg.cn^\n##QQ音乐\n@@||i2.y.qq.com^\n##百度输入法\n@@||res.mi.baidu.com^\n@@||www.paypalobjects.com^\n##抖音\n@@||auth.zijieapi.com^\n@@||aweme.snssdk.com^\n@@||v*.douyinvod.com^\n@@||lf3-cdn-tos.bytegoofy.com^\n@@||saveu*-normal-lf.zijieapi.com^\n@@||lf-webcast-gr-sourcecdn.bytegecko.com^\n@@||lf*-webcast-cdn-tos-ncdn.bytegecko.com^\n@@||n*-lf-webcast-cdn-tos-ncdn.bytegecko.com^\n@@||n*-lf*-dy-ncdn-tos-sub.bytegecko.com^\n@@||lf*-dy-cdn-tos.bytegecko.com^\n@@||lf*-static.bytednsdoc.com^\n@@||lf-webcast-sourcecdn-tos.bytegecko.com^\n@@||lf*-webcast-cdn-tos.bytegecko.com^\n@@||lf*-webcastcdn-tos.douyinstatic.com^\n@@||ma*-normal-lf.zijieapi.com^\n@@||vcs.zijieapi.com^\n@@||pitaya.bytedance.com^\n@@||abtest3-misc-lf.zijieapi.com^\n@@||p*-piu.byteimg.com^\n@@||p*-ecom-fe.byteimg.com^\n@@||p*-ecop-imagex.byteimg.com^\n@@||p*-im.byteimg.com^\n@@||p9-dy.byteimg.com\n@@||p6-dy-ipv6.byteimg.com^\n@@||p3-notice.byteimg.com^\n@@||f-cpay.snssdk.com^\n@@||f-cashloan.snssdk.com^\n@@||p3-cpay.byteimg.com^\n@@||p3-insurance.byteimg.com^\n@@||lf*-infras.bytetos.com^\n@@||lf1-cdn-tos.bytescm.com^\n@@||survey.feelgood.cn^\n@@||p*-sdbk2-media.byteimg.com^\n@@||vcs-quality.zijieapi.com^\n@@||lf*-baike.searchpstatp.com^\n##乐播投屏\n@@||tvapp.hpplay.cn^\n@@||h5.hpplay.com.cn^\n@@||sl.hpplay.cn^\n@@||conf.hpplay.cn^\n##菜鸟裹裹\n@@||wgo.mmstat.com^\n@@||gm.mmstat.com^\n@@||cainiao-app-logs.cn-zhangjiakou.log.aliyuncs.com^\n@@||na61-na62.wagbridge.alibaba.tanx.com^\n##快手\n@@||d*.static.yximgs.com^\n@@||d*-magic.static.yximgs.com^\n@@||bs-origin.pull.yximgs.com^\n@@||live*.static.yximgs.com^\n@@||*m.chenzhongtech.com^\n@@||js-gamezone.static.yximgs.com^\n@@||ali-gamezone.static.yximgs.com^\n##番茄小说\n@@||p*-novel.byteimg.com^\n@@||p*-novel-sign.byteimg.com^\n##皮皮虾\n@@||p*-ppx-sign.byteimg.com^\n@@||p*-ppx-report.byteimg.com^\n@@||saveu.zijieapi.com^\n@@||vas.snssdk.com^\n@@||bgp.video-upload-v*.lf.bytelb.net^\n@@||lf-cdn-tos.bytescm.com^\n##阿里云\n@@||click.aliyun.com^\n##闲鱼\n@@||dinamicx.alibabausercontent.com^\n##今日头条极速版\n@@||lf3-search.searchpstatp.com^\n@@||monsetting.toutiao.com^\n@@||gecko.zijieapi.com^\n@@||polaris.zijieapi.com^\n@@||tnc3-bjlgy.zijieapi.com^\n##囧次元\n@@||wxsnsdy.wxs.qq.com^\n##BT1207\n@@||lf3-cdn-tos.bytecdntp.com^\n##原神\n@@||ad-log-upload.mihoyo.com^\n##菠菜\n@@||meiqia.com^\n##当鸟动漫\n@@||mooc-image.nosdn.127.net^\n##头条PRO\n@@||ib.snssdk.com^\n@@||ic.snssdk.com^\n@@||is.snssdk.com^\n##360防骚扰大师\n@@||s.mvconf.f.360.cn^\n@@||scan.call.f.360.cn^\n@@||shouji.360.cn^\n##淘宝\n||s.click.taobao.com^\n##樱花动漫\n@@||v16m-default.akamaized.net^\n##快手极速版\n@@||v*.kwaicdn.com^\n@@||ali.static.yximgs.com^\n##360摄像机\n@@||passport.360.cn^\n@@||report.passport.360.cn^\n@@||app.home.360.cn^\n@@||jia.360.cn^\n@@||fastconn-api.iot.360.cn^\n@@||speed.live.360.cn^\n@@||cloudcontrol.live.360.cn^\n@@||sdk.live.360.cn^\n@@||mdm.openapi.360.cn^\n@@||dp.push.dc.360.cn^\n@@||mall.360.cn^\n##夸克\n@@||download.uc.cn^\n@@||soutiapi.quark.cn^\n@@||souti-ims.sm.cn^\n##蓝奏\n@@||develope-oss.lanzoug.com^\n##网易有道词典\n@@||ydlunacommon-cdn.nosdn.127.net^\n@@||luna-dict-community.nosdn.127.net^\n@@||foundation.youdao.com^\n##火山引擎\n@@||portal.volccdn.com^\n##360扫地机\n@@||smart.360.cn^\n##有柿\n@@||p3-iz-e-sign.byteimg.com^\n##淘小说\n@@||tybook.taoyuewenhua.net^\n##火山翻译\n@@||p3-scmimg.bytescm.com^\n@@||ipolyfill-origin.bytedance.com^\n@@||lf3-cdn-tos.bytescm.com^\n##快影\n@@||ali-ky.static.yximgs.com^\n##剪映\n@@||p*-faceu-img-sign.byteimg.com^\n@@||p*-passport.byteacctimg.com^\n##网易严选\n@@||yanxuan-dl.nosdn.127.net^\n@@||yanxuan-item.nosdn.127.net^\n##网易邮箱大师\n@@||onegoods.nosdn.127.net^\n##Faceu激萌\n@@||effect.snssdk.com^\n##风行视频\n@@||cp2-cdn-play.funshion.com^\n@@||img.funshion.com^\n@@||img*.funshion.com^\n@@||puser.funshion.com^\n@@||pvip.funshion.com^\n@@||po.funshion.com^\n@@||pm.funshion.com^\n##酷我音乐\n@@||search.kuwo.cn^\n@@||mobilist.kuwo.cn^\n@@||ncomment.kuwo.cn^\n@@||vip1.kuwo.cn^\n##钉钉\n@@||adash-emas.cn-hangzhou.aliyuncs.com^\n!-------------黑名单--------------------\n#信息流及评论区广告\n||s2s.adjust.cn^\n||ctobsnssdk.com^\n||pangolin.snssdk.com^\n||ulogs.umeng.com^\n||aaid.umeng.com^\n||mssdk-bu.bytedance.com^\n#帖子详情好物推荐\n||api2.coolapk.com/v6/feed/detail$replace=/&quot;include_goods_ids&quot;:\\[.*?]\\,&quot;include_goods&quot;:\\[.*?]\\,/ /\n#帖子详情赞助内容\n||api2.coolapk.com/v6/feed/detail$replace=/\\,&quot;detailSponsorCard&quot;:{.*}/}}/\n#发现页去除酷品\n||api2.coolapk.com/v6/main/init$replace=/{&quot;id&quot;:1170.*?}\\,/ /\n#应用游戏页去除推广视频\n||api2.coolapk.com/v6/page/dataList$replace=/{&quot;entityType&quot;:&quot;card&quot;\\,&quot;entityTemplate&quot;:&quot;apkImageCard&quot;.*?\\\\u0022}&quot;}\\,/ /\n#去除首页还有什么值得买推广\n||api2.coolapk.com/v6/main/indexV8$replace=/{&quot;entityType&quot;:&quot;card&quot;\\,&quot;entityTemplate&quot;:&quot;listCard&quot;.*?}&quot;}\\,/ /\n#夏普电视开机广告\n||flnet.com^\n||miaozhen.com^\n#QQ浏览器小说广告\n||kde.qq.com^\n#字节系 AD API SDK 屏蔽\n/pangolin-sdk.*/\n/pangolin-sdk-toutiao.*/\n/pangolin-sdk-toutiao1.*/\n/pglstatp-toutiao.*/\n||bsccdn.net^\n||dig.zjurl.cn^\n||dig.bdurl.net^\n||ad.zijieapi.com^\n||apps.bytesfield.com^\n||pangolin-sdk-toutiao.com^\n||pangolin-sdk-toutiao1.com^\n||pangolin-sdk-toutiao-b.com^\n||pglstatp-toutiao.com^\n||vcs.zijieapi.com^\n||ads*-normal.amemv.com^\n||ads*-normal-lq.amemv.com^\n||ads*-normal-lf.amemv.com^\n||ads*-normal-hl.amemv.com^\n||ads*-normal.zijieapi.com^\n||ads*-normal-lq.zijieapi.com^\n||ads*-normal-lf.zijieapi.com^\n||ads*-normal-hl.zijieapi.com^\n||ads*-normal-lq.zijieapi.com.w.cdngslb.com^\n||ads*-normal-lq.zijieapi.com.queniuiq.com^\n||p*-ad-sign.byteimg.com^\n||p*-ad.byteimg.com^\n||lf*-orange.byteorge.com^\n||v*-novelapp.fqnovelvod.com^\n||sf*-ttcdn-tos.pstatp.com^\n||p*-local-ads.byteimg.com.queniuso.com^\n||p*-local-ads.byteimg.com^\n||localads.chengzijianzhan.cn^\n||localads.chengzijianzhan.cn.queniurc.com^\n||ads*-normal-lf.zijieapi.com.dhcp^\n#七猫免费小说\n||cdn-new-ad.wtzw.com^\n||a-remad.qm989.com^\n||open.kuaishouzt.com^\n#得间\n||*-ad.wtzw.com^\n||aiclk.com^\n||smad.ms.zhangyue.net^\n||adx.ads.heytapmobi.com^\n||uapi.ads.heytapmobi.com^\n||saad.ms.zhangyue.net^\n||sentry-app.ireader.com^\n#熊猫小说\n||cdn-ad.wtzw.com^\n||a-qmad.qimao.com^\n#笔趣阁网页广告\n||hlryzsqo.icu^\n||scenery.hk^\n||xiedao.hk^\n||bcnsport.us^\n||ncbtsdea.xyz^\n||vjyaghdw.icu^\n#书旗\n||dsp-x.jd.com^\n||xlog.jd.com^\n||api.th.wanzjhb.com^\n||janapi.jd.com^\n||partner.uc.cn^\n||mos.m.taobao.com^\n||sdk-log.partner.sm.cn^\n||opehs.tanx.com^\n#OPPO\n||sms.ads.heytapmobi.com^\n||cl-data.ads.heytapmobi.com^\n!-----------------待定-----------------\n#小米浏览器热点资讯\n||hot.browser.miui.com^\n#未知来源 广告/日志\n||toblog.ctobsnssdk.com^\n#gamepass日志\n||catalog.gamepass.com^\n#杂七杂八\n||googleads.g.doubleclick.nes^\n||ads.lingyuexiu.cn^\n||adscfg.togothermany.cn^\n||ads-marketing-vivofs.vivo.com.cn\n||adsfs.heytapimage.com^\n||sdklog.adintl.cn^\n||sdk-infra.inmobi.cn^\n||sv.adintl.cn^\n||trc-sdk.inmobi.cn^\n||sdk.beizi.biz^\n||sdk.alibaba.com.ailbaba.me^\n||telemetry-sdk-metrics.inmobi.cn^\n||api-htp.beizi.biz^\n||sdkcfg.adintl.cn^\n||p1-lm.adukwai.com^\n||union.w.inmobi.cn^\n||adskp.org^\n||api.open.adsaide.cn^\n||ads-video-al.xhscdn.com^\n||ads-img-al.xhscdn.com^\n||ads.cup.com.cn^\n||bdapi.ads.heytapmobi.com^\n||adx-cn.ads.vungle.com^\n||config.ads.vungle.com^\n||ads.themoneytizer.com^\n||ads.sunmaker.com^\n||ad.letmeads.com^\n||mads.meituan.com^\n||st-adsgame.vivo.com.cn^\n||bjm.1vkx.cn^\n||img.juhagngj.xyz^\n||img.ayghbngnazh.xyz^\n||img.leiahngiuheong.xyz^\n||img.soweonglsh.xyz^\n||img.yuwhehan.xyz^\n||img.syebgng.xyz^\n||img.mjhsghnwg.xyz^\n||img.wqpamgngnb.xyz^\n||img.huanagyehgn.xyz^\n||img.shznegh.xyz^\n||img.zhangilihew.xyz^\n||yh.yanghetp.vip^\n||tu.mt20230625tu.com^\n||www.9129666tp.com^\n||line.txwlwwvvimg.com^\n||uu5512uu.com^\n||xoxo.xoxoimg.com^\n||ad-log-upload.mihoyo.com^\n||ad-trace.lenovomm.com^\n||ad-sdk.lenovomm.com^\n||p*-ad-aweme.byteimg.com^\n||ad-r.soulapp.cn^\n||v*-ad.video.yximgs.com^\n||soul-ad.soulapp.cn^\n||mkt-advertisement.tuhu.cn^\n||advert.swiftpass.cn^\n||mktadsodr.ppdai.com^\n||iadsdk.qtlcdn.com^\n||ck.ads.oppomobile.com^\n||mobads-pre-config.cdn.bcebos.com^\n||api.adserver.jvmai.com^\n||mbads.paas.cmbchina.com^\n||skadsdk.appsflyer.com^\n||cm.ads.oppomobile.com^\n||ads.chuanmeixing.com^\n||ads.bjlxin.cn^\n||sis.jpush.io^\n||ad.e.kuaishou.com^\n||ali2.a.yximgs.com^\n||open-set-api.shenshiads.com^\n||adsdkapi.dxys.pro^\n||adsdk.9imobi.com^\n||uapi-ads.vivo.com.cn^\n||adsfilebssdlbig.ali.kugou.com^\n||ad.cyapi.cn^\n||ad.tianmu.mobi^\n||multi-az-ad.kuaishou.cn^\n||ad.qingcigame.com^\n||ads.tiktok.com^\n||ads.linkedin.com^\n||advice-ads.s3.amazonaws.com^\n||ads30.adcolony.com^\n||adsfile20.bssdlbig.kugou.com^\n||adsfileretry.service.kugou.com^\n||adserver.unityads.unity3d.com^\n||adserviceretry.kglink.cn^\n||ck.ads.heytapmobi.com^\n||ads.games.laohu.com^\n||ads-drcn.platform.hihonorcloud.com^\n||ads-lb.serving-sys.cn^\n||ads.serving-sys.cn^\n||ads.uutest.cn^\n||adserver.html.it^\n||adsmind.ugdtimg.com^\n||mobads.baidu.com^\n||ads.api.fakeloc.cc^\n||api.installer.xiaomi.com^\n||applog.snssdk.com^\n||adservice.aiyouaiyou.cn^\n||ads-shopping.shouqianba.com^\n||ads.superawesome.tv^\n||ads.zhinengxiyifang.cn^\n||ads.huawei.com^\n||cross-ad-api.afunapp.com^\n||t.jmads.net^\n||adserv.ontek.com.tr^\n||optimus-ads.amap.com^\n||adashx.ut.cainiao.com^\n||image-ad.sm.cn^\n||h-adashx.ut.dingtalk.com^\n||adashx.ut.dingtalk.com^\n||v6-adashx.ut.cainiao.com^\n||api-access.pangolin-sdk-toutiao.com.queniuyk.com^\n||opencdnpddpic2.cdn.vsjwtcdn.com^\n||opencdnpddvod2.jomodns.com^\n||b1-data.ads.heytapmobi.com^\n||googleads.g.doubleclick.net^\n||stg-data.ads.heytapmobi.com^\n||is.snssdk.com.queniukw.com^\n||i.snssdk.com^\n||o2o-ad-dev.oss-cn-shanghai.aliyuncs.com^、\n||4pn.cn^\n||4a9.cn^\n||3p4.cn^\n||y4n.cn^\n||u7x.cn^\n||aqdlt.com^\n||aqdz91.com^\n||aqdz149.com^\n||share.jileme.net^\n||settings.ttwebview.com^\n||st-ads-jssdk.vivo.com.cn^\n||xmadsdk.cos.tx.xmcdn.com^\n||ads-video-1251524319.cos.ap-shanghai.myqcloud.com^\n||line3-adscore-api.biligame.net^\n||adsmind.gdtimg.com.svideolego-dk.sched.dcloudstc.com^\n||configv2.unityads.unitychina.cn^\n||abtest3-misc-hl.zijieapi.com^\n||abtest3-misc-lq.zijieapi.com^\n||abtest3-misc-lf.zijieapi.com^\n||monitor.ads.8le8le.com^\n||leads.baidu.com^\n||x61.inyvf.top^\n||b2.ghj7l.com^\n||nosup-jd1.127.net^\n||p*-webcast-sign.douyinpic.com^\n||v6-adashx.ut.ele.me^\n||h-adashx.ut.cainiao.com^\n||h-adashx4bc.ut.taobao.com^\n||v6-adashx.ut.amap.com^\n||ad-audit-provider.soulapp.cn^\n||o2o-ad-log-gateway.alibaba.com^\n||ad-sdk-config.youdao.com^\n||m1.ad.10010.com.w.cdngslb.com^\n||business-ad.cdn-go.cn^\n||market-ad.oss-cn-shenzhen.aliyuncs.com^\n||advertising.yandex.ru^\n||advertising.apple.com^\n||advertising.yahoo.com^\n||na61-na62.wagbridge.advertisement.tanx.com^\n||ads.pinterest.com^\n||ads-api.tiktok.com^\n||partnerads-test.ysm.yahoo.net^\n||partnerads.ysm.yahoo.com^\n||adsbdfs.heytapimage.com^\n||s364-ad-canl5.nbjz.db9w.com^\n||ads3.fingersoft.net^\n||ads-sg.tiktok.com^\n||dm.kantsuu.com^\n||dm.video.weibocdn.com^\n||dm.wo.com.cn^\n||ad.mcloud.139.com^\n||ad.zhuanzhuan.com^\n||adserving-oss.bangdao-tech.com^\n||v6-novelapp-nxyd.fqnovelvod.com^\n||wcp.taobao.com^\n||ad.bczcdn.com^\n||miav-cse.avlyun.com^\n||a0.app.xiaomi.com^\n||miui-fxcse.avlyun.com^\n||ads5-normal-lq.zijieapi.com.c.dsa.cdnbuild.net^\n||adsdk.state.duoku.com^\n||ads5-normal-lf.zijieapi.com.skyworth^\n||ads3-normal-lf.zijieapi.com.skyworth^\n||ad.oceanengine.com.w.cdngslb.com^\n||ad.oceanengine.com.queniukw.com^\n||ad.oceanengine.com^\n||ad.k98y.cn^\n||ads.api.lerist.cc^\n||ads.chinaums.com^\n||ad-monitor.sh9130.com^\n||ads.ysepay.com^\n||imp.ads.rgyun.com^\n||c.ads.rgyun.com^\n||dsp.fcbox.com^\n||hugelog.fcbox.com^\n||ads.adfox.ru^\n||ad.xmmnsl.com^\n||ad-*file.myqcloud.com^\n||ads.iosappsworld.com^\n||ads.adfox.ru^\n||data-ads.oneplusmobile.com^\n||dgstatic.jd.com^\n||wdr160gz.dayugslb.com^\n||tnc3-alisc1.zijieapi.com.queniuyk.com^\n||ad.partner.gifshow.com^\n||ads.lejuliang.com^\n||o2o-ad-dev.oss-cn-shanghai.aliyuncs.com^\n||ad-display.diwodiwo.xyz^\n||metrics-ad-drcn.dt.hihonorcloud.com^\n||ads.95516.com^\n||p10528-ad-server.ejoy.com^\n||zlsdk.1rtb.net^\n||ads.aralego.com^\n||hw-ot-ad.a.yximgs.com^\n||pmp-ad.cdn.bcebos.com^\n||market-ad.oss-accelerate.aliyuncs.com^\n||api-ad-adapter-sg.wps.com^\n||p10528-ad-server.ejoy.com^\n||o2o-ad-dev.oss-cn-shanghai.aliyuncs.com.gds.alibabadns.com^\n||metrics-ad-drcn.dt.hihonorcloud.com^\n||ad-ga.nocode.com^\n||ai.advertisement.alibabacorp.sm.cn^\n||cdn-cbd-advert-prod.shuxinyc.com^\n||websgsadvert.sanguosha.com^\n||ms-ads-monitoring-events.presage.io^\n||ssp-adx.ads.heytapmobi.com^\n||ir-sdk.dun.163.com^\n||adx.halomobi.com^\n||api.jietuhb.com^\n||adsmartgetqtt.guangtuikeji.com^\n||tracker.medproad.com^\n||adtracker.medproad.com^\n||creative.medproad.com^\n||content-understand-strategy.sm.cn^\n||lbs.netease.im^\n||ac.dun.163yun.com^\n||advert-prod.hyrz.qq.com^\n||ads.ishansong.com^\n||ads-api.twitter.com^\n||multi-az-ad.kuaishou.com^\n||sdkconfig.ad.xiaomi.com.mshome.net^\n||hw-ot-ad.a.yximgs.com^\n||ad.99jiasu.com^\n||ad.tk.cn^\n||*-ad.adbkwai.com^\n||*-ad.gskwai.com^\n||cdn.wwads.cn^\n||ae.iads.unity3d.com^\n||o.iads.unity3d.com^\n||ads.ubestkid.com^\n||ad.xunkids.com^\n||*-ad.adukwai.com^\n||ads.lejuliang.cn^\n||ads.inmobi.com^\n||sdk.1rtb.net^\n||volcsirius.com^\n||vegslb.com^\n||weilayun.com^\n||adx-bj.anythinktech.com^\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/AdFSApplication.java",
    "content": "package org.fordes.adfs;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.config.AdFSProperties;\nimport org.fordes.adfs.constant.Constants;\nimport org.fordes.adfs.handler.Parser;\nimport org.fordes.adfs.handler.rule.Handler;\nimport org.fordes.adfs.model.Rule;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\nimport java.io.BufferedReader;\nimport java.io.BufferedWriter;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.nio.file.StandardOpenOption;\nimport java.time.Duration;\nimport java.time.LocalDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicLong;\n\n@Slf4j\n@Component\n@SpringBootApplication\npublic class AdFSApplication {\n\n    private final ApplicationContext context;\n    private final Set<AdFSProperties.InputProperties> inputs;\n    private final AdFSProperties.OutputProperties output;\n    private final Parser parser;\n\n    private final Map<String, Output> outputMap;\n\n    public record Output(AdFSProperties.OutputItem item, Path tempFile, AtomicLong count) {\n    }\n\n    public static void main(String[] args) {\n        SpringApplication.run(AdFSApplication.class, args);\n    }\n\n    public AdFSApplication(ApplicationContext context, AdFSProperties properties, Parser parser) {\n        this.context = context;\n        this.parser = parser;\n        this.inputs = properties.getInput();\n        this.output = properties.getOutput();\n        this.outputMap = new HashMap<>(properties.getOutput().files().size(), 1);\n        properties.getOutput().files().forEach(file -> {\n            try {\n                Path tempFile = Files.createTempFile(file.name(), \".tmp\");\n                this.outputMap.put(file.name(), new Output(file, tempFile, new AtomicLong(0L)));\n            } catch (IOException e) {\n                log.error(\"create temp file failed\", e);\n                this.exit();\n            }\n        });\n    }\n\n    @Bean\n    public ApplicationRunner start() {\n        return args -> {\n            long start = System.currentTimeMillis();\n\n\n            Flux.fromIterable(inputs)\n                    .flatMap(parser::handle, 1)\n                    .flatMap(rule -> this.output(output.files(), rule))\n                    .groupBy(Tuple2::getT1, Tuple2::getT2)\n                    .flatMap(group -> group\n                            .bufferTimeout(5000, Duration.ofSeconds(1))\n                            .flatMap(batch -> asyncBatchWrite(group.key().name(), batch))\n                            .subscribeOn(Schedulers.boundedElastic())\n                    )\n                    .then(Mono.defer(this::createOutputDirectory))\n                    .then(Mono.defer(this::processOutputFiles))\n                    .doOnError(ex -> {\n                        log.error(\"processing failed\", ex);\n                        this.exit();\n                    })\n                    .doFinally(signal -> {\n                        log.info(\"all done, cost: {} ms\", System.currentTimeMillis() - start);\n                        this.exit();\n                    })\n                    .subscribe();\n        };\n    }\n\n    public Flux<Tuple2<AdFSProperties.OutputItem, String>> output(Set<AdFSProperties.OutputItem> outputs, Rule rule) {\n        return Flux.fromIterable(outputs)\n                .filter(file -> file.rule().isEmpty() || file.rule().contains(rule.getSourceName()))\n                .filter(file -> file.filter().isEmpty() || file.filter().contains(rule.getType()))\n                .flatMap(file -> {\n                    Handler handler = Handler.getHandler(file.type());\n                    String content = handler.format(rule);\n                    return content != null ? Mono.just(Tuples.of(file, content)) : Mono.empty();\n                });\n    }\n\n    private Mono<Path> createOutputDirectory() {\n        return Mono.fromCallable(() -> Files.createDirectories(Path.of(output.path())))\n                .subscribeOn(Schedulers.boundedElastic())\n                .onErrorResume(ex -> {\n                    log.error(\"create output dir failed\", ex);\n                    return Mono.error(ex);\n                });\n    }\n\n    private Mono<Void> processOutputFiles() {\n        Path dir = Path.of(output.path());\n        return Flux.fromIterable(output.files())\n                .flatMap(file -> {\n                    Output opt = outputMap.get(file.name());\n                    Path tempFile = opt.tempFile;\n                    Path targetFile = dir.resolve(file.name());\n                    String header = buildHeader(file, output.fileHeader(), Long.toString(opt.count.get()));\n\n                    log.info(\"[{}] written completed, total size => {}\", file.name(), opt.count.get());\n                    return prependAndMove(targetFile, tempFile, header).subscribeOn(Schedulers.boundedElastic());\n                })\n                .then();\n    }\n\n    private Mono<Void> asyncBatchWrite(String fileName, List<String> batch) {\n        Output opt = outputMap.get(fileName);\n        opt.count().addAndGet(batch.size());\n\n        return asyncBatchWrite(opt.tempFile, batch);\n    }\n\n    private Mono<Void> asyncBatchWrite(Path path, List<String> batch) {\n        return Mono.fromCallable(() -> {\n                    Files.write(path, batch, StandardCharsets.UTF_8,\n                            StandardOpenOption.CREATE, StandardOpenOption.APPEND);\n                    return null;\n                })\n                .onErrorResume(e -> {\n                    log.error(\"Write failed\", e);\n                    return Mono.empty();\n                })\n                .then();\n    }\n\n    private Mono<Path> prependAndMove(Path targetFile, Path tempFile, String header) {\n        return Mono.fromCallable(() -> {\n            if (Files.exists(targetFile)) {\n                Path intermediateFile = Files.createTempFile(targetFile.getFileName().toString(), \".intermediate\");\n\n                try (BufferedWriter writer = Files.newBufferedWriter(intermediateFile, StandardCharsets.UTF_8);\n                     BufferedReader tempReader = Files.newBufferedReader(tempFile, StandardCharsets.UTF_8)) {\n\n                    if (!header.isBlank()) {\n                        writer.write(header);\n                    }\n\n                    tempReader.transferTo(writer);\n                }\n\n                Files.move(intermediateFile, targetFile, StandardCopyOption.REPLACE_EXISTING);\n                Files.deleteIfExists(tempFile);\n            } else {\n                try (BufferedWriter writer = Files.newBufferedWriter(tempFile, StandardCharsets.UTF_8,\n                        StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {\n\n                    if (!header.isBlank()) {\n                        List<String> lines = Files.readAllLines(tempFile, StandardCharsets.UTF_8);\n                        Files.writeString(tempFile, header, StandardCharsets.UTF_8);\n                        Files.write(tempFile, lines, StandardCharsets.UTF_8, StandardOpenOption.APPEND);\n                    }\n                }\n\n                Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING);\n            }\n\n            return targetFile;\n        });\n    }\n\n    private void exit() {\n        int exit = SpringApplication.exit(this.context, () -> 0);\n        System.exit(exit);\n    }\n\n    private String buildHeader(AdFSProperties.OutputItem config, String parentHeader, String total) {\n        Handler handler = Handler.getHandler(config.type());\n        StringBuilder builder = new StringBuilder();\n\n        String template = config.fileHeader().isBlank() ? parentHeader : config.fileHeader();\n        if (!template.isBlank()) {\n            String header = handler.commented(template\n                            .replace(Constants.Placeholder.HEADER_DATE, LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\")))\n                            .replace(Constants.Placeholder.HEADER_NAME, config.name())\n                            .replace(Constants.Placeholder.HEADER_DESC, config.desc())\n                            .replace(Constants.Placeholder.HEADER_TYPE, config.type().name().toLowerCase()))\n                    .replace(Constants.Placeholder.HEADER_TOTAL, total);\n            builder.append(header).append(System.lineSeparator());\n        }\n\n        Optional.ofNullable(handler.headFormat()).filter(StringUtils::hasText)\n                .ifPresent(e -> builder.append(e).append(System.lineSeparator()));\n\n        return builder.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/config/AdFSProperties.java",
    "content": "package org.fordes.adfs.config;\n\nimport jakarta.validation.Valid;\nimport jakarta.validation.constraints.*;\nimport lombok.Data;\nimport org.fordes.adfs.enums.RuleSet;\nimport org.fordes.adfs.model.Rule;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.boot.context.properties.bind.DefaultValue;\nimport org.springframework.context.annotation.Configuration;\n\nimport java.util.*;\n\n/**\n *\n * @author fordes on 2025/10/29\n */\n@Data\n@Configuration\n@ConfigurationProperties(prefix = \"application\")\npublic class AdFSProperties implements InitializingBean {\n\n    @NotNull\n    private Config config;\n\n    @NotNull\n    @NotEmpty\n    private Set<InputProperties> input;\n\n    @NotNull\n    private OutputProperties output;\n\n    /**\n     * @see #input\n     */\n    @Deprecated\n    private Map<String, Set<InputProperties>> rule;\n\n    @Override\n    public void afterPropertiesSet() {\n        if (rule != null && !rule.isEmpty()) {\n            rule.forEach((k, v) -> this.input.addAll(v));\n        }\n    }\n\n    public record OutputProperties(\n\n            @DefaultValue(\"\")\n            String fileHeader,\n\n            @DefaultValue(\"rule\")\n            String path,\n\n            @NotEmpty\n            Set<@Valid @NotNull OutputItem> files\n    ) {\n    }\n\n    public record InputProperties(\n            @NotBlank\n            String name,\n\n            @DefaultValue(\"EASYLIST\")\n            RuleSet type,\n\n            @NotBlank\n            String path\n    ) {\n\n        @Override\n        public boolean equals(Object obj) {\n            if (obj instanceof InputProperties prop) {\n                return prop.path.equals(this.path) || prop.name.equals(this.name);\n            }\n            return false;\n        }\n\n        @Override\n        public int hashCode() {\n            return path.hashCode();\n        }\n    }\n\n    public record OutputItem(\n            @NotBlank\n            String name,\n\n            @NotNull\n            RuleSet type,\n\n            @DefaultValue(\"\")\n            String desc,\n\n            @DefaultValue(\"\")\n            String fileHeader,\n\n            @NotEmpty\n            @DefaultValue({})\n            Set<Rule.Type> filter,\n\n            @NotEmpty\n            @DefaultValue({})\n            Set<@NotBlank String> rule\n\n    ) {\n\n    }\n\n    public record Config(\n            @Positive @DefaultValue(\"0.0001\")\n            Double faultTolerance,\n\n            @Positive @DefaultValue(\"2000000\")\n            Long expectedQuantity,\n\n            @Min(1) @DefaultValue(\"6\")\n            Integer warnLimit,\n\n            @DefaultValue()\n            Set<String> exclude,\n\n            @DefaultValue\n            DomainDetect domainDetect,\n\n            @DefaultValue\n            Tracking tracking\n    ) {\n\n    }\n\n    public record DomainDetect(\n            @DefaultValue(\"false\")\n            Boolean enable,\n\n            @Positive @DefaultValue(\"1000\")\n            Integer timeout,\n\n            @Positive @DefaultValue(\"600\")\n            Integer cacheTtlMin,\n\n            @Positive @DefaultValue(\"86400\")\n            Integer cacheTtlMax,\n\n            @Positive @DefaultValue(\"300\")\n            Integer cacheNegativeTtl,\n\n            @Positive @DefaultValue(\"128\")\n            Integer concurrency,\n\n            @DefaultValue\n            List<String> provider) {\n\n    }\n\n    public record Tracking(@DefaultValue(\"false\") Boolean enable,\n                           @DefaultValue(\"logs/tracking.list\") String path) {\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/constant/Constants.java",
    "content": "package org.fordes.adfs.constant;\n\nimport java.io.File;\nimport java.util.Set;\n\npublic class Constants {\n\n    public static final String ROOT_PATH = System.getProperty(\"user.dir\");\n    public static final String FILE_SEPARATOR = File.separator;\n    public static final Set<String> LOCAL_IPS = Set.of(\"0.0.0.0\", \"127.0.0.1\", \"::1\");\n    public static final Set<String> LOCAL_DOMAINS = Set.of(\"localhost\", \"localhost.localdomain\", \"local\", \"ip6-localhost\", \"ip6-loopback\");\n    public static final String LOCAL_V4 = \"127.0.0.1\";\n    public static final String UNKNOWN_IP = \"0.0.0.0\";\n    public static final String LOCAL_V6 = \"::1\";\n    public static final String LOCALHOST = \"localhost\";\n    public static final String DOUBLE_AT = \"@@\";\n    public static final String IMPORTANT = \"important\";\n    public static final String DOMAIN = \"domain\";\n    public static final String TAB = \"\\t\";\n    public static final String PAYLOAD = \"payload\";\n\n    public static final String DNSMASQ_HEADER = \"address=/\";\n    public static final String SMARTDNS_HEADER = \"address /\";\n\n    public record Collector() {\n        public static final String PARSER = \"parser\";\n        public static final String DNS_CHECK = \"dns-check\";\n    }\n\n    public record Placeholder() {\n        public static final String HEADER_DATE = \"${date}\";\n        public static final String HEADER_NAME = \"${name}\";\n        public static final String HEADER_TOTAL = \"${total}\";\n        public static final String HEADER_TYPE = \"${type}\";\n        public static final String HEADER_DESC = \"${desc}\";\n    }\n\n    public record Symbol() {\n\n        public static final String EMPTY = \"\";\n        public static final String DOT = \".\";\n        public static final String EXCLAMATION = \"!\";\n        public static final String HASH = \"#\";\n        public static final String AT = \"@\";\n        public static final String PERCENT = \"%\";\n        public static final String DOLLAR = \"$\";\n        public static final String UNDERLINE = \"_\";\n        public static final String DASH = \"-\";\n        public static final String TILDE = \"~\";\n        public static final String COMMA = \",\";\n        public static final String SLASH = \"/\";\n        public static final String LEFT_BRACKETS = \"[\";\n        public static final String RIGHT_BRACKETS = \"]\";\n        public static final String OR = \"||\";\n        public static final String ASTERISK = \"*\";\n        public static final String QUESTION_MARK = \"?\";\n        public static final String A = \"a\";\n        public static final String CARET = \"^\";\n        public static final String WHITESPACE = \" \";\n        public static final String CR = \"\\r\";\n        public static final String LF = \"\\n\";\n        public static final String TAB = \"\\t\";\n        public static final String CRLF = CR + LF;\n        public static final String QUOTE = \"\\\"\";\n        public static final String SINGLE_QUOTE = \"'\";\n        public static final String ADD = \"+\";\n        public static final String COLON = \":\";\n        public static final String EQUAL = \"=\";\n    }\n\n\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/constant/RegConstants.java",
    "content": "package org.fordes.adfs.constant;\n\nimport java.util.regex.Pattern;\n\n/**\n * @author fordes on 2024/4/9\n */\npublic class RegConstants {\n\n    public static final Pattern PATTERN_PATH_ABSOLUTE = Pattern.compile(\"^[a-zA-Z]:([/\\\\\\\\].*)?\");\n    public static Pattern PATTERN_IP = Pattern.compile(\"((?:(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d?\\\\d)\\\\.){3}(?:25[0-5]|2[0-4]\\\\d|[01]?\\\\d?\\\\d))\");\n    public static Pattern PATTERN_DOMAIN = Pattern.compile(\"(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$\");\n    public static Pattern DOMAIN_PART = Pattern.compile(\"^([-a-zA-Z0-9]{0,62})+$\");\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/enums/HandleType.java",
    "content": "package org.fordes.adfs.enums;\n\nimport lombok.AllArgsConstructor;\n\n@AllArgsConstructor\npublic enum HandleType {\n\n    LOCAL,\n\n    REMOTE,\n\n    ;\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/enums/RuleSet.java",
    "content": "package org.fordes.adfs.enums;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\nimport java.util.stream.Stream;\n\n@Getter\n@AllArgsConstructor\npublic enum RuleSet {\n\n    EASYLIST,\n    DNSMASQ,\n    CLASH,\n    SMARTDNS,\n    HOSTS,\n    ;\n\n    public static RuleSet of(String name) {\n        return Stream.of(values())\n                .filter(v -> v.name().equalsIgnoreCase(name))\n                .findFirst()\n                .orElseThrow(() -> new IllegalArgumentException(\"unknown format: \" + name));\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/Parser.java",
    "content": "package org.fordes.adfs.handler;\n\nimport bloomfilter.mutable.BloomFilter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.config.AdFSProperties;\nimport org.fordes.adfs.constant.Constants;\nimport org.fordes.adfs.enums.HandleType;\nimport org.fordes.adfs.handler.dns.DnsDetector;\nimport org.fordes.adfs.handler.fetch.Fetcher;\nimport org.fordes.adfs.handler.rule.Handler;\nimport org.fordes.adfs.model.Rule;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.atomic.AtomicLong;\n\nimport static org.fordes.adfs.config.AdFSProperties.Config;\nimport static org.fordes.adfs.config.AdFSProperties.InputProperties;\n\n@Slf4j\n@Component\npublic class Parser {\n\n    protected final BloomFilter<Rule> filter;\n    protected final Config config;\n    protected final DnsDetector detector;\n    protected final Tracker tracker;\n\n    public Parser(AdFSProperties properties, Optional<DnsDetector> detector,\n                  Optional<Tracker> tracker) {\n\n        this.config = properties.getConfig();\n        this.detector = detector.orElse(null);\n        this.tracker = tracker.orElse(null);\n        this.filter = BloomFilter.apply(config.expectedQuantity(), config.faultTolerance(), rule -> rule.hashCode());\n    }\n\n    public Flux<Rule> handle(InputProperties prop) {\n        if (prop.path().startsWith(\"http\")) {\n            return this.handle(prop, HandleType.REMOTE);\n        }\n        return this.handle(prop, HandleType.LOCAL);\n    }\n\n    public Flux<Rule> handle(InputProperties prop, HandleType type) {\n\n        AtomicLong total = new AtomicLong(0L);\n        AtomicLong invalid = new AtomicLong(0L);\n        AtomicLong repeat = new AtomicLong(0L);\n        AtomicLong effective = new AtomicLong(0L);\n        Set<String> exclude = Optional.ofNullable(config.exclude()).orElseGet(Set::of);\n        long start = System.currentTimeMillis();\n\n        return Flux.just(prop)\n                .flatMap(p -> {\n                    Fetcher fetcher = Fetcher.getFetcher(type);\n                    return fetcher.fetch(p.path());\n                })\n                .filter(StringUtils::hasText)\n                .flatMap(line -> {\n                    Handler handler = Handler.getHandler(prop.type());\n\n                    if (handler.isComment(line)) {\n                        return Mono.empty();\n                    }\n\n                    Rule rule = handler.parse(line);\n                    total.incrementAndGet();\n                    if (Rule.EMPTY.equals(rule)) {\n                        invalid.incrementAndGet();\n                        log.debug(\"[{}] parse fail: {}\", prop.name(), line);\n                        if (tracker != null) {\n                            tracker.writeSync(Constants.Collector.PARSER, prop.name(), line);\n                        }\n                        return Mono.empty();\n                    }\n\n                    rule.setSourceName(prop.name());\n                    return Mono.just(rule);\n                })\n                .flatMap(e -> {\n\n                    if (e.getTarget() != null && exclude.contains(e.getTarget())) {\n                        log.info(\"exclude rule: {}\", e.getOrigin());\n                        return Mono.empty();\n                    }\n\n                    if (filter.mightContain(e)) {\n                        log.debug(\"already exists rule: {}\", e.getOrigin());\n                        repeat.incrementAndGet();\n                        return Mono.empty();\n                    }\n\n                    if (e.getOrigin().length() <= config.warnLimit()) {\n                        log.warn(\"[{}] suspicious rule => {}\", prop.name(), e.getOrigin());\n                    }\n\n                    return Mono.just(e);\n\n                })\n                .onErrorResume(ex -> {\n                    log.error(\"Parse {} failed\", prop.name(), ex);\n                    return Mono.empty();\n                })\n                .flatMap(rule -> {\n\n                    /**\n                     * 假设有规则 ||example.org^\n                     * 通过DNS查询 example.org 是否存在 A/AAAA/CNAME 记录作为判断依据\n                     * 不可避免的误判是，example.org 没有有效记录，而其存在有效子域如 test.example.org\n                     */\n                    if (detector != null && Rule.Type.BASIC.equals(rule.getType())\n                            && Rule.Scope.DOMAIN.equals(rule.getScope())) {\n\n                        return Flux.just(rule.getTarget())\n                                .flatMap(e -> detector.lookup(e), 1)\n                                .flatMap(e -> {\n                                    if (!e) {\n                                        invalid.incrementAndGet();\n                                        log.debug(\"[{}] dns check invalid rule => {}\", prop.name(), rule.getOrigin());\n                                        if (tracker != null) {\n                                            tracker.writeSync(Constants.Collector.DNS_CHECK,prop.name(), rule.getOrigin());\n                                        }\n                                        return Mono.empty();\n                                    }\n                                    return Mono.just(rule);\n                                });\n                    }\n                    return Mono.just(rule);\n                }, config.domainDetect().concurrency())\n                .flatMap(e -> {\n                    filter.add(e);\n                    effective.incrementAndGet();\n                    return Mono.just(e);\n\n                })\n                .doFinally(signal -> {\n                    log.info(\"[{}] parsing cost {} ms, total: {}, effective: {}, repeat: {}, invalid: {}\",\n                            prop.name(), System.currentTimeMillis() - start,\n                            total.get(), effective.get(), repeat.get(), invalid.get());\n                });\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/Tracker.java",
    "content": "package org.fordes.adfs.handler;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.config.AdFSProperties;\nimport org.fordes.adfs.constant.Constants;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.stereotype.Component;\nimport reactor.core.Disposable;\nimport reactor.core.publisher.Mono;\nimport reactor.core.publisher.Sinks;\nimport reactor.core.scheduler.Schedulers;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.time.Duration;\nimport java.util.List;\n\n/**\n * @author Chengfs on 2025/10/31\n */\n@Slf4j\n@Component\n@ConditionalOnProperty(prefix = \"application.config.tracking\", name = \"enable\", havingValue = \"true\")\npublic class Tracker implements DisposableBean {\n\n    private final Path file;\n    private final Sinks.Many<String> sink;\n    private final Disposable subscription;\n\n    public Tracker(AdFSProperties properties) throws IOException {\n        var config = properties.getConfig().tracking();\n\n        this.file = Path.of(config.path());\n        if (file.getParent() != null) {\n            Files.createDirectories(file.getParent());\n        }\n        Files.writeString(file, Constants.Symbol.EMPTY, StandardCharsets.UTF_8,\n                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n\n        this.sink = Sinks.many().unicast().onBackpressureBuffer();\n        this.subscription = sink.asFlux()\n                .bufferTimeout(5000, Duration.ofSeconds(30))\n                .filter(batch -> !batch.isEmpty())\n                .concatMap(this::writeBatch)\n                .retry()\n                .subscribe();\n\n        log.info(\"lost collector is enabled, path: {}\", file);\n    }\n\n    public Mono<Void> write(String source, String ruleName, String rule) {\n        String line = String.format(\"[%s] [%s] %s\\n\", source, ruleName, rule);\n        return Mono.fromRunnable(() -> sink.tryEmitNext(line));\n    }\n\n    public void writeSync(String source, String ruleName, String rule) {\n        write(source,ruleName, rule).subscribe();\n    }\n\n    private Mono<Void> writeBatch(List<String> lines) {\n        return Mono.fromCallable(() -> {\n                    StringBuilder sb = new StringBuilder(lines.size() * 100);\n                    lines.forEach(sb::append);\n                    Files.writeString(file, sb.toString(), StandardCharsets.UTF_8,\n                            StandardOpenOption.APPEND, StandardOpenOption.CREATE);\n                    return null;\n                })\n                .subscribeOn(Schedulers.boundedElastic())\n                .then();\n    }\n\n    @Override\n    public void destroy() throws Exception {\n        sink.tryEmitComplete();\n        if (subscription != null) {\n            Thread.sleep(2000);\n            subscription.dispose();\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/dns/DnsDetector.java",
    "content": "package org.fordes.adfs.handler.dns;\n\nimport io.netty.channel.EventLoop;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.nio.NioDatagramChannel;\nimport io.netty.handler.codec.dns.DnsRecord;\nimport io.netty.resolver.ResolvedAddressTypes;\nimport io.netty.resolver.dns.*;\nimport io.netty.util.concurrent.Future;\nimport jakarta.annotation.PreDestroy;\nimport lombok.Data;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.config.AdFSProperties;\nimport org.fordes.adfs.constant.Constants;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\n\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.net.UnknownHostException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.LongAdder;\nimport java.util.stream.IntStream;\n\n@Data\n@Slf4j\n@Component\n@ConditionalOnProperty(prefix = \"application.config.domain-detect\", name = \"enable\", havingValue = \"true\")\npublic class DnsDetector {\n\n    private final NioEventLoopGroup group;\n    private final List<DnsNameResolver> resolvers;\n    private final AtomicLong nextIndex;\n    private final LongAdder cacheHits;\n\n    public DnsDetector(AdFSProperties properties) {\n        var config = properties.getConfig().domainDetect();\n        int concurrency = config.concurrency();\n\n        this.group = new NioEventLoopGroup(concurrency);\n        this.resolvers = new ArrayList<>(concurrency);\n        this.nextIndex = new AtomicLong(0);\n        this.cacheHits = new LongAdder();\n\n\n        //初始化\n        DnsServerAddressStreamProvider provider = buildProvider(config.provider());\n        DnsCache sharedCache = this.buildDnsCache(config.cacheTtlMin(), config.cacheTtlMax(), config.cacheNegativeTtl());\n        DnsCnameCache sharedCnameCache = new DefaultDnsCnameCache(config.cacheTtlMin(), config.cacheTtlMax());\n        IntStream.range(0, concurrency).forEach(i -> {\n            EventLoop loop = group.next();\n            DnsNameResolver resolver = new DnsNameResolverBuilder(loop)\n                    .datagramChannelFactory(NioDatagramChannel::new)\n                    .nameServerProvider(provider)\n                    .queryTimeoutMillis(config.timeout())\n                    .maxQueriesPerResolve(2)\n                    .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED)\n                    .resolveCache(sharedCache)\n                    .cnameCache(sharedCnameCache)\n                    .optResourceEnabled(false)\n                    .build();\n            resolvers.add(resolver);\n        });\n\n        log.info(\"dns detector init success, concurrency:{}\", concurrency);\n    }\n\n    private DnsServerAddressStreamProvider buildProvider(List<String> provider) {\n        if (provider.isEmpty()) {\n            return DnsServerAddressStreamProviders.platformDefault();\n        }\n\n        InetSocketAddress[] addresses = provider.stream()\n                .map(e -> {\n                    String[] parts = e.split(Constants.Symbol.COLON);\n                    return new InetSocketAddress(parts[0], parts.length > 1 ? Integer.parseInt(parts[1]) : 53);\n                })\n                .toArray(InetSocketAddress[]::new);\n\n        return new SequentialDnsServerAddressStreamProvider(addresses);\n    }\n\n    private DnsCache buildDnsCache(int minTtl, int maxTtl, int negativeTtl) {\n        return new DefaultDnsCache(minTtl, maxTtl, negativeTtl) {\n            @Override\n            public List<? extends DnsCacheEntry> get(String hostname, DnsRecord[] additionals) {\n                List<? extends DnsCacheEntry> result = super.get(hostname, additionals);\n                if (result != null && !result.isEmpty()) {\n                    cacheHits.increment();\n                }\n                return result;\n            }\n        };\n    }\n\n    public Mono<Boolean> lookup(String domain) {\n        if (domain == null || domain.isEmpty()) {\n            return Mono.just(true);\n        }\n\n        String normalizedDomain = domain.toLowerCase().trim();\n        DnsNameResolver resolver = resolvers.get((int) (nextIndex.getAndIncrement() % resolvers.size()));\n        return lookup(resolver, normalizedDomain);\n    }\n\n    private Mono<Boolean> lookup(DnsNameResolver resolver, String domain) {\n        return Mono.create(sink -> {\n\n            Future<List<InetAddress>> future = resolver.resolveAll(domain);\n            future.addListener(result -> {\n\n                boolean res = true;\n                if (!result.isSuccess()) {\n                    Throwable cause = result.cause();\n\n                    if (cause instanceof UnknownHostException) {\n                        res = false;\n                    } else {\n                        log.warn(\"dns check failed: {} => {}\", domain, cause.getMessage());\n                    }\n                }\n                sink.success(res);\n                log.debug(\"dns check done, available: {}\", resolvers.size());\n            });\n        });\n    }\n\n\n    @PreDestroy\n    public void destroy() {\n        try {\n            log.info(\"dns detector shutdown, total queries:{}, cache hits:{}\", nextIndex.get(), cacheHits.sum());\n\n            resolvers.forEach(DnsNameResolver::close);\n            group.shutdownGracefully().await(10, TimeUnit.SECONDS);\n\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            log.error(\"Error during shutdown\", e);\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/fetch/Fetcher.java",
    "content": "package org.fordes.adfs.handler.fetch;\n\nimport jakarta.annotation.Nonnull;\nimport org.fordes.adfs.enums.HandleType;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport reactor.core.publisher.Flux;\n\nimport java.nio.charset.Charset;\n\npublic abstract class Fetcher {\n\n    protected @Nonnull Charset charset() {\n        return Charset.defaultCharset();\n    }\n\n    public abstract Flux<String> fetch(@Nonnull String path);\n\n    protected Flux<String> fetch(Flux<DataBuffer> buffers) {\n        final StringBuilder lineBuffer = new StringBuilder();\n        return buffers.flatMap(data -> {\n                    String chunk = data.toString(this.charset());\n                    DataBufferUtils.release(data);\n\n                    lineBuffer.append(chunk);\n                    String full = lineBuffer.toString();\n\n                    String[] lines = full.split(\"\\\\r?\\\\n\", -1);\n                    int len = lines.length;\n\n                    lineBuffer.setLength(0);\n\n                    if (!full.endsWith(\"\\n\") && !full.endsWith(\"\\r\")) {\n                        lineBuffer.append(lines[len - 1]);\n                        len--;\n                    }\n\n                    return Flux.fromArray(lines).take(len);\n                })\n                .concatWith(Flux.defer(() -> {\n                    if (lineBuffer.length() > 0) {\n                        return Flux.just(lineBuffer.toString());\n                    } else {\n                        return Flux.empty();\n                    }\n                }));\n    }\n\n    public final static Fetcher getFetcher(HandleType type) {\n        switch (type) {\n            case LOCAL:\n                return new LocalFetcher();\n            case REMOTE:\n                return new HttpFetcher();\n        }\n        throw new IllegalArgumentException(\"unsupported handle type: \" + type);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/fetch/HttpFetcher.java",
    "content": "package org.fordes.adfs.handler.fetch;\n\nimport io.netty.channel.ChannelOption;\nimport io.netty.handler.timeout.ReadTimeoutHandler;\nimport io.netty.handler.timeout.WriteTimeoutHandler;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.client.reactive.ReactorClientHttpConnector;\nimport org.springframework.web.reactive.function.client.ExchangeStrategies;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.core.publisher.Flux;\nimport reactor.netty.http.client.HttpClient;\n\nimport java.net.URI;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.concurrent.TimeUnit;\n\n@Slf4j\npublic class HttpFetcher extends Fetcher {\n\n    private final WebClient webClient;\n    private Integer connectTimeout = 10_000;\n    private Integer readTimeout = 30_000;\n    private Integer writeTimeout = 30_000;\n    private Integer bufferSize = 4096;\n\n    public HttpFetcher() {\n\n        HttpClient httpClient = HttpClient.create()\n                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectTimeout) // 连接超时 10 秒\n                .responseTimeout(Duration.ofSeconds(30))              // 响应超时 30 秒\n                .doOnConnected(conn -> conn\n                        .addHandlerLast(new ReadTimeoutHandler(this.readTimeout, TimeUnit.MILLISECONDS))   // 读超时\n                        .addHandlerLast(new WriteTimeoutHandler(this.writeTimeout, TimeUnit.MILLISECONDS))  // 写超时\n                );\n\n        ExchangeStrategies strategies = ExchangeStrategies.builder()\n                .codecs(configurer -> configurer\n                        .defaultCodecs()\n                        .maxInMemorySize(this.bufferSize)\n                )\n                .build();\n\n        this.webClient = WebClient.builder()\n                .clientConnector(new ReactorClientHttpConnector(httpClient))\n                .exchangeStrategies(strategies)\n                .defaultHeader(HttpHeaders.CONNECTION, \"keep-alive\")\n//                .defaultHeader(HttpHeaders.ACCEPT_CHARSET, this.charset().displayName())\n//                .defaultHeader(HttpHeaders.ACCEPT_ENCODING, \"gzip, deflate, br\")\n                .build();\n    }\n\n    public HttpFetcher(Integer connectTimeout, Integer readTimeout, Integer writeTimeout, Integer bufferSize) {\n        this();\n        this.connectTimeout = connectTimeout;\n        this.readTimeout = readTimeout;\n        this.writeTimeout = writeTimeout;\n        this.bufferSize = bufferSize;\n    }\n\n    @Override\n    public Flux<String> fetch(String path) {\n        Flux<DataBuffer> data = webClient.get()\n                .uri(URI.create(path))\n                .retrieve()\n                .bodyToFlux(DataBuffer.class)\n                .onErrorResume(e -> {\n                    log.error(\"http rule => {}, fetch failed  => {}\", path, e.getMessage(), e);\n                    return Flux.empty();\n                });\n\n        return this.fetch(data);\n    }\n\n    @Override\n    protected Charset charset() {\n        return StandardCharsets.UTF_8;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/fetch/LocalFetcher.java",
    "content": "package org.fordes.adfs.handler.fetch;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.util.Util;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport org.springframework.core.io.buffer.DefaultDataBufferFactory;\nimport reactor.core.publisher.Flux;\n\nimport java.nio.charset.Charset;\nimport java.nio.file.Path;\n\n@Slf4j\npublic class LocalFetcher extends Fetcher {\n\n    private int bufferSize = 4096;\n\n    public LocalFetcher() {\n        super();\n    }\n\n    public LocalFetcher(int bufferSize) {\n        super();\n        this.bufferSize = bufferSize;\n    }\n\n    @Override\n    public Flux<String> fetch(String path) {\n\n        Flux<DataBuffer> data = Flux.just(path)\n                .map(Util::normalizePath)\n                .map(Path::of)\n                .flatMap(p -> DataBufferUtils.read(p, new DefaultDataBufferFactory(), this.bufferSize))\n                .onErrorResume(e -> {\n                    log.error(\"local rule => {}, read failed  => {}\", path, e.getMessage(), e);\n                    return Flux.empty();\n                });\n\n        return this.fetch(data);\n    }\n\n    @Override\n    protected Charset charset() {\n        return super.charset();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/ClashHandler.java",
    "content": "package org.fordes.adfs.handler.rule;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.enums.RuleSet;\nimport org.fordes.adfs.model.Rule;\nimport org.fordes.adfs.util.Util;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport static org.fordes.adfs.constant.Constants.*;\nimport static org.fordes.adfs.constant.Constants.Symbol.CRLF;\nimport static org.fordes.adfs.constant.RegConstants.PATTERN_DOMAIN;\n\n/**\n * @author fordes123 on 2024/5/27\n */\n@Slf4j\n@Component\npublic final class ClashHandler extends Handler implements InitializingBean {\n\n    @Override\n    public Rule parse(String line) {\n        //跳过文件头\n        if (line.startsWith(PAYLOAD)) {\n            return Rule.EMPTY;\n        }\n\n        Rule rule = new Rule();\n        rule.setOrigin(line);\n        rule.setSourceType(RuleSet.EASYLIST);\n\n        //只匹配 domain 规则，ipcidr、classical 规则暂不支持\n        String content = (line.startsWith(Symbol.DASH) ? line.substring(Symbol.DASH.length()) : line).trim();\n        if (content.startsWith(Symbol.SINGLE_QUOTE)) {\n            content = Util.subBetween(line, Symbol.SINGLE_QUOTE, Symbol.SINGLE_QUOTE).trim();\n        } else if (content.startsWith(Symbol.QUOTE)) {\n            content = Util.subBetween(line, Symbol.QUOTE, Symbol.QUOTE).trim();\n        }\n\n        //通配符 * 一次只能匹配一级域名，无法转换为easylist\n        if (content.startsWith(Symbol.ASTERISK)) {\n            rule.setType(Rule.Type.UNKNOWN);\n            return rule;\n        }\n\n        //通配符 +\n        if (content.startsWith(Symbol.ADD)) {\n            content = content.substring(content.startsWith(\"+.\") ? 2 : 1);\n            rule.setControls(Set.of(Rule.Control.OVERLAY));\n        }\n\n        //判断是否是domain\n        boolean haveAsterisk = content.contains(Symbol.ASTERISK);\n        String temp = haveAsterisk ? content.replace(Symbol.ASTERISK, Symbol.A) : content;\n        if (PATTERN_DOMAIN.matcher(temp).matches()) {\n            rule.setType(haveAsterisk ? Rule.Type.WILDCARD : Rule.Type.BASIC);\n        }\n\n        rule.setTarget(content);\n        rule.setDest(UNKNOWN_IP);\n        rule.setMode(Rule.Mode.DENY);\n        rule.setScope(Rule.Scope.DOMAIN);\n        if (rule.getType() == null) {\n            rule.setType(Rule.Type.UNKNOWN);\n        }\n        return rule;\n    }\n\n    @Override\n    public String format(Rule rule) {\n        if (Rule.Type.UNKNOWN == rule.getType()) {\n            if (RuleSet.CLASH == rule.getSourceType()) {\n                return rule.getOrigin();\n            }\n            return null;\n        } else if (rule.getMode() == Rule.Mode.DENY && rule.getScope() == Rule.Scope.DOMAIN) {\n            StringBuilder builder = new StringBuilder();\n            builder.append(Symbol.WHITESPACE).append(Symbol.WHITESPACE).append(Symbol.DASH).append(Symbol.WHITESPACE).append(Symbol.QUOTE);\n\n            Set<Rule.Control> controls = Optional.ofNullable(rule.getControls()).orElse(Set.of());\n            if (controls.contains(Rule.Control.OVERLAY)) {\n                builder.append(Symbol.ADD).append(Symbol.DOT);\n            }\n            builder.append(rule.getTarget());\n            builder.append(Symbol.QUOTE);\n            return builder.toString();\n        }\n        return null;\n    }\n\n    @Override\n    public String headFormat() {\n        return PAYLOAD + Symbol.COLON;\n    }\n\n    @Override\n    public boolean isComment(String line) {\n        return line.startsWith(Symbol.HASH);\n    }\n\n    @Override\n    public String commented(String value) {\n        return Util.splitIgnoreBlank(value, Symbol.LF).stream()\n                .map(e -> Symbol.HASH + e.trim())\n                .collect(Collectors.joining(CRLF));\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        this.register(RuleSet.CLASH, this);\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/DnsmasqHandler.java",
    "content": "package org.fordes.adfs.handler.rule;\n\nimport org.fordes.adfs.enums.RuleSet;\nimport org.fordes.adfs.model.Rule;\nimport org.fordes.adfs.util.Util;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.stereotype.Component;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport static org.fordes.adfs.constant.Constants.*;\n\n/**\n * @author fordes123 on 2024/5/27\n */\n@Component\npublic final class DnsmasqHandler extends Handler implements InitializingBean {\n\n    @Override\n    public Rule parse(String line) {\n\n        String content = Util.subAfter(line, DNSMASQ_HEADER, true);\n        List<String> data = Util.splitIgnoreBlank(content, Symbol.SLASH);\n        if (data.size() == 1 || data.size() == 2) {\n            String domain = data.getFirst();\n            String ip = data.size() > 1 ? data.get(1) : null;\n\n            Rule rule = new Rule();\n            rule.setSourceType(RuleSet.DNSMASQ);\n            rule.setOrigin(line);\n            rule.setTarget(domain);\n            rule.setDest(ip);\n            rule.setScope(Rule.Scope.DOMAIN);\n            rule.setType(Rule.Type.BASIC);\n            rule.setMode((ip == null || LOCAL_IPS.contains(ip)) ? Rule.Mode.DENY : Rule.Mode.REWRITE);\n            return rule;\n        }\n        return Rule.EMPTY;\n    }\n\n    @Override\n    public String format(Rule rule) {\n        if (Rule.Scope.DOMAIN == rule.getScope() &&\n                Rule.Type.BASIC == rule.getType() &&\n                Rule.Mode.ALLOW != rule.getMode()) {\n\n            StringBuilder builder = new StringBuilder();\n            builder.append(DNSMASQ_HEADER)\n                    .append(rule.getTarget());\n            if (Rule.Mode.REWRITE.equals(rule.getMode())) {\n                builder.append(Symbol.SLASH)\n                        .append(rule.getDest());\n            }\n            builder.append(Symbol.SLASH);\n            return builder.toString();\n        }\n        return null;\n    }\n\n    @Override\n    public String commented(String value) {\n        return Util.splitIgnoreBlank(value, Symbol.LF).stream()\n                .map(e -> Symbol.HASH + Symbol.WHITESPACE + e.trim())\n                .collect(Collectors.joining(Symbol.CRLF));\n    }\n\n    @Override\n    public boolean isComment(String line) {\n        return line.startsWith(Symbol.HASH);\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        this.register(RuleSet.DNSMASQ, this);\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/EasylistHandler.java",
    "content": "package org.fordes.adfs.handler.rule;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.enums.RuleSet;\nimport org.fordes.adfs.model.Rule;\nimport org.fordes.adfs.util.Util;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Arrays;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\nimport static org.fordes.adfs.constant.Constants.*;\n\n/**\n * @author fordes123 on 2024/5/27\n */\n@Slf4j\n@Component\npublic final class EasylistHandler extends Handler implements InitializingBean {\n\n    @Override\n    public Rule parse(String line) {\n        Rule rule = new Rule();\n        rule.setOrigin(line);\n        rule.setSourceType(RuleSet.EASYLIST);\n        rule.setMode(Rule.Mode.DENY);\n\n        if (line.startsWith(DOUBLE_AT)) {\n            rule.setMode(Rule.Mode.ALLOW);\n            line = line.substring(2);\n        }\n\n        int _head = 0;\n        if (line.startsWith(Symbol.OR)) {\n            _head = Symbol.OR.length();\n            rule.getControls().add(Rule.Control.OVERLAY);\n        }\n\n\n        //修饰部分\n        int _tail = line.indexOf(Symbol.CARET);\n        if (_tail > 0) {\n            rule.getControls().add(Rule.Control.QUALIFIER);\n\n            String modify = line.substring(_tail + 1);\n            if (!modify.isEmpty()) {\n                modify = modify.startsWith(Symbol.DOLLAR) ? modify.substring(1) : modify;\n                String[] array = modify.split(Symbol.COMMA);\n                if (Arrays.stream(array).allMatch(IMPORTANT::equals)) {\n                    rule.getControls().add(Rule.Control.IMPORTANT);\n                } else {\n                    rule.setType(Rule.Type.UNKNOWN);\n                    return rule;\n                }\n            }\n        } else if (line.endsWith(Symbol.DOLLAR + IMPORTANT)) {\n            rule.getControls().add(Rule.Control.IMPORTANT);\n            _tail = line.length() - (Symbol.DOLLAR.length() + IMPORTANT.length());\n        }\n\n\n        //内容部分\n        String content = line.substring(_head, _tail > 0 ? _tail : line.length());\n\n        if (content.startsWith(Symbol.SLASH) && content.endsWith(Symbol.SLASH)) {\n            content = content.substring(1, content.length() - 1);\n            rule.setType(Rule.Type.UNKNOWN);\n        }\n\n        //判断是否为基本或通配规则\n        Util.isBaseRule(content, (origin, e) -> {\n            if (rule.getType() == null) {\n                rule.setType(e);\n            }\n            rule.setScope(Rule.Scope.DOMAIN);\n            rule.setTarget(origin);\n            if (Rule.Mode.DENY.equals(rule.getMode())) {\n                rule.setDest(UNKNOWN_IP);\n            }\n        }, e -> {\n            Map.Entry<String, String> entry = Util.parseHosts(e);\n            if (entry != null) {\n                rule.setSourceType(RuleSet.HOSTS);\n                rule.setTarget(entry.getValue());\n                rule.setMode(LOCAL_IPS.contains(entry.getKey()) && !LOCAL_DOMAINS.contains(entry.getValue()) ? Rule.Mode.DENY : Rule.Mode.REWRITE);\n                rule.setDest(Rule.Mode.DENY == rule.getMode() ? UNKNOWN_IP : entry.getKey());\n                rule.setScope(Rule.Scope.DOMAIN);\n                rule.setType(Rule.Type.BASIC);\n            } else {\n                rule.setType(Rule.Type.UNKNOWN);\n            }\n        });\n        return rule;\n    }\n\n    @Override\n    public String format(Rule rule) {\n        if (Rule.Type.UNKNOWN != rule.getType() && Rule.Mode.REWRITE != rule.getMode()) {\n\n            StringBuilder builder = new StringBuilder();\n            Optional.of(rule.getMode())\n                    .filter(Rule.Mode.ALLOW::equals)\n                    .ifPresent(m -> builder.append(DOUBLE_AT));\n\n            Optional.of(rule.getControls())\n                    .filter(e -> e.contains(Rule.Control.OVERLAY))\n                    .ifPresent(c -> builder.append(Symbol.OR));\n\n            builder.append(rule.getTarget());\n\n            Optional.of(rule.getControls())\n                    .filter(e -> e.contains(Rule.Control.QUALIFIER))\n                    .ifPresent(c -> builder.append(Symbol.CARET));\n\n            Optional.of(rule.getControls())\n                    .filter(e -> e.contains(Rule.Control.IMPORTANT))\n                    .ifPresent(c -> builder.append(Symbol.DOLLAR).append(IMPORTANT));\n            return builder.toString();\n        }\n\n        //同源未知规则可直接写出\n        if (Rule.Type.UNKNOWN == rule.getType() && RuleSet.EASYLIST == rule.getSourceType()) {\n            return rule.getOrigin();\n        }\n        return null;\n    }\n\n    @Override\n    public String commented(String value) {\n        return Util.splitIgnoreBlank(value, Symbol.LF).stream()\n                .map(e -> Symbol.EXCLAMATION + Symbol.WHITESPACE + e.trim())\n                .collect(Collectors.joining(Symbol.CRLF));\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        this.register(RuleSet.EASYLIST, this);\n    }\n\n    @Override\n    public boolean isComment(String line) {\n        return Util.startWithAny(line, Symbol.HASH, Symbol.EXCLAMATION) || Util.between(line, Symbol.LEFT_BRACKETS, Symbol.RIGHT_BRACKETS);\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/Handler.java",
    "content": "package org.fordes.adfs.handler.rule;\n\nimport jakarta.annotation.Nonnull;\nimport jakarta.annotation.Nullable;\nimport org.fordes.adfs.constant.Constants;\nimport org.fordes.adfs.enums.RuleSet;\nimport org.fordes.adfs.model.Rule;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic abstract sealed class Handler permits EasylistHandler, DnsmasqHandler, ClashHandler,\n        SmartdnsHandler, HostsHandler {\n\n    private static final Map<RuleSet, Handler> handlerMap = new HashMap<>(RuleSet.values().length, 1);\n\n    /**\n     * 解析规则<br/>\n     * 返回 {@link Rule#EMPTY} 即表示解析失败\n     *\n     * @param line 规则文本\n     * @return {@link Rule}\n     */\n    public abstract @Nonnull Rule parse(String line);\n\n    /**\n     * 转换规则<br/>\n     *\n     * @param rule {@link Rule} null 表示无法转换或失败\n     * @return 规则文本\n     */\n    public abstract @Nullable String format(Rule rule);\n\n    /**\n     * 生成注释\n     * @param value 目标内容\n     * @return  注释\n     */\n    public abstract String commented(String value);\n\n    /**\n     * 某些规则格式拥有固定的头部内容，可实现此方法以返回\n     */\n    public String headFormat() {\n        return Constants.Symbol.EMPTY;\n    }\n\n    /**\n     * 某些规则格式拥有固定的尾部内容，可实现此方法以返回\n     */\n    public String tailFormat() {\n        return Constants.Symbol.EMPTY;\n    }\n\n    /**\n     * 验证规则文本是否为注释<br/>\n     * 并不强制子类实现此方法，且不是注释不表示此规则有效\n     *\n     * @param line 规则文本\n     * @return 默认 false\n     */\n    public boolean isComment(String line) {\n        return false;\n    }\n\n    /**\n     * 根据 RuleSet 获取 Handler\n     *\n     * @param type {@link RuleSet}\n     * @return {@link Handler}\n     */\n    public static Handler getHandler(RuleSet type) {\n        return handlerMap.get(type);\n    }\n\n    protected void register(RuleSet type, Handler handler) {\n        handlerMap.put(type, handler);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/HostsHandler.java",
    "content": "package org.fordes.adfs.handler.rule;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.enums.RuleSet;\nimport org.fordes.adfs.model.Rule;\nimport org.fordes.adfs.util.Util;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\nimport static org.fordes.adfs.constant.Constants.*;\n\n/**\n * @author fordes123 on 2024/5/27\n */\n@Slf4j\n@Component\npublic final class HostsHandler extends Handler implements InitializingBean {\n\n    @Override\n    public Rule parse(String line) {\n        Map.Entry<String, String> entry = Util.parseHosts(line);\n        if (entry == null || Objects.equals(entry.getKey(), entry.getValue())) {\n            return Rule.EMPTY;\n        }\n\n        Rule rule = new Rule();\n        rule.setSourceType(RuleSet.HOSTS);\n        rule.setOrigin(line);\n        rule.setTarget(entry.getValue());\n        rule.setMode(LOCAL_IPS.contains(entry.getKey()) && !LOCAL_DOMAINS.contains(entry.getValue()) ? Rule.Mode.DENY : Rule.Mode.REWRITE);\n        rule.setDest(Rule.Mode.DENY == rule.getMode() ? UNKNOWN_IP : entry.getKey());\n        rule.setScope(Rule.Scope.DOMAIN);\n        rule.setType(Rule.Type.BASIC);\n        return rule;\n    }\n\n    @Override\n    public String format(Rule rule) {\n        if (Rule.Scope.DOMAIN == rule.getScope() &&\n                Rule.Type.BASIC == rule.getType() &&\n                Rule.Mode.ALLOW != rule.getMode()) {\n            return Optional.ofNullable(rule.getDest()).orElse(UNKNOWN_IP) + Symbol.TAB + rule.getTarget();\n        }\n        return null;\n    }\n\n    @Override\n    public String commented(String value) {\n        return Util.splitIgnoreBlank(value, Symbol.LF).stream()\n                .map(e -> Symbol.HASH + Symbol.WHITESPACE + e.trim())\n                .collect(Collectors.joining(Symbol.CRLF));\n    }\n\n    @Override\n    public boolean isComment(String line) {\n        return line.startsWith(Symbol.HASH);\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        this.register(RuleSet.HOSTS, this);\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/SmartdnsHandler.java",
    "content": "package org.fordes.adfs.handler.rule;\n\nimport org.fordes.adfs.enums.RuleSet;\nimport org.fordes.adfs.model.Rule;\nimport org.fordes.adfs.util.Util;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.stereotype.Component;\n\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport static org.fordes.adfs.constant.Constants.*;\nimport static org.fordes.adfs.model.Rule.Mode.ALLOW;\n\n/**\n * @author fordes123 on 2024/5/27\n */\n@Component\npublic final class SmartdnsHandler extends Handler implements InitializingBean {\n\n    @Override\n    public Rule parse(String line) {\n\n        String content = Util.subAfter(line, SMARTDNS_HEADER, true);\n        List<String> data = Util.splitIgnoreBlank(content, Symbol.SLASH);\n        if (data.size() == 2) {\n            String domain = data.getFirst();\n            String control = data.get(1);\n            Rule rule = new Rule();\n            rule.setOrigin(line);\n            rule.setSourceType(RuleSet.EASYLIST);\n\n            switch (control) {\n                case Symbol.HASH -> rule.setMode(Rule.Mode.DENY);\n                case Symbol.DASH -> rule.setMode(ALLOW);\n                default -> {\n                    //未知或不支持的控制符 如 #6 #4\n                    rule.setType(Rule.Type.UNKNOWN);\n                    return rule;\n                }\n            }\n\n            //仅匹配主域名\n            if (domain.startsWith(Symbol.DASH)) {\n                domain = domain.substring(0, line.length() - Symbol.DASH.length());\n            } else {\n                rule.setControls(Set.of(Rule.Control.OVERLAY));\n            }\n\n            if (domain.startsWith(Symbol.DOT)) {\n                domain = domain.substring(0, line.length() - Symbol.DOT.length());\n            }\n\n            rule.setType(domain.startsWith(Symbol.ASTERISK) ? Rule.Type.WILDCARD : Rule.Type.BASIC);\n            rule.setTarget(domain);\n            rule.setDest(UNKNOWN_IP);\n            rule.setScope(Rule.Scope.DOMAIN);\n            return rule;\n        }\n        return Rule.EMPTY;\n    }\n\n    @Override\n    public String format(Rule rule) {\n        if (Rule.Type.UNKNOWN == rule.getType()) {\n            if (RuleSet.SMARTDNS == rule.getSourceType()) {\n                return rule.getOrigin();\n            }\n            return null;\n        } else if (rule.getMode() != Rule.Mode.REWRITE && rule.getScope() == Rule.Scope.DOMAIN) {\n\n            switch (rule.getType()) {\n                case BASIC -> {\n                    return SMARTDNS_HEADER +\n                            (!rule.getControls().contains(Rule.Control.OVERLAY) ?\n                                    (Symbol.ASTERISK + Symbol.DOT) : Symbol.EMPTY) +\n                            rule.getTarget() +\n                            Symbol.SLASH +\n                            (Rule.Mode.DENY.equals(rule.getMode()) ? Symbol.HASH : Symbol.DASH);\n                }\n                case WILDCARD -> {\n                    String domain = rule.getTarget();\n                    if (domain.lastIndexOf(Symbol.ASTERISK) == 0) {\n                        return SMARTDNS_HEADER + domain + Symbol.SLASH + Symbol.DASH;\n                    }\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public String commented(String value) {\n        return Util.splitIgnoreBlank(value, Symbol.LF).stream()\n                .map(e -> Symbol.HASH + Symbol.WHITESPACE + e.trim())\n                .collect(Collectors.joining(Symbol.CRLF));\n    }\n\n    @Override\n    public boolean isComment(String line) {\n        return line.startsWith(Symbol.HASH);\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        this.register(RuleSet.SMARTDNS, this);\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/model/Rule.java",
    "content": "package org.fordes.adfs.model;\n\nimport lombok.Data;\nimport org.fordes.adfs.enums.RuleSet;\n\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.Set;\n\n/**\n * @author fordes123 on 2024/5/27\n */\n@Data\npublic class Rule {\n\n    /**\n     * 规则来源名称，及 {@link org.fordes.adfs.config.AdFSProperties.InputProperties#name}\n     */\n    private String sourceName;\n\n    /**\n     * 规则来源类型 {@link RuleSet}\n     */\n    private RuleSet sourceType;\n\n    /**\n     * 原始规则\n     */\n    private String origin;\n\n    /**\n     * 作用目标\n     */\n    private String target;\n\n    /**\n     * 重定向/重写目标\n     */\n    private String dest;\n\n    /**\n     * 模式 {@link Mode}\n     */\n    private Mode mode;\n\n    /**\n     * 作用范围 {@link Scope}\n     */\n    private Scope scope;\n\n    /**\n     * 规则类型 {@link Type}\n     */\n    private Type type;\n\n    /**\n     * 控制符集 {@link Control}\n     */\n    private Set<Control> controls = new HashSet<>(Control.values().length, 1.0f);\n\n    public static final Rule EMPTY = new Rule();\n\n    /**\n     * 规则控制参数\n     */\n    public enum Control {\n        /**\n         * 最高优先级\n         */\n        IMPORTANT,\n\n        /**\n         * 覆盖子域名\n         */\n\n        OVERLAY,\n\n        /**\n         * 限定符，通常是 ^\n         */\n        QUALIFIER,\n\n        ;\n    }\n\n    /**\n     * 规则模式\n     */\n    public enum Mode {\n\n        /**\n         * 阻止\n         */\n        DENY,\n\n        /**\n         * 解除阻止\n         */\n        ALLOW,\n\n        /**\n         * 重写<br/>\n         * 通常 hosts规则指向特定ip(非localhost)时即为重写\n         */\n        REWRITE,\n\n        ;\n    }\n\n    /**\n     * 规则类型\n     */\n    public enum Type {\n        /**\n         * 基本规则，不包含任何控制、匹配符号, 可以转换为 hosts\n         */\n        BASIC,\n\n        /**\n         * 通配规则，仅使用通配符\n         */\n        WILDCARD,\n\n        /**\n         * 其他规则，如使用了正则、高级修饰符号等，这表示目前无法支持\n         */\n        UNKNOWN,\n\n        ;\n    }\n\n    /**\n     * 作用域\n     */\n    public enum Scope {\n        /**\n         * ipv4或ipv6地址\n         */\n        HOST,\n\n        /**\n         * 域名\n         */\n        DOMAIN,\n\n        /**\n         * 路径、文件等\n         */\n        PATH,\n\n        ;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o instanceof Rule rule) {\n            if (Type.UNKNOWN == this.type || Type.UNKNOWN == rule.getType()) {\n                return Objects.equals(this.origin, rule.origin);\n            }\n            return Objects.equals(this.target, rule.target) &&\n                    this.mode == rule.mode &&\n                    this.scope == rule.scope &&\n                    this.type == rule.type;\n        }\n        return false;\n    }\n\n    @Override\n    public int hashCode() {\n        if (Type.UNKNOWN == this.type) {\n            return Objects.hash(this.origin);\n        }\n        return Objects.hash(getTarget(), getMode(), getScope(), getType());\n    }\n\n    @Override\n    public String toString() {\n        if (Type.UNKNOWN == this.type) {\n            return \"Rule{\" +\n                    \"origin='\" + origin + '\\'' +\n                    '}';\n        }\n        return \"Rule{\" +\n                \"target='\" + target + '\\'' +\n                \", mode=\" + mode +\n                \", scope=\" + scope +\n                \", type=\" + type +\n                '}';\n    }\n}"
  },
  {
    "path": "src/main/java/org/fordes/adfs/util/Util.java",
    "content": "package org.fordes.adfs.util;\n\nimport jakarta.annotation.Nonnull;\nimport jakarta.annotation.Nullable;\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.model.Rule;\nimport org.springframework.util.ObjectUtils;\nimport org.springframework.util.StringUtils;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\n\nimport static org.fordes.adfs.constant.Constants.*;\nimport static org.fordes.adfs.constant.RegConstants.*;\n\n/**\n * @author fordes123 on 2022/9/19\n */\n@Slf4j\npublic class Util {\n\n    /**\n     * 给定字符串是否以特定前缀开始\n     *\n     * @param str      给定字符串\n     * @param prefixes 前缀\n     * @return 给定字符串是否以特定前缀开始\n     */\n    public static boolean startWithAny(String str, String... prefixes) {\n        if (!StringUtils.hasText(str) || ObjectUtils.isEmpty(prefixes)) {\n            return false;\n        }\n        return Arrays.stream(prefixes).anyMatch(str::startsWith);\n    }\n\n    /**\n     * 给定字符串是否以特定字符串开始和结束\n     *\n     * @param str   给定字符串\n     * @param start 开始字符串\n     * @param end   结束字符串\n     * @return 给定字符串是否以特定字符串开始和结束\n     */\n    public static boolean between(String str, String start, String end) {\n        if (StringUtils.hasLength(str) && StringUtils.hasLength(start) && StringUtils.hasLength(end)) {\n            return str.startsWith(start) && str.endsWith(end);\n        }\n        return false;\n    }\n\n    /**\n     * 截取分隔字符串之前的字符串，不包括分隔字符串<br/>\n     * 截取不到时返回空串\n     *\n     * @param str    被截取的字符串\n     * @param flag   分隔字符串\n     * @param isLast 是否是最后一个\n     * @return 分隔字符串之前的字符串\n     */\n    public static String subBefore(String str, String flag, boolean isLast) {\n        if (StringUtils.hasLength(str) && StringUtils.hasLength(flag)) {\n            int index = isLast ? str.lastIndexOf(flag) : str.indexOf(flag);\n            if (index >= 0) {\n                return str.substring(0, index);\n            }\n        }\n        return Symbol.EMPTY;\n    }\n\n    /**\n     * 截取分隔字符串之后的字符串，不包括分隔字符串<br/>\n     * 截取不到时返回空串\n     *\n     * @param content 被截取的字符串\n     * @param flag    分隔字符串\n     * @param isLast  是否是最后一个\n     * @return 分隔字符串之后的字符串\n     */\n    public static String subAfter(String content, String flag, boolean isLast) {\n        if (StringUtils.hasLength(content) && StringUtils.hasLength(flag)) {\n            int index = isLast ? content.lastIndexOf(flag) : content.indexOf(flag);\n            if (index >= 0) {\n                return content.substring(index + flag.length());\n            }\n        }\n        return Symbol.EMPTY;\n    }\n\n    /**\n     * 截取分隔字符串之间的字符串，不包括分隔字符串<br/>\n     * 截取不到时返回空串\n     *\n     * @param content 被截取的字符串\n     * @param start   开始分隔字符串\n     * @param end     结束分隔字符串\n     * @return 分隔字符串之间的字符串\n     */\n    public static String subBetween(String content, String start, String end) {\n        if (StringUtils.hasLength(content) && StringUtils.hasLength(start) && StringUtils.hasLength(end)) {\n            int startIndex = content.indexOf(start);\n            int endIndex = content.lastIndexOf(end);\n            if (startIndex >= 0 && endIndex > 0 && startIndex < endIndex) {\n                return content.substring(startIndex + start.length(), endIndex);\n            }\n        }\n        return Symbol.EMPTY;\n    }\n\n    /**\n     * 切分字符串并移除空项\n     *\n     * @param str  待切分字符串\n     * @param flag 分隔符\n     * @return 切分后的字符串\n     */\n    public static List<String> splitIgnoreBlank(String str, String flag) {\n        if (!StringUtils.hasLength(str) || !StringUtils.hasLength(flag)) {\n            return List.of();\n        }\n        return Arrays.stream(str.split(flag))\n                .filter(e -> !e.isBlank())\n                .toList();\n    }\n\n    /**\n     * 给定字符串是等于任一字符串\n     *\n     * @param str    给定字符串\n     * @param values 任意字符串\n     * @return 给定字符串是等于任一字符串\n     */\n    public static boolean equalsAny(String str, String... values) {\n        if (!StringUtils.hasLength(str) || ObjectUtils.isEmpty(values)) {\n            return false;\n        }\n        return Arrays.asList(values).contains(str);\n    }\n\n    /**\n     * 解析hosts规则，如不是则返回null\n     *\n     * @param content 待解析字符串\n     * @return {@link Map.Entry} key:ip, value:域名\n     */\n    public static @Nullable Map.Entry<String, String> parseHosts(String content) {\n        if (content.contains(Symbol.TAB)) {\n            content = content.replace(TAB, Symbol.WHITESPACE);\n        }\n        List<String> list = splitIgnoreBlank(content, Symbol.WHITESPACE);\n        if (list.size() == 2) {\n            String ip = list.get(0).trim();\n            String domain = list.get(1).trim();\n\n            if (PATTERN_IP.matcher(ip).matches() && PATTERN_DOMAIN.matcher(domain).matches()) {\n                return Map.entry(ip, domain);\n            }\n        }\n        return null;\n    }\n\n    /**\n     * 休眠线程，忽略中断异常\n     *\n     * @param millis 休眠时间，毫秒\n     */\n    public static void sleep(long millis) {\n        if (millis > 0L) {\n            try {\n                Thread.sleep(millis);\n            } catch (InterruptedException ignored) {\n            }\n        }\n    }\n\n    /**\n     * 转换相对路径为绝对路径\n     *\n     * @param path 路径\n     * @return 规范化后的路径\n     */\n    public static String normalizePath(@Nonnull String path) {\n\n        boolean isAbsPath = '/' == path.charAt(0) || PATTERN_PATH_ABSOLUTE.matcher(path).matches();\n\n        if (!isAbsPath) {\n            if (path.startsWith(Symbol.DOT)) {\n                path = path.substring(1);\n            }\n            if (path.startsWith(FILE_SEPARATOR)) {\n                path = path.substring(FILE_SEPARATOR.length());\n            }\n            path = ROOT_PATH + FILE_SEPARATOR + path;\n        }\n        return path;\n    }\n\n\n    public static void isBaseRule(String content, BiConsumer<String, Rule.Type> ifPresent, Consumer<String> orElse) {\n        String temp = content;\n        if (temp.contains(Symbol.ASTERISK)) {\n            temp = content.replace(Symbol.ASTERISK, Symbol.A);\n        }\n\n        if (temp.startsWith(Symbol.DOT)) {\n            temp = temp.substring(1);\n        }\n\n        if (temp.endsWith(Symbol.DOT)) {\n            temp = temp.substring(0, temp.length() - 1);\n        }\n\n        if (PATTERN_DOMAIN.matcher(temp).matches()) {\n            ifPresent.accept(content, content.equals(temp) ? Rule.Type.BASIC : Rule.Type.WILDCARD);\n        } else if (DOMAIN_PART.matcher(temp).matches()) {\n            ifPresent.accept(content, Rule.Type.WILDCARD);\n        } else {\n            orElse.accept(content);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/resources/application-dev.yml",
    "content": "logging:\n  level:\n    org.fordes.adfs: info\n\napplication:\n\n  input:\n    - name: anti-ad smartdns\n      path: https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/anti-ad-smartdns.conf\n      type: smartdns\n\n    - name: anti-ad dnsmasq\n      path: https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/adblock-for-dnsmasq.conf\n      type: dnsmasq\n\n    - name: AdGuard 基础过滤器\n      path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_2_Base/filter.txt\n      type: easylist\n\n    - name: Loyalsoldier/clash-rules 广告域名列表\n      path: https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt\n      type: clash\n\n    - name: 1Hosts lite\n      path: https://o0.pages.dev/Lite/hosts.txt\n      type: hosts\n\n  output:\n    files:\n      - name: easylist.txt\n        type: easylist\n        filter:\n          - basic\n          - wildcard\n          - unknown\n\n      - name: dns.txt\n        type: easylist\n        file-header: |\n          Test ${total}\n        filter:\n          - basic\n          - wildcard\n\n      - name: modify.txt\n        type: easylist\n        filter:\n          - unknown\n\n      - name: dnsmasq.conf\n        type: dnsmasq\n\n      - name: clash.yaml\n        type: clash\n\n      - name: smartdns.conf\n        type: smartdns\n\n      - name: hosts\n        type: hosts\n\n  config:\n    domain-detect:\n      enable: true\n      concurrency: 256\n      provider:\n        - 1.1.1.1"
  },
  {
    "path": "src/main/resources/application.yml",
    "content": "spring:\n  application:\n    name: \"@project.name@\"\n    version: \"@project.version@\"\n  profiles:\n    active: prod\n\nlogging:\n  file:\n    path: ./logs\n\napplication:\n  input:\n  config:\n    tracking:\n      enable: true\n      path: logs/tracking.list"
  },
  {
    "path": "src/main/resources/banner.txt",
    "content": "${AnsiColor.BRIGHT_GREEN}██████╗ ██████╗        █████╗ ██████╗ \n${AnsiColor.BRIGHT_GREEN}██╔══██╗██╔══██╗      ██╔══██╗██╔══██╗\n${AnsiColor.BRIGHT_GREEN}██║  ██║██║  ██║█████╗███████║██║  ██║\n${AnsiColor.BRIGHT_GREEN}██║  ██║██║  ██║╚════╝██╔══██║██║  ██║\n${AnsiColor.BRIGHT_GREEN}██████╔╝██████╔╝      ██║  ██║██████╔╝\n${AnsiColor.BRIGHT_GREEN}╚═════╝ ╚═════╝       ╚═╝  ╚═╝╚═════╝                                   \n${AnsiColor.BRIGHT_BLACK}  ─────────────────────────────────────────────────────────\n${AnsiColor.BRIGHT_WHITE}  ➤ Runtime ${AnsiColor.BRIGHT_BLACK}${os.name} • Java ${java.version} • Spring Boot v${spring-boot.version}\n${AnsiColor.BRIGHT_WHITE}  ➤ Profile ${AnsiColor.BRIGHT_YELLOW}${spring.profiles.active:default} ${AnsiColor.BRIGHT_BLACK}• ${AnsiColor.BRIGHT_WHITE}${server.port:N/A} ${AnsiColor.BRIGHT_BLACK}• ${AnsiColor.BRIGHT_BLACK}PID ${AnsiColor.BRIGHT_WHITE}${PID}\n${AnsiColor.BRIGHT_WHITE}  ➤ Issue tracker: ${AnsiColor.BRIGHT_BLACK}https://github.com/fordes123/ad-filters-subscriber/issues\n${AnsiColor.BRIGHT_WHITE}"
  },
  {
    "path": "src/main/resources/logback-spring.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<configuration scanPeriod=\"60 seconds\" debug=\"false\">\n\n    <!-- 定义日志的根目录 -->\n    <springProperty scope=\"context\" name=\"LOG_HOME\" source=\"logging.file.path\" defaultValue=\"./logs\"/>\n\n    <!-- 定义日志文件名称 -->\n    <springProperty scope=\"context\" name=\"APP_NAME\" source=\"spring.application.name\" defaultValue=\"application\"/>\n\n    <!-- 彩色日志渲染-->\n    <conversionRule conversionWord=\"clr\" class=\"org.springframework.boot.logging.logback.ColorConverter\"/>\n    <conversionRule conversionWord=\"wex\" class=\"org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter\"/>\n    <conversionRule conversionWord=\"wEx\" class=\"org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter\"/>\n\n    <!-- 控制台输出 -->\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%clr(%d{${LOG_DATEFORMAT_PATTERN:-HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(---){faint} %clr([%10.10t]){faint} %clr(%-40.40logger{39}){cyan} %clr(%-6L){yellow} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>\n            <charset>UTF-8</charset>\n        </encoder>\n    </appender>\n\n    <!-- 文件滚动记录 -->\n    <appender name=\"rollingFileAppender\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n        <file>${LOG_HOME}/${APP_NAME}.log</file>\n        <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n            <!-- daily rollover -->\n            <fileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>\n            <!-- keep 30 days' worth of history capped at 3GB total size -->\n            <maxHistory>30</maxHistory>\n            <totalSizeCap>3GB</totalSizeCap>\n        </rollingPolicy>\n        <append>true</append>\n        <encoder>\n            <pattern>%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>\n            <charset>UTF-8</charset>\n        </encoder>\n    </appender>\n\n    <appender name=\"asyncFileAppender\" class=\"ch.qos.logback.classic.AsyncAppender\">\n        <includeCallerData>true</includeCallerData>\n        <queueSize>4096</queueSize>\n        <neverBlock>true</neverBlock>\n        <appender-ref ref=\"rollingFileAppender\"/>\n    </appender>\n\n    <springProfile name=\"dev\">\n        <logger name=\"org.fordes\" additivity=\"true\" level=\"debug\"/>\n    </springProfile>\n\n    <springProfile name=\"prod\">\n        <logger name=\"org.fordes\" additivity=\"true\" level=\"info\"/>\n    </springProfile>\n\n    <root level=\"info\">\n        <appender-ref ref=\"STDOUT\"/>\n        <appender-ref ref=\"asyncFileAppender\"/>\n    </root>\n</configuration>\n"
  }
]