Skip to content

Commit

Permalink
feat: respect aliases in scriptlet exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
seia-soto committed Jul 29, 2024
1 parent b248842 commit 5301b35
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 32 deletions.
9 changes: 7 additions & 2 deletions packages/adblocker/src/engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,10 @@ export default class FilterEngine extends EventEmitter<EngineEventHandlers> {
) {
injectionsDisabled = true;
}
unhideExceptions.set(unhide.getSelector(), unhide);
unhideExceptions.set(
unhide.getNormalizedScriptInjectionSelector(this.resources.js) ?? unhide.getSelector(),
unhide,
);
}

const injections: CosmeticFilter[] = [];
Expand All @@ -894,7 +897,9 @@ export default class FilterEngine extends EventEmitter<EngineEventHandlers> {
// 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;
Expand Down
28 changes: 25 additions & 3 deletions packages/adblocker/src/filters/cosmetic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;';
Expand Down Expand Up @@ -774,16 +775,17 @@ export default class CosmeticFilter implements IFilter {
return { name: parts[0], args };
}

public getScript(js: Map<string, string>): string | undefined {
public getScript(js: Map<string, Resource>): string | undefined {
const parsed = this.parseScript();
if (parsed === undefined) {
return undefined;
}

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, '\\$&');
Expand All @@ -796,6 +798,26 @@ export default class CosmeticFilter implements IFilter {
return undefined;
}

public getNormalizedScriptInjectionSelector(js: Map<string, Resource>): 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;
}
Expand Down
111 changes: 84 additions & 27 deletions packages/adblocker/src/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -41,17 +41,42 @@ export default class Resources {
const resources: Map<string, Resource> = 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<string, string> = new Map();
resources.forEach(({ contentType, body }, name) => {
if (contentType === 'application/javascript') {
js.set(name, body);
const js: Map<string, Resource> = new Map();
resources.forEach((resource, name) => {
if (resource.contentType === 'application/javascript') {
js.set(name, resource);
}
});

Expand All @@ -63,7 +88,7 @@ export default class Resources {
}

public static parse(data: string, { checksum }: { checksum: string }): Resources {
const typeToResource: Map<string, Map<string, string>> = new Map();
const typeToResource: Map<string, Map<string, { body: string; aliasOf?: string }>> = new Map();
const trimComments = (str: string) => str.replace(/^\s*#.*$/gm, '');
const chunks = data.split('\n\n');

Expand All @@ -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) {
Expand All @@ -85,27 +113,47 @@ 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<string, string> = typeToResource.get('application/javascript') || new Map();
const js: Map<string, Resource> = 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,
});
}
}

// Create a mapping from resource name to { contentType, data }
// used for request redirection.
const resourcesByName: Map<string, Resource> = 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,
});
}
});
});

Expand All @@ -117,7 +165,7 @@ export default class Resources {
}

public readonly checksum: string;
public readonly js: Map<string, string>;
public readonly js: Map<string, Resource>;
public readonly resources: Map<string, Resource>;

constructor({ checksum = '', js = new Map(), resources = new Map() }: Partial<Resources> = {}) {
Expand All @@ -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;
Expand All @@ -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);
}
});
}
}

0 comments on commit 5301b35

Please sign in to comment.