Skip to content

Commit

Permalink
improve validator
Browse files Browse the repository at this point in the history
  • Loading branch information
slavaleleka committed Sep 19, 2024
1 parent 74eef05 commit a9ee6b6
Showing 1 changed file with 136 additions and 23 deletions.
159 changes: 136 additions & 23 deletions src/main/validator.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
const { RuleConverter, RuleParser, CommentRuleParser } = require('@adguard/agtree');
const {
RuleValidator,
RuleFactory,
CommentRuleParser,
CosmeticRuleType,
RuleConverter,
RuleParser,
RuleCategory,
RegExpUtils,
defaultParserOptions,
AnyRule,
} = require('@adguard/agtree');
const {
RuleFactory,
CosmeticRule,
NetworkRule,
} = require('@adguard/tsurlfilter');

const logger = require('./utils/log');
Expand All @@ -25,15 +33,104 @@ const excludeRule = (excluded, warning, rule) => {
}
};

/**
* @typedef {object} ValidationResult
* @property {boolean} valid Whether the rule is valid.
* @property {string|null} error Error message if the rule is invalid.
*/

/**
* Class to validate filter rules.
*/
class RuleValidator {
/**
* Creates validation result for rule.
*
* @param {boolean} valid Whether the rule is valid.
* @param {string} [error] Error message if the rule is invalid.
*/
static createValidationResult(valid, error) {
if (error) {
return { valid, error };
}

return { valid, error: null };
}

/**
* Validates regexp pattern.
*
* @param {string} pattern Regexp pattern to validate.
* @param {string} ruleText Rule text.
*
* @throws {SyntaxError} If the pattern is invalid, otherwise nothing.
*/
static validateRegexp(pattern, ruleText) {
if (!RegExpUtils.isRegexPattern(pattern)) {
return;
}

try {
new RegExp(pattern.slice(1, -1));
} catch (e) {
throw new SyntaxError(`Rule has invalid regex pattern: "${ruleText}"`);
}
}

/**
* Validates rule node.
*
* @param {AnyRule} ruleNode Rule node to validate.
*
* @returns {ValidationResult} Validation result.
*/
static validate(ruleNode) {
if (ruleNode.category === RuleCategory.Invalid) {
return RuleValidator.createValidationResult(
false,
ruleNode.error.message,
);
}

if (ruleNode.category === RuleCategory.Empty || ruleNode.category === RuleCategory.Comment) {
return RuleValidator.createValidationResult(true);
}

const ruleText = RuleParser.generate(ruleNode);

try {
// Validate cosmetic rules
if (ruleNode.category === RuleCategory.Cosmetic) {
new CosmeticRule(ruleNode, 0);
return RuleValidator.createValidationResult(true);
}

// Validate network rules
const rule = new NetworkRule(ruleNode, 0);
RuleValidator.validateRegexp(rule.getPattern(), ruleText);
} catch (error) {
// TODO: add getErrorMessage as a helper
const message = error instanceof Error ? error.message : String(error);
const errorMessage = `Error: "${message}" in the rule: "${ruleText}"`;
return RuleValidator.createValidationResult(false, errorMessage);
}

return RuleValidator.createValidationResult(true);

// TODO: validate host rules
}
}

/**
* Removes invalid rules from the list of rules
* and logs process in the excluded list
* and logs process in the excluded list.
*
* @param {string[]} list of rule texts
* @param {string[]} excluded - list of messages with validation results
* @param {string[]} invalid
* @param {string} filterName
* @returns {Array}
* @param {string[]} list List of rule texts.
* @param {string[]} excluded List of messages with validation results.
* @param {string[]} invalid List of messages with validation errors.
* @param {string} filterName Name of the filter.
*
* @returns {string[]} List of valid rules.
*/
const validate = function (list, excluded, invalid = [], filterName) { // eslint-disable-line default-param-last
if (!list) {
Expand All @@ -45,14 +142,20 @@ const validate = function (list, excluded, invalid = [], filterName) { // eslint
return true;
}

let convertedRules;

// unsupported UBO HTML rules throws error while conversion
// https://github.com/AdguardTeam/tsurlfilter/issues/55
let convertedRuleNodes;
try {
const ruleNode = RuleParser.parse(rule);
// FIXME: check if parsing UBO and ABP syntax is needed
const ruleNode = RuleParser.parse(ruleText, {
...defaultParserOptions,
// tolerant mode is used for rather quick syntax validation
tolerant: true,
isLocIncluded: false,
includeRaws: false,
parseAbpSpecificRules: false,
parseUboSpecificRules: false,
});
const conversionResult = RuleConverter.convertToAdg(ruleNode);
convertedRules = conversionResult.result.map((r) => RuleParser.generate(r));
convertedRuleNodes = conversionResult.result;
} catch (e) {
logger.error(`Invalid rule in ${filterName}: ${ruleText}`);
excludeRule(excluded, e.message, ruleText);
Expand All @@ -62,14 +165,25 @@ const validate = function (list, excluded, invalid = [], filterName) { // eslint

// optional chaining is needed for the length property because convertedRules can be undefined
// if RuleParser.parse() or RuleConverter.convertToAdg() throws an error
if (convertedRules?.length === 0) {
if (convertedRuleNodes?.length === 0) {
return false;
}

for (let i = 0; i < convertedRules.length; i += 1) {
const convertedRuleText = convertedRules[i];
for (let i = 0; i < convertedRuleNodes.length; i += 1) {
const convertedRuleNode = convertedRuleNodes[i];

const validationResult = RuleValidator.validate(convertedRuleText);
let validationResult = RuleValidator.validate(convertedRuleNode);

// TODO: remove this checking when $header is fixed
// https://github.com/AdguardTeam/AdguardBrowserExtension/issues/2942
if (
!validationResult.valid
&& validationResult.error.includes('$header rules are not compatible with some other modifiers')
) {
// $header rules are not compatible with other modifiers ONLY in the tsurlfilter
// but it is fine for corelibs
validationResult = { valid: true };
}

if (!validationResult.valid) {
// log source rule text to the excluded log
Expand All @@ -81,14 +195,13 @@ const validate = function (list, excluded, invalid = [], filterName) { // eslint
return false;
}

// TODO: improve validation since agtree parsing is used
// so RuleConverter.convertToAdg() returns `result` array with non-string rule nodes
const rule = RuleFactory.createRule(convertedRuleText);
const rule = RuleFactory.createRule(convertedRuleNode);

// It is impossible to bundle jsdom into tsurlfilter, so we check if rules are valid in the compiler
if (rule instanceof CosmeticRule && rule.getType() === CosmeticRuleType.ElementHiding) {
if (rule instanceof CosmeticRule && rule.getType() === CosmeticRuleType.ElementHidingRule) {
const validationResult = extendedCssValidator.validateCssSelector(rule.getContent());
if (!validationResult.ok) {
// TODO: rule selector can be validated by agtree
logger.error(`Invalid rule selector in ${filterName}: ${ruleText}`);
// log source rule text to the excluded log
excludeRule(excluded, `! ${validationResult.error} in rule:`, ruleText);
Expand Down

0 comments on commit a9ee6b6

Please sign in to comment.