GitHub - ryanbr/fop-rs: FOP Filter Orderer and Preener re-written in Rust

11 min read Original article ↗

A Rust port of the EasyList FOP tool for sorting and cleaning ad-blocking filter lists.

Features

  • Filter sorting: Alphabetically sorts blocking rules and element hiding rules
  • Domain combining: Merges rules with identical selectors/patterns but different domains
  • Option normalization: Converts uBO-specific options to standard ABP format (can be disabled)
  • Wildcard cleanup: Removes unnecessary wildcards from filters
  • Validation: Removes invalid/overly-broad rules (TLD-only, too short, etc.)
  • Git integration: Commit changes directly to repositories (can be disabled)
  • easylist_adservers.txt validation: Ensures rules start with | or /
  • :has-text() merging: Combines rules with same base selector into single regex
  • Parallel processing: Uses all CPU cores for faster processing via Rayon

Extended Syntax Support

FOP preserves extended filter syntax from various adblockers:

uBlock Origin

  • Scriptlet injection: ##+js(...), ##^script:has-text(...)
  • Procedural cosmetics: :has-text(), :has(), :upward(), :remove(), :style(), :matches-css(), :xpath(), :remove-attr(), :remove-class(), :watch-attr(), :min-text-length(), :others()
  • Network options: redirect=, redirect-rule=, removeparam=, denyallow=, replace=, header=, permissions=, to=, from=, method=
  • Regex domain rules: /regex/##+js(...)

AdGuard

  • Scriptlet injection: #%#//scriptlet(...)
  • CSS injection: #$#body { ... }
  • Exceptions: #@%#, #@$#

Adblock Plus

  • Extended selectors: :-abp-has(), :-abp-contains(), :-abp-properties()
  • Action syntax: {remove: true;}, {height:...}, {display:...}
  • Snippets: #$#hide-if-contains, #$#simulate-mouse-event

Speed Comparison (FOP.py vs FOP Rust)

foprs

Installation

Building from source (optional)

