mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
581 lines
21 KiB
JavaScript
581 lines
21 KiB
JavaScript
/*******************************************************************************
|
|
|
|
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
|
|
Copyright (C) 2014-present Raymond Hill
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
|
|
|
Home: https://github.com/gorhill/uBlock
|
|
*/
|
|
|
|
import * as sfp from './static-filtering-parser.js';
|
|
import punycode from './punycode.js';
|
|
import redirectResourceMap from './redirect-resources.js';
|
|
|
|
/******************************************************************************/
|
|
|
|
const validResourceTypes = [
|
|
'main_frame',
|
|
'sub_frame',
|
|
'stylesheet',
|
|
'script',
|
|
'image',
|
|
'font',
|
|
'object',
|
|
'xmlhttprequest',
|
|
'ping',
|
|
'csp_report',
|
|
'media',
|
|
'websocket',
|
|
'webtransport',
|
|
'webbundle',
|
|
'other',
|
|
];
|
|
|
|
/******************************************************************************/
|
|
|
|
const validRedirectResources = (( ) => {
|
|
const out = new Map();
|
|
for ( const [ name, resource ] of redirectResourceMap ) {
|
|
out.set(name, name);
|
|
if ( resource.alias === undefined ) { continue; }
|
|
if ( typeof resource.alias === 'string' ) {
|
|
out.set(resource.alias, name);
|
|
continue;
|
|
}
|
|
if ( Array.isArray(resource.alias) ) {
|
|
for ( const alias of resource.alias ) {
|
|
out.set(alias, name);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
})();
|
|
|
|
const isNotEntity = hn => hn.endsWith('.*') === false;
|
|
|
|
/******************************************************************************/
|
|
|
|
function toSuperDomain(hn) {
|
|
const pos = hn.indexOf('.');
|
|
if ( pos === -1 ) { return; }
|
|
return hn.slice(pos+1);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function parseHostnameList(iter) {
|
|
const out = {
|
|
included: {
|
|
good: [],
|
|
bad: [],
|
|
},
|
|
excluded: {
|
|
good: [],
|
|
bad: [],
|
|
},
|
|
};
|
|
for ( let { hn, not, bad } of iter ) {
|
|
bad ||= hn.includes('/') || hn.includes('*');
|
|
const hnAscii = bad === false && hn.startsWith('xn--')
|
|
? punycode.toASCII(hn)
|
|
: hn;
|
|
const destination = not ? out.excluded : out.included;
|
|
if ( bad ) {
|
|
destination.bad.push(hnAscii);
|
|
} else {
|
|
destination.good.push(hnAscii);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function ownerFromPropertyPath(root, path) {
|
|
let owner = root;
|
|
let prop = path;
|
|
for (;;) {
|
|
if ( owner instanceof Object === false ) { break; }
|
|
const pos = prop.indexOf('.');
|
|
if ( pos === -1 ) { break; }
|
|
owner = owner[prop.slice(0, pos)];
|
|
prop = prop.slice(pos+1)
|
|
}
|
|
return { owner: owner ?? undefined, prop };
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function mergeArrays(rules, propertyPath) {
|
|
const out = [];
|
|
const distinctRules = new Map();
|
|
for ( const rule of rules ) {
|
|
const { id } = rule;
|
|
const { owner, prop } = ownerFromPropertyPath(rule, propertyPath);
|
|
if ( owner === undefined || Array.isArray(owner[prop]) === false ) {
|
|
out.push(rule);
|
|
continue;
|
|
}
|
|
const collection = owner[prop] || [];
|
|
owner[prop] = undefined;
|
|
rule.id = undefined;
|
|
const hash = JSON.stringify(rule);
|
|
const details = distinctRules.get(hash) ||
|
|
{ id, collection: new Set() };
|
|
if ( details.collection.size === 0 ) {
|
|
distinctRules.set(hash, details);
|
|
}
|
|
for ( const hn of collection ) {
|
|
details.collection.add(hn);
|
|
}
|
|
}
|
|
for ( const [ hash, { id, collection } ] of distinctRules ) {
|
|
const rule = JSON.parse(hash);
|
|
if ( id ) {
|
|
rule.id = id;
|
|
}
|
|
if ( collection.size !== 0 ) {
|
|
const { owner, prop } = ownerFromPropertyPath(rule, propertyPath);
|
|
owner[prop] = Array.from(collection).sort();
|
|
}
|
|
out.push(rule);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function minimizeRuleset(rules) {
|
|
rules = mergeArrays(rules, 'condition.requestDomains');
|
|
rules = mergeArrays(rules, 'condition.excludedRequestDomains');
|
|
rules = mergeArrays(rules, 'condition.initiatorDomains');
|
|
rules = mergeArrays(rules, 'condition.excludedInitiatorDomains');
|
|
rules = mergeArrays(rules, 'condition.resourceTypes');
|
|
rules = mergeArrays(rules, 'condition.excludedRequestMethods');
|
|
rules = mergeArrays(rules, 'condition.requestMethods');
|
|
rules = mergeArrays(rules, 'condition.excludedResourceTypes');
|
|
rules = mergeArrays(rules, 'action.redirect.transform.queryTransform.removeParams');
|
|
return rules;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function minimizeRules(rules) {
|
|
const hostnameListProp = [
|
|
'requestDomains',
|
|
'excludedRequestDomains',
|
|
'initiatorDomains',
|
|
'excludedInitiatorDomains',
|
|
];
|
|
for ( const rule of rules ) {
|
|
for ( const prop of hostnameListProp ) {
|
|
const hostnames = rule.condition[prop];
|
|
if ( hostnames === undefined ) { continue; }
|
|
if ( hostnames.length === 1 ) { continue; }
|
|
const hnSet = new Set(hostnames);
|
|
for ( let hn of hnSet ) {
|
|
for (;;) {
|
|
const hnup = toSuperDomain(hn);
|
|
if ( hnup === undefined ) { break; }
|
|
if ( hnSet.has(hnup) ) { hnSet.delete(hn); }
|
|
hn = hnup;
|
|
}
|
|
}
|
|
if ( hnSet.size === hostnames.length ) { continue; }
|
|
rule.condition[prop] = Array.from(hnSet).sort();
|
|
}
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function dropEntities(rule, prop) {
|
|
const { condition } = rule;
|
|
if ( condition[prop] === undefined ) { return; }
|
|
const sanitized = condition[prop].filter(a => isNotEntity(a));
|
|
if ( sanitized.length === condition[prop].length ) { return; }
|
|
if ( sanitized.length === 0 ) { return 0; }
|
|
condition.requestDomains = sanitized;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function validateRules(rules) {
|
|
const out = [];
|
|
for ( const rule of rules ) {
|
|
const { condition } = rule;
|
|
// "Only one of resourceTypes and excludedResourceTypes should be specified"
|
|
if ( condition.resourceTypes ) {
|
|
if ( condition.excludedResourceTypes ) { continue; }
|
|
}
|
|
// "Only one of requestMethods and excludedRequestMethods should be specified"
|
|
if ( condition.requestMethods ) {
|
|
if ( condition.excludedRequestMethods ) { continue; }
|
|
}
|
|
// Drop entity-based hostnames
|
|
if ( dropEntities(rule, 'requestDomains') === 0 ) { continue; }
|
|
if ( dropEntities(rule, 'excludedRequestDomains') === 0 ) { continue; }
|
|
if ( dropEntities(rule, 'initiatorDomains') === 0 ) { continue; }
|
|
if ( dropEntities(rule, 'excludedInitiatorDomains') === 0 ) { continue; }
|
|
// regexSubstitution requires regexFilter
|
|
if ( rule.action?.redirect?.regexSubstitution ) {
|
|
if ( rule.condition.regexFilter === undefined ) { continue; }
|
|
}
|
|
out.push(rule);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
export function parseNetworkFilter(parser) {
|
|
if ( parser.isNetworkFilter() === false ) { return; }
|
|
if ( parser.hasError() ) { return; }
|
|
|
|
const rule = {
|
|
action: { type: 'block' },
|
|
condition: { },
|
|
};
|
|
if ( parser.isException() ) {
|
|
rule.action.type = 'allow';
|
|
}
|
|
|
|
let pattern = parser.getNetPattern();
|
|
if ( parser.isHostnamePattern() ) {
|
|
rule.condition.requestDomains = [ pattern ];
|
|
} else if ( parser.isPlainPattern() || parser.isGenericPattern() ) {
|
|
if ( parser.isLeftHnAnchored() ) {
|
|
pattern = `||${pattern}`;
|
|
} else if ( parser.isLeftAnchored() ) {
|
|
pattern = `|${pattern}`;
|
|
}
|
|
if ( parser.isRightAnchored() ) {
|
|
pattern = `${pattern}|`;
|
|
}
|
|
rule.condition.urlFilter = pattern;
|
|
} else if ( parser.isRegexPattern() ) {
|
|
rule.condition.regexFilter = pattern;
|
|
} else if ( parser.isAnyPattern() === false ) {
|
|
rule.condition.urlFilter = pattern;
|
|
}
|
|
|
|
const defaultResourceTypes = new Set();
|
|
let defaultUrlFilter = '';
|
|
|
|
const initiatorDomains = new Set();
|
|
const excludedInitiatorDomains = new Set();
|
|
const requestDomains = new Set();
|
|
const excludedRequestDomains = new Set();
|
|
const requestMethods = new Set();
|
|
const excludedRequestMethods = new Set();
|
|
const resourceTypes = new Set();
|
|
const excludedResourceTypes = new Set();
|
|
|
|
const processResourceType = (resourceType, nodeType) => {
|
|
const not = parser.isNegatedOption(nodeType)
|
|
if ( validResourceTypes.includes(resourceType) === false ) {
|
|
if ( not ) { return; }
|
|
}
|
|
if ( not ) {
|
|
excludedResourceTypes.add(resourceType);
|
|
} else {
|
|
resourceTypes.add(resourceType);
|
|
}
|
|
};
|
|
|
|
let priority = 0;
|
|
|
|
for ( const type of parser.getNodeTypes() ) {
|
|
switch ( type ) {
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_1P:
|
|
rule.condition.domainType = parser.isNegatedOption(type)
|
|
? 'thirdParty'
|
|
: 'firstParty';
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_STRICT1P:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_STRICT3P:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_BADFILTER:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_CNAME:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_EHIDE:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_GENERICBLOCK:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_GHIDE:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_IPADDRESS:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_REPLACE:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_SHIDE:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_URLSKIP:
|
|
return;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_INLINEFONT:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_INLINESCRIPT:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_POPUNDER:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_POPUP:
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_WEBRTC:
|
|
processResourceType('', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_3P:
|
|
rule.condition.domainType = parser.isNegatedOption(type)
|
|
? 'firstParty'
|
|
: 'thirdParty';
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_ALL:
|
|
validResourceTypes.forEach(a => resourceTypes.add(a));
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_CSP:
|
|
if ( rule.action.responseHeaders ) { return; }
|
|
rule.action.type = 'modifyHeaders';
|
|
rule.action.responseHeaders = [ {
|
|
header: 'content-security-policy',
|
|
operation: 'append',
|
|
value: parser.getNetOptionValue(type),
|
|
} ];
|
|
defaultResourceTypes.add('main_frame');
|
|
defaultResourceTypes.add('sub_frame');
|
|
defaultResourceTypes.add('object');
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_CSS:
|
|
processResourceType('stylesheet', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_DENYALLOW: {
|
|
const { included, excluded } = parseHostnameList(
|
|
parser.getNetFilterDenyallowOptionIterator()
|
|
);
|
|
if ( excluded.good.length !== 0 || excluded.bad.length !== 0 ) { return; }
|
|
if ( included.bad.length !== 0 ) { return; }
|
|
if ( included.good.length === 0 ) { return; }
|
|
for ( const hn of included.good ) {
|
|
excludedRequestDomains.add(hn);
|
|
}
|
|
break;
|
|
}
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_DOC:
|
|
processResourceType('main_frame', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_FONT:
|
|
processResourceType('font', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_FRAME:
|
|
processResourceType('sub_frame', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_FROM: {
|
|
const { included, excluded } = parseHostnameList(
|
|
parser.getNetFilterFromOptionIterator()
|
|
);
|
|
if ( included.good.length === 0 ) {
|
|
if ( included.bad.length !== 0 ) { return; }
|
|
}
|
|
if ( excluded.bad.length !== 0 ) { return; }
|
|
for ( const hn of included.good ) {
|
|
initiatorDomains.add(hn);
|
|
}
|
|
for ( const hn of excluded.good ) {
|
|
excludedInitiatorDomains.add(hn);
|
|
}
|
|
break;
|
|
}
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_HEADER: {
|
|
const details = sfp.parseHeaderValue(parser.getNetOptionValue(type));
|
|
const headerInfo = {
|
|
header: details.name,
|
|
};
|
|
if ( details.value !== '' ) {
|
|
if ( details.isRegex ) { return; }
|
|
headerInfo.values = [ details.value ];
|
|
}
|
|
rule.condition.responseHeaders = [ headerInfo ];
|
|
break;
|
|
}
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_IMAGE:
|
|
processResourceType('image', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_IMPORTANT:
|
|
priority += 30;
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_MATCHCASE:
|
|
rule.condition.isUrlFilterCaseSensitive = true;
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_MEDIA:
|
|
processResourceType('media', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_METHOD: {
|
|
const value = parser.getNetOptionValue(type);
|
|
for ( const method of value.toLowerCase().split('|') ) {
|
|
const not = method.charCodeAt(0) === 0x7E /* '~' */;
|
|
if ( not ) {
|
|
excludedRequestMethods.add(method.slice(1));
|
|
} else {
|
|
requestMethods.add(method);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_OBJECT:
|
|
processResourceType('object', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_OTHER:
|
|
processResourceType('other', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_PERMISSIONS:
|
|
if ( rule.action.responseHeaders ) { return; }
|
|
rule.action.type = 'modifyHeaders';
|
|
rule.action.responseHeaders = [ {
|
|
header: 'permissions-policy',
|
|
operation: 'append',
|
|
value: parser.getNetOptionValue(type),
|
|
} ];
|
|
defaultResourceTypes.add('main_frame');
|
|
defaultResourceTypes.add('sub_frame');
|
|
defaultResourceTypes.add('object');
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_PING:
|
|
processResourceType('ping', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_REASON:
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECT: {
|
|
if ( rule.action.type !== 'block' ) { return; }
|
|
let value = parser.getNetOptionValue(type);
|
|
const match = /:(\d+)$/.exec(value);
|
|
if ( match ) {
|
|
const subpriority = parseInt(match[1], 10);
|
|
priority += Math.min(subpriority, 8);
|
|
value = value.slice(0, match.index);
|
|
}
|
|
if ( validRedirectResources.has(value) === false ) { return; }
|
|
rule.action.type = 'redirect';
|
|
rule.action.redirect = {
|
|
extensionPath: `/web_accessible_resources/${validRedirectResources.get(value)}`,
|
|
};
|
|
priority += 11;
|
|
break;
|
|
}
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM: {
|
|
const details = sfp.parseQueryPruneValue(parser.getNetOptionValue(type));
|
|
if ( details.bad ) { return; }
|
|
if ( details.not ) { return; }
|
|
if ( details.re ) { return; }
|
|
const removeParams = [];
|
|
if ( details.name ) {
|
|
removeParams.push(details.name);
|
|
}
|
|
rule.action.type = 'redirect';
|
|
rule.action.redirect = {
|
|
transform: { queryTransform: { removeParams } }
|
|
};
|
|
defaultResourceTypes.add('main_frame');
|
|
defaultResourceTypes.add('sub_frame');
|
|
defaultResourceTypes.add('xmlhttprequest');
|
|
defaultUrlFilter = '?';
|
|
break;
|
|
}
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_SCRIPT:
|
|
processResourceType('script', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_TO: {
|
|
const { included, excluded } = parseHostnameList(
|
|
parser.getNetFilterToOptionIterator()
|
|
);
|
|
if ( included.good.length === 0 ) {
|
|
if ( included.bad.length !== 0 ) { return; }
|
|
}
|
|
if ( excluded.bad.length !== 0 ) { return; }
|
|
for ( const hn of included.good ) {
|
|
requestDomains.add(hn);
|
|
}
|
|
for ( const hn of excluded.good ) {
|
|
excludedRequestDomains.add(hn);
|
|
}
|
|
break;
|
|
}
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM: {
|
|
const parsed = sfp.parseReplaceByRegexValue(parser.getNetOptionValue(type));
|
|
if ( parsed === undefined ) { return; }
|
|
if ( parsed.re ) { return; }
|
|
rule.action.type = 'redirect';
|
|
rule.action.redirect = {
|
|
regexSubstitution: parsed.replacement.replace(/\$(\d+)/g, '\\$1'),
|
|
};
|
|
break;
|
|
}
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_XHR:
|
|
processResourceType('xmlhttprequest', type);
|
|
break;
|
|
case sfp.NODE_TYPE_NET_OPTION_NAME_WEBSOCKET:
|
|
processResourceType('websocket', type);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if ( pattern === '*' && defaultUrlFilter !== '' ) {
|
|
rule.condition.urlFilter = defaultUrlFilter;
|
|
}
|
|
if ( initiatorDomains.size !== 0 ) {
|
|
rule.condition.initiatorDomains = Array.from(initiatorDomains).sort();
|
|
}
|
|
if ( excludedInitiatorDomains.size !== 0 ) {
|
|
rule.condition.excludedInitiatorDomains = Array.from(excludedInitiatorDomains).sort();
|
|
}
|
|
if ( requestDomains.size !== 0 ) {
|
|
rule.condition.requestDomains = Array.from(requestDomains).sort();
|
|
}
|
|
if ( excludedRequestDomains.size !== 0 ) {
|
|
rule.condition.excludedRequestDomains = Array.from(excludedRequestDomains).sort();
|
|
}
|
|
if ( requestMethods.size !== 0 ) {
|
|
rule.condition.requestMethods = Array.from(requestMethods).sort();
|
|
}
|
|
if ( excludedRequestMethods.size !== 0 ) {
|
|
rule.condition.excludedRequestMethods = Array.from(excludedRequestMethods).sort();
|
|
}
|
|
if ( resourceTypes.size === 0 && excludedResourceTypes.size === 0 ) {
|
|
defaultResourceTypes.forEach(a => resourceTypes.add(a));
|
|
}
|
|
if ( resourceTypes.size !== 0 ) {
|
|
const types = Array.from(resourceTypes).filter(a => a !== '').sort();
|
|
if ( types.length === 0 ) { return; }
|
|
rule.condition.resourceTypes = types;
|
|
}
|
|
if ( excludedResourceTypes.size !== 0 ) {
|
|
if ( resourceTypes.size !== 0 ) { return; }
|
|
rule.condition.excludedResourceTypes = Array.from(excludedResourceTypes).sort();
|
|
}
|
|
if ( priority !== 0 ) {
|
|
rule.priority = priority;
|
|
}
|
|
return rule;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
export function parseFilters(text) {
|
|
if ( text.startsWith('---') ) { return; }
|
|
if ( text.endsWith('---') ) { return; }
|
|
const lines = text.split(/\n/);
|
|
if ( lines.some(a => a.startsWith(' ')) ) { return; }
|
|
let rules = [];
|
|
const parser = new sfp.AstFilterParser({ trustedSource: true });
|
|
for ( const line of lines ) {
|
|
parser.parse(line);
|
|
if ( parser.isNetworkFilter() === false ) { continue; }
|
|
const rule = parseNetworkFilter(parser);
|
|
if ( rule === undefined ) { continue; }
|
|
rules.push(rule);
|
|
}
|
|
rules = minimizeRuleset(rules);
|
|
rules = minimizeRules(rules);
|
|
rules = validateRules(rules);
|
|
return rules;
|
|
}
|