Skip to content

Instantly share code, notes, and snippets.

@seia-soto
Created February 9, 2024 07:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seia-soto/78c9e85c006702c2a3f2057183dd7f5a to your computer and use it in GitHub Desktop.
Save seia-soto/78c9e85c006702c2a3f2057183dd7f5a to your computer and use it in GitHub Desktop.
Preprocessor design 2
import { StaticDataView, sizeOfUTF8 } from './data-view';
import { fastStartsWith } from './utils';
export type EnvKeys =
| 'ext_ghostery'
| 'ext_abp'
| 'ext_ublock'
| 'ext_ubol'
| 'ext_devbuild'
| 'env_chromium'
| 'env_edge'
| 'env_firefox'
| 'env_mobile'
| 'env_safari'
| 'env_mv3'
| 'false'
| 'cap_html_filtering'
| 'cap_user_stylesheet'
| 'adguard'
| 'adguard_app_android'
| 'adguard_app_ios'
| 'adguard_app_mac'
| 'adguard_app_windows'
| 'adguard_ext_android_cb'
| 'adguard_ext_chromium'
| 'adguard_ext_edge'
| 'adguard_ext_firefox'
| 'adguard_ext_opera'
| 'adguard_ext_safari'
| (string & {});
export type Env = Map<EnvKeys, boolean>;
export const enum PreprocessorTypes {
INVALID = 0,
BEGIF = 1,
ELSE = 2,
ENDIF = 3,
}
export function detectPreprocessor(line: string) {
if (line.charCodeAt(1) !== 35 /* '#' */) {
return PreprocessorTypes.INVALID;
}
const command = line.slice(2);
if (fastStartsWith(command, 'if ')) {
return PreprocessorTypes.BEGIF;
}
if (command === 'else') {
return PreprocessorTypes.ELSE;
}
if (command === 'endif') {
return PreprocessorTypes.ENDIF;
}
return PreprocessorTypes.INVALID;
}
const operatorPattern = /(\|\||&&)/g;
const identifierPattern = /^(!?[a-z0-9_]+)$/;
const tokenize = (expression: string) => expression.split(operatorPattern);
const matchIdentifier = (identifier: string) => identifier.match(identifierPattern);
const evaluate = (expression: string, env: Env) => {
const tokens = tokenize(expression);
let result = false;
let isContinuedByAndOperator = false;
for (let i = 0; i < tokens.length; i++) {
if (i % 2) {
if (tokens[i][0] === '|') {
isContinuedByAndOperator = false;
} else if (tokens[i][0] === '&') {
isContinuedByAndOperator = true;
} else {
// Invalid expression
return false;
}
} else {
const match = matchIdentifier(tokens[i]);
if (!match) {
// Invalid expression
return false;
}
let identifier = match[1];
const isNegated = identifier.charCodeAt(0) === 33; /* '!' */
let isPositive = false;
if (isNegated) {
identifier = identifier.slice(1);
isPositive = !env.get(identifier);
} else {
isPositive = !!env.get(identifier);
}
if (isContinuedByAndOperator) {
result &&= isPositive;
} else {
result ||= isPositive;
}
}
}
return result;
};
export default class Preprocessor {
public static parse(line: string): Preprocessor | null {
return new this({
condition: line.slice(5 /* '!#if '.length */).replace(/ */g, ''),
});
}
public static deserialize(view: StaticDataView): Preprocessor {
const condition = view.getUTF8();
const negatives = new Set<number>();
for (let i = 0, l = view.getUint32(); i < l; i++) {
negatives.add(view.getUint32());
}
return new this({
condition,
negatives,
});
}
public readonly condition: string;
public readonly negatives: Set<number>;
public result: boolean | undefined;
constructor({
condition,
negatives = new Set(),
}: {
condition: string;
negatives?: Set<number>;
}) {
this.condition = condition;
this.negatives = negatives;
}
evaluate(env: Env) {
if (!this.result) {
this.result = evaluate(this.condition, env);
}
return this.result;
}
flush() {
this.result = undefined;
}
isEnvQualifiedFilter(env: Env, filter: number) {
if (this.negatives.has(filter)) {
return !this.evaluate(env);
}
return this.evaluate(env);
}
serialize(view: StaticDataView) {
view.pushUTF8(this.condition);
view.pushUint32(this.negatives.size);
for (const filter of this.negatives) {
view.pushUint32(filter);
}
}
getSerializedSize() {
let estimatedSize = sizeOfUTF8(this.condition);
estimatedSize += (1 + this.negatives.size) * 4;
return estimatedSize;
}
}
import Preprocessor, { Env } from '../../preprocessor';
export type PreprocessorDiff = Map<number, Preprocessor[]>;
export default class PreprocessorBucket {
public readonly conditions: Map<string, Preprocessor>;
public readonly disabled: Set<number>;
public readonly bindings: Map<number, Preprocessor[]>;
public env: Env;
constructor({
env,
disabled = new Set(),
bindings = new Map(),
}: {
env: Env;
disabled?: Set<number>;
bindings?: Map<number, Preprocessor[]>;
}) {
this.env = env;
this.disabled = disabled;
this.bindings = bindings;
// Build conditions bindings
this.conditions = new Map();
for (const preprocessors of this.bindings.values()) {
for (const preprocessor of preprocessors) {
if (!this.conditions.has(preprocessor.condition)) {
this.conditions.set(preprocessor.condition, preprocessor);
}
}
}
}
public update({ added, removed }: { added?: PreprocessorDiff; removed?: PreprocessorDiff }) {
if (added) {
for (const [filter, preprocessors] of added.entries()) {
let bindings: Preprocessor[] = [];
if (!this.bindings.has(filter)) {
this.bindings.set(filter, bindings);
} else {
bindings = this.bindings.get(filter)!;
}
let enabled = true;
// Find missing preprocessors and update `this.conditions`
for (const preprocessor of preprocessors) {
let local = this.conditions.get(preprocessor.condition);
if (!local) {
local = preprocessor;
this.conditions.set(local.condition, local);
}
bindings.push(local);
// Preprocesor.isEnvQualifiedFilter checks if the filter is in `else` block
if (enabled && !local.isEnvQualifiedFilter(this.env, filter)) {
enabled = false;
}
}
if (enabled && this.disabled.has(filter)) {
this.disabled.delete(filter);
} else {
this.disabled.add(filter);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment