Full Code of afwfv/DD-AD for AI

main 8564f80644f0 cached
34 files
103.5 KB
29.0k tokens
113 symbols
1 requests
Download .txt
Repository: afwfv/DD-AD
Branch: main
Commit: 8564f80644f0
Files: 34
Total size: 103.5 KB

Directory structure:
gitextract_rl4mzeuj/

├── .devcontainer/
│   ├── Dockerfile
│   └── devcontainer.json
├── .github/
│   └── workflows/
│       └── auto-update.yml
├── .gitignore
├── LICENSE
├── README.md
├── config/
│   ├── application-example.yaml
│   └── application.yaml
├── pom.xml
├── rule/
│   └── DD-AD.txt
└── src/
    └── main/
        ├── java/
        │   └── org/
        │       └── fordes/
        │           └── adfs/
        │               ├── AdFSApplication.java
        │               ├── config/
        │               │   └── AdFSProperties.java
        │               ├── constant/
        │               │   ├── Constants.java
        │               │   └── RegConstants.java
        │               ├── enums/
        │               │   ├── HandleType.java
        │               │   └── RuleSet.java
        │               ├── handler/
        │               │   ├── Parser.java
        │               │   ├── Tracker.java
        │               │   ├── dns/
        │               │   │   └── DnsDetector.java
        │               │   ├── fetch/
        │               │   │   ├── Fetcher.java
        │               │   │   ├── HttpFetcher.java
        │               │   │   └── LocalFetcher.java
        │               │   └── rule/
        │               │       ├── ClashHandler.java
        │               │       ├── DnsmasqHandler.java
        │               │       ├── EasylistHandler.java
        │               │       ├── Handler.java
        │               │       ├── HostsHandler.java
        │               │       └── SmartdnsHandler.java
        │               ├── model/
        │               │   └── Rule.java
        │               └── util/
        │                   └── Util.java
        └── resources/
            ├── application-dev.yml
            ├── application.yml
            ├── banner.txt
            └── logback-spring.xml

================================================
FILE CONTENTS
================================================

================================================
FILE: .devcontainer/Dockerfile
================================================
# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 11, 17, 11-bullseye, 17-bullseye, 11-buster, 17-buster
ARG VARIANT=21-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/java:${VARIANT}

# [Option] Install Maven
ARG INSTALL_MAVEN="true"
ARG MAVEN_VERSION="3.9.0"
# [Option] Install Gradle
ARG INSTALL_GRADLE="false"
ARG GRADLE_VERSION=""
RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \
    && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi

# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
#     && apt-get -y install --no-install-recommends <your-package-list-here>

# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1


================================================
FILE: .devcontainer/devcontainer.json
================================================
{
  "name": "CodeSpace",
  "dockerFile": "Dockerfile",
  "extensions": [
    "vscjava.vscode-java-pack",
    "vscjava.vscode-java-debug",
    "vscjava.vscode-java-test",
    "vscjava.vscode-spring-boot-dashboard",
    "vmware.vscode-spring-boot",
    "vscjava.vscode-maven"
  ]
}

================================================
FILE: .github/workflows/auto-update.yml
================================================
name: Update Filters
on:
  push:
    paths-ignore:
      - 'README.md'
      - 'README_en.md'
      - '.github/**'
      - '.devcontainer/**'
      - 'LICENSE'
  schedule:
    - cron: 0 */8 * * *
  workflow_dispatch:
    inputs:
      profile:
        required: false
        default: 'prod'
        type: string
        description: 'application running profile'

      should-commit:
        required: false
        default: true
        type: boolean
        description: 'Whether to commit output files'

      release-branch:
        required: false
        default: 'release'
        type: string
        description: 'Release branch name, if should-commit is false, this parameter will be ignored'
env:
  TZ: Asia/Shanghai

jobs:
  Update_Filters:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      actions: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ github.head_ref }}

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: maven

      - name: Set default values
        id: defaults
        run: |
          echo "profile=${{ github.event.inputs.profile || 'prod' }}" >> $GITHUB_OUTPUT
          echo "should-commit=${{ github.event.inputs.should-commit || 'true' }}" >> $GITHUB_OUTPUT
          echo "release-branch=${{ github.event.inputs.release-branch || 'release' }}" >> $GITHUB_OUTPUT

      - name: Run Jar
        run:  mvn spring-boot:run -Dspring-boot.run.profiles=${{ steps.defaults.outputs.profile }}

      - name: Archive tracking log
        if: hashFiles('logs/tracking.list') != ''
        uses: actions/upload-artifact@v4
        with:
          name: tracking-log
          path: logs/tracking.list

      - name: Clean up
        if: steps.defaults.outputs.should-commit == 'true'
        run: |
          find . -maxdepth 1 ! -name 'rule' ! -name '.' ! -name '.git' -exec rm -rf {} +
          mv rule/* .
          rm -rf rule

      - name: Set current date
        id: date
        if: steps.defaults.outputs.should-commit == 'true'
        run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT

      - name: Commit Changes
        id: commit
        if: steps.defaults.outputs.should-commit == 'true'
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: 'chore: ✅ auto release at ${{ steps.date.outputs.date }}'
          branch: ${{ steps.defaults.outputs.release-branch }}
          skip_dirty_check: true
          push_options: '--force'
          create_branch: true
          skip_fetch: true
          skip_checkout: true
          commit_options: '--allow-empty'

      - name: Delete Workflow Runs
        uses: Mattraks/delete-workflow-runs@v2
        with:
          token: ${{secrets.GITHUB_TOKEN}}
          repository: ${{ github.repository }}
          retain_days: 1
          keep_minimum_runs: 1


================================================
FILE: .gitignore
================================================
# Compiled class file
*.class

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

.idea
target
src/test

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 以谶

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# DD-AD

## [***✈️ 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)

## 🎯 ***规则订阅***

> AdGuard客户端(软件、扩展)、AdBlock、AdBlockPlus、uBlock Origin 推荐使用:`easylist`</br>
> AdGuardHome 推荐使用:`dns.txt`</br>
> AdAway 等其他仅支持 hosts 的工具,推荐使用:`hosts.txt`

| 文件              | 说明                          |        github        |         ghproxy         |         jsdelivr           |
|-----------------|:----------------------------|:--------------------:|:------------------------:|:-------------------------:|
| `easylist.txt`  | 完整主规则                       | [订阅][easylist-raw] | [订阅][easylist-ghproxy] | [订阅][easylist-jsdelivr] |
| `modify.txt`    | 不含 DNS 过滤规则的 `easylist.txt` |  [订阅][modify-raw]  |  [订阅][modify-ghproxy]  |  [订阅][modify-jsdelivr]  |
| `dns.txt`       | 仅含 DNS 过滤规则的 `easylist.txt` |   [订阅][dns-raw]    |   [订阅][dns-ghproxy]    |   [订阅][dns-jsdelivr]    |
| `dnsmasq.conf`  | dnsmasq 及其衍生版本              | [订阅][dnsmasq-raw]  | [订阅][dnsmasq-ghproxy]  | [订阅][dnsmasq-jsdelivr]  |
| `clash.yaml`    | clash 及其衍生版本                |  [订阅][clash-raw]   |  [订阅][clash-ghproxy]   |  [订阅][clash-jsdelivr]   |
| `smartdns.conf` | smartdns                    | [订阅][smartdns-raw] | [订阅][smartdns-ghproxy] | [订阅][smartdns-jsdelivr] |
| `hosts`         | 几乎所有操作系统原生支持                |  [订阅][hosts-raw]   |  [订阅][hosts-ghproxy]   |  [订阅][hosts-jsdelivr]   |
| `DD-AD.txt`   | 本仓库维护的私有规则,以 easylist 形式提供  | [订阅][DD-AD-raw]  | [订阅][DD-AD-ghproxy]  | [订阅][DD-AD-jsdelivr]  |

[easylist-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/easylist.txt

[easylist-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/easylist.txt

[easylist-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/easylist.txt

[modify-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/modify.txt

[modify-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/modify.txt

[modify-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/modify.txt

[dns-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dns.txt

[dns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dns.txt

[dns-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/dns.txt

[dnsmasq-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dnsmasq.conf

[dnsmasq-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dnsmasq.conf

[dnsmasq-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/dnsmasq.conf

[clash-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/clash.yaml

[clash-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/clash.yaml

[clash-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/clash.yaml

[smartdns-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/smartdns.conf

[smartdns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/smartdns.conf

[smartdns-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/smartdns.conf

[hosts-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/hosts

[hosts-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/hosts

[hosts-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/hosts

[DD-AD-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/DD-AD.txt

[DD-AD-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/DD-AD.txt

[DD-AD-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/DD-AD.txt

### 说明

广告过滤规则整合,使用 *[ad-filters-subscriber](https://github.com/fordes123/ad-filters-subscriber)*

1.**针对番茄小说广告添加了规则**

2.**针对七猫小说广告添加了规则**

3.**私人dns:dd.afwfv.cn**


================================================
FILE: config/application-example.yaml
================================================
##########################################
## !!! 示例配置,修改无效 !!!
## !!! example config, modify invalid !!!
##########################################
application:

  # 规则源配置
  # !使用前请删除下方示例配置, 注意缩进
  input:

    - name: 'Subscription 1'               #必要参数: 规则名称,请勿重复
      path: 'https://example.org/rule.txt' #必要参数: 规则 url (http/https) 或 本地文件位置 (绝对/相对路径)
      type: easylist                       #可选参数: 规则类型:easylist (默认)、dnsmasq、clash、smartdns、hosts

    - name: 'Subscription 2'
      path: 'rule/local.txt'
      type: hosts

  #输出配置
  output:
    #文件头配置,将自动作为注释添加至每个规则文件开始
    #可使用占位符 ${name}、${type}、${desc} 以及 ${date} (当前日期)、${total} (规则总数)
    file_header: |
      ADFS AdBlock ${type}
      Last Modified: ${date}
      Total Size: ${total}
      Homepage: https://github.com/fordes123/ad-filters-subscriber/

    #输出规则文件列表 (!注意缩进,且每个类型只能输出一个文件)
    files:
      - name: easylist.txt     #必要参数: 文件名
        type: easylist         #必要参数: 文件类型: easylist、dnsmasq、clash、smartdns、hosts
        file_header:           #可选参数: 文件头配置,将自动作为注释添加至每个规则文件开始 (此处优先于 output.file_header)
        desc: 'ADFS EasyList'  #可选参数: 文件描述,可在file_header中通过 ${} 中使用
        filter:
          - basic              #基本规则,不包含任何控制、匹配符号, 可以转换为 hosts
          - wildcard           #通配规则,仅使用通配符
          - unknown            #其他规则,如使用了正则、高级修饰符号的规则,这些规则目前无法转换为其他格式

        rule:                  #可选参数: 限定此文件使用的规则源,如果不指定则使用 input 中的所有规则源
          - Subscription 1
          - Subscription 2

      - name: dns.txt
        type: easylist
        filter:
          - basic
          - wildcard

      - name: dnsmasq.txt
        type: dnsmasq

      - name: clash.yaml
        type: clash

      - name: smartdns.txt
        type: smartdns

      - name: hosts.txt
        type: hosts


  # 程序运行配置(可选)
  # !删除即使用默认配置
  config:

    expected_quantity: 2000000  #预期规则数量
    fault_tolerance: 0.001 #容错率
    warn_limit: 6 #警告阈值, 原始规则长度小于该值时会输出警告日志

    # 域名检测,启用时将进行解析以验证域名有效性
    # !开启此功能可能导致处理时间大幅延长
    # !仅支持特定规则,具体原因请查看项目主页说明
    domain-detect:
      enable: false   # 是否启用
      timeout: 2000   # 查询超时(毫秒)
      concurrency: 10 # 并发查询数
      cache-ttl-min: 600            # 缓存最小TTL(秒)
      cache-ttl-max: 86400          # 缓存最大TTL(秒)
      cache-negative-ttl: 300       # 失败结果缓存TTL(秒)
      provider:       # 使用的 DNS 服务器,仅支持 IP,多个之间具备优先级; 为空则使用默认DNS
        - 223.5.5.5

    # 排除指定内容
    # 在解析结束后,规则内容命中此列表将直接认定为无效
    exclude:
      - "www.example.org"

    # 通过文件收集程序认定无效的规则,用于排查问题
    tracking:
      enable: true
      path: logs/tracking.list

================================================
FILE: config/application.yaml
================================================
# 参考 `application-example.yaml` 并在处添加订阅规则,请注意缩进
# Refer to `application-example.yaml` and add subscription rules there,please note the indentation

application:
  config:
    domain-detect:
      enable: true
      timeout: 3000
      concurrency: 128
      provider:
        - 1.1.1.1
        - 8.8.8.8

    exclude:
      # 屏蔽此域会导致Edge浏览器翻译不可用
      - "edge.microsoft.com"
      # 屏蔽此域会导致小米天气网络异常
      - "metok.sys.miui.com"
      # 屏蔽此域会导致10086 App登陆异常
      - "client.app.coc.10086.cn"
      # 屏蔽此域会导致MSN页面无法正常加载
      - "assets.msn.com"

  rule:
    remote:
      - name: AdGuard 基础过滤器
        path: https://filters.adtidy.org/extension/ublock/filters/2_optimized.txt
        type: easylist

      - name: AdGuard 移动广告过滤器
        path: https://filters.adtidy.org/extension/ublock/filters/11_optimized.txt
        type: easylist

      - name: AdGuard 中文过滤器
        path: https://filters.adtidy.org/extension/ublock/filters/224_optimized.txt
        type: easylist

      - name: NoAppDownload
        path: https://raw.githubusercontent.com/Noyllopa/NoAppDownload/master/NoAppDownload.txt
        type: easylist

      - name: xndeye web-ad-rule
        path: https://raw.githubusercontent.com/xndeye/web-ad-rule/master/easylist.txt
        type: easylist

      - name: xinggsf movie
        path: https://raw.githubusercontent.com/xinggsf/Adblock-Plus-Rule/master/mv.txt

      - name: cjxlist annoyance
        path: https://raw.githubusercontent.com/cjx82630/cjxlist/master/cjx-annoyance.txt

      - name: ABP filters
        path: https://easylist-downloads.adblockplus.org/antiadblockfilters.txt

      - name: ABP 反广告过滤
        path: https://easylist-downloads.adblockplus.org/abp-filters-anti-cv.txt

      - name: uBlockOrigin privacy
        path: https://raw.githubusercontent.com/uBlockOrigin/uAssets/refs/heads/master/filters/privacy.txt


    local:
      - name: 'DD-AD'
        path: 'rule/DD-AD.txt'
        type: easylist

  output:
    file_header: |
      Title: DD-AD of ${type}
      Last Modified: ${date}
      Homepage: https://github.com/afwfv/DD-AD

    files:
      - name: easylist.txt
        type: easylist
        filter:
          - basic
          - wildcard
          - unknown

      - name: dns.txt
        type: easylist
        file_header: |
          Title: DD-AD 
          Last Modified: ${date}
          Homepage: https://github.com/afwfv/DD-AD
        filter:
          - basic
          - wildcard

      - name: modify.txt
        type: easylist
        filter:
          - unknown

      - name: dnsmasq.conf
        type: dnsmasq

      - name: clash.yaml
        type: clash

      - name: smartdns.conf
        type: smartdns

      - name: hosts
        type: hosts


================================================
FILE: pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <name>ad-filters-subscriber</name>
    <groupId>org.fordes</groupId>
    <description>AD Filters Subscriber</description>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>ad-filters-subscriber</artifactId>
    <version>1.4.0</version>

    <properties>
        <java.version>21</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>3.5.7</spring-boot.version>
        <maven-compiler-plugin.version>3.12.1</maven-compiler-plugin.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.alexandrnikitin</groupId>
            <artifactId>bloom-filter_2.13</artifactId>
            <version>0.13.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven-compiler-plugin.version}</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>UTF-8</encoding>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
        <filters>
            <filter>src/main/resources/application.yml</filter>
        </filters>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

</project>


================================================
FILE: rule/DD-AD.txt
================================================
! Title: AFWFV DD-AD list of DD-AD
! Last Modified: 2025-11-05 00:00:00
! Homepage: https://github.com/afwfv/DD-AD/
! Description: 对本仓库上游规则的补充与修正, 不定期更新.
!-------------白名单---------------
#图床
@@||pic.imgdb.cn
#微信支付
@@||payapp.weixin.qq.com^
#夸克网盘
@@||pan.quark.cn^
@@||drive.quark.cn^
#夸克首页
@@||myquark.cn^
#小米骚扰拦截 数据库更新
@@||api.sec.miui.com^
#小米推送(mipush)
@@||cn.app.chat.xiaomi.net^
#个推
@@||sdk.open.talk.gepush.com^
#公共CDN
@@||bootcdn.net^
#斗鱼
@@||msg.douyu.com^
@@||bjfesdk.douyucdn.cn^
@@||gift.douyucdn.cn^
#360
@@||360.cn^
@@down.360safe.com^
#酷安
@@||316.coolapk1s.com^
@@||api.coolapk.com^
@@||avatar.coolapk.com^
@@||umengjmacs.m.taobao.com^
#抖音网页
@@||lf3-cdn-tos.bytegoofy.com^
@@||lf6-cdn-tos.bytegoofy.com^
@@||lf1-cdn-tos.bytegoofy.com^
#京东
@@||m.jingxi.com^
@@||gia.jd.com^
#笔趣阁
@@||www.bqgyy.com^
#皮皮虾pro
@@||is.snssdk.com^
@@||ib.snssdk.com^
#qq
@@||c.pc.qq.com^
#163
||dl.reg.163.com^
#放行log文件,减少重复请求造成的发热
@@||log-api.pangolin-sdk-toutiao-b.com^
@@||log-api.pangolin-sdk-toutiao.com^
@@||dig.lianjia.com^
##豆包
@@||lf-c-flwb.bytetos.com^
@@||p3-search.byteimg.com^
@@||p3-flow-imagex-sign.byteimg.com^
@@||p6-flow-imagex-sign.byteimg.com^
@@||p9-flow-imagex-sign.byteimg.com^
##今日头条
@@||mssdk3-normal-lf.zijieapi.com^
@@||www.toutiao.com^
@@||m.toutiao.com^
@@||app.toutiao.com^
@@||lf3-short.ibytedapm.com^
@@||ttwid.bytedance.com^
@@||lf3-cdn2-tos.bytescm.com^
@@||lf*-cdn-tos.bytegoofy.com^
@@||*ad*.ksyungslb.com^
@@||p*-img.searchpstatp.com^
@@||lf*material.searchpstatp.com^
@@||p*.toutiaoimg.com^
##百度云盘
@@||staticsns.cdn.bcebos.com^
@@||tcbox.baidu.com^
@@||nsclick.baidu.com^
##谷歌广告
||tpc.googlesyndication.com^
##虎牙直播
@@||ylog.huya.com^
##懂车帝
@@||p1-dcd.byteimg.com^
@@||p3-dcd.byteimg.com^
@@||p9-dcd.byteimg.com^
@@||p3-dcd-sign.byteimg.com^
@@||p6-dcd-sign.byteimg.com^
@@||p9-dcd-sign.byteimg.com^
@@||open.bytedanceapi.com^
##百度搜索
@@||ms.bdimg.com^
@@||feed-image.baidu.com^
##咪咕视频
@@||v.miguvideo.com^
##Appshare
@@||edu-image.nosdn.127.net^
##快看视频
@@||yanxuan.nosdn.127.net^
##中国移动
@@||qwhdcdn.cmcc-cs.cn^
##Omofun
@@||pic2.zhimg.com^
@@||lf6-imcloud-file-sign.bytetos.com^
@@||lf3-imcloud-file-sign.bytetos.com^
##巴哈姆特動畫瘋
@@||data.flurry.com^
##飞猪
@@||cdn.ynuf.aliapp.org^
##酷安
@@||necaptcha.nosdn.127.net^
##微信
@@||cm-10-*.getui.com^
##网易企业邮箱
@@||mimg.127.net^
##QQ
@@||qzonestyle.gtimg.cn^
@@||tianshu.gtimg.cn^
##QQ音乐
@@||i2.y.qq.com^
##百度输入法
@@||res.mi.baidu.com^
@@||www.paypalobjects.com^
##抖音
@@||auth.zijieapi.com^
@@||aweme.snssdk.com^
@@||v*.douyinvod.com^
@@||lf3-cdn-tos.bytegoofy.com^
@@||saveu*-normal-lf.zijieapi.com^
@@||lf-webcast-gr-sourcecdn.bytegecko.com^
@@||lf*-webcast-cdn-tos-ncdn.bytegecko.com^
@@||n*-lf-webcast-cdn-tos-ncdn.bytegecko.com^
@@||n*-lf*-dy-ncdn-tos-sub.bytegecko.com^
@@||lf*-dy-cdn-tos.bytegecko.com^
@@||lf*-static.bytednsdoc.com^
@@||lf-webcast-sourcecdn-tos.bytegecko.com^
@@||lf*-webcast-cdn-tos.bytegecko.com^
@@||lf*-webcastcdn-tos.douyinstatic.com^
@@||ma*-normal-lf.zijieapi.com^
@@||vcs.zijieapi.com^
@@||pitaya.bytedance.com^
@@||abtest3-misc-lf.zijieapi.com^
@@||p*-piu.byteimg.com^
@@||p*-ecom-fe.byteimg.com^
@@||p*-ecop-imagex.byteimg.com^
@@||p*-im.byteimg.com^
@@||p9-dy.byteimg.com
@@||p6-dy-ipv6.byteimg.com^
@@||p3-notice.byteimg.com^
@@||f-cpay.snssdk.com^
@@||f-cashloan.snssdk.com^
@@||p3-cpay.byteimg.com^
@@||p3-insurance.byteimg.com^
@@||lf*-infras.bytetos.com^
@@||lf1-cdn-tos.bytescm.com^
@@||survey.feelgood.cn^
@@||p*-sdbk2-media.byteimg.com^
@@||vcs-quality.zijieapi.com^
@@||lf*-baike.searchpstatp.com^
##乐播投屏
@@||tvapp.hpplay.cn^
@@||h5.hpplay.com.cn^
@@||sl.hpplay.cn^
@@||conf.hpplay.cn^
##菜鸟裹裹
@@||wgo.mmstat.com^
@@||gm.mmstat.com^
@@||cainiao-app-logs.cn-zhangjiakou.log.aliyuncs.com^
@@||na61-na62.wagbridge.alibaba.tanx.com^
##快手
@@||d*.static.yximgs.com^
@@||d*-magic.static.yximgs.com^
@@||bs-origin.pull.yximgs.com^
@@||live*.static.yximgs.com^
@@||*m.chenzhongtech.com^
@@||js-gamezone.static.yximgs.com^
@@||ali-gamezone.static.yximgs.com^
##番茄小说
@@||p*-novel.byteimg.com^
@@||p*-novel-sign.byteimg.com^
##皮皮虾
@@||p*-ppx-sign.byteimg.com^
@@||p*-ppx-report.byteimg.com^
@@||saveu.zijieapi.com^
@@||vas.snssdk.com^
@@||bgp.video-upload-v*.lf.bytelb.net^
@@||lf-cdn-tos.bytescm.com^
##阿里云
@@||click.aliyun.com^
##闲鱼
@@||dinamicx.alibabausercontent.com^
##今日头条极速版
@@||lf3-search.searchpstatp.com^
@@||monsetting.toutiao.com^
@@||gecko.zijieapi.com^
@@||polaris.zijieapi.com^
@@||tnc3-bjlgy.zijieapi.com^
##囧次元
@@||wxsnsdy.wxs.qq.com^
##BT1207
@@||lf3-cdn-tos.bytecdntp.com^
##原神
@@||ad-log-upload.mihoyo.com^
##菠菜
@@||meiqia.com^
##当鸟动漫
@@||mooc-image.nosdn.127.net^
##头条PRO
@@||ib.snssdk.com^
@@||ic.snssdk.com^
@@||is.snssdk.com^
##360防骚扰大师
@@||s.mvconf.f.360.cn^
@@||scan.call.f.360.cn^
@@||shouji.360.cn^
##淘宝
||s.click.taobao.com^
##樱花动漫
@@||v16m-default.akamaized.net^
##快手极速版
@@||v*.kwaicdn.com^
@@||ali.static.yximgs.com^
##360摄像机
@@||passport.360.cn^
@@||report.passport.360.cn^
@@||app.home.360.cn^
@@||jia.360.cn^
@@||fastconn-api.iot.360.cn^
@@||speed.live.360.cn^
@@||cloudcontrol.live.360.cn^
@@||sdk.live.360.cn^
@@||mdm.openapi.360.cn^
@@||dp.push.dc.360.cn^
@@||mall.360.cn^
##夸克
@@||download.uc.cn^
@@||soutiapi.quark.cn^
@@||souti-ims.sm.cn^
##蓝奏
@@||develope-oss.lanzoug.com^
##网易有道词典
@@||ydlunacommon-cdn.nosdn.127.net^
@@||luna-dict-community.nosdn.127.net^
@@||foundation.youdao.com^
##火山引擎
@@||portal.volccdn.com^
##360扫地机
@@||smart.360.cn^
##有柿
@@||p3-iz-e-sign.byteimg.com^
##淘小说
@@||tybook.taoyuewenhua.net^
##火山翻译
@@||p3-scmimg.bytescm.com^
@@||ipolyfill-origin.bytedance.com^
@@||lf3-cdn-tos.bytescm.com^
##快影
@@||ali-ky.static.yximgs.com^
##剪映
@@||p*-faceu-img-sign.byteimg.com^
@@||p*-passport.byteacctimg.com^
##网易严选
@@||yanxuan-dl.nosdn.127.net^
@@||yanxuan-item.nosdn.127.net^
##网易邮箱大师
@@||onegoods.nosdn.127.net^
##Faceu激萌
@@||effect.snssdk.com^
##风行视频
@@||cp2-cdn-play.funshion.com^
@@||img.funshion.com^
@@||img*.funshion.com^
@@||puser.funshion.com^
@@||pvip.funshion.com^
@@||po.funshion.com^
@@||pm.funshion.com^
##酷我音乐
@@||search.kuwo.cn^
@@||mobilist.kuwo.cn^
@@||ncomment.kuwo.cn^
@@||vip1.kuwo.cn^
##钉钉
@@||adash-emas.cn-hangzhou.aliyuncs.com^
!-------------黑名单--------------------
#信息流及评论区广告
||s2s.adjust.cn^
||ctobsnssdk.com^
||pangolin.snssdk.com^
||ulogs.umeng.com^
||aaid.umeng.com^
||mssdk-bu.bytedance.com^
#帖子详情好物推荐
||api2.coolapk.com/v6/feed/detail$replace=/&quot;include_goods_ids&quot;:\[.*?]\,&quot;include_goods&quot;:\[.*?]\,/ /
#帖子详情赞助内容
||api2.coolapk.com/v6/feed/detail$replace=/\,&quot;detailSponsorCard&quot;:{.*}/}}/
#发现页去除酷品
||api2.coolapk.com/v6/main/init$replace=/{&quot;id&quot;:1170.*?}\,/ /
#应用游戏页去除推广视频
||api2.coolapk.com/v6/page/dataList$replace=/{&quot;entityType&quot;:&quot;card&quot;\,&quot;entityTemplate&quot;:&quot;apkImageCard&quot;.*?\\u0022}&quot;}\,/ /
#去除首页还有什么值得买推广
||api2.coolapk.com/v6/main/indexV8$replace=/{&quot;entityType&quot;:&quot;card&quot;\,&quot;entityTemplate&quot;:&quot;listCard&quot;.*?}&quot;}\,/ /
#夏普电视开机广告
||flnet.com^
||miaozhen.com^
#QQ浏览器小说广告
||kde.qq.com^
#字节系 AD API SDK 屏蔽
/pangolin-sdk.*/
/pangolin-sdk-toutiao.*/
/pangolin-sdk-toutiao1.*/
/pglstatp-toutiao.*/
||bsccdn.net^
||dig.zjurl.cn^
||dig.bdurl.net^
||ad.zijieapi.com^
||apps.bytesfield.com^
||pangolin-sdk-toutiao.com^
||pangolin-sdk-toutiao1.com^
||pangolin-sdk-toutiao-b.com^
||pglstatp-toutiao.com^
||vcs.zijieapi.com^
||ads*-normal.amemv.com^
||ads*-normal-lq.amemv.com^
||ads*-normal-lf.amemv.com^
||ads*-normal-hl.amemv.com^
||ads*-normal.zijieapi.com^
||ads*-normal-lq.zijieapi.com^
||ads*-normal-lf.zijieapi.com^
||ads*-normal-hl.zijieapi.com^
||ads*-normal-lq.zijieapi.com.w.cdngslb.com^
||ads*-normal-lq.zijieapi.com.queniuiq.com^
||p*-ad-sign.byteimg.com^
||p*-ad.byteimg.com^
||lf*-orange.byteorge.com^
||v*-novelapp.fqnovelvod.com^
||sf*-ttcdn-tos.pstatp.com^
||p*-local-ads.byteimg.com.queniuso.com^
||p*-local-ads.byteimg.com^
||localads.chengzijianzhan.cn^
||localads.chengzijianzhan.cn.queniurc.com^
||ads*-normal-lf.zijieapi.com.dhcp^
#七猫免费小说
||cdn-new-ad.wtzw.com^
||a-remad.qm989.com^
||open.kuaishouzt.com^
#得间
||*-ad.wtzw.com^
||aiclk.com^
||smad.ms.zhangyue.net^
||adx.ads.heytapmobi.com^
||uapi.ads.heytapmobi.com^
||saad.ms.zhangyue.net^
||sentry-app.ireader.com^
#熊猫小说
||cdn-ad.wtzw.com^
||a-qmad.qimao.com^
#笔趣阁网页广告
||hlryzsqo.icu^
||scenery.hk^
||xiedao.hk^
||bcnsport.us^
||ncbtsdea.xyz^
||vjyaghdw.icu^
#书旗
||dsp-x.jd.com^
||xlog.jd.com^
||api.th.wanzjhb.com^
||janapi.jd.com^
||partner.uc.cn^
||mos.m.taobao.com^
||sdk-log.partner.sm.cn^
||opehs.tanx.com^
#OPPO
||sms.ads.heytapmobi.com^
||cl-data.ads.heytapmobi.com^
!-----------------待定-----------------
#小米浏览器热点资讯
||hot.browser.miui.com^
#未知来源 广告/日志
||toblog.ctobsnssdk.com^
#gamepass日志
||catalog.gamepass.com^
#杂七杂八
||googleads.g.doubleclick.nes^
||ads.lingyuexiu.cn^
||adscfg.togothermany.cn^
||ads-marketing-vivofs.vivo.com.cn
||adsfs.heytapimage.com^
||sdklog.adintl.cn^
||sdk-infra.inmobi.cn^
||sv.adintl.cn^
||trc-sdk.inmobi.cn^
||sdk.beizi.biz^
||sdk.alibaba.com.ailbaba.me^
||telemetry-sdk-metrics.inmobi.cn^
||api-htp.beizi.biz^
||sdkcfg.adintl.cn^
||p1-lm.adukwai.com^
||union.w.inmobi.cn^
||adskp.org^
||api.open.adsaide.cn^
||ads-video-al.xhscdn.com^
||ads-img-al.xhscdn.com^
||ads.cup.com.cn^
||bdapi.ads.heytapmobi.com^
||adx-cn.ads.vungle.com^
||config.ads.vungle.com^
||ads.themoneytizer.com^
||ads.sunmaker.com^
||ad.letmeads.com^
||mads.meituan.com^
||st-adsgame.vivo.com.cn^
||bjm.1vkx.cn^
||img.juhagngj.xyz^
||img.ayghbngnazh.xyz^
||img.leiahngiuheong.xyz^
||img.soweonglsh.xyz^
||img.yuwhehan.xyz^
||img.syebgng.xyz^
||img.mjhsghnwg.xyz^
||img.wqpamgngnb.xyz^
||img.huanagyehgn.xyz^
||img.shznegh.xyz^
||img.zhangilihew.xyz^
||yh.yanghetp.vip^
||tu.mt20230625tu.com^
||www.9129666tp.com^
||line.txwlwwvvimg.com^
||uu5512uu.com^
||xoxo.xoxoimg.com^
||ad-log-upload.mihoyo.com^
||ad-trace.lenovomm.com^
||ad-sdk.lenovomm.com^
||p*-ad-aweme.byteimg.com^
||ad-r.soulapp.cn^
||v*-ad.video.yximgs.com^
||soul-ad.soulapp.cn^
||mkt-advertisement.tuhu.cn^
||advert.swiftpass.cn^
||mktadsodr.ppdai.com^
||iadsdk.qtlcdn.com^
||ck.ads.oppomobile.com^
||mobads-pre-config.cdn.bcebos.com^
||api.adserver.jvmai.com^
||mbads.paas.cmbchina.com^
||skadsdk.appsflyer.com^
||cm.ads.oppomobile.com^
||ads.chuanmeixing.com^
||ads.bjlxin.cn^
||sis.jpush.io^
||ad.e.kuaishou.com^
||ali2.a.yximgs.com^
||open-set-api.shenshiads.com^
||adsdkapi.dxys.pro^
||adsdk.9imobi.com^
||uapi-ads.vivo.com.cn^
||adsfilebssdlbig.ali.kugou.com^
||ad.cyapi.cn^
||ad.tianmu.mobi^
||multi-az-ad.kuaishou.cn^
||ad.qingcigame.com^
||ads.tiktok.com^
||ads.linkedin.com^
||advice-ads.s3.amazonaws.com^
||ads30.adcolony.com^
||adsfile20.bssdlbig.kugou.com^
||adsfileretry.service.kugou.com^
||adserver.unityads.unity3d.com^
||adserviceretry.kglink.cn^
||ck.ads.heytapmobi.com^
||ads.games.laohu.com^
||ads-drcn.platform.hihonorcloud.com^
||ads-lb.serving-sys.cn^
||ads.serving-sys.cn^
||ads.uutest.cn^
||adserver.html.it^
||adsmind.ugdtimg.com^
||mobads.baidu.com^
||ads.api.fakeloc.cc^
||api.installer.xiaomi.com^
||applog.snssdk.com^
||adservice.aiyouaiyou.cn^
||ads-shopping.shouqianba.com^
||ads.superawesome.tv^
||ads.zhinengxiyifang.cn^
||ads.huawei.com^
||cross-ad-api.afunapp.com^
||t.jmads.net^
||adserv.ontek.com.tr^
||optimus-ads.amap.com^
||adashx.ut.cainiao.com^
||image-ad.sm.cn^
||h-adashx.ut.dingtalk.com^
||adashx.ut.dingtalk.com^
||v6-adashx.ut.cainiao.com^
||api-access.pangolin-sdk-toutiao.com.queniuyk.com^
||opencdnpddpic2.cdn.vsjwtcdn.com^
||opencdnpddvod2.jomodns.com^
||b1-data.ads.heytapmobi.com^
||googleads.g.doubleclick.net^
||stg-data.ads.heytapmobi.com^
||is.snssdk.com.queniukw.com^
||i.snssdk.com^
||o2o-ad-dev.oss-cn-shanghai.aliyuncs.com^、
||4pn.cn^
||4a9.cn^
||3p4.cn^
||y4n.cn^
||u7x.cn^
||aqdlt.com^
||aqdz91.com^
||aqdz149.com^
||share.jileme.net^
||settings.ttwebview.com^
||st-ads-jssdk.vivo.com.cn^
||xmadsdk.cos.tx.xmcdn.com^
||ads-video-1251524319.cos.ap-shanghai.myqcloud.com^
||line3-adscore-api.biligame.net^
||adsmind.gdtimg.com.svideolego-dk.sched.dcloudstc.com^
||configv2.unityads.unitychina.cn^
||abtest3-misc-hl.zijieapi.com^
||abtest3-misc-lq.zijieapi.com^
||abtest3-misc-lf.zijieapi.com^
||monitor.ads.8le8le.com^
||leads.baidu.com^
||x61.inyvf.top^
||b2.ghj7l.com^
||nosup-jd1.127.net^
||p*-webcast-sign.douyinpic.com^
||v6-adashx.ut.ele.me^
||h-adashx.ut.cainiao.com^
||h-adashx4bc.ut.taobao.com^
||v6-adashx.ut.amap.com^
||ad-audit-provider.soulapp.cn^
||o2o-ad-log-gateway.alibaba.com^
||ad-sdk-config.youdao.com^
||m1.ad.10010.com.w.cdngslb.com^
||business-ad.cdn-go.cn^
||market-ad.oss-cn-shenzhen.aliyuncs.com^
||advertising.yandex.ru^
||advertising.apple.com^
||advertising.yahoo.com^
||na61-na62.wagbridge.advertisement.tanx.com^
||ads.pinterest.com^
||ads-api.tiktok.com^
||partnerads-test.ysm.yahoo.net^
||partnerads.ysm.yahoo.com^
||adsbdfs.heytapimage.com^
||s364-ad-canl5.nbjz.db9w.com^
||ads3.fingersoft.net^
||ads-sg.tiktok.com^
||dm.kantsuu.com^
||dm.video.weibocdn.com^
||dm.wo.com.cn^
||ad.mcloud.139.com^
||ad.zhuanzhuan.com^
||adserving-oss.bangdao-tech.com^
||v6-novelapp-nxyd.fqnovelvod.com^
||wcp.taobao.com^
||ad.bczcdn.com^
||miav-cse.avlyun.com^
||a0.app.xiaomi.com^
||miui-fxcse.avlyun.com^
||ads5-normal-lq.zijieapi.com.c.dsa.cdnbuild.net^
||adsdk.state.duoku.com^
||ads5-normal-lf.zijieapi.com.skyworth^
||ads3-normal-lf.zijieapi.com.skyworth^
||ad.oceanengine.com.w.cdngslb.com^
||ad.oceanengine.com.queniukw.com^
||ad.oceanengine.com^
||ad.k98y.cn^
||ads.api.lerist.cc^
||ads.chinaums.com^
||ad-monitor.sh9130.com^
||ads.ysepay.com^
||imp.ads.rgyun.com^
||c.ads.rgyun.com^
||dsp.fcbox.com^
||hugelog.fcbox.com^
||ads.adfox.ru^
||ad.xmmnsl.com^
||ad-*file.myqcloud.com^
||ads.iosappsworld.com^
||ads.adfox.ru^
||data-ads.oneplusmobile.com^
||dgstatic.jd.com^
||wdr160gz.dayugslb.com^
||tnc3-alisc1.zijieapi.com.queniuyk.com^
||ad.partner.gifshow.com^
||ads.lejuliang.com^
||o2o-ad-dev.oss-cn-shanghai.aliyuncs.com^
||ad-display.diwodiwo.xyz^
||metrics-ad-drcn.dt.hihonorcloud.com^
||ads.95516.com^
||p10528-ad-server.ejoy.com^
||zlsdk.1rtb.net^
||ads.aralego.com^
||hw-ot-ad.a.yximgs.com^
||pmp-ad.cdn.bcebos.com^
||market-ad.oss-accelerate.aliyuncs.com^
||api-ad-adapter-sg.wps.com^
||p10528-ad-server.ejoy.com^
||o2o-ad-dev.oss-cn-shanghai.aliyuncs.com.gds.alibabadns.com^
||metrics-ad-drcn.dt.hihonorcloud.com^
||ad-ga.nocode.com^
||ai.advertisement.alibabacorp.sm.cn^
||cdn-cbd-advert-prod.shuxinyc.com^
||websgsadvert.sanguosha.com^
||ms-ads-monitoring-events.presage.io^
||ssp-adx.ads.heytapmobi.com^
||ir-sdk.dun.163.com^
||adx.halomobi.com^
||api.jietuhb.com^
||adsmartgetqtt.guangtuikeji.com^
||tracker.medproad.com^
||adtracker.medproad.com^
||creative.medproad.com^
||content-understand-strategy.sm.cn^
||lbs.netease.im^
||ac.dun.163yun.com^
||advert-prod.hyrz.qq.com^
||ads.ishansong.com^
||ads-api.twitter.com^
||multi-az-ad.kuaishou.com^
||sdkconfig.ad.xiaomi.com.mshome.net^
||hw-ot-ad.a.yximgs.com^
||ad.99jiasu.com^
||ad.tk.cn^
||*-ad.adbkwai.com^
||*-ad.gskwai.com^
||cdn.wwads.cn^
||ae.iads.unity3d.com^
||o.iads.unity3d.com^
||ads.ubestkid.com^
||ad.xunkids.com^
||*-ad.adukwai.com^
||ads.lejuliang.cn^
||ads.inmobi.com^
||sdk.1rtb.net^
||volcsirius.com^
||vegslb.com^
||weilayun.com^
||adx-bj.anythinktech.com^


