From 79e73e1236e1be8dfcc1d7af416b703a9aa2be3a Mon Sep 17 00:00:00 2001 From: Krzysztof Modras Date: Tue, 21 Apr 2026 21:08:15 +0200 Subject: [PATCH 1/4] feat(safari): strip DNR rules WebKit cannot compile Safari silently drops rules whose urlFilter/regexFilter the WebKit URL filter parser rejects, so any pattern using features like disjunctions, character classes, or arbitrary atom repetitions is a no-op for Safari users. The Xcode build now runs the validator from ghostery/WebKit against every ruleset and removes the rejected rules before packaging, so the shipped lists match what Safari will actually enforce. The validator binary is downloaded on demand by a postinstall hook from the latest ghostery/WebKit release and kept out of the repo. --- .gitignore | 3 + package.json | 1 + scripts/download-validate-dnr-rules.js | 56 +++++++++++++++++ scripts/filter-invalid-dnr-rules.js | 84 ++++++++++++++++++++++++++ xcode/ci_scripts/build.sh | 3 + 5 files changed, 147 insertions(+) create mode 100644 scripts/download-validate-dnr-rules.js create mode 100644 scripts/filter-invalid-dnr-rules.js diff --git a/.gitignore b/.gitignore index f2d165d067..9c138e1bab 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ npm-debug.log src/rule_resources src/static_pages +# Downloaded binaries +scripts/bin/ + # Build dist/ web-ext-artifacts/ diff --git a/package.json b/package.json index d9f1b8ce92..254158a298 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "10.5.39", "type": "module", "scripts": { + "postinstall": "node scripts/download-validate-dnr-rules.js", "build": "node scripts/build.js", "start": "npm run build -- --watch", "start:update": "./scripts/update.sh", diff --git a/scripts/download-validate-dnr-rules.js b/scripts/download-validate-dnr-rules.js new file mode 100644 index 0000000000..eba86bb2e7 --- /dev/null +++ b/scripts/download-validate-dnr-rules.js @@ -0,0 +1,56 @@ +/** + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2017-present Ghostery GmbH. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PLATFORM_MAP = { + 'darwin-arm64': 'macos-arm64', + 'darwin-x64': 'macos-arm64', + 'linux-x64': 'linux-x64', +}; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const binDir = resolve(__dirname, 'bin'); +const binPath = resolve(binDir, 'validate-dnr-rules'); + +const key = `${process.platform}-${process.arch}`; +const suffix = PLATFORM_MAP[key]; + +if (!suffix) { + console.log(`[validate-dnr-rules] Skipping download: no binary available for ${key}.`); + process.exit(0); +} + +if (existsSync(binPath)) { + process.exit(0); +} + +const url = `https://github.com/ghostery/WebKit/releases/latest/download/validate-dnr-rules-${suffix}`; + +try { + console.log(`[validate-dnr-rules] Downloading ${url}`); + const res = await fetch(url, { redirect: 'follow' }); + if (!res.ok) { + throw new Error(`HTTP ${res.status} ${res.statusText}`); + } + const buf = Buffer.from(await res.arrayBuffer()); + mkdirSync(binDir, { recursive: true }); + writeFileSync(binPath, buf); + chmodSync(binPath, 0o755); + console.log(`[validate-dnr-rules] Saved to ${binPath}`); +} catch (err) { + console.warn( + `[validate-dnr-rules] Download failed: ${err.message}. Safari builds will not filter invalid DNR rules until this succeeds.`, + ); + process.exit(0); +} diff --git a/scripts/filter-invalid-dnr-rules.js b/scripts/filter-invalid-dnr-rules.js new file mode 100644 index 0000000000..9dfeb073fe --- /dev/null +++ b/scripts/filter-invalid-dnr-rules.js @@ -0,0 +1,84 @@ +/** + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2017-present Ghostery GmbH. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const binPath = resolve(__dirname, 'bin', 'validate-dnr-rules'); +const rulesDir = resolve(process.cwd(), 'dist', 'rule_resources'); + +if (!existsSync(binPath)) { + console.error( + `[filter-invalid-dnr-rules] Validator binary not found at ${binPath}.\n` + + `Run 'npm install' to download it from https://github.com/ghostery/WebKit/releases.`, + ); + process.exit(1); +} + +if (!existsSync(rulesDir)) { + console.error(`[filter-invalid-dnr-rules] Rules directory not found: ${rulesDir}`); + process.exit(1); +} + +const files = readdirSync(rulesDir) + .filter((f) => f.startsWith('dnr-') && f.endsWith('.json') && !f.endsWith('.metadata.json')) + .map((f) => join(rulesDir, f)); + +const ERROR_RE = /^\s*ERROR: Rule (-?\d+)/gm; + +let totalRemoved = 0; + +for (const file of files) { + const result = spawnSync(binPath, [file], { + encoding: 'utf8', + maxBuffer: 256 * 1024 * 1024, + }); + + if (result.error) { + console.error(`[filter-invalid-dnr-rules] Failed to spawn validator: ${result.error.message}`); + process.exit(1); + } + + if (result.status === 0) { + continue; + } + + const invalidIds = new Set(); + for (const m of result.stdout.matchAll(ERROR_RE)) { + invalidIds.add(Number(m[1])); + } + + if (invalidIds.size === 0) { + console.error( + `[filter-invalid-dnr-rules] Validator failed for ${file} but produced no parseable errors:\n${result.stdout}${result.stderr}`, + ); + process.exit(1); + } + + const rules = JSON.parse(readFileSync(file, 'utf8')); + const filtered = rules.filter((r) => !invalidIds.has(r.id)); + const removed = rules.length - filtered.length; + + writeFileSync(file, JSON.stringify(filtered)); + totalRemoved += removed; + + const rel = file.replace(process.cwd() + '/', ''); + console.log( + `[filter-invalid-dnr-rules] ${rel}: removed ${removed}/${rules.length} invalid rule(s)`, + ); +} + +console.log( + `[filter-invalid-dnr-rules] Removed ${totalRemoved} rule(s) across ${files.length} ruleset(s).`, +); diff --git a/xcode/ci_scripts/build.sh b/xcode/ci_scripts/build.sh index bb96d5deba..b77b3829e9 100755 --- a/xcode/ci_scripts/build.sh +++ b/xcode/ci_scripts/build.sh @@ -12,6 +12,9 @@ test -f /usr/local/opt/asdf/libexec/asdf.sh && . /usr/local/opt/asdf/libexec/asd # run build script npm run build -- --clean +# strip DNR rules that WebKit's URL filter parser cannot compile +node scripts/filter-invalid-dnr-rules.js + # rewrite manifest background.service_worker into background.scripts/persistent node -e ' const fs = require("fs"); From 68b845ec20cccc3041861b96041de94643fce2d1 Mon Sep 17 00:00:00 2001 From: Krzysztof Modras Date: Tue, 21 Apr 2026 21:23:31 +0200 Subject: [PATCH 2/4] refactor(safari): use import.meta.dirname in dnr validator scripts --- scripts/download-validate-dnr-rules.js | 6 ++---- scripts/filter-invalid-dnr-rules.js | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/download-validate-dnr-rules.js b/scripts/download-validate-dnr-rules.js index eba86bb2e7..ffe5d90817 100644 --- a/scripts/download-validate-dnr-rules.js +++ b/scripts/download-validate-dnr-rules.js @@ -10,8 +10,7 @@ */ import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { resolve } from 'node:path'; const PLATFORM_MAP = { 'darwin-arm64': 'macos-arm64', @@ -19,8 +18,7 @@ const PLATFORM_MAP = { 'linux-x64': 'linux-x64', }; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const binDir = resolve(__dirname, 'bin'); +const binDir = resolve(import.meta.dirname, 'bin'); const binPath = resolve(binDir, 'validate-dnr-rules'); const key = `${process.platform}-${process.arch}`; diff --git a/scripts/filter-invalid-dnr-rules.js b/scripts/filter-invalid-dnr-rules.js index 9dfeb073fe..f397743593 100644 --- a/scripts/filter-invalid-dnr-rules.js +++ b/scripts/filter-invalid-dnr-rules.js @@ -10,12 +10,10 @@ */ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { join, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const binPath = resolve(__dirname, 'bin', 'validate-dnr-rules'); +const binPath = resolve(import.meta.dirname, 'bin', 'validate-dnr-rules'); const rulesDir = resolve(process.cwd(), 'dist', 'rule_resources'); if (!existsSync(binPath)) { From f90a02c0cd00180b8f417ab07284b04e0f61ecac Mon Sep 17 00:00:00 2001 From: Krzysztof Modras Date: Tue, 21 Apr 2026 21:29:40 +0200 Subject: [PATCH 3/4] refactor(safari): align dnr validator scripts with repo patterns --- scripts/download-validate-dnr-rules.js | 8 ++++---- scripts/filter-invalid-dnr-rules.js | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/download-validate-dnr-rules.js b/scripts/download-validate-dnr-rules.js index ffe5d90817..edd28d481e 100644 --- a/scripts/download-validate-dnr-rules.js +++ b/scripts/download-validate-dnr-rules.js @@ -35,15 +35,15 @@ if (existsSync(binPath)) { const url = `https://github.com/ghostery/WebKit/releases/latest/download/validate-dnr-rules-${suffix}`; +console.log(`[validate-dnr-rules] Downloading ${url}`); + try { - console.log(`[validate-dnr-rules] Downloading ${url}`); - const res = await fetch(url, { redirect: 'follow' }); + const res = await fetch(url); if (!res.ok) { throw new Error(`HTTP ${res.status} ${res.statusText}`); } - const buf = Buffer.from(await res.arrayBuffer()); mkdirSync(binDir, { recursive: true }); - writeFileSync(binPath, buf); + writeFileSync(binPath, new Uint8Array(await res.arrayBuffer())); chmodSync(binPath, 0o755); console.log(`[validate-dnr-rules] Saved to ${binPath}`); } catch (err) { diff --git a/scripts/filter-invalid-dnr-rules.js b/scripts/filter-invalid-dnr-rules.js index f397743593..4b40d8978d 100644 --- a/scripts/filter-invalid-dnr-rules.js +++ b/scripts/filter-invalid-dnr-rules.js @@ -10,7 +10,7 @@ */ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { join, resolve } from 'node:path'; +import { join, relative, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; const binPath = resolve(import.meta.dirname, 'bin', 'validate-dnr-rules'); @@ -71,9 +71,8 @@ for (const file of files) { writeFileSync(file, JSON.stringify(filtered)); totalRemoved += removed; - const rel = file.replace(process.cwd() + '/', ''); console.log( - `[filter-invalid-dnr-rules] ${rel}: removed ${removed}/${rules.length} invalid rule(s)`, + `[filter-invalid-dnr-rules] ${relative(process.cwd(), file)}: removed ${removed}/${rules.length} invalid rule(s)`, ); } From c088bc3bccf3311cf2723188086a50aefd8c4d76 Mon Sep 17 00:00:00 2001 From: Krzysztof Modras Date: Wed, 22 Apr 2026 10:02:51 +0200 Subject: [PATCH 4/4] refactor(safari): download dnr validator on demand, drop postinstall hook --- package.json | 1 - scripts/download-validate-dnr-rules.js | 54 -------------------------- scripts/filter-invalid-dnr-rules.js | 41 +++++++++++++++---- 3 files changed, 34 insertions(+), 62 deletions(-) delete mode 100644 scripts/download-validate-dnr-rules.js diff --git a/package.json b/package.json index 254158a298..d9f1b8ce92 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "version": "10.5.39", "type": "module", "scripts": { - "postinstall": "node scripts/download-validate-dnr-rules.js", "build": "node scripts/build.js", "start": "npm run build -- --watch", "start:update": "./scripts/update.sh", diff --git a/scripts/download-validate-dnr-rules.js b/scripts/download-validate-dnr-rules.js deleted file mode 100644 index edd28d481e..0000000000 --- a/scripts/download-validate-dnr-rules.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2017-present Ghostery GmbH. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'; -import { resolve } from 'node:path'; - -const PLATFORM_MAP = { - 'darwin-arm64': 'macos-arm64', - 'darwin-x64': 'macos-arm64', - 'linux-x64': 'linux-x64', -}; - -const binDir = resolve(import.meta.dirname, 'bin'); -const binPath = resolve(binDir, 'validate-dnr-rules'); - -const key = `${process.platform}-${process.arch}`; -const suffix = PLATFORM_MAP[key]; - -if (!suffix) { - console.log(`[validate-dnr-rules] Skipping download: no binary available for ${key}.`); - process.exit(0); -} - -if (existsSync(binPath)) { - process.exit(0); -} - -const url = `https://github.com/ghostery/WebKit/releases/latest/download/validate-dnr-rules-${suffix}`; - -console.log(`[validate-dnr-rules] Downloading ${url}`); - -try { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`HTTP ${res.status} ${res.statusText}`); - } - mkdirSync(binDir, { recursive: true }); - writeFileSync(binPath, new Uint8Array(await res.arrayBuffer())); - chmodSync(binPath, 0o755); - console.log(`[validate-dnr-rules] Saved to ${binPath}`); -} catch (err) { - console.warn( - `[validate-dnr-rules] Download failed: ${err.message}. Safari builds will not filter invalid DNR rules until this succeeds.`, - ); - process.exit(0); -} diff --git a/scripts/filter-invalid-dnr-rules.js b/scripts/filter-invalid-dnr-rules.js index 4b40d8978d..426cb4a972 100644 --- a/scripts/filter-invalid-dnr-rules.js +++ b/scripts/filter-invalid-dnr-rules.js @@ -9,19 +9,46 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { + chmodSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + writeFileSync, +} from 'node:fs'; import { join, relative, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; -const binPath = resolve(import.meta.dirname, 'bin', 'validate-dnr-rules'); +const PLATFORM_MAP = { + 'darwin-arm64': 'macos-arm64', + 'darwin-x64': 'macos-arm64', + 'linux-x64': 'linux-x64', +}; + +const binDir = resolve(import.meta.dirname, 'bin'); +const binPath = resolve(binDir, 'validate-dnr-rules'); const rulesDir = resolve(process.cwd(), 'dist', 'rule_resources'); if (!existsSync(binPath)) { - console.error( - `[filter-invalid-dnr-rules] Validator binary not found at ${binPath}.\n` + - `Run 'npm install' to download it from https://github.com/ghostery/WebKit/releases.`, - ); - process.exit(1); + const key = `${process.platform}-${process.arch}`; + const suffix = PLATFORM_MAP[key]; + + if (!suffix) { + console.error(`[filter-invalid-dnr-rules] No validator binary available for ${key}.`); + process.exit(1); + } + + const url = `https://github.com/ghostery/WebKit/releases/latest/download/validate-dnr-rules-${suffix}`; + console.log(`[filter-invalid-dnr-rules] Downloading ${url}`); + + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to download validator: ${res.status} ${res.statusText}`); + } + mkdirSync(binDir, { recursive: true }); + writeFileSync(binPath, new Uint8Array(await res.arrayBuffer())); + chmodSync(binPath, 0o755); } if (!existsSync(rulesDir)) {