@adguard/agtree
Version:
Tool set for working with adblock filter lists
175 lines (172 loc) • 8.07 kB
JavaScript
/*
* AGTree v3.2.2 (build date: Tue, 08 Jul 2025 13:39:47 GMT)
* (c) 2025 Adguard Software Ltd.
* Released under the MIT license
* https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
*/
import { AdblockSyntax } from '../utils/adblockers.js';
import { UNDERSCORE, NEWLINE, SPACE } from '../utils/constants.js';
import { VALIDATION_ERROR_PREFIX, SOURCE_DATA_ERROR_PREFIX, BLOCKER_PREFIX } from './constants.js';
import { isValidNoopModifier, getInvalidValidationResult, getValueRequiredValidationResult } from './helpers.js';
import { validateValue } from './value.js';
import { clone } from '../utils/clone.js';
import { modifiersCompatibilityTable } from '../compatibility-tables/modifiers.js';
import { GenericPlatform } from '../compatibility-tables/platforms.js';
/**
* @file Validator for modifiers.
*/
const convertSyntaxToGenericPlatform = (syntax) => {
switch (syntax) {
case AdblockSyntax.Adg:
return GenericPlatform.AdgAny;
case AdblockSyntax.Ubo:
return GenericPlatform.UboAny;
case AdblockSyntax.Abp:
return GenericPlatform.AbpAny;
default:
throw new Error(`Unknown syntax: ${syntax}`);
}
};
/**
* Fully checks whether the given `modifier` valid for given blocker `syntax`:
* is it supported by the blocker, deprecated, assignable, negatable, etc.
*
* @param syntax Adblock syntax to check the modifier for.
* 'Common' is not supported, it should be specific — 'AdGuard', 'uBlockOrigin', or 'AdblockPlus'.
* @param modifier Parsed modifier AST node.
* @param isException Whether the modifier is used in exception rule.
* Needed to check whether the modifier is allowed only in blocking or exception rules.
*
* @returns Result of modifier validation.
*/
const validateForSpecificSyntax = (syntax, modifier, isException) => {
if (syntax === AdblockSyntax.Common) {
throw new Error(`Syntax should be specific, '${AdblockSyntax.Common}' is not supported`);
}
const modifierName = modifier.name.value;
const blockerPrefix = BLOCKER_PREFIX[syntax];
if (!blockerPrefix) {
throw new Error(`Unknown syntax: ${syntax}`);
}
// needed for validation of negation, assignment, etc.
const specificBlockerData = modifiersCompatibilityTable.getFirst(modifierName, convertSyntaxToGenericPlatform(syntax));
// if no specific blocker data is found
if (!specificBlockerData) {
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NOT_SUPPORTED}: '${modifierName}'`);
}
// e.g. 'object-subrequest'
if (specificBlockerData.removed) {
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.REMOVED}: '${modifierName}'`);
}
if (specificBlockerData.deprecated) {
if (!specificBlockerData.deprecationMessage) {
throw new Error(`${SOURCE_DATA_ERROR_PREFIX.NO_DEPRECATION_MESSAGE}: '${modifierName}'`);
}
// prepare the message which is multiline in the yaml file
const warn = specificBlockerData.deprecationMessage.replace(NEWLINE, SPACE);
return {
valid: true,
warn,
};
}
if (specificBlockerData.blockOnly && isException) {
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.BLOCK_ONLY}: '${modifierName}'`);
}
if (specificBlockerData.exceptionOnly && !isException) {
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.EXCEPTION_ONLY}: '${modifierName}'`);
}
// e.g. '~domain=example.com'
if (!specificBlockerData.negatable && modifier.exception) {
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NOT_NEGATABLE_MODIFIER}: '${modifierName}'`);
}
// e.g. 'domain'
if (specificBlockerData.assignable) {
if (!modifier.value) {
// TODO: ditch value_optional after custom validators are implemented for value_format for all modifiers.
// This checking should be done in each separate custom validator,
// because $csp and $permissions without value can be used only in extension rules,
// but $cookie with no value can be used in both blocking and exception rules.
/**
* Some assignable modifiers can be used without a value,
* e.g. '@@||example.com^$cookie'.
*/
if (specificBlockerData.valueOptional) {
return { valid: true };
}
// for other assignable modifiers the value is required
return getValueRequiredValidationResult(modifierName);
}
/**
* TODO: consider to return `{ valid: true, warn: 'Modifier value may be specified' }` (???)
* for $stealth modifier without a value
* but only after the extension will support value for $stealth:
* https://github.com/AdguardTeam/AdguardBrowserExtension/issues/2107
*/
if (!specificBlockerData.valueFormat) {
throw new Error(`${SOURCE_DATA_ERROR_PREFIX.NO_VALUE_FORMAT_FOR_ASSIGNABLE}: '${modifierName}'`);
}
return validateValue(modifier, specificBlockerData.valueFormat, specificBlockerData.valueFormatFlags);
}
if (modifier?.value) {
// e.g. 'third-party=true'
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_FORBIDDEN}: '${modifierName}'`);
}
return { valid: true };
};
// TODO: move to modifier.ts and use index.ts only for exporting
/**
* Modifier validator class.
*/
class ModifierValidator {
/**
* Simply checks whether the modifier exists in any adblocker.
*
* **Deprecated** and **removed** modifiers are considered as **existent**.
*
* @param modifier Already parsed modifier AST node.
*
* @returns True if modifier exists, false otherwise.
*/
// eslint-disable-next-line class-methods-use-this
exists = (modifier) => {
return modifiersCompatibilityTable.existsAny(modifier.name.value);
};
/**
* Checks whether the given `modifier` is valid for specified `syntax`.
*
* For `Common` syntax it simply checks whether the modifier exists.
* For specific syntax the validation is more complex —
* deprecated, assignable, negatable and other requirements are checked.
*
* @param syntax Adblock syntax to check the modifier for.
* @param rawModifier Modifier AST node.
* @param isException Whether the modifier is used in exception rule, default to false.
* Needed to check whether the modifier is allowed only in blocking or exception rules.
*
* @returns Result of modifier validation.
*/
validate = (syntax, rawModifier, isException = false) => {
const modifier = clone(rawModifier);
// special case: handle noop modifier which may be used as multiple underscores (not just one)
// https://adguard.com/kb/general/ad-filtering/create-own-filters/#noop-modifier
if (modifier.name.value.startsWith(UNDERSCORE)) {
// check whether the modifier value contains something else besides underscores
if (!isValidNoopModifier(modifier.name.value)) {
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_NOOP}: '${modifier.name.value}'`);
}
// otherwise, replace the modifier value with single underscore.
// it is needed to check whether the modifier is supported by specific adblocker due to the syntax
modifier.name.value = UNDERSCORE;
}
if (!this.exists(modifier)) {
return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NOT_EXISTENT}: '${modifier.name.value}'`);
}
// for 'Common' syntax we cannot check something more
if (syntax === AdblockSyntax.Common) {
return { valid: true };
}
return validateForSpecificSyntax(syntax, modifier, isException);
};
}
const modifierValidator = new ModifierValidator();
export { modifierValidator };