================================================
FILE: src/main/java/org/fordes/adfs/AdFSApplication.java
================================================
package org.fordes.adfs;

import lombok.extern.slf4j.Slf4j;
import org.fordes.adfs.config.AdFSProperties;
import org.fordes.adfs.constant.Constants;
import org.fordes.adfs.handler.Parser;
import org.fordes.adfs.handler.rule.Handler;
import org.fordes.adfs.model.Rule;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

@Slf4j
@Component
@SpringBootApplication
public class AdFSApplication {

    private final ApplicationContext context;
    private final Set<AdFSProperties.InputProperties> inputs;
    private final AdFSProperties.OutputProperties output;
    private final Parser parser;

    private final Map<String, Output> outputMap;

    public record Output(AdFSProperties.OutputItem item, Path tempFile, AtomicLong count) {
    }

    public static void main(String[] args) {
        SpringApplication.run(AdFSApplication.class, args);
    }

    public AdFSApplication(ApplicationContext context, AdFSProperties properties, Parser parser) {
        this.context = context;
        this.parser = parser;
        this.inputs = properties.getInput();
        this.output = properties.getOutput();
        this.outputMap = new HashMap<>(properties.getOutput().files().size(), 1);
        properties.getOutput().files().forEach(file -> {
            try {
                Path tempFile = Files.createTempFile(file.name(), ".tmp");
                this.outputMap.put(file.name(), new Output(file, tempFile, new AtomicLong(0L)));
            } catch (IOException e) {
                log.error("create temp file failed", e);
                this.exit();
            }
        });
    }

    @Bean
    public ApplicationRunner start() {
        return args -> {
            long start = System.currentTimeMillis();


            Flux.fromIterable(inputs)
                    .flatMap(parser::handle, 1)
                    .flatMap(rule -> this.output(output.files(), rule))
                    .groupBy(Tuple2::getT1, Tuple2::getT2)
                    .flatMap(group -> group
                            .bufferTimeout(5000, Duration.ofSeconds(1))
                            .flatMap(batch -> asyncBatchWrite(group.key().name(), batch))
                            .subscribeOn(Schedulers.boundedElastic())
                    )
                    .then(Mono.defer(this::createOutputDirectory))
                    .then(Mono.defer(this::processOutputFiles))
                    .doOnError(ex -> {
                        log.error("processing failed", ex);
                        this.exit();
                    })
                    .doFinally(signal -> {
                        log.info("all done, cost: {} ms", System.currentTimeMillis() - start);
                        this.exit();
                    })
                    .subscribe();
        };
    }

    public Flux<Tuple2<AdFSProperties.OutputItem, String>> output(Set<AdFSProperties.OutputItem> outputs, Rule rule) {
        return Flux.fromIterable(outputs)
                .filter(file -> file.rule().isEmpty() || file.rule().contains(rule.getSourceName()))
                .filter(file -> file.filter().isEmpty() || file.filter().contains(rule.getType()))
                .flatMap(file -> {
                    Handler handler = Handler.getHandler(file.type());
                    String content = handler.format(rule);
                    return content != null ? Mono.just(Tuples.of(file, content)) : Mono.empty();
                });
    }

    private Mono<Path> createOutputDirectory() {
        return Mono.fromCallable(() -> Files.createDirectories(Path.of(output.path())))
                .subscribeOn(Schedulers.boundedElastic())
                .onErrorResume(ex -> {
                    log.error("create output dir failed", ex);
                    return Mono.error(ex);
                });
    }

    private Mono<Void> processOutputFiles() {
        Path dir = Path.of(output.path());
        return Flux.fromIterable(output.files())
                .flatMap(file -> {
                    Output opt = outputMap.get(file.name());
                    Path tempFile = opt.tempFile;
                    Path targetFile = dir.resolve(file.name());
                    String header = buildHeader(file, output.fileHeader(), Long.toString(opt.count.get()));

                    log.info("[{}] written completed, total size => {}", file.name(), opt.count.get());
                    return prependAndMove(targetFile, tempFile, header).subscribeOn(Schedulers.boundedElastic());
                })
                .then();
    }

    private Mono<Void> asyncBatchWrite(String fileName, List<String> batch) {
        Output opt = outputMap.get(fileName);
        opt.count().addAndGet(batch.size());

        return asyncBatchWrite(opt.tempFile, batch);
    }

    private Mono<Void> asyncBatchWrite(Path path, List<String> batch) {
        return Mono.fromCallable(() -> {
                    Files.write(path, batch, StandardCharsets.UTF_8,
                            StandardOpenOption.CREATE, StandardOpenOption.APPEND);
                    return null;
                })
                .onErrorResume(e -> {
                    log.error("Write failed", e);
                    return Mono.empty();
                })
                .then();
    }

