Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.DS_Store
__pycache__/
*.pyc
*.zip

*.qrx
dist/
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Quartz Ad Blocker

Optional ad blocking for Quartz as a WebExtension package.
Optional ad blocking for Quartz as a single `.qrx` WebExtension package.

## Install in Quartz

1. Build or download this repository.
1. Build or download `QuartzAdBlocker.qrx`.
2. Open Quartz on macOS 15.4 or later.
3. Choose **Extensions > Install Extension...**.
4. Select the `Extension` directory in this repository, or a ZIP archive of that directory.
4. Select the `.qrx` package.

Quartz loads installed extensions before the first page navigation on later launches.

Expand All @@ -25,3 +25,10 @@ python3 Tools/generate_rules.py

The generator reads `Filters/` and writes `Extension/rules/rules.json` plus `Extension/rules/metadata.json`.

## Build QRX Package

```sh
python3 Tools/package_qrx.py
```

The packager writes `dist/QuartzAdBlocker.qrx`. The `.qrx` file is a Zip-compatible extension package with `manifest.json` at the archive root, which is the single file Quartz installs.
3 changes: 1 addition & 2 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ This repository contains filter assets and generated rules derived from the form
- Original asset: `uBlock0_1.71.0.chromium.zip`
- License: GPL-3.0-or-later, preserved at `Filters/uBlockOrigin/LICENSE.txt`

The generated `Extension/rules/rules.json` file is produced from the compatible network-filter subset of the filter files in `Filters/`.

The generated `Extension/rules/rules.json` file is produced from the compatible network-filter subset of the filter files in `Filters/`. The `.qrx` package is a Zip-compatible archive of the generated extension files.
83 changes: 83 additions & 0 deletions Tools/package_qrx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
import argparse
import hashlib
import json
from pathlib import Path
import sys
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo


ROOT = Path(__file__).resolve().parents[1]
EXTENSION_ROOT = ROOT / "Extension"
DEFAULT_OUTPUT = ROOT / "dist" / "QuartzAdBlocker.qrx"
ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0)


def iter_extension_files():
for path in sorted(EXTENSION_ROOT.rglob("*")):
if path.is_file() and path.name != ".DS_Store":
yield path


def load_manifest():
manifest_path = EXTENSION_ROOT / "manifest.json"
try:
with manifest_path.open(encoding="utf-8") as handle:
manifest = json.load(handle)
except FileNotFoundError:
sys.exit(f"Missing required extension manifest: {manifest_path}")
except json.JSONDecodeError as error:
sys.exit(f"Invalid JSON in {manifest_path}: {error}")

required_fields = ("manifest_version", "name", "version")
missing_fields = [field for field in required_fields if field not in manifest]
if missing_fields:
missing = ", ".join(missing_fields)
sys.exit(f"Extension manifest is missing required field(s): {missing}")

return manifest


def make_zip_info(path):
archive_name = path.relative_to(EXTENSION_ROOT).as_posix()
info = ZipInfo(archive_name, ZIP_TIMESTAMP)
info.compress_type = ZIP_DEFLATED
info.external_attr = 0o644 << 16
return info


def package_qrx(output_path):
manifest = load_manifest()
files = list(iter_extension_files())
if not files:
sys.exit(f"No files found to package in {EXTENSION_ROOT}")

output_path.parent.mkdir(parents=True, exist_ok=True)
with ZipFile(output_path, "w") as archive:
for path in files:
archive.writestr(make_zip_info(path), path.read_bytes())

digest = hashlib.sha256(output_path.read_bytes()).hexdigest()
print(f"Wrote {output_path}")
print(f"Packaged {len(files)} files for {manifest['name']} {manifest['version']}")
print(f"SHA-256: {digest}")


def main():
parser = argparse.ArgumentParser(
description="Package the Quartz Ad Blocker extension as a single .qrx file."
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=DEFAULT_OUTPUT,
help=f"Destination .qrx path. Defaults to {DEFAULT_OUTPUT}",
)
args = parser.parse_args()

package_qrx(args.output.resolve())


if __name__ == "__main__":
main()