From 5301b355a8cd4d6dc8bd305fef630dc554c4d06a Mon Sep 17 00:00:00 2001 From: HoJeong Go Date: Tue, 30 Jul 2024 04:54:08 +0900 Subject: [PATCH] feat: respect aliases in scriptlet exceptions fixes https://github.com/ghostery/adblocker/issues/4058 --- packages/adblocker/src/engine/engine.ts | 9 +- packages/adblocker/src/filters/cosmetic.ts | 28 +++++- packages/adblocker/src/resources.ts | 111 ++++++++++++++++----- 3 files changed, 116 insertions(+), 32 deletions(-) diff --git a/packages/adblocker/src/engine/engine.ts b/packages/adblocker/src/engine/engine.ts index 95bd977609..dcc1e74385 100644 --- a/packages/adblocker/src/engine/engine.ts +++ b/packages/adblocker/src/engine/engine.ts @@ -883,7 +883,10 @@ export default class FilterEngine extends EventEmitter { ) { injectionsDisabled = true; } - unhideExceptions.set(unhide.getSelector(), unhide); + unhideExceptions.set( + unhide.getNormalizedScriptInjectionSelector(this.resources.js) ?? unhide.getSelector(), + unhide, + ); } const injections: CosmeticFilter[] = []; @@ -894,7 +897,9 @@ export default class FilterEngine extends EventEmitter { // Apply unhide rules + dispatch for (const filter of filters) { // Make sure `rule` is not un-hidden by a #@# filter - const exception = unhideExceptions.get(filter.getSelector()); + const exception = unhideExceptions.get( + filter.getNormalizedScriptInjectionSelector(this.resources.js) ?? filter.getSelector(), + ); if (exception !== undefined) { continue; diff --git a/packages/adblocker/src/filters/cosmetic.ts b/packages/adblocker/src/filters/cosmetic.ts index cf11eaef62..94c9af330f 100644 --- a/packages/adblocker/src/filters/cosmetic.ts +++ b/packages/adblocker/src/filters/cosmetic.ts @@ -39,6 +39,7 @@ import { } from '../utils.js'; import IFilter from './interface.js'; import { HTMLSelector, extractHTMLSelectorFromRule } from '../html-filtering.js'; +import { Resource } from '../resources.js'; const EMPTY_TOKENS: [Uint32Array] = [EMPTY_UINT32_ARRAY]; export const DEFAULT_HIDDING_STYLE: string = 'display: none !important;'; @@ -774,7 +775,7 @@ export default class CosmeticFilter implements IFilter { return { name: parts[0], args }; } - public getScript(js: Map): string | undefined { + public getScript(js: Map): string | undefined { const parsed = this.parseScript(); if (parsed === undefined) { return undefined; @@ -782,8 +783,9 @@ export default class CosmeticFilter implements IFilter { const { name, args } = parsed; - let script = js.get(name); - if (script !== undefined) { + const resource = js.get(name); + if (resource !== undefined) { + let script = resource.body; for (let i = 0; i < args.length; i += 1) { // escape some characters so they wont get evaluated with escape characters during script injection const arg = args[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -796,6 +798,26 @@ export default class CosmeticFilter implements IFilter { return undefined; } + public getNormalizedScriptInjectionSelector(js: Map): string | undefined { + if (this.isScriptInject() === false) { + return undefined; + } + + const selector = this.getSelector(); + + const firstCommaIndex = selector.indexOf(','); + if (firstCommaIndex === -1) { + return undefined; + } + + const originResourceName = js.get(selector.slice(1, firstCommaIndex))?.aliasOf; + if (originResourceName === undefined) { + return undefined; + } + + return '(' + originResourceName + selector.slice(firstCommaIndex); + } + public hasHostnameConstraint(): boolean { return this.domains !== undefined; } diff --git a/packages/adblocker/src/resources.ts b/packages/adblocker/src/resources.ts index 641da98975..23670313d0 100644 --- a/packages/adblocker/src/resources.ts +++ b/packages/adblocker/src/resources.ts @@ -20,12 +20,12 @@ function btoaPolyfill(buffer: string): string { return buffer; } -interface Resource { +export interface Resource { contentType: string; body: string; + aliasOf?: string; } -// TODO - support # alias // TODO - support empty resource body /** @@ -41,17 +41,42 @@ export default class Resources { const resources: Map = new Map(); const numberOfResources = buffer.getUint16(); for (let i = 0; i < numberOfResources; i += 1) { - resources.set(buffer.getASCII(), { - contentType: buffer.getASCII(), - body: buffer.getUTF8(), - }); + const name = buffer.getASCII(); + const isAlias = buffer.getBool(); + if (isAlias === true) { + resources.set(name, { + contentType: buffer.getASCII(), + body: buffer.getUTF8(), + }); + } else { + resources.set(name, { + contentType: '', + body: '', + aliasOf: buffer.getASCII(), + }); + } + } + + // Fill aliases after deserializing everything + for (const resource of resources.values()) { + if (resource.aliasOf === undefined) { + continue; + } + + const origin = resources.get(resource.aliasOf); + if (origin === undefined) { + continue; + } + + resource.body = origin.body; + resource.contentType = origin.contentType; } // Deserialize `js` - const js: Map = new Map(); - resources.forEach(({ contentType, body }, name) => { - if (contentType === 'application/javascript') { - js.set(name, body); + const js: Map = new Map(); + resources.forEach((resource, name) => { + if (resource.contentType === 'application/javascript') { + js.set(name, resource); } }); @@ -63,7 +88,7 @@ export default class Resources { } public static parse(data: string, { checksum }: { checksum: string }): Resources { - const typeToResource: Map> = new Map(); + const typeToResource: Map> = new Map(); const trimComments = (str: string) => str.replace(/^\s*#.*$/gm, ''); const chunks = data.split('\n\n'); @@ -72,8 +97,11 @@ export default class Resources { if (resource.length !== 0) { const firstNewLine = resource.indexOf('\n'); const split = resource.slice(0, firstNewLine).split(/\s+/); - const name = split[0]; - const type = split[1]; + const [name, type] = split; + const aliases = (split[2] || '') + .split(',') + .map((alias) => alias.trim()) + .filter((alias) => alias.length !== 0); const body = resource.slice(firstNewLine + 1); if (name === undefined || type === undefined || body === undefined) { @@ -85,15 +113,27 @@ export default class Resources { resources = new Map(); typeToResource.set(type, resources); } - resources.set(name, body); + resources.set(name, { + body, + }); + for (const alias of aliases) { + resources.set(alias, { + body, + aliasOf: name, + }); + } } } // The resource containing javascirpts to be injected - const js: Map = typeToResource.get('application/javascript') || new Map(); + const js: Map = typeToResource.get('application/javascript') || new Map(); for (const [key, value] of js.entries()) { if (key.endsWith('.js')) { - js.set(key.slice(0, -3), value); + js.set(key.slice(0, -3), { + contentType: value.contentType, + body: value.body, + aliasOf: key, + }); } } @@ -101,11 +141,19 @@ export default class Resources { // used for request redirection. const resourcesByName: Map = new Map(); typeToResource.forEach((resources, contentType) => { - resources.forEach((resource: string, name: string) => { - resourcesByName.set(name, { - contentType, - body: resource, - }); + resources.forEach((resource, name) => { + if (resource.aliasOf === undefined) { + resourcesByName.set(name, { + contentType, + body: resource.body, + }); + } else { + resourcesByName.set(name, { + contentType, + body: resource.body, + aliasOf: resource.aliasOf, + }); + } }); }); @@ -117,7 +165,7 @@ export default class Resources { } public readonly checksum: string; - public readonly js: Map; + public readonly js: Map; public readonly resources: Map; constructor({ checksum = '', js = new Map(), resources = new Map() }: Partial = {}) { @@ -142,8 +190,13 @@ export default class Resources { public getSerializedSize(): number { let estimatedSize = sizeOfASCII(this.checksum) + 2 * sizeOfByte(); // resources.size - this.resources.forEach(({ contentType, body }, name) => { - estimatedSize += sizeOfASCII(name) + sizeOfASCII(contentType) + sizeOfUTF8(body); + this.resources.forEach(({ contentType, body, aliasOf }, name) => { + estimatedSize += sizeOfASCII(name); + if (aliasOf === undefined) { + estimatedSize += sizeOfASCII(contentType) + sizeOfUTF8(body); + } else { + estimatedSize += sizeOfASCII(aliasOf); + } }); return estimatedSize; @@ -155,10 +208,14 @@ export default class Resources { // Serialize `resources` buffer.pushUint16(this.resources.size); - this.resources.forEach(({ contentType, body }, name) => { + this.resources.forEach(({ contentType, body, aliasOf }, name) => { buffer.pushASCII(name); - buffer.pushASCII(contentType); - buffer.pushUTF8(body); + if (aliasOf === undefined) { + buffer.pushASCII(contentType); + buffer.pushUTF8(body); + } else { + buffer.pushASCII(aliasOf); + } }); } }