    private Mono<Path> prependAndMove(Path targetFile, Path tempFile, String header) {
        return Mono.fromCallable(() -> {
            if (Files.exists(targetFile)) {
                Path intermediateFile = Files.createTempFile(targetFile.getFileName().toString(), ".intermediate");

                try (BufferedWriter writer = Files.newBufferedWriter(intermediateFile, StandardCharsets.UTF_8);
                     BufferedReader tempReader = Files.newBufferedReader(tempFile, StandardCharsets.UTF_8)) {

                    if (!header.isBlank()) {
                        writer.write(header);
                    }

                    tempReader.transferTo(writer);
                }

                Files.move(intermediateFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(tempFile);
            } else {
                try (BufferedWriter writer = Files.newBufferedWriter(tempFile, StandardCharsets.UTF_8,
                        StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {

                    if (!header.isBlank()) {
                        List<String> lines = Files.readAllLines(tempFile, StandardCharsets.UTF_8);
                        Files.writeString(tempFile, header, StandardCharsets.UTF_8);
                        Files.write(tempFile, lines, StandardCharsets.UTF_8, StandardOpenOption.APPEND);
                    }
                }

                Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
            }

            return targetFile;
        });
    }

    private void exit() {
        int exit = SpringApplication.exit(this.context, () -> 0);
        System.exit(exit);
    }

    private String buildHeader(AdFSProperties.OutputItem config, String parentHeader, String total) {
        Handler handler = Handler.getHandler(config.type());
        StringBuilder builder = new StringBuilder();

        String template = config.fileHeader().isBlank() ? parentHeader : config.fileHeader();
        if (!template.isBlank()) {
            String header = handler.commented(template
                            .replace(Constants.Placeholder.HEADER_DATE, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                            .replace(Constants.Placeholder.HEADER_NAME, config.name())
                            .replace(Constants.Placeholder.HEADER_DESC, config.desc())
                            .replace(Constants.Placeholder.HEADER_TYPE, config.type().name().toLowerCase()))
                    .replace(Constants.Placeholder.HEADER_TOTAL, total);
            builder.append(header).append(System.lineSeparator());
        }

        Optional.ofNullable(handler.headFormat()).filter(StringUtils::hasText)
                .ifPresent(e -> builder.append(e).append(System.lineSeparator()));

        return builder.toString();
    }
}


================================================
FILE: src/main/java/org/fordes/adfs/config/AdFSProperties.java
================================================
package org.fordes.adfs.config;

import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.fordes.adfs.enums.RuleSet;
import org.fordes.adfs.model.Rule;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.context.annotation.Configuration;

import java.util.*;

/**
 *
 * @author fordes on 2025/10/29
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "application")
public class AdFSProperties implements InitializingBean {

    @NotNull
    private Config config;

    @NotNull
    @NotEmpty
    private Set<InputProperties> input;

    @NotNull
    private OutputProperties output;

    /**
     * @see #input
     */
    @Deprecated
    private Map<String, Set<InputProperties>> rule;

    @Override
    public void afterPropertiesSet() {
        if (rule != null && !rule.isEmpty()) {
            rule.forEach((k, v) -> this.input.addAll(v));
        }
    }

    public record OutputProperties(

            @DefaultValue("")
            String fileHeader,

            @DefaultValue("rule")
            String path,

            @NotEmpty
            Set<@Valid @NotNull OutputItem> files
    ) {
    }

    public record InputProperties(
            @NotBlank
            String name,

            @DefaultValue("EASYLIST")
            RuleSet type,

            @NotBlank
            String path
    ) {

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof InputProperties prop) {
                return prop.path.equals(this.path) || prop.name.equals(this.name);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return path.hashCode();
        }
    }

    public record OutputItem(
            @NotBlank
            String name,

            @NotNull
            RuleSet type,

            @DefaultValue("")
            String desc,

            @DefaultValue("")
            String fileHeader,

            @NotEmpty
            @DefaultValue({})
            Set<Rule.Type> filter,

            @NotEmpty
            @DefaultValue({})
            Set<@NotBlank String> rule

    ) {

    }

    public record Config(
            @Positive @DefaultValue("0.0001")
            Double faultTolerance,

            @Positive @DefaultValue("2000000")
            Long expectedQuantity,

            @Min(1) @DefaultValue("6")
            Integer warnLimit,

            @DefaultValue()
            Set<String> exclude,

            @DefaultValue
            DomainDetect domainDetect,

            @DefaultValue
            Tracking tracking
    ) {

    }

    public record DomainDetect(
            @DefaultValue("false")
            Boolean enable,

            @Positive @DefaultValue("1000")
            Integer timeout,

            @Positive @DefaultValue("600")
            Integer cacheTtlMin,

            @Positive @DefaultValue("86400")
            Integer cacheTtlMax,

            @Positive @DefaultValue("300")
            Integer cacheNegativeTtl,

            @Positive @DefaultValue("128")
            Integer concurrency,

            @DefaultValue
            List<String> provider) {

    }

    public record Tracking(@DefaultValue("false") Boolean enable,
                           @DefaultValue("logs/tracking.list") String path) {
    }
}

================================================
FILE: src/main/java/org/fordes/adfs/constant/Constants.java
================================================
package org.fordes.adfs.constant;

import java.io.File;
import java.util.Set;

public class Constants {

    public static final String ROOT_PATH = System.getProperty("user.dir");
    public static final String FILE_SEPARATOR = File.separator;
    public static final Set<String> LOCAL_IPS = Set.of("0.0.0.0", "127.0.0.1", "::1");
    public static final Set<String> LOCAL_DOMAINS = Set.of("localhost", "localhost.localdomain", "local", "ip6-localhost", "ip6-loopback");
    public static final String LOCAL_V4 = "127.0.0.1";
    public static final String UNKNOWN_IP = "0.0.0.0";
    public static final String LOCAL_V6 = "::1";
    public static final String LOCALHOST = "localhost";
    public static final String DOUBLE_AT = "@@";
    public static final String IMPORTANT = "important";
    public static final String DOMAIN = "domain";
    public static final String TAB = "\t";
    public static final String PAYLOAD = "payload";

    public static final String DNSMASQ_HEADER = "address=/";
    public static final String SMARTDNS_HEADER = "address /";

    public record Collector() {
        public static final String PARSER = "parser";
        public static final String DNS_CHECK = "dns-check";
    }

    public record Placeholder() {
        public static final String HEADER_DATE = "${date}";
        public static final String HEADER_NAME = "${name}";
        public static final String HEADER_TOTAL = "${total}";
        public static final String HEADER_TYPE = "${type}";
        public static final String HEADER_DESC = "${desc}";
    }

    public record Symbol() {

        public static final String EMPTY = "";
        public static final String DOT = ".";
        public static final String EXCLAMATION = "!";
        public static final String HASH = "#";
        public static final String AT = "@";
        public static final String PERCENT = "%";
        public static final String DOLLAR = "$";
        public static final String UNDERLINE = "_";
        public static final String DASH = "-";
        public static final String TILDE = "~";
        public static final String COMMA = ",";
        public static final String SLASH = "/";
        public static final String LEFT_BRACKETS = "[";
        public static final String RIGHT_BRACKETS = "]";
        public static final String OR = "||";
        public static final String ASTERISK = "*";
        public static final String QUESTION_MARK = "?";
        public static final String A = "a";
        public static final String CARET = "^";
        public static final String WHITESPACE = " ";
        public static final String CR = "\r";
        public static final String LF = "\n";
        public static final String TAB = "\t";
        public static final String CRLF = CR + LF;
        public static final String QUOTE = "\"";
        public static final String SINGLE_QUOTE = "'";
        public static final String ADD = "+";
        public static final String COLON = ":";
        public static final String EQUAL = "=";
    }



}


================================================
FILE: src/main/java/org/fordes/adfs/constant/RegConstants.java
================================================
package org.fordes.adfs.constant;

import java.util.regex.Pattern;

/**
 * @author fordes on 2024/4/9
 */
public class RegConstants {

    public static final Pattern PATTERN_PATH_ABSOLUTE = Pattern.compile("^[a-zA-Z]:([/\\\\].*)?");
    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))");
    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})+$");
    public static Pattern DOMAIN_PART = Pattern.compile("^([-a-zA-Z0-9]{0,62})+$");
}

================================================
FILE: src/main/java/org/fordes/adfs/enums/HandleType.java
================================================
package org.fordes.adfs.enums;

import lombok.AllArgsConstructor;

@AllArgsConstructor
public enum HandleType {

    LOCAL,

    REMOTE,

    ;
}


================================================
FILE: src/main/java/org/fordes/adfs/enums/RuleSet.java
================================================
package org.fordes.adfs.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.stream.Stream;

@Getter
@AllArgsConstructor
public enum RuleSet {

    EASYLIST,
    DNSMASQ,
    CLASH,
    SMARTDNS,
    HOSTS,
    ;

    public static RuleSet of(String name) {
        return Stream.of(values())
                .filter(v -> v.name().equalsIgnoreCase(name))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("unknown format: " + name));
    }

}


================================================
FILE: src/main/java/org/fordes/adfs/handler/Parser.java
================================================
package org.fordes.adfs.handler;

import bloomfilter.mutable.BloomFilter;
import lombok.extern.slf4j.Slf4j;
import org.fordes.adfs.config.AdFSProperties;
import org.fordes.adfs.constant.Constants;
import org.fordes.adfs.enums.HandleType;
import org.fordes.adfs.handler.dns.DnsDetector;
import org.fordes.adfs.handler.fetch.Fetcher;
import org.fordes.adfs.handler.rule.Handler;
import org.fordes.adfs.model.Rule;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import static org.fordes.adfs.config.AdFSProperties.Config;
import static org.fordes.adfs.config.AdFSProperties.InputProperties;

@Slf4j
@Component
public class Parser {

    protected final BloomFilter<Rule> filter;
    protected final Config config;
    protected final DnsDetector detector;
    protected final Tracker tracker;

    public Parser(AdFSProperties properties, Optional<DnsDetector> detector,
                  Optional<Tracker> tracker) {

        this.config = properties.getConfig();
        this.detector = detector.orElse(null);
        this.tracker = tracker.orElse(null);
        this.filter = BloomFilter.apply(config.expectedQuantity(), config.faultTolerance(), rule -> rule.hashCode());
    }

    public Flux<Rule> handle(InputProperties prop) {
        if (prop.path().startsWith("http")) {
            return this.handle(prop, HandleType.REMOTE);
        }
        return this.handle(prop, HandleType.LOCAL);
    }

    public Flux<Rule> handle(InputProperties prop, HandleType type) {

        AtomicLong total = new AtomicLong(0L);
        AtomicLong invalid = new AtomicLong(0L);
        AtomicLong repeat = new AtomicLong(0L);
        AtomicLong effective = new AtomicLong(0L);
        Set<String> exclude = Optional.ofNullable(config.exclude()).orElseGet(Set::of);
        long start = System.currentTimeMillis();

        return Flux.just(prop)
                .flatMap(p -> {
                    Fetcher fetcher = Fetcher.getFetcher(type);
                    return fetcher.fetch(p.path());
                })
                .filter(StringUtils::hasText)
                .flatMap(line -> {
                    Handler handler = Handler.getHandler(prop.type());

                    if (handler.isComment(line)) {
                        return Mono.empty();
                    }

                    Rule rule = handler.parse(line);
                    total.incrementAndGet();
                    if (Rule.EMPTY.equals(rule)) {
                        invalid.incrementAndGet();
                        log.debug("[{}] parse fail: {}", prop.name(), line);
                        if (tracker != null) {
                            tracker.writeSync(Constants.Collector.PARSER, prop.name(), line);
                        }
                        return Mono.empty();
                    }

                    rule.setSourceName(prop.name());
                    return Mono.just(rule);
                })
                .flatMap(e -> {

                    if (e.getTarget() != null && exclude.contains(e.getTarget())) {
                        log.info("exclude rule: {}", e.getOrigin());
                        return Mono.empty();
                    }

                    if (filter.mightContain(e)) {
                        log.debug("already exists rule: {}", e.getOrigin());
                        repeat.incrementAndGet();
                        return Mono.empty();
                    }

                    if (e.getOrigin().length() <= config.warnLimit()) {
                        log.warn("[{}] suspicious rule => {}", prop.name(), e.getOrigin());
                    }

                    return Mono.just(e);

                })
                .onErrorResume(ex -> {
                    log.error("Parse {} failed", prop.name(), ex);
                    return Mono.empty();
                })
                .flatMap(rule -> {

                    /**
                     * 假设有规则 ||example.org^
                     * 通过DNS查询 example.org 是否存在 A/AAAA/CNAME 记录作为判断依据
                     * 不可避免的误判是,example.org 没有有效记录,而其存在有效子域如 test.example.org
                     */
                    if (detector != null && Rule.Type.BASIC.equals(rule.getType())
                            && Rule.Scope.DOMAIN.equals(rule.getScope())) {

                        return Flux.just(rule.getTarget())
                                .flatMap(e -> detector.lookup(e), 1)
                                .flatMap(e -> {
                                    if (!e) {
                                        invalid.incrementAndGet();
                                        log.debug("[{}] dns check invalid rule => {}", prop.name(), rule.getOrigin());
                                        if (tracker != null) {
                                            tracker.writeSync(Constants.Collector.DNS_CHECK,prop.name(), rule.getOrigin());
                                        }
                                        return Mono.empty();
                                    }
                                    return Mono.just(rule);
                                });
                    }
                    return Mono.just(rule);
                }, config.domainDetect().concurrency())
                .flatMap(e -> {
                    filter.add(e);
                    effective.incrementAndGet();
                    return Mono.just(e);

                })
                .doFinally(signal -> {
                    log.info("[{}] parsing cost {} ms, total: {}, effective: {}, repeat: {}, invalid: {}",
                            prop.name(), System.currentTimeMillis() - start,
                            total.get(), effective.get(), repeat.get(), invalid.get());
                });

    }
}


================================================
FILE: src/main/java/org/fordes/adfs/handler/Tracker.java
================================================
package org.fordes.adfs.handler;

import lombok.extern.slf4j.Slf4j;
import org.fordes.adfs.config.AdFSProperties;
import org.fordes.adfs.constant.Constants;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import reactor.core.Disposable;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.core.scheduler.Schedulers;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.List;

/**
 * @author Chengfs on 2025/10/31
 */
@Slf4j
@Component
@ConditionalOnProperty(prefix = "application.config.tracking", name = "enable", havingValue = "true")
public class Tracker implements DisposableBean {

    private final Path file;
    private final Sinks.Many<String> sink;
    private final Disposable subscription;