Requires Rust 1.80+ (https://rustup.rs)

cd fop-rs
cargo build --release
cp target/release/fop /usr/local/bin/  # or add to PATH

Usage

# Sort filters in current directory (with commit prompt)
fop

# Sort filters in a specific directory
fop /path/to/easylist

# Sort without commit prompt (like FOP-nocommit.py)
fop --no-commit /path/to/easylist
fop -n .

# Sort without converting uBO options to ABP format
fop --no-ubo-convert /path/to/ublock-filters

# Sort multiple directories
fop -n ~/easylist ~/easyprivacy ~/fanboy-addon

Command Line Options

Option Description
-n, --no-commit Just sort files, skip Git commit prompts
--just-sort Alias for --no-commit
--no-ubo-convert Skip uBO to ABP option conversion (keep xhr, 3p, 1p, etc.)
--no-msg-check Skip commit message format validation (M:/A:/P:)
--disable-ignored Disable hardcoded ignored files and folders for testing
--no-sort Don't sort rules, just combine
--alt-sort More correct sorting method
--localhost Sort hosts file entries (0.0.0.0/127.0.0.1 domain)
--localhost-files= Specific files to sort as localhost format (comma-separated)
--parse-adguard Globally Parse AdGuard extended CSS (#$?#, #@$?#, $$, $@$)
--parse-adguard=FILE Files to parse as AdGuard extended CSS (comma-separated)
--no-color Disable colored output
--no-large-warning Disable large change warning prompt
--backup Create .backup files before modifying
--keep-empty-lines Keep empty lines in output
--ignore-dot-domains Don't skip rules without dot in domain
--ignorefiles= Additional files to ignore (comma-separated, partial names)
--ignoredirs= Additional directories to ignore (comma-separated, partial names)
--ignore-all-but= Only process these files, ignore all others (comma-separated)
--file-extensions= File extensions to process (default: .txt)
--comments= Comment line prefixes (default: !)
--warning-output= Output warnings to file instead of stderr
--git-message= Git commit message (skip interactive prompt)
--history= Predefined commit messages for arrow key selection (comma-separated)
--create-pr[=TITLE] Create PR branch instead of committing to current branch
--git-pr-branch=NAME Base branch for PR (default: auto-detect main/master)
--pr-show-changes Include rule changes (combines, merges, typos) in PR body
--fix-typos Fix cosmetic rule typos in all files during sort
--fix-typos-on-add Check cosmetic rule typos in git additions before commit
--auto-fix Auto-fix typos without prompting (use with --fix-typos-on-add)
--only-sort-changed Only process files changed according to git
--check-banned-list=FILE Check for banned domains in git additions
--auto-banned-remove Auto-remove banned domains and commit
--ci CI mode - exit with error code on failures (banned domains)
--rebase-on-fail Auto rebase and retry push if it fails
--commit-mask=N Mask URLs in commit messages (1=[.], 2=(.), 3=space, 4=preserve subdomain dot, 5=Unicode lookalike )
--commit-mask-users=u1,u2 Restrict --commit-mask to these git config user.name values (lowercased)
--commit-mask-bare Also mask bare hostnames without http(s)://. Risks false positives on filenames
--commit-mask-exempt-hosts=h1,h2 Additional apex hosts exempt from masking (e.g. self-hosted Gitea/Forgejo/GitLab)
--commit-url-template=TMPL Override the Commit successful: URL template. Placeholders: {base}, {sha}. Auto-detects Bitbucket (/commits/); everything else defaults to /commit/
--ignore-config Ignore .fopconfig file, use only CLI args
--output Output changed files with --changed suffix (no overwrite)
--check-file=FILE Process a single file
--output-diff=FILE Output changes as diff (no files modified)
--quiet Limit console output, less verbose
--limited-quiet Suppress directory listing only
--add-timestamp=FILES Update timestamp for specific files only (comma-separated)
--validate-checksum=FILES Validate checksum for specific files (exit 1 on failure)
--validate-checksum-and-fix=FILES Validate and fix invalid checksums
--add-checksum=FILES Add/update checksum for specific files (comma-separated)
--add-timestamp Update timestamp in file header (Last Modified/Last Updated)
--config-file= Custom config file path
--show-config Show applied configuration and exit
--git-binary=<path> Path to git binary (default: git in PATH)
--benchmark Benchmark sorting performance (3 iterations, dry-run)
--abp-convert Convert ABP extended selectors to uBO format
--convert-trusted Convert trusted scriptlets to non-trusted when value is safe
-h, --help Show help message
-V, --version Show version number

Configuration File

Create .fopconfig in your working directory or home directory:

# Skip commit prompt
no-commit = false

# Skip uBO to ABP option conversion
no-ubo-convert = false

# Skip commit message format validation
no-msg-check = false

# Skip sorting (only tidy and combine rules)
no-sort = false

# Alternative sorting method
alt-sort = false

# Sort hosts file entries
localhost = false

# Disable colored output
no-color = false

# Disable large change warning prompt
no-large-warning = false

# Create .backup files before modifying
backup = false

# Keep empty lines in output
keep-empty-lines = false

# Don't skip rules without dot in domain
ignore-dot-domains = false

# Comment line prefixes
comments = !

# Output warnings to file
warning-output =

# Additional files to ignore
ignorefiles = .json,.backup,.bak,.swp,.gz

# Additional directories to ignore
ignoredirs =

# File extensions to process
file-extensions = txt

# Create PR branch instead of committing
create-pr =

# Include rule changes in PR body
pr-show-changes = false

# Base branch for PR (default: auto-detect)
git-pr-branch =

# Convert trusted scriptlets to non-trusted when value is safe
convert-trusted = false

# Fix cosmetic typos during sort
fix-typos = false

# Check typos in git additions
fix-typos-on-add = false

# Auto-fix without prompting
auto-fix = false

# Only sort git-changed files (skip unchanged)
only-sort-changed = false

# Path to banned domain list file
check-banned-list =

# Auto-remove banned domains and commit
auto-banned-remove = false

# CI mode - exit with error code on failures
ci = false

# Auto rebase and retry if push fails
rebase-on-fail = false

# Mask URLs in commit messages: 1=[.], 2=(.), 3=space, 4=preserve subdomain dot, 5=Unicode lookalike
# github.com, gitlab.com, and codeberg.org (and subdomains) are always exempt so PR/issue links stay clickable.
commit-mask =

# If non-empty, --commit-mask only applies when git user.name matches one of these (case-insensitive).
commit-mask-users =

# Also mask bare hostnames (no http/https). Risks FP on filenames like config.toml.
commit-mask-bare = false

# Additional apex hosts (and subdomains) exempt from masking. Adds to the
# built-in github.com / gitlab.com / codeberg.org list. Use for self-hosted
# Gitea, Forgejo, or private GitLab instances.
commit-mask-exempt-hosts =

# Override the 'Commit successful:' URL template (placeholders: {base}, {sha}).
# Default is auto-detected per host: bitbucket.org gets {base}/commits/{sha},
# everything else gets {base}/commit/{sha}. Override here if your platform
# uses something different (e.g. cgit, trac, custom redirector).
commit-url-template =

# Suppress most output (for CI)
quiet = false

# Users allowed to push directly when create-pr is enabled (comma-separated, case-insensitive)
direct-push-users =

# Predefined commit messages for arrow key selection (comma-separated)
# Use up/down arrows at commit prompt to cycle through these
history = A: ,P: ,M: Update,M: Cleanup,M: Sort,M: Adjust

Command line arguments override config file settings.

Per-File Overrides

Add [filename] sections to override settings for specific files:

# Global settings apply to all files
fix-typos = true

# Override settings for specific files
[adguard_base.txt]
parse-adguard = true

[hosts.txt]
localhost = true

[easylist.txt]
add-checksum = true
add-timestamp = true

Supported per-file options: no-sort, alt-sort, parse-adguard, localhost, add-checksum, add-timestamp, no-ubo-convert, abp-convert, convert-trusted, keep-empty-lines, ignore-dot-domains, fix-typos.

Commit Message URL Masking

--commit-mask=N defangs URLs in commit messages so they're stored in git history in a non-clickable form. Useful for filter-list maintainers who don't want third-party scrapers, search engines, or auto-linkers to follow URLs cited in commit messages. Masking happens after commit-message validation (so A:/P: prefixes still validate against the real URL) but before git commit -m, so the masked form is what lands in git.

Levels

Using https://www.example.com/foo/bar.html and https://www.example.co.nz/foo as inputs:

Level Style www.example.com/... www.example.co.nz/foo
1 [.] https://www[.]example[.]com/foo/bar.html https://www[.]example[.]co[.]nz/foo
2 (.) https://www(.)example(.)com/foo/bar.html https://www(.)example(.)co(.)nz/foo
3 space https://www example com/foo/bar.html https://www example co nz/foo
4 preserve subdomain dot https://www.example[.]com/foo/bar.html https://www.example[.]co[.]nz/foo
5 Unicode (U+2024) https://www․example․com/foo/bar.html https://www․example․co․nz/foo

Notes:

  • Path/query/fragment dots are never masked (only host dots), so file extensions like bar.html and query values like ?t=1.2 stay readable.
  • Level 4 masks only the registrable domain plus eTLD ("eTLD+1"). Compound TLDs (co.uk, co.nz, com.au, co.jp, com.ng, com.hk, co.il, com.vn, etc. — ~25 country codes) are recognised so example.co.uk is treated as a 2-label TLD. Apex-only inputs (example.com, no subdomain) still defang the only dot.
  • Level 5 is the strongest defang: U+2024 is visually nearly identical to . but is not in IDNA UTS #46 normalization tables, so URLs containing it won't resolve in browsers, curl, or most parsers — even after copy/paste. Reviewers won't realise the link is dead until they try to click it.
  • Unknown levels (0, 6, 99…) silently fall through to level 1.
  • Exempt hosts: github.com, gitlab.com, codeberg.org, and any of their subdomains (gist.github.com, docs.gitlab.com, etc.) are never masked, so PR/issue links remain clickable on the hosting platform. Lookalike hosts like notgitlab.com or evil.gitlab.com.attacker.com still get masked. For self-hosted Gitea/Forgejo/GitLab, add your host with --commit-mask-exempt-hosts=git.company.internal,....

Choosing a level

When to use Level
Maximum visibility ("clearly defanged") 1, 2, or 3
Want the host structure to remain readable, only break TLD 4
Want the URL to look real but stay broken (anti-scraper) 5

Restricting masking by user

--commit-mask-users=name1,name2,... (or commit-mask-users = ... in config) gates masking on the current git config user.name. If the list is empty, masking applies to whoever runs fop. If the list is non-empty, only matching users (case-insensitive) get masking; everyone else commits with the original URL.

commit-mask = 4
commit-mask-users = fanboynz, ryanbr

Bare hostnames (--commit-mask-bare)

Off by default. When enabled, also matches hostnames without an http(s):// prefix (e.g. forums.lanik.us or www.example.com.ng). The matcher requires at least one dot and a 2+ letter final label, so version numbers like 1.2.3 are not matched. Filenames like config.toml will match — that's the documented false-positive cost of opting in. Scheme URLs still take priority where both forms appear, and the host-exempt list (github.com, gitlab.com) applies to bare matches too.

Display label

After a successful commit, fop prints Commit message: followed by what landed in git. If masking actually changed the message, the label becomes Commit message (masked): instead, so it's obvious the displayed text is the masked form.

Commit message (masked):  A: https://www.example[.]com/foo
Commit successful:        https://github.com/easylist/easylist/commit/abc1234

If commit-mask is unset, or every URL was github/gitlab (a no-op mask), the plain Commit message: label is shown.

Platform Support

Pre-built Binaries

Platform Binary Optimization Compatible Devices
Linux
x86_64 linux-x86_64 Baseline All 64-bit Intel/AMD
x86_64 linux-x86_64-v3 AVX2 Intel Haswell+ / AMD Excavator+ (~2015+)
x86 linux-x86_32 Baseline 32-bit systems, older hardware
ARM64 linux-arm64 Baseline Raspberry Pi 3/4/5, Orange Pi 3/4/5, all ARM64
ARM64 linux-arm64-n1 Neoverse N1 Pi 5, Orange Pi 5, AWS Graviton2+, Ampere Altra
RISC-V linux-riscv64 Baseline SiFive, StarFive VisionFive 2, Milk-V
macOS
Intel macos-x86_64 Baseline All Intel Macs
Apple Silicon macos-arm64 Apple M1 M1, M2, M3, M4, M5 Macs
Windows
x86_64 windows-x86_64.exe Baseline All 64-bit Windows
x86_64 windows-x86_64-v3.exe AVX2 Intel Haswell+ / AMD Excavator+ (~2015+)
x86 windows-x86_32.exe Baseline 32-bit Windows
ARM64 windows-arm64-v2.exe Cortex-A78 Surface Pro X, Snapdragon laptops

Which binary should I use?

Linux/Windows x86_64:

  • Use -v3 for CPUs from ~2015+ (Haswell, Ryzen) - ~10-20% faster due to AVX2
  • Use baseline if unsure or on older CPUs

Linux ARM64 (Raspberry Pi / Orange Pi):

  • Use linux-arm64 for Pi 3, Pi 4, Orange Pi 3/4, or if unsure
  • Use linux-arm64-n1 for Pi 5, Orange Pi 5, AWS Graviton2+ (~10-20% faster)

npm install downloads baseline binaries for maximum compatibility. Optimized versions are available from GitHub Releases.

Build Details

Binary Target RUSTFLAGS
linux-x86_64 Native -
linux-x86_64-v3 Native -C target-cpu=x86-64-v3
linux-x86_32 i686-unknown-linux-gnu -
linux-arm64 aarch64-unknown-linux-gnu -
linux-arm64-n1 aarch64-unknown-linux-gnu -C target-cpu=neoverse-n1
linux-riscv64 riscv64gc-unknown-linux-gnu -
macos-x86_64 x86_64-apple-darwin -
macos-arm64 aarch64-apple-darwin -C target-cpu=apple-m1
windows-x86_64.exe x86_64-pc-windows-gnu -
windows-x86_64-v3.exe x86_64-pc-windows-gnu -C target-cpu=x86-64-v3
windows-x86_32.exe i686-pc-windows-gnu -
windows-arm64-v2.exe aarch64-pc-windows-msvc -C target-cpu=cortex-a78

Unsupported platform? FOP.rs builds from source on any platform with Rust 1.80+:

Migrating from Python FOP

This Rust version is a drop-in replacement for both FOP.py and FOP-nocommit.py:

Python Rust Equivalent
python3 FOP.py fop
python3 FOP-nocommit.py fop --no-commit or fop -n

Performance

The Rust version is significantly faster than Python FOP due to:

  • Compiled native code
  • Parallel file processing with Rayon
  • Optimized regex handling
  • Efficient memory management

Windows PowerShell Note

If you see UnauthorizedAccess or PSSecurityException when running fop in PowerShell, run this once to fix permanently:

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

Alternatively, use Git Bash, MINGW64, or WSL.

Security Warnings

Pre-built binaries aren't code-signed, so your OS may show a warning on first run.

macOS

If blocked by Gatekeeper ("cannot be opened because the developer cannot be verified"):

xattr -d com.apple.quarantine ./fop-mac

Windows

If SmartScreen blocks the file:

  1. Click "More info"
  2. Click "Run anyway"

Alternative: Install via npm (npm install -g fop-cli) to avoid these warnings entirely.

License

GPL-3.0 (same as original Python FOP)

Credits

  • Original Python FOP by Michael (EasyList project)
  • Rust port maintains feature parity with Python version 3.9