Created
February 9, 2024 07:55
-
-
Save seia-soto/78c9e85c006702c2a3f2057183dd7f5a to your computer and use it in GitHub Desktop.
Preprocessor design 2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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