    public Tracker(AdFSProperties properties) throws IOException {
        var config = properties.getConfig().tracking();

        this.file = Path.of(config.path());
        if (file.getParent() != null) {
            Files.createDirectories(file.getParent());
        }
        Files.writeString(file, Constants.Symbol.EMPTY, StandardCharsets.UTF_8,
                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

        this.sink = Sinks.many().unicast().onBackpressureBuffer();
        this.subscription = sink.asFlux()
                .bufferTimeout(5000, Duration.ofSeconds(30))
                .filter(batch -> !batch.isEmpty())
                .concatMap(this::writeBatch)
                .retry()
                .subscribe();

        log.info("lost collector is enabled, path: {}", file);
    }

    public Mono<Void> write(String source, String ruleName, String rule) {
        String line = String.format("[%s] [%s] %s\n", source, ruleName, rule);
        return Mono.fromRunnable(() -> sink.tryEmitNext(line));
    }

    public void writeSync(String source, String ruleName, String rule) {
        write(source,ruleName, rule).subscribe();
    }

    private Mono<Void> writeBatch(List<String> lines) {
        return Mono.fromCallable(() -> {
                    StringBuilder sb = new StringBuilder(lines.size() * 100);
                    lines.forEach(sb::append);
                    Files.writeString(file, sb.toString(), StandardCharsets.UTF_8,
                            StandardOpenOption.APPEND, StandardOpenOption.CREATE);
                    return null;
                })
                .subscribeOn(Schedulers.boundedElastic())
                .then();
    }

    @Override
    public void destroy() throws Exception {
        sink.tryEmitComplete();
        if (subscription != null) {
            Thread.sleep(2000);
            subscription.dispose();
        }
    }
}

================================================
FILE: src/main/java/org/fordes/adfs/handler/dns/DnsDetector.java
================================================
package org.fordes.adfs.handler.dns;

import io.netty.channel.EventLoop;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.handler.codec.dns.DnsRecord;
import io.netty.resolver.ResolvedAddressTypes;
import io.netty.resolver.dns.*;
import io.netty.util.concurrent.Future;
import jakarta.annotation.PreDestroy;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.fordes.adfs.config.AdFSProperties;
import org.fordes.adfs.constant.Constants;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.stream.IntStream;

@Data
@Slf4j
@Component
@ConditionalOnProperty(prefix = "application.config.domain-detect", name = "enable", havingValue = "true")
public class DnsDetector {

    private final NioEventLoopGroup group;
    private final List<DnsNameResolver> resolvers;
    private final AtomicLong nextIndex;
    private final LongAdder cacheHits;

    public DnsDetector(AdFSProperties properties) {
        var config = properties.getConfig().domainDetect();
        int concurrency = config.concurrency();

        this.group = new NioEventLoopGroup(concurrency);
        this.resolvers = new ArrayList<>(concurrency);
        this.nextIndex = new AtomicLong(0);
        this.cacheHits = new LongAdder();


        //初始化
        DnsServerAddressStreamProvider provider = buildProvider(config.provider());
        DnsCache sharedCache = this.buildDnsCache(config.cacheTtlMin(), config.cacheTtlMax(), config.cacheNegativeTtl());
        DnsCnameCache sharedCnameCache = new DefaultDnsCnameCache(config.cacheTtlMin(), config.cacheTtlMax());
        IntStream.range(0, concurrency).forEach(i -> {
            EventLoop loop = group.next();
            DnsNameResolver resolver = new DnsNameResolverBuilder(loop)
                    .datagramChannelFactory(NioDatagramChannel::new)
                    .nameServerProvider(provider)
                    .queryTimeoutMillis(config.timeout())
                    .maxQueriesPerResolve(2)
                    .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED)
                    .resolveCache(sharedCache)
                    .cnameCache(sharedCnameCache)
                    .optResourceEnabled(false)
                    .build();
            resolvers.add(resolver);
        });

        log.info("dns detector init success, concurrency:{}", concurrency);
    }

    private DnsServerAddressStreamProvider buildProvider(List<String> provider) {
        if (provider.isEmpty()) {
            return DnsServerAddressStreamProviders.platformDefault();
        }

        InetSocketAddress[] addresses = provider.stream()
                .map(e -> {
                    String[] parts = e.split(Constants.Symbol.COLON);
                    return new InetSocketAddress(parts[0], parts.length > 1 ? Integer.parseInt(parts[1]) : 53);
                })
                .toArray(InetSocketAddress[]::new);

        return new SequentialDnsServerAddressStreamProvider(addresses);
    }

    private DnsCache buildDnsCache(int minTtl, int maxTtl, int negativeTtl) {
        return new DefaultDnsCache(minTtl, maxTtl, negativeTtl) {
            @Override
            public List<? extends DnsCacheEntry> get(String hostname, DnsRecord[] additionals) {
                List<? extends DnsCacheEntry> result = super.get(hostname, additionals);
                if (result != null && !result.isEmpty()) {
                    cacheHits.increment();
                }
                return result;
            }
        };
    }

    public Mono<Boolean> lookup(String domain) {
        if (domain == null || domain.isEmpty()) {
            return Mono.just(true);
        }

        String normalizedDomain = domain.toLowerCase().trim();
        DnsNameResolver resolver = resolvers.get((int) (nextIndex.getAndIncrement() % resolvers.size()));
        return lookup(resolver, normalizedDomain);
    }

    private Mono<Boolean> lookup(DnsNameResolver resolver, String domain) {
        return Mono.create(sink -> {

            Future<List<InetAddress>> future = resolver.resolveAll(domain);
            future.addListener(result -> {

                boolean res = true;
                if (!result.isSuccess()) {
                    Throwable cause = result.cause();

                    if (cause instanceof UnknownHostException) {
                        res = false;
                    } else {
                        log.warn("dns check failed: {} => {}", domain, cause.getMessage());
                    }
                }
                sink.success(res);
                log.debug("dns check done, available: {}", resolvers.size());
            });
        });
    }


    @PreDestroy
    public void destroy() {
        try {
            log.info("dns detector shutdown, total queries:{}, cache hits:{}", nextIndex.get(), cacheHits.sum());

            resolvers.forEach(DnsNameResolver::close);
            group.shutdownGracefully().await(10, TimeUnit.SECONDS);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("Error during shutdown", e);
        }
    }
}

================================================
FILE: src/main/java/org/fordes/adfs/handler/fetch/Fetcher.java
================================================
package org.fordes.adfs.handler.fetch;

import jakarta.annotation.Nonnull;
import org.fordes.adfs.enums.HandleType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import reactor.core.publisher.Flux;

import java.nio.charset.Charset;

public abstract class Fetcher {

    protected @Nonnull Charset charset() {
        return Charset.defaultCharset();
    }

    public abstract Flux<String> fetch(@Nonnull String path);

    protected Flux<String> fetch(Flux<DataBuffer> buffers) {
        final StringBuilder lineBuffer = new StringBuilder();
        return buffers.flatMap(data -> {
                    String chunk = data.toString(this.charset());
                    DataBufferUtils.release(data);

                    lineBuffer.append(chunk);
                    String full = lineBuffer.toString();

                    String[] lines = full.split("\\r?\\n", -1);
                    int len = lines.length;

                    lineBuffer.setLength(0);

                    if (!full.endsWith("\n") && !full.endsWith("\r")) {
                        lineBuffer.append(lines[len - 1]);
                        len--;
                    }

                    return Flux.fromArray(lines).take(len);
                })
                .concatWith(Flux.defer(() -> {
                    if (lineBuffer.length() > 0) {
                        return Flux.just(lineBuffer.toString());
                    } else {
                        return Flux.empty();
                    }
                }));
    }

    public final static Fetcher getFetcher(HandleType type) {
        switch (type) {
            case LOCAL:
                return new LocalFetcher();
            case REMOTE:
                return new HttpFetcher();
        }
        throw new IllegalArgumentException("unsupported handle type: " + type);
    }
}


================================================
FILE: src/main/java/org/fordes/adfs/handler/fetch/HttpFetcher.java
================================================
package org.fordes.adfs.handler.fetch;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.netty.http.client.HttpClient;

import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

@Slf4j
public class HttpFetcher extends Fetcher {

    private final WebClient webClient;
    private Integer connectTimeout = 10_000;
    private Integer readTimeout = 30_000;
    private Integer writeTimeout = 30_000;
    private Integer bufferSize = 4096;

    public HttpFetcher() {

        HttpClient httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectTimeout) // 连接超时 10 秒
                .responseTimeout(Duration.ofSeconds(30))              // 响应超时 30 秒
                .doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(this.readTimeout, TimeUnit.MILLISECONDS))   // 读超时
                        .addHandlerLast(new WriteTimeoutHandler(this.writeTimeout, TimeUnit.MILLISECONDS))  // 写超时
                );

        ExchangeStrategies strategies = ExchangeStrategies.builder()
                .codecs(configurer -> configurer
                        .defaultCodecs()
                        .maxInMemorySize(this.bufferSize)
                )
                .build();

        this.webClient = WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .exchangeStrategies(strategies)
                .defaultHeader(HttpHeaders.CONNECTION, "keep-alive")
//                .defaultHeader(HttpHeaders.ACCEPT_CHARSET, this.charset().displayName())
//                .defaultHeader(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate, br")
                .build();
    }

    public HttpFetcher(Integer connectTimeout, Integer readTimeout, Integer writeTimeout, Integer bufferSize) {
        this();
        this.connectTimeout = connectTimeout;
        this.readTimeout = readTimeout;
        this.writeTimeout = writeTimeout;
        this.bufferSize = bufferSize;
    }

    @Override
    public Flux<String> fetch(String path) {
        Flux<DataBuffer> data = webClient.get()
                .uri(URI.create(path))
                .retrieve()
                .bodyToFlux(DataBuffer.class)
                .onErrorResume(e -> {
                    log.error("http rule => {}, fetch failed  => {}", path, e.getMessage(), e);
                    return Flux.empty();
                });

        return this.fetch(data);
    }

    @Override
    protected Charset charset() {
        return StandardCharsets.UTF_8;
    }

}


================================================
FILE: src/main/java/org/fordes/adfs/handler/fetch/LocalFetcher.java
================================================
package org.fordes.adfs.handler.fetch;

import lombok.extern.slf4j.Slf4j;
import org.fordes.adfs.util.Util;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import reactor.core.publisher.Flux;

import java.nio.charset.Charset;
import java.nio.file.Path;

@Slf4j
public class LocalFetcher extends Fetcher {

    private int bufferSize = 4096;

    public LocalFetcher() {
        super();
    }

    public LocalFetcher(int bufferSize) {
        super();
        this.bufferSize = bufferSize;
    }

    @Override
    public Flux<String> fetch(String path) {

        Flux<DataBuffer> data = Flux.just(path)
                .map(Util::normalizePath)
                .map(Path::of)
                .flatMap(p -> DataBufferUtils.read(p, new DefaultDataBufferFactory(), this.bufferSize))
                .onErrorResume(e -> {
                    log.error("local rule => {}, read failed  => {}", path, e.getMessage(), e);
                    return Flux.empty();
                });

        return this.fetch(data);
    }

    @Override
    protected Charset charset() {
        return super.charset();
    }

}


================================================
FILE: src/main/java/org/fordes/adfs/handler/rule/ClashHandler.java
================================================
package org.fordes.adfs.handler.rule;

import lombok.extern.slf4j.Slf4j;
import org.fordes.adfs.enums.RuleSet;
import org.fordes.adfs.model.Rule;
import org.fordes.adfs.util.Util;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static org.fordes.adfs.constant.Constants.*;
import static org.fordes.adfs.constant.Constants.Symbol.CRLF;
import static org.fordes.adfs.constant.RegConstants.PATTERN_DOMAIN;

/**
 * @author fordes123 on 2024/5/27
 */
@Slf4j
@Component
public final class ClashHandler extends Handler implements InitializingBean {

    @Override
    public Rule parse(String line) {
        //跳过文件头
        if (line.startsWith(PAYLOAD)) {
            return Rule.EMPTY;
        }

        Rule rule = new Rule();
        rule.setOrigin(line);
        rule.setSourceType(RuleSet.EASYLIST);

        //只匹配 domain 规则,ipcidr、classical 规则暂不支持
        String content = (line.startsWith(Symbol.DASH) ? line.substring(Symbol.DASH.length()) : line).trim();
        if (content.startsWith(Symbol.SINGLE_QUOTE)) {
            content = Util.subBetween(line, Symbol.SINGLE_QUOTE, Symbol.SINGLE_QUOTE).trim();
        } else if (content.startsWith(Symbol.QUOTE)) {
            content = Util.subBetween(line, Symbol.QUOTE, Symbol.QUOTE).trim();
        }

        //通配符 * 一次只能匹配一级域名,无法转换为easylist
        if (content.startsWith(Symbol.ASTERISK)) {
            rule.setType(Rule.Type.UNKNOWN);
            return rule;
        }

        //通配符 +
        if (content.startsWith(Symbol.ADD)) {
            content = content.substring(content.startsWith("+.") ? 2 : 1);
            rule.setControls(Set.of(Rule.Control.OVERLAY));
        }

        //判断是否是domain
        boolean haveAsterisk = content.contains(Symbol.ASTERISK);
        String temp = haveAsterisk ? content.replace(Symbol.ASTERISK, Symbol.A) : content;
        if (PATTERN_DOMAIN.matcher(temp).matches()) {
            rule.setType(haveAsterisk ? Rule.Type.WILDCARD : Rule.Type.BASIC);
        }

        rule.setTarget(content);
        rule.setDest(UNKNOWN_IP);
        rule.setMode(Rule.Mode.DENY);
        rule.setScope(Rule.Scope.DOMAIN);
        if (rule.getType() == null) {
            rule.setType(Rule.Type.UNKNOWN);
        }
        return rule;
    }

    @Override
    public String format(Rule rule) {
        if (Rule.Type.UNKNOWN == rule.getType()) {
            if (RuleSet.CLASH == rule.getSourceType()) {
                return rule.getOrigin();
            }
            return null;
        } else if (rule.getMode() == Rule.Mode.DENY && rule.getScope() == Rule.Scope.DOMAIN) {
            StringBuilder builder = new StringBuilder();
            builder.append(Symbol.WHITESPACE).append(Symbol.WHITESPACE).append(Symbol.DASH).append(Symbol.WHITESPACE).append(Symbol.QUOTE);

            Set<Rule.Control> controls = Optional.ofNullable(rule.getControls()).orElse(Set.of());
            if (controls.contains(Rule.Control.OVERLAY)) {
                builder.append(Symbol.ADD).append(Symbol.DOT);
            }
            builder.append(rule.getTarget());
            builder.append(Symbol.QUOTE);
            return builder.toString();
        }
        return null;
    }

    @Override
    public String headFormat() {
        return PAYLOAD + Symbol.COLON;
    }

    @Override
    public boolean isComment(String line) {
        return line.startsWith(Symbol.HASH);
    }

    @Override
    public String commented(String value) {
        return Util.splitIgnoreBlank(value, Symbol.LF).stream()
                .map(e -> Symbol.HASH + e.trim())
                .collect(Collectors.joining(CRLF));
    }

    @Override
    public void afterPropertiesSet() {
        this.register(RuleSet.CLASH, this);
    }
}

================================================
FILE: src/main/java/org/fordes/adfs/handler/rule/DnsmasqHandler.java
================================================
package org.fordes.adfs.handler.rule;

import org.fordes.adfs.enums.RuleSet;
import org.fordes.adfs.model.Rule;
import org.fordes.adfs.util.Util;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;

import static org.fordes.adfs.constant.Constants.*;

/**
 * @author fordes123 on 2024/5/27
 */
@Component
public final class DnsmasqHandler extends Handler implements InitializingBean {

    @Override
    public Rule parse(String line) {

        String content = Util.subAfter(line, DNSMASQ_HEADER, true);
        List<String> data = Util.splitIgnoreBlank(content, Symbol.SLASH);
        if (data.size() == 1 || data.size() == 2) {
            String domain = data.getFirst();
            String ip = data.size() > 1 ? data.get(1) : null;

            Rule rule = new Rule();
            rule.setSourceType(RuleSet.DNSMASQ);
            rule.setOrigin(line);
            rule.setTarget(domain);
            rule.setDest(ip);
            rule.setScope(Rule.Scope.DOMAIN);
            rule.setType(Rule.Type.BASIC);
            rule.setMode((ip == null || LOCAL_IPS.contains(ip)) ? Rule.Mode.DENY : Rule.Mode.REWRITE);
            return rule;
        }
        return Rule.EMPTY;
    }

    @Override
    public String format(Rule rule) {
        if (Rule.Scope.DOMAIN == rule.getScope() &&
                Rule.Type.BASIC == rule.getType() &&
                Rule.Mode.ALLOW != rule.getMode()) {

            StringBuilder builder = new StringBuilder();
            builder.append(DNSMASQ_HEADER)
                    .append(rule.getTarget());
            if (Rule.Mode.REWRITE.equals(rule.getMode())) {
                builder.append(Symbol.SLASH)
                        .append(rule.getDest());
            }
            builder.append(Symbol.SLASH);
            return builder.toString();
        }
        return null;
    }

    @Override
    public String commented(String value) {
        return Util.splitIgnoreBlank(value, Symbol.LF).stream()
                .map(e -> Symbol.HASH + Symbol.WHITESPACE + e.trim())
                .collect(Collectors.joining(Symbol.CRLF));
    }

    @Override
    public boolean isComment(String line) {
        return line.startsWith(Symbol.HASH);
    }

    @Override
    public void afterPropertiesSet() {
        this.register(RuleSet.DNSMASQ, this);
    }
}

================================================
FILE: src/main/java/org/fordes/adfs/handler/rule/EasylistHandler.java
================================================
package org.fordes.adfs.handler.rule;

import lombok.extern.slf4j.Slf4j;
import org.fordes.adfs.enums.RuleSet;
import org.fordes.adfs.model.Rule;
import org.fordes.adfs.util.Util;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.fordes.adfs.constant.Constants.*;

/**
 * @author fordes123 on 2024/5/27
 */
@Slf4j
@Component
public final class EasylistHandler extends Handler implements InitializingBean {

    @Override
    public Rule parse(String line) {
        Rule rule = new Rule();
        rule.setOrigin(line);
        rule.setSourceType(RuleSet.EASYLIST);
        rule.setMode(Rule.Mode.DENY);

        if (line.startsWith(DOUBLE_AT)) {
            rule.setMode(Rule.Mode.ALLOW);
            line = line.substring(2);
        }

        int _head = 0;
        if (line.startsWith(Symbol.OR)) {
            _head = Symbol.OR.length();
            rule.getControls().add(Rule.Control.OVERLAY);
        }


        //修饰部分
        int _tail = line.indexOf(Symbol.CARET);
        if (_tail > 0) {
            rule.getControls().add(Rule.Control.QUALIFIER);

            String modify = line.substring(_tail + 1);
            if (!modify.isEmpty()) {
                modify = modify.startsWith(Symbol.DOLLAR) ? modify.substring(1) : modify;
                String[] array = modify.split(Symbol.COMMA);
                if (Arrays.stream(array).allMatch(IMPORTANT::equals)) {
                    rule.getControls().add(Rule.Control.IMPORTANT);
                } else {
                    rule.setType(Rule.Type.UNKNOWN);
                    return rule;
                }
            }
        } else if (line.endsWith(Symbol.DOLLAR + IMPORTANT)) {
            rule.getControls().add(Rule.Control.IMPORTANT);
            _tail = line.length() - (Symbol.DOLLAR.length() + IMPORTANT.length());
        }


        //内容部分
        String content = line.substring(_head, _tail > 0 ? _tail : line.length());

        if (content.startsWith(Symbol.SLASH) && content.endsWith(Symbol.SLASH)) {
            content = content.substring(1, content.length() - 1);
            rule.setType(Rule.Type.UNKNOWN);
        }

        //判断是否为基本或通配规则
        Util.isBaseRule(content, (origin, e) -> {
            if (rule.getType() == null) {
                rule.setType(e);
            }
            rule.setScope(Rule.Scope.DOMAIN);
            rule.setTarget(origin);
            if (Rule.Mode.DENY.equals(rule.getMode())) {
                rule.setDest(UNKNOWN_IP);
            }
        }, e -> {
            Map.Entry<String, String> entry = Util.parseHosts(e);
            if (entry != null) {
                rule.setSourceType(RuleSet.HOSTS);
                rule.setTarget(entry.getValue());
                rule.setMode(LOCAL_IPS.contains(entry.getKey()) && !LOCAL_DOMAINS.contains(entry.getValue()) ? Rule.Mode.DENY : Rule.Mode.REWRITE);
                rule.setDest(Rule.Mode.DENY == rule.getMode() ? UNKNOWN_IP : entry.getKey());
                rule.setScope(Rule.Scope.DOMAIN);
                rule.setType(Rule.Type.BASIC);
            } else {
                rule.setType(Rule.Type.UNKNOWN);
            }
        });
        return rule;
    }

    @Override
    public String format(Rule rule) {
        if (Rule.Type.UNKNOWN != rule.getType() && Rule.Mode.REWRITE != rule.getMode()) {

            StringBuilder builder = new StringBuilder();
            Optional.of(rule.getMode())
                    .filter(Rule.Mode.ALLOW::equals)
                    .ifPresent(m -> builder.append(DOUBLE_AT));

            Optional.of(rule.getControls())
                    .filter(e -> e.contains(Rule.Control.OVERLAY))
                    .ifPresent(c -> builder.append(Symbol.OR));

            builder.append(rule.getTarget());

            Optional.of(rule.getControls())
                    .filter(e -> e.contains(Rule.Control.QUALIFIER))
                    .ifPresent(c -> builder.append(Symbol.CARET));

            Optional.of(rule.getControls())
                    .filter(e -> e.contains(Rule.Control.IMPORTANT))
                    .ifPresent(c -> builder.append(Symbol.DOLLAR).append(IMPORTANT));
            return builder.toString();
        }

        //同源未知规则可直接写出
        if (Rule.Type.UNKNOWN == rule.getType() && RuleSet.EASYLIST == rule.getSourceType()) {
            return rule.getOrigin();
        }
        return null;
    }

    @Override
    public String commented(String value) {
        return Util.splitIgnoreBlank(value, Symbol.LF).stream()
                .map(e -> Symbol.EXCLAMATION + Symbol.WHITESPACE + e.trim())
                .collect(Collectors.joining(Symbol.CRLF));
    }

    @Override
    public void afterPropertiesSet() {
        this.register(RuleSet.EASYLIST, this);
    }

    @Override
    public boolean isComment(String line) {
        return Util.startWithAny(line, Symbol.HASH, Symbol.EXCLAMATION) || Util.between(line, Symbol.LEFT_BRACKETS, Symbol.RIGHT_BRACKETS);
    }
}

================================================
FILE: src/main/java/org/fordes/adfs/handler/rule/Handler.java
================================================
package org.fordes.adfs.handler.rule;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.fordes.adfs.constant.Constants;
import org.fordes.adfs.enums.RuleSet;
import org.fordes.adfs.model.Rule;

import java.util.HashMap;
import java.util.Map;

public abstract sealed class Handler permits EasylistHandler, DnsmasqHandler, ClashHandler,
        SmartdnsHandler, HostsHandler {

    private static final Map<RuleSet, Handler> handlerMap = new HashMap<>(RuleSet.values().length, 1);

    /**
     * 解析规则<br/>
     * 返回 {@link Rule#EMPTY} 即表示解析失败
     *
     * @param line 规则文本
     * @return {@link Rule}
     */
    public abstract @Nonnull Rule parse(String line);

    /**
     * 转换规则<br/>
     *
     * @param rule {@link Rule} null 表示无法转换或失败
     * @return 规则文本
     */
    public abstract @Nullable String format(Rule rule);

    /**
     * 生成注释
     * @param value 目标内容
     * @return  注释
     */
    public abstract String commented(String value);

    /**
     * 某些规则格式拥有固定的头部内容,可实现此方法以返回
     */
    public String headFormat() {
        return Constants.Symbol.EMPTY;
    }

    /**
     * 某些规则格式拥有固定的尾部内容,可实现此方法以返回
     */
    public String tailFormat() {
        return Constants.Symbol.EMPTY;
    }

    /**
     * 验证规则文本是否为注释<br/>
     * 并不强制子类实现此方法,且不是注释不表示此规则有效
     *
     * @param line 规则文本
     * @return 默认 false
     */
    public boolean isComment(String line) {
        return false;
    }

    /**
     * 根据 RuleSet 获取 Handler
     *
     * @param type {@link RuleSet}
     * @return {@link Handler}
     */
    public static Handler getHandler(RuleSet type) {
        return handlerMap.get(type);
    }

    protected void register(RuleSet type, Handler handler) {
        handlerMap.put(type, handler);
    }
}


================================================
FILE: src/main/java/org/fordes/adfs/handler/rule/HostsHandler.java
================================================
package org.fordes.adfs.handler.rule;

import lombok.extern.slf4j.Slf4j;
import org.fordes.adfs.enums.RuleSet;
import org.fordes.adfs.model.Rule;
import org.fordes.adfs.util.Util;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.fordes.adfs.constant.Constants.*;

/**
 * @author fordes123 on 2024/5/27
 */
@Slf4j
@Component
public final class HostsHandler extends Handler implements InitializingBean {

    @Override
    public Rule parse(String line) {
        Map.Entry<String, String> entry = Util.parseHosts(line);
        if (entry == null || Objects.equals(entry.getKey(), entry.getValue())) {
            return Rule.EMPTY;
        }

        Rule rule = new Rule();
        rule.setSourceType(RuleSet.HOSTS);
        rule.setOrigin(line);
        rule.setTarget(entry.getValue());
        rule.setMode(LOCAL_IPS.contains(entry.getKey()) && !LOCAL_DOMAINS.contains(entry.getValue()) ? Rule.Mode.DENY : Rule.Mode.REWRITE);
        rule.setDest(Rule.Mode.DENY == rule.getMode() ? UNKNOWN_IP : entry.getKey());
        rule.setScope(Rule.Scope.DOMAIN);
        rule.setType(Rule.Type.BASIC);
        return rule;
    }

    @Override
    public String format(Rule rule) {
        if (Rule.Scope.DOMAIN == rule.getScope() &&
                Rule.Type.BASIC == rule.getType() &&
                Rule.Mode.ALLOW != rule.getMode()) {
            return Optional.ofNullable(rule.getDest()).orElse(UNKNOWN_IP) + Symbol.TAB + rule.getTarget();
        }
        return null;
    }

    @Override
    public String commented(String value) {
        return Util.splitIgnoreBlank(value, Symbol.LF).stream()
                .map(e -> Symbol.HASH + Symbol.WHITESPACE + e.trim())
                .collect(Collectors.joining(Symbol.CRLF));
    }

    @Override
    public boolean isComment(String line) {
        return line.startsWith(Symbol.HASH);
    }

    @Override
    public void afterPropertiesSet() {
        this.register(RuleSet.HOSTS, this);
    }
}

================================================
FILE: src/main/java/org/fordes/adfs/handler/rule/SmartdnsHandler.java
================================================
package org.fordes.adfs.handler.rule;

import org.fordes.adfs.enums.RuleSet;
import org.fordes.adfs.model.Rule;
import org.fordes.adfs.util.Util;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static org.fordes.adfs.constant.Constants.*;
import static org.fordes.adfs.model.Rule.Mode.ALLOW;

/**
 * @author fordes123 on 2024/5/27
 */
@Component
public final class SmartdnsHandler extends Handler implements InitializingBean {

    @Override
    public Rule parse(String line) {

        String content = Util.subAfter(line, SMARTDNS_HEADER, true);
        List<String> data = Util.splitIgnoreBlank(content, Symbol.SLASH);
        if (data.size() == 2) {
            String domain = data.getFirst();
            String control = data.get(1);
            Rule rule = new Rule();
            rule.setOrigin(line);
            rule.setSourceType(RuleSet.EASYLIST);

            switch (control) {
                case Symbol.HASH -> rule.setMode(Rule.Mode.DENY);
                case Symbol.DASH -> rule.setMode(ALLOW);
                default -> {
                    //未知或不支持的控制符 如 #6 #4
                    rule.setType(Rule.Type.UNKNOWN);
                    return rule;
                }
            }

            //仅匹配主域名
            if (domain.startsWith(Symbol.DASH)) {
                domain = domain.substring(0, line.length() - Symbol.DASH.length());
            } else {
                rule.setControls(Set.of(Rule.Control.OVERLAY));
            }

            if (domain.startsWith(Symbol.DOT)) {
                domain = domain.substring(0, line.length() - Symbol.DOT.length());
            }

            rule.setType(domain.startsWith(Symbol.ASTERISK) ? Rule.Type.WILDCARD : Rule.Type.BASIC);
            rule.setTarget(domain);
            rule.setDest(UNKNOWN_IP);
            rule.setScope(Rule.Scope.DOMAIN);
            return rule;
        }
        return Rule.EMPTY;
    }

    @Override
    public String format(Rule rule) {
        if (Rule.Type.UNKNOWN == rule.getType()) {
            if (RuleSet.SMARTDNS == rule.getSourceType()) {
                return rule.getOrigin();
            }
            return null;
        } else if (rule.getMode() != Rule.Mode.REWRITE && rule.getScope() == Rule.Scope.DOMAIN) {

            switch (rule.getType()) {
                case BASIC -> {
                    return SMARTDNS_HEADER +
                            (!rule.getControls().contains(Rule.Control.OVERLAY) ?
                                    (Symbol.ASTERISK + Symbol.DOT) : Symbol.EMPTY) +
                            rule.getTarget() +
                            Symbol.SLASH +
                            (Rule.Mode.DENY.equals(rule.getMode()) ? Symbol.HASH : Symbol.DASH);
                }
                case WILDCARD -> {
                    String domain = rule.getTarget();
                    if (domain.lastIndexOf(Symbol.ASTERISK) == 0) {
                        return SMARTDNS_HEADER + domain + Symbol.SLASH + Symbol.DASH;
                    }
                }
            }
        }
        return null;
    }

    @Override
    public String commented(String value) {
        return Util.splitIgnoreBlank(value, Symbol.LF).stream()
                .map(e -> Symbol.HASH + Symbol.WHITESPACE + e.trim())
                .collect(Collectors.joining(Symbol.CRLF));
    }

    @Override
    public boolean isComment(String line) {
        return line.startsWith(Symbol.HASH);
    }

    @Override
    public void afterPropertiesSet() {
        this.register(RuleSet.SMARTDNS, this);
    }
}

================================================
FILE: src/main/java/org/fordes/adfs/model/Rule.java
================================================
package org.fordes.adfs.model;

import lombok.Data;
import org.fordes.adfs.enums.RuleSet;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
 * @author fordes123 on 2024/5/27
 */
@Data
public class Rule {

    /**
     * 规则来源名称,及 {@link org.fordes.adfs.config.AdFSProperties.InputProperties#name}
     */
    private String sourceName;

    /**
     * 规则来源类型 {@link RuleSet}
     */
    private RuleSet sourceType;

    /**
     * 原始规则
     */
    private String origin;

    /**
     * 作用目标
     */
    private String target;

    /**
     * 重定向/重写目标
     */
    private String dest;

    /**
     * 模式 {@link Mode}
     */
    private Mode mode;

    /**
     * 作用范围 {@link Scope}
     */
    private Scope scope;

    /**
     * 规则类型 {@link Type}
     */
    private Type type;

    /**
     * 控制符集 {@link Control}
     */
    private Set<Control> controls = new HashSet<>(Control.values().length, 1.0f);

