diff --git a/packages/adblocker/src/engine/domains.ts b/packages/adblocker/src/engine/domains.ts index f207d7c0c7..54617a3ccb 100644 --- a/packages/adblocker/src/engine/domains.ts +++ b/packages/adblocker/src/engine/domains.ts @@ -14,7 +14,11 @@ import { binLookup, hasUnicode, HASH_INTERNAL_MULT } from '../utils.js'; export class Domains { public static parse( value: string, - { delimiter = ',', debug = false }: { delimiter?: string; debug?: boolean } = {}, + { + delimiter = ',', + debug = false, + negate = false, + }: { delimiter?: string; debug?: boolean; negate?: boolean } = {}, ): Domains | undefined { const parts = value.split(delimiter); @@ -32,8 +36,11 @@ export class Domains { const notEntities: number[] = []; const hostnames: number[] = []; const notHostnames: number[] = []; + const rawParts: string[] = []; + + for (const rawHostname of parts) { + let hostname = rawHostname; - for (let hostname of parts) { if (hasUnicode(hostname)) { hostname = toASCII(hostname); } @@ -50,18 +57,25 @@ export class Domains { negation === true || entity === true ? hostname.slice(start, end) : hostname, ); - if (negation) { + // If conditionally negated value of `negation` by `negate` is `1` + if (+negation ^ +negate) { if (entity) { notEntities.push(hash); } else { notHostnames.push(hash); } + if (debug) { + rawParts.push(negation ? rawHostname : `~${rawHostname}`); + } } else { if (entity) { entities.push(hash); } else { hostnames.push(hash); } + if (debug) { + rawParts.push(negation ? rawHostname.slice(1) : rawHostname); + } } } @@ -70,7 +84,7 @@ export class Domains { hostnames: hostnames.length !== 0 ? new Uint32Array(hostnames).sort() : undefined, notEntities: notEntities.length !== 0 ? new Uint32Array(notEntities).sort() : undefined, notHostnames: notHostnames.length !== 0 ? new Uint32Array(notHostnames).sort() : undefined, - parts: debug === true ? parts.join(delimiter) : undefined, + parts: debug === true ? rawParts.join(delimiter) : undefined, }); } diff --git a/packages/adblocker/src/filters/network.ts b/packages/adblocker/src/filters/network.ts index fba70e2530..f1a31188f2 100644 --- a/packages/adblocker/src/filters/network.ts +++ b/packages/adblocker/src/filters/network.ts @@ -731,8 +731,13 @@ export default class NetworkFilter implements IFilter { const value = rawOption[1]; switch (option) { + case 'to': case 'denyallow': { - denyallow = Domains.parse(value, { delimiter: '|', debug }); + denyallow = Domains.parse(value, { + delimiter: '|', + debug, + negate: option === 'to', + }); if (denyallow === undefined) { return null; } diff --git a/packages/adblocker/test/matching.test.ts b/packages/adblocker/test/matching.test.ts index 5d5c283ff9..73e709bbfc 100644 --- a/packages/adblocker/test/matching.test.ts +++ b/packages/adblocker/test/matching.test.ts @@ -396,6 +396,23 @@ describe('#NetworkFilter.match', () => { url: 'https://sub.y.com/bar', type: 'script', }); + + // to + expect(f`*$3p,from=a.com,to=b.com`).to.matchRequest({ + sourceUrl: 'https://a.com', + url: 'https://b.com/bar', + type: 'script', + }); + expect(f`*$frame,3p,from=a.com|b.com,to=~c.com`).to.not.matchRequest({ + sourceUrl: 'https://a.com', + url: 'https://c.com/bar', + type: 'sub_frame', + }); + expect(f`$frame,csp=non-relevant,to=~safe.com,from=c.com|d.com`).to.not.matchRequest({ + sourceUrl: 'https://c.com', + url: 'https://safe.com/foo', + type: 'sub_frame', + }); }); }); diff --git a/packages/adblocker/test/parsing.test.ts b/packages/adblocker/test/parsing.test.ts index fbcfcb999f..ab3afe0f1e 100644 --- a/packages/adblocker/test/parsing.test.ts +++ b/packages/adblocker/test/parsing.test.ts @@ -806,6 +806,29 @@ describe('Network filters', () => { denyallow: undefined, }); }); + + context('to', () => { + it('reverses domains condition', () => { + network('||foo.com$to=foo.com|~bar.com,denyallow=bar.com|~foo.com', { + denyallow: { + hostnames: h(['bar.com']), + entities: undefined, + notHostnames: h(['foo.com']), + notEntities: undefined, + parts: undefined, + }, + }); + network('||foo.com$to=bar.com|baz.com', { + denyallow: { + hostnames: undefined, + entities: undefined, + notHostnames: h(['bar.com', 'baz.com']), + notEntities: undefined, + parts: undefined, + }, + }); + }); + }); }); describe('redirect', () => {