    public static final Rule EMPTY = new Rule();

    /**
     * 规则控制参数
     */
    public enum Control {
        /**
         * 最高优先级
         */
        IMPORTANT,

        /**
         * 覆盖子域名
         */

        OVERLAY,

        /**
         * 限定符,通常是 ^
         */
        QUALIFIER,

        ;
    }

    /**
     * 规则模式
     */
    public enum Mode {

        /**
         * 阻止
         */
        DENY,

        /**
         * 解除阻止
         */
        ALLOW,

        /**
         * 重写<br/>
         * 通常 hosts规则指向特定ip(非localhost)时即为重写
         */
        REWRITE,

        ;
    }

    /**
     * 规则类型
     */
    public enum Type {
        /**
         * 基本规则,不包含任何控制、匹配符号, 可以转换为 hosts
         */
        BASIC,

        /**
         * 通配规则,仅使用通配符
         */
        WILDCARD,

        /**
         * 其他规则,如使用了正则、高级修饰符号等,这表示目前无法支持
         */
        UNKNOWN,

        ;
    }

    /**
     * 作用域
     */
    public enum Scope {
        /**
         * ipv4或ipv6地址
         */
        HOST,

        /**
         * 域名
         */
        DOMAIN,

        /**
         * 路径、文件等
         */
        PATH,

        ;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof Rule rule) {
            if (Type.UNKNOWN == this.type || Type.UNKNOWN == rule.getType()) {
                return Objects.equals(this.origin, rule.origin);
            }
            return Objects.equals(this.target, rule.target) &&
                    this.mode == rule.mode &&
                    this.scope == rule.scope &&
                    this.type == rule.type;
        }
        return false;
    }

    @Override
    public int hashCode() {
        if (Type.UNKNOWN == this.type) {
            return Objects.hash(this.origin);
        }
        return Objects.hash(getTarget(), getMode(), getScope(), getType());
    }

    @Override
    public String toString() {
        if (Type.UNKNOWN == this.type) {
            return "Rule{" +
                    "origin='" + origin + '\'' +
                    '}';
        }
        return "Rule{" +
                "target='" + target + '\'' +
                ", mode=" + mode +
                ", scope=" + scope +
                ", type=" + type +
                '}';
    }
}

================================================
FILE: src/main/java/org/fordes/adfs/util/Util.java
================================================
package org.fordes.adfs.util;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.fordes.adfs.model.Rule;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import static org.fordes.adfs.constant.Constants.*;
import static org.fordes.adfs.constant.RegConstants.*;

/**
 * @author fordes123 on 2022/9/19
 */
@Slf4j
public class Util {

    /**
     * 给定字符串是否以特定前缀开始
     *
     * @param str      给定字符串
     * @param prefixes 前缀
     * @return 给定字符串是否以特定前缀开始
     */
    public static boolean startWithAny(String str, String... prefixes) {
        if (!StringUtils.hasText(str) || ObjectUtils.isEmpty(prefixes)) {
            return false;
        }
        return Arrays.stream(prefixes).anyMatch(str::startsWith);
    }

    /**
     * 给定字符串是否以特定字符串开始和结束
     *
     * @param str   给定字符串
     * @param start 开始字符串
     * @param end   结束字符串
     * @return 给定字符串是否以特定字符串开始和结束
     */
    public static boolean between(String str, String start, String end) {
        if (StringUtils.hasLength(str) && StringUtils.hasLength(start) && StringUtils.hasLength(end)) {
            return str.startsWith(start) && str.endsWith(end);
        }
        return false;
    }

    /**
     * 截取分隔字符串之前的字符串,不包括分隔字符串<br/>
     * 截取不到时返回空串
     *
     * @param str    被截取的字符串
     * @param flag   分隔字符串
     * @param isLast 是否是最后一个
     * @return 分隔字符串之前的字符串
     */
    public static String subBefore(String str, String flag, boolean isLast) {
        if (StringUtils.hasLength(str) && StringUtils.hasLength(flag)) {
            int index = isLast ? str.lastIndexOf(flag) : str.indexOf(flag);
            if (index >= 0) {
                return str.substring(0, index);
            }
        }
        return Symbol.EMPTY;
    }

    /**
     * 截取分隔字符串之后的字符串,不包括分隔字符串<br/>
     * 截取不到时返回空串
     *
     * @param content 被截取的字符串
     * @param flag    分隔字符串
     * @param isLast  是否是最后一个
     * @return 分隔字符串之后的字符串
     */
    public static String subAfter(String content, String flag, boolean isLast) {
        if (StringUtils.hasLength(content) && StringUtils.hasLength(flag)) {
            int index = isLast ? content.lastIndexOf(flag) : content.indexOf(flag);
            if (index >= 0) {
                return content.substring(index + flag.length());
            }
        }
        return Symbol.EMPTY;
    }

    /**
     * 截取分隔字符串之间的字符串,不包括分隔字符串<br/>
     * 截取不到时返回空串
     *
     * @param content 被截取的字符串
     * @param start   开始分隔字符串
     * @param end     结束分隔字符串
     * @return 分隔字符串之间的字符串
     */
    public static String subBetween(String content, String start, String end) {
        if (StringUtils.hasLength(content) && StringUtils.hasLength(start) && StringUtils.hasLength(end)) {
            int startIndex = content.indexOf(start);
            int endIndex = content.lastIndexOf(end);
            if (startIndex >= 0 && endIndex > 0 && startIndex < endIndex) {
                return content.substring(startIndex + start.length(), endIndex);
            }
        }
        return Symbol.EMPTY;
    }

    /**
     * 切分字符串并移除空项
     *
     * @param str  待切分字符串
     * @param flag 分隔符
     * @return 切分后的字符串
     */
    public static List<String> splitIgnoreBlank(String str, String flag) {
        if (!StringUtils.hasLength(str) || !StringUtils.hasLength(flag)) {
            return List.of();
        }
        return Arrays.stream(str.split(flag))
                .filter(e -> !e.isBlank())
                .toList();
    }

    /**
     * 给定字符串是等于任一字符串
     *
     * @param str    给定字符串
     * @param values 任意字符串
     * @return 给定字符串是等于任一字符串
     */
    public static boolean equalsAny(String str, String... values) {
        if (!StringUtils.hasLength(str) || ObjectUtils.isEmpty(values)) {
            return false;
        }
        return Arrays.asList(values).contains(str);
    }

    /**
     * 解析hosts规则,如不是则返回null
     *
     * @param content 待解析字符串
     * @return {@link Map.Entry} key:ip, value:域名
     */
    public static @Nullable Map.Entry<String, String> parseHosts(String content) {
        if (content.contains(Symbol.TAB)) {
            content = content.replace(TAB, Symbol.WHITESPACE);
        }
        List<String> list = splitIgnoreBlank(content, Symbol.WHITESPACE);
        if (list.size() == 2) {
            String ip = list.get(0).trim();
            String domain = list.get(1).trim();

            if (PATTERN_IP.matcher(ip).matches() && PATTERN_DOMAIN.matcher(domain).matches()) {
                return Map.entry(ip, domain);
            }
        }
        return null;
    }

    /**
     * 休眠线程,忽略中断异常
     *
     * @param millis 休眠时间,毫秒
     */
    public static void sleep(long millis) {
        if (millis > 0L) {
            try {
                Thread.sleep(millis);
            } catch (InterruptedException ignored) {
            }
        }
    }

    /**
     * 转换相对路径为绝对路径
     *
     * @param path 路径
     * @return 规范化后的路径
     */
    public static String normalizePath(@Nonnull String path) {

        boolean isAbsPath = '/' == path.charAt(0) || PATTERN_PATH_ABSOLUTE.matcher(path).matches();

        if (!isAbsPath) {
            if (path.startsWith(Symbol.DOT)) {
                path = path.substring(1);
            }
            if (path.startsWith(FILE_SEPARATOR)) {
                path = path.substring(FILE_SEPARATOR.length());
            }
            path = ROOT_PATH + FILE_SEPARATOR + path;
        }
        return path;
    }


    public static void isBaseRule(String content, BiConsumer<String, Rule.Type> ifPresent, Consumer<String> orElse) {
        String temp = content;
        if (temp.contains(Symbol.ASTERISK)) {
            temp = content.replace(Symbol.ASTERISK, Symbol.A);
        }

        if (temp.startsWith(Symbol.DOT)) {
            temp = temp.substring(1);
        }

        if (temp.endsWith(Symbol.DOT)) {
            temp = temp.substring(0, temp.length() - 1);
        }

        if (PATTERN_DOMAIN.matcher(temp).matches()) {
            ifPresent.accept(content, content.equals(temp) ? Rule.Type.BASIC : Rule.Type.WILDCARD);
        } else if (DOMAIN_PART.matcher(temp).matches()) {
            ifPresent.accept(content, Rule.Type.WILDCARD);
        } else {
            orElse.accept(content);
        }
    }
}


================================================
FILE: src/main/resources/application-dev.yml
================================================
logging:
  level:
    org.fordes.adfs: info

application:

  input:
    - name: anti-ad smartdns
      path: https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/anti-ad-smartdns.conf
      type: smartdns

    - name: anti-ad dnsmasq
      path: https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/adblock-for-dnsmasq.conf
      type: dnsmasq

    - name: AdGuard 基础过滤器
      path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_2_Base/filter.txt
      type: easylist

    - name: Loyalsoldier/clash-rules 广告域名列表
      path: https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt
      type: clash

    - name: 1Hosts lite
      path: https://o0.pages.dev/Lite/hosts.txt
      type: hosts

  output:
    files:
      - name: easylist.txt
        type: easylist
        filter:
          - basic
          - wildcard
          - unknown

      - name: dns.txt
        type: easylist
        file-header: |
          Test ${total}
        filter:
          - basic
          - wildcard

      - name: modify.txt
        type: easylist
        filter:
          - unknown

      - name: dnsmasq.conf
        type: dnsmasq

      - name: clash.yaml
        type: clash

      - name: smartdns.conf
        type: smartdns

      - name: hosts
        type: hosts

  config:
    domain-detect:
      enable: true
      concurrency: 256
      provider:
        - 1.1.1.1

================================================
FILE: src/main/resources/application.yml
================================================
spring:
  application:
    name: "@project.name@"
    version: "@project.version@"
  profiles:
    active: prod

logging:
  file:
    path: ./logs

application:
  input:
  config:
    tracking:
      enable: true
      path: logs/tracking.list

================================================
FILE: src/main/resources/banner.txt
================================================
${AnsiColor.BRIGHT_GREEN}██████╗ ██████╗        █████╗ ██████╗ 
${AnsiColor.BRIGHT_GREEN}██╔══██╗██╔══██╗      ██╔══██╗██╔══██╗
${AnsiColor.BRIGHT_GREEN}██║  ██║██║  ██║█████╗███████║██║  ██║
${AnsiColor.BRIGHT_GREEN}██║  ██║██║  ██║╚════╝██╔══██║██║  ██║
${AnsiColor.BRIGHT_GREEN}██████╔╝██████╔╝      ██║  ██║██████╔╝
${AnsiColor.BRIGHT_GREEN}╚═════╝ ╚═════╝       ╚═╝  ╚═╝╚═════╝                                   
${AnsiColor.BRIGHT_BLACK}  ─────────────────────────────────────────────────────────
${AnsiColor.BRIGHT_WHITE}  ➤ Runtime ${AnsiColor.BRIGHT_BLACK}${os.name} • Java ${java.version} • Spring Boot v${spring-boot.version}
${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}
${AnsiColor.BRIGHT_WHITE}  ➤ Issue tracker: ${AnsiColor.BRIGHT_BLACK}https://github.com/fordes123/ad-filters-subscriber/issues
${AnsiColor.BRIGHT_WHITE}

================================================
FILE: src/main/resources/logback-spring.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<configuration scanPeriod="60 seconds" debug="false">

    <!-- 定义日志的根目录 -->
    <springProperty scope="context" name="LOG_HOME" source="logging.file.path" defaultValue="./logs"/>

    <!-- 定义日志文件名称 -->
    <springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="application"/>

    <!-- 彩色日志渲染-->
    <conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex" class="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx" class="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>

    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <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>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 文件滚动记录 -->
    <appender name="rollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/${APP_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- daily rollover -->
            <fileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- keep 30 days' worth of history capped at 3GB total size -->
            <maxHistory>30</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <append>true</append>
        <encoder>
            <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>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="asyncFileAppender" class="ch.qos.logback.classic.AsyncAppender">
        <includeCallerData>true</includeCallerData>
        <queueSize>4096</queueSize>
        <neverBlock>true</neverBlock>
        <appender-ref ref="rollingFileAppender"/>
    </appender>

    <springProfile name="dev">
        <logger name="org.fordes" additivity="true" level="debug"/>
    </springProfile>

    <springProfile name="prod">
        <logger name="org.fordes" additivity="true" level="info"/>
    </springProfile>

    <root level="info">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="asyncFileAppender"/>
    </root>
</configuration>
Download .txt
gitextract_rl4mzeuj/

├── .devcontainer/
│   ├── Dockerfile
│   └── devcontainer.json
├── .github/
│   └── workflows/
│       └── auto-update.yml
├── .gitignore
├── LICENSE
├── README.md
├── config/
│   ├── application-example.yaml
│   └── application.yaml
├── pom.xml
├── rule/
│   └── DD-AD.txt
└── src/
    └── main/
        ├── java/
        │   └── org/
        │       └── fordes/
        │           └── adfs/
        │               ├── AdFSApplication.java
        │               ├── config/
        │               │   └── AdFSProperties.java
        │               ├── constant/
        │               │   ├── Constants.java
        │               │   └── RegConstants.java
        │               ├── enums/
        │               │   ├── HandleType.java
        │               │   └── RuleSet.java
        │               ├── handler/
        │               │   ├── Parser.java
        │               │   ├── Tracker.java
        │               │   ├── dns/
        │               │   │   └── DnsDetector.java
        │               │   ├── fetch/
        │               │   │   ├── Fetcher.java
        │               │   │   ├── HttpFetcher.java
        │               │   │   └── LocalFetcher.java
        │               │   └── rule/
        │               │       ├── ClashHandler.java
        │               │       ├── DnsmasqHandler.java
        │               │       ├── EasylistHandler.java
        │               │       ├── Handler.java
        │               │       ├── HostsHandler.java
        │               │       └── SmartdnsHandler.java
        │               ├── model/
        │               │   └── Rule.java
        │               └── util/
        │                   └── Util.java
        └── resources/
            ├── application-dev.yml
            ├── application.yml
            ├── banner.txt
            └── logback-spring.xml
Download .txt
SYMBOL INDEX (113 symbols across 20 files)

FILE: src/main/java/org/fordes/adfs/AdFSApplication.java
  class AdFSApplication (line 36) | @Slf4j
    method main (line 51) | public static void main(String[] args) {
    method AdFSApplication (line 55) | public AdFSApplication(ApplicationContext context, AdFSProperties prop...
    method start (line 72) | @Bean
    method output (line 101) | public Flux<Tuple2<AdFSProperties.OutputItem, String>> output(Set<AdFS...
    method createOutputDirectory (line 112) | private Mono<Path> createOutputDirectory() {
    method processOutputFiles (line 121) | private Mono<Void> processOutputFiles() {
    method asyncBatchWrite (line 136) | private Mono<Void> asyncBatchWrite(String fileName, List<String> batch) {
    method asyncBatchWrite (line 143) | private Mono<Void> asyncBatchWrite(Path path, List<String> batch) {
    method prependAndMove (line 156) | private Mono<Path> prependAndMove(Path targetFile, Path tempFile, Stri...
    method exit (line 191) | private void exit() {
    method buildHeader (line 196) | private String buildHeader(AdFSProperties.OutputItem config, String pa...

FILE: src/main/java/org/fordes/adfs/config/AdFSProperties.java
  class AdFSProperties (line 19) | @Data
    method afterPropertiesSet (line 40) | @Override
    method equals (line 71) | @Override
    method hashCode (line 79) | @Override

FILE: src/main/java/org/fordes/adfs/constant/Constants.java
  class Constants (line 6) | public class Constants {

FILE: src/main/java/org/fordes/adfs/constant/RegConstants.java
  class RegConstants (line 8) | public class RegConstants {

FILE: src/main/java/org/fordes/adfs/enums/HandleType.java
  type HandleType (line 5) | @AllArgsConstructor

FILE: src/main/java/org/fordes/adfs/enums/RuleSet.java
  type RuleSet (line 8) | @Getter
    method of (line 19) | public static RuleSet of(String name) {

FILE: src/main/java/org/fordes/adfs/handler/Parser.java
  class Parser (line 24) | @Slf4j
    method Parser (line 33) | public Parser(AdFSProperties properties, Optional<DnsDetector> detector,
    method handle (line 42) | public Flux<Rule> handle(InputProperties prop) {
    method handle (line 49) | public Flux<Rule> handle(InputProperties prop, HandleType type) {

FILE: src/main/java/org/fordes/adfs/handler/Tracker.java
  class Tracker (line 25) | @Slf4j
    method Tracker (line 34) | public Tracker(AdFSProperties properties) throws IOException {
    method write (line 55) | public Mono<Void> write(String source, String ruleName, String rule) {
    method writeSync (line 60) | public void writeSync(String source, String ruleName, String rule) {
    method writeBatch (line 64) | private Mono<Void> writeBatch(List<String> lines) {
    method destroy (line 76) | @Override

FILE: src/main/java/org/fordes/adfs/handler/dns/DnsDetector.java
  class DnsDetector (line 29) | @Data
    method DnsDetector (line 40) | public DnsDetector(AdFSProperties properties) {
    method buildProvider (line 72) | private DnsServerAddressStreamProvider buildProvider(List<String> prov...
    method buildDnsCache (line 87) | private DnsCache buildDnsCache(int minTtl, int maxTtl, int negativeTtl) {
    method lookup (line 100) | public Mono<Boolean> lookup(String domain) {
    method lookup (line 110) | private Mono<Boolean> lookup(DnsNameResolver resolver, String domain) {
    method destroy (line 133) | @PreDestroy

FILE: src/main/java/org/fordes/adfs/handler/fetch/Fetcher.java
  class Fetcher (line 11) | public abstract class Fetcher {
    method charset (line 13) | protected @Nonnull Charset charset() {
    method fetch (line 17) | public abstract Flux<String> fetch(@Nonnull String path);
    method fetch (line 19) | protected Flux<String> fetch(Flux<DataBuffer> buffers) {
    method getFetcher (line 49) | public final static Fetcher getFetcher(HandleType type) {

FILE: src/main/java/org/fordes/adfs/handler/fetch/HttpFetcher.java
  class HttpFetcher (line 21) | @Slf4j
    method HttpFetcher (line 30) | public HttpFetcher() {
    method HttpFetcher (line 56) | public HttpFetcher(Integer connectTimeout, Integer readTimeout, Intege...
    method fetch (line 64) | @Override
    method charset (line 78) | @Override

FILE: src/main/java/org/fordes/adfs/handler/fetch/LocalFetcher.java
  class LocalFetcher (line 13) | @Slf4j
    method LocalFetcher (line 18) | public LocalFetcher() {
    method LocalFetcher (line 22) | public LocalFetcher(int bufferSize) {
    method fetch (line 27) | @Override
    method charset (line 42) | @Override

FILE: src/main/java/org/fordes/adfs/handler/rule/ClashHandler.java
  class ClashHandler (line 21) | @Slf4j
    method parse (line 25) | @Override
    method format (line 73) | @Override
    method headFormat (line 95) | @Override
    method isComment (line 100) | @Override
    method commented (line 105) | @Override
    method afterPropertiesSet (line 112) | @Override

FILE: src/main/java/org/fordes/adfs/handler/rule/DnsmasqHandler.java
  class DnsmasqHandler (line 17) | @Component
    method parse (line 20) | @Override
    method format (line 42) | @Override
    method commented (line 61) | @Override
    method isComment (line 68) | @Override
    method afterPropertiesSet (line 73) | @Override

FILE: src/main/java/org/fordes/adfs/handler/rule/EasylistHandler.java
  class EasylistHandler (line 20) | @Slf4j
    method parse (line 24) | @Override
    method format (line 99) | @Override
    method commented (line 131) | @Override
    method afterPropertiesSet (line 138) | @Override
    method isComment (line 143) | @Override

FILE: src/main/java/org/fordes/adfs/handler/rule/Handler.java
  class Handler (line 12) | public abstract sealed class Handler permits EasylistHandler, DnsmasqHan...
    method parse (line 24) | public abstract @Nonnull Rule parse(String line);
    method format (line 32) | public abstract @Nullable String format(Rule rule);
    method commented (line 39) | public abstract String commented(String value);
    method headFormat (line 44) | public String headFormat() {
    method tailFormat (line 51) | public String tailFormat() {
    method isComment (line 62) | public boolean isComment(String line) {
    method getHandler (line 72) | public static Handler getHandler(RuleSet type) {
    method register (line 76) | protected void register(RuleSet type, Handler handler) {

FILE: src/main/java/org/fordes/adfs/handler/rule/HostsHandler.java
  class HostsHandler (line 20) | @Slf4j
    method parse (line 24) | @Override
    method format (line 42) | @Override
    method commented (line 52) | @Override
    method isComment (line 59) | @Override
    method afterPropertiesSet (line 64) | @Override

FILE: src/main/java/org/fordes/adfs/handler/rule/SmartdnsHandler.java
  class SmartdnsHandler (line 19) | @Component
    method parse (line 22) | @Override
    method format (line 64) | @Override
    method commented (line 93) | @Override
    method isComment (line 100) | @Override
    method afterPropertiesSet (line 105) | @Override

FILE: src/main/java/org/fordes/adfs/model/Rule.java
  class Rule (line 13) | @Data
    type Control (line 66) | public enum Control {
    type Mode (line 89) | public enum Mode {
    type Type (line 113) | public enum Type {
    type Scope (line 135) | public enum Scope {
    method equals (line 154) | @Override
    method hashCode (line 169) | @Override
    method toString (line 177) | @Override

FILE: src/main/java/org/fordes/adfs/util/Util.java
  class Util (line 22) | @Slf4j
    method startWithAny (line 32) | public static boolean startWithAny(String str, String... prefixes) {
    method between (line 47) | public static boolean between(String str, String start, String end) {
    method subBefore (line 63) | public static String subBefore(String str, String flag, boolean isLast) {
    method subAfter (line 82) | public static String subAfter(String content, String flag, boolean isL...
    method subBetween (line 101) | public static String subBetween(String content, String start, String e...
    method splitIgnoreBlank (line 119) | public static List<String> splitIgnoreBlank(String str, String flag) {
    method equalsAny (line 135) | public static boolean equalsAny(String str, String... values) {
    method parseHosts (line 148) | public static @Nullable Map.Entry<String, String> parseHosts(String co...
    method sleep (line 169) | public static void sleep(long millis) {
    method normalizePath (line 184) | public static String normalizePath(@Nonnull String path) {
    method isBaseRule (line 201) | public static void isBaseRule(String content, BiConsumer<String, Rule....
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (117K chars).
[
  {
    "path": ".devcontainer/Dockerfile",
    "chars": 1300,
    "preview": "# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 11, 17, 11-bullseye, 17-bullseye, 11-bust"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "chars": 279,
    "preview": "{\n  \"name\": \"CodeSpace\",\n  \"dockerFile\": \"Dockerfile\",\n  \"extensions\": [\n    \"vscjava.vscode-java-pack\",\n    \"vscjava.vs"
  },
  {
    "path": ".github/workflows/auto-update.yml",
    "chars": 2991,
    "preview": "name: Update Filters\non:\n  push:\n    paths-ignore:\n      - 'README.md'\n      - 'README_en.md'\n      - '.github/**'\n     "
  },
  {
    "path": ".gitignore",
    "chars": 300,
    "preview": "# 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# Packa"
  },
  {
    "path": "LICENSE",
    "chars": 1059,
    "preview": "MIT License\n\nCopyright (c) 2022 以谶\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this"
  },
  {
    "path": "README.md",
    "chars": 4190,
    "preview": "# DD-AD\n\n## [***✈️ tg 频道***](https://t.me/DDadsss)</br>[***🐧 QQ群***](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=z4tq1QhIH"
  },
  {
    "path": "config/application-example.yaml",
    "chars": 2527,
    "preview": "##########################################\n## !!! 示例配置,修改无效 !!!\n## !!! example config, modify invalid !!!\n##############"
  },
  {
    "path": "config/application.yaml",
    "chars": 2726,
    "preview": "# 参考 `application-example.yaml` 并在处添加订阅规则,请注意缩进\n# Refer to `application-example.yaml` and add subscription rules there,p"
  },
  {
    "path": "pom.xml",
    "chars": 3662,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
  },
  {
    "path": "rule/DD-AD.txt",
    "chars": 15163,
    "preview": "! 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! De"
  },
  {
    "path": "src/main/java/org/fordes/adfs/AdFSApplication.java",
    "chars": 9084,
    "preview": "package org.fordes.adfs;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.config.AdFSProperties;\nimport org.for"
  },
  {
    "path": "src/main/java/org/fordes/adfs/config/AdFSProperties.java",
    "chars": 3511,
    "preview": "package org.fordes.adfs.config;\n\nimport jakarta.validation.Valid;\nimport jakarta.validation.constraints.*;\nimport lombok"
  },
  {
    "path": "src/main/java/org/fordes/adfs/constant/Constants.java",
    "chars": 3026,
    "preview": "package org.fordes.adfs.constant;\n\nimport java.io.File;\nimport java.util.Set;\n\npublic class Constants {\n\n    public stat"
  },
  {
    "path": "src/main/java/org/fordes/adfs/constant/RegConstants.java",
    "chars": 602,
    "preview": "package org.fordes.adfs.constant;\n\nimport java.util.regex.Pattern;\n\n/**\n * @author fordes on 2024/4/9\n */\npublic class R"
  },
  {
    "path": "src/main/java/org/fordes/adfs/enums/HandleType.java",
    "chars": 146,
    "preview": "package org.fordes.adfs.enums;\n\nimport lombok.AllArgsConstructor;\n\n@AllArgsConstructor\npublic enum HandleType {\n\n    LOC"
  },
  {
    "path": "src/main/java/org/fordes/adfs/enums/RuleSet.java",
    "chars": 515,
    "preview": "package org.fordes.adfs.enums;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\n\nimport java.util.stream.Stream;"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/Parser.java",
    "chars": 5943,
    "preview": "package org.fordes.adfs.handler;\n\nimport bloomfilter.mutable.BloomFilter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.f"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/Tracker.java",
    "chars": 2959,
    "preview": "package org.fordes.adfs.handler;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.config.AdFSProperties;\nimport"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/dns/DnsDetector.java",
    "chars": 5568,
    "preview": "package org.fordes.adfs.handler.dns;\n\nimport io.netty.channel.EventLoop;\nimport io.netty.channel.nio.NioEventLoopGroup;\n"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/fetch/Fetcher.java",
    "chars": 1897,
    "preview": "package org.fordes.adfs.handler.fetch;\n\nimport jakarta.annotation.Nonnull;\nimport org.fordes.adfs.enums.HandleType;\nimpo"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/fetch/HttpFetcher.java",
    "chars": 3153,
    "preview": "package org.fordes.adfs.handler.fetch;\n\nimport io.netty.channel.ChannelOption;\nimport io.netty.handler.timeout.ReadTimeo"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/fetch/LocalFetcher.java",
    "chars": 1241,
    "preview": "package org.fordes.adfs.handler.fetch;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.util.Util;\nimport org.s"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/ClashHandler.java",
    "chars": 3851,
    "preview": "package org.fordes.adfs.handler.rule;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.enums.RuleSet;\nimport or"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/DnsmasqHandler.java",
    "chars": 2421,
    "preview": "package org.fordes.adfs.handler.rule;\n\nimport org.fordes.adfs.enums.RuleSet;\nimport org.fordes.adfs.model.Rule;\nimport o"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/EasylistHandler.java",
    "chars": 5116,
    "preview": "package org.fordes.adfs.handler.rule;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.enums.RuleSet;\nimport or"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/Handler.java",
    "chars": 1766,
    "preview": "package org.fordes.adfs.handler.rule;\n\nimport jakarta.annotation.Nonnull;\nimport jakarta.annotation.Nullable;\nimport org"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/HostsHandler.java",
    "chars": 2141,
    "preview": "package org.fordes.adfs.handler.rule;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.fordes.adfs.enums.RuleSet;\nimport or"
  },
  {
    "path": "src/main/java/org/fordes/adfs/handler/rule/SmartdnsHandler.java",
    "chars": 3687,
    "preview": "package org.fordes.adfs.handler.rule;\n\nimport org.fordes.adfs.enums.RuleSet;\nimport org.fordes.adfs.model.Rule;\nimport o"
  },
  {
    "path": "src/main/java/org/fordes/adfs/model/Rule.java",
    "chars": 3222,
    "preview": "package org.fordes.adfs.model;\n\nimport lombok.Data;\nimport org.fordes.adfs.enums.RuleSet;\n\nimport java.util.HashSet;\nimp"
  },
  {
    "path": "src/main/java/org/fordes/adfs/util/Util.java",
    "chars": 6464,
    "preview": "package org.fordes.adfs.util;\n\nimport jakarta.annotation.Nonnull;\nimport jakarta.annotation.Nullable;\nimport lombok.exte"
  },
  {
    "path": "src/main/resources/application-dev.yml",
    "chars": 1466,
    "preview": "logging:\n  level:\n    org.fordes.adfs: info\n\napplication:\n\n  input:\n    - name: anti-ad smartdns\n      path: https://raw"
  },
  {
    "path": "src/main/resources/application.yml",
    "chars": 243,
    "preview": "spring:\n  application:\n    name: \"@project.name@\"\n    version: \"@project.version@\"\n  profiles:\n    active: prod\n\nlogging"
  },
  {
    "path": "src/main/resources/banner.txt",
    "chars": 1045,
    "preview": "${AnsiColor.BRIGHT_GREEN}██████╗ ██████╗        █████╗ ██████╗ \n${AnsiColor.BRIGHT_GREEN}██╔══██╗██╔══██╗      ██╔══██╗█"
  },
  {
    "path": "src/main/resources/logback-spring.xml",
    "chars": 2731,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<configuration scanPeriod=\"60 seconds\" debug=\"false\">\n\n    <!-- 定义日志的根目录 -->\n   "
  }
]

About this extraction

This page contains the full source code of the afwfv/DD-AD GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (103.5 KB), approximately 29.0k tokens, and a symbol index with 113 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!