@adguard/agtree 2.0.0-alpha.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agtree.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * AGTree v2.0.0-alpha.0 (build date: Mon, 29 Jul 2024 13:43:09 GMT)
2
+ * AGTree v2.0.0 (build date: Thu, 15 Aug 2024 15:06:58 GMT)
3
3
  * (c) 2024 Adguard Software Ltd.
4
4
  * Released under the MIT license
5
5
  * https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
@@ -763,6 +763,15 @@ class StringUtils {
763
763
  }
764
764
  }
765
765
 
766
+ /**
767
+ * Possible operators in the logical expression.
768
+ */
769
+ var OperatorValue;
770
+ (function (OperatorValue) {
771
+ OperatorValue["Not"] = "!";
772
+ OperatorValue["And"] = "&&";
773
+ OperatorValue["Or"] = "||";
774
+ })(OperatorValue || (OperatorValue = {}));
766
775
  /**
767
776
  * Represents the different comment markers that can be used in an adblock rule.
768
777
  *
@@ -2958,15 +2967,6 @@ var ParenthesisNodeBinaryPropMap;
2958
2967
  ParenthesisNodeBinaryPropMap[ParenthesisNodeBinaryPropMap["Start"] = 2] = "Start";
2959
2968
  ParenthesisNodeBinaryPropMap[ParenthesisNodeBinaryPropMap["End"] = 3] = "End";
2960
2969
  })(ParenthesisNodeBinaryPropMap || (ParenthesisNodeBinaryPropMap = {}));
2961
- /**
2962
- * Possible operators in the logical expression.
2963
- */
2964
- var OperatorValue;
2965
- (function (OperatorValue) {
2966
- OperatorValue["Not"] = "!";
2967
- OperatorValue["And"] = "&&";
2968
- OperatorValue["Or"] = "||";
2969
- })(OperatorValue || (OperatorValue = {}));
2970
2970
  /**
2971
2971
  * Possible token types in the logical expression.
2972
2972
  */
@@ -3012,7 +3012,9 @@ const getOperatorOrFail = binary => {
3012
3012
  /**
3013
3013
  * Serialization map for known variables.
3014
3014
  */
3015
- const KNOWN_VARIABLES_MAP = new Map([['ext_abp', 0], ['ext_ublock', 1], ['ext_ubol', 2], ['ext_devbuild', 3], ['env_chromium', 4], ['env_edge', 5], ['env_firefox', 6], ['env_mobile', 7], ['env_safari', 8], ['env_mv3', 9], ['false', 10], ['cap_html_filtering', 11], ['cap_user_stylesheet', 12], ['adguard', 13], ['adguard_app_windows', 14], ['adguard_app_mac', 15], ['adguard_app_android', 16], ['adguard_app_ios', 17], ['adguard_ext_safari', 18], ['adguard_ext_chromium', 19], ['adguard_ext_firefox', 20], ['adguard_ext_edge', 21], ['adguard_ext_opera', 22], ['adguard_ext_android_cb', 23]]);
3015
+ const KNOWN_VARIABLES_MAP = new Map([['ext_abp', 0], ['ext_ublock', 1], ['ext_ubol', 2], ['ext_devbuild', 3], ['env_chromium', 4], ['env_edge', 5], ['env_firefox', 6], ['env_mobile', 7], ['env_safari', 8], ['env_mv3', 9], ['false', 10], ['cap_html_filtering', 11], ['cap_user_stylesheet', 12], ['adguard', 13], ['adguard_app_windows', 14], ['adguard_app_mac', 15], ['adguard_app_android', 16], ['adguard_app_ios', 17], ['adguard_ext_safari', 18], ['adguard_ext_chromium', 19], ['adguard_ext_firefox', 20], ['adguard_ext_edge', 21], ['adguard_ext_opera', 22], ['adguard_ext_android_cb', 23]
3016
+ // TODO: Add 'adguard_ext_chromium_mv3' to the list
3017
+ ]);
3016
3018
  /**
3017
3019
  * Deserialization map for known variables.
3018
3020
  */
@@ -5716,7 +5718,7 @@ class CssTokenStream {
5716
5718
  * @returns An array containing the number of tokens skipped and the number of tokens skipped without leading and
5717
5719
  * trailing whitespace tokens.
5718
5720
  */
5719
- skipUntilEx(type, balance) {
5721
+ skipUntilExt(type, balance) {
5720
5722
  let i = this.index;
5721
5723
  let firstNonWsToken = -1; // -1 means no non-whitespace token found yet
5722
5724
  let lastNonWsToken = -1; // -1 means no non-whitespace token found yet
@@ -5944,7 +5946,7 @@ class AdgCssInjectionParser extends ParserBase {
5944
5946
  // └ this one
5945
5947
  const {
5946
5948
  skippedTrimmed: selectorTokensLength
5947
- } = stream.skipUntilEx(cssTokenizer.TokenType.OpenCurlyBracket, balanceShift + 1);
5949
+ } = stream.skipUntilExt(cssTokenizer.TokenType.OpenCurlyBracket, balanceShift + 1);
5948
5950
  stream.expect(cssTokenizer.TokenType.OpenCurlyBracket);
5949
5951
  // If the skipped tokens count is 0 without leading and trailing whitespace characters, then the selector list
5950
5952
  // is empty
@@ -6184,6 +6186,7 @@ class AbpSnippetInjectionBodyParser extends ParserBase {
6184
6186
  *
6185
6187
  * @note Only 256 values can be represented this way.
6186
6188
  */
6189
+ // TODO: Update this map with the actual values
6187
6190
  static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-inline-script', 0], ['abort-on-property-read', 1], ['abort-on-property-write', 2], ['json-prune', 3], ['log', 4], ['prevent-listener', 5], ['cookie-remover', 6], ['override-property-read', 7], ['abort-on-iframe-property-read', 8], ['abort-on-iframe-property-write', 9], ['freeze-element', 10], ['json-override', 11], ['simulate-mouse-event', 12], ['strip-fetch-query-parameter', 13], ['hide-if-contains', 14], ['hide-if-contains-image', 15], ['hide-if-contains-image-hash', 16], ['hide-if-contains-similar-text', 17], ['hide-if-contains-visible-text', 18], ['hide-if-contains-and-matches-style', 19], ['hide-if-graph-matches', 20], ['hide-if-has-and-matches-style', 21], ['hide-if-labelled-by', 22], ['hide-if-matches-xpath', 23], ['hide-if-matches-computed-xpath', 24], ['hide-if-shadow-contains', 25], ['debug', 26], ['trace', 27], ['race', 28]]);
6188
6191
  /**
6189
6192
  * Value map for binary deserialization. This helps to reduce the size of the serialized data,
@@ -6311,6 +6314,7 @@ class UboScriptletInjectionBodyParser extends ParserBase {
6311
6314
  *
6312
6315
  * @note Only 256 values can be represented this way.
6313
6316
  */
6317
+ // TODO: Update this map with the actual values
6314
6318
  static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-script.js', 0], ['acs.js', 1], ['abort-current-inline-script.js', 2], ['acis.js', 3], ['abort-on-property-read.js', 4], ['aopr.js', 5], ['abort-on-property-write.js', 6], ['aopw.js', 7], ['abort-on-stack-trace.js', 8], ['aost.js', 9], ['adjust-setInterval.js', 10], ['nano-setInterval-booster.js', 11], ['nano-sib.js', 12], ['adjust-setTimeout.js', 13], ['nano-setTimeout-booster.js', 14], ['nano-stb.js', 15], ['close-window.js', 16], ['window-close-if.js', 17], ['disable-newtab-links.js', 18], ['evaldata-prune.js', 19], ['json-prune.js', 20], ['addEventListener-logger.js', 21], ['aell.js', 22], ['m3u-prune.js', 23], ['nowebrtc.js', 24], ['addEventListener-defuser.js', 25], ['aeld.js', 26], ['prevent-addEventListener.js', 27], ['adfly-defuser.js', 28], ['noeval-if.js', 29], ['prevent-eval-if.js', 30], ['no-fetch-if.js', 31], ['prevent-fetch.js', 32], ['no-xhr-if.js', 33], ['prevent-xhr.js', 34], ['prevent-refresh.js', 35], ['refresh-defuser.js', 36], ['no-requestAnimationFrame-if.js', 37], ['norafif.js', 38], ['prevent-requestAnimationFrame.js', 39], ['no-setInterval-if.js', 40], ['nosiif.js', 41], ['prevent-setInterval.js', 42], ['setInterval-defuser.js', 43], ['no-setTimeout-if.js', 44], ['nostif.js', 45], ['prevent-setTimeout.js', 46], ['setTimeout-defuser.js', 47], ['no-window-open-if.js', 48], ['nowoif.js', 49], ['prevent-window-open.js', 50], ['window.open-defuser.js', 51], ['remove-attr.js', 52], ['ra.js', 53], ['remove-class.js', 54], ['rc.js', 55], ['remove-cookie.js', 56], ['cookie-remover.js', 57], ['remove-node-text.js', 58], ['rmnt.js', 59], ['set-attr.js', 60], ['set-constant.js', 61], ['set.js', 62], ['set-cookie.js', 63], ['set-local-storage-item.js', 64], ['set-session-storage-item.js', 65], ['xml-prune.js', 66], ['webrtc-if.js', 67], ['overlay-buster.js', 68], ['alert-buster.js', 69], ['golem.de.js', 70], ['href-sanitizer.js', 71], ['call-nothrow.js', 72], ['window.name-defuser.js', 73], ['spoof-css.js', 74], ['trusted-set-constant.js', 75], ['trusted-set.js', 76], ['trusted-set-cookie.js', 77], ['trusted-set-local-storage-item.js', 78], ['trusted-replace-fetch-response.js', 79], ['json-prune-fetch-response.js', 80], ['json-prune-xhr-response.js', 81], ['trusted-replace-xhr-response.js', 82], ['multiup.js', 83], ['prevent-canvas.js', 84], ['set-cookie-reload.js', 85], ['trusted-set-cookie-reload.js', 86], ['trusted-click-element.js', 87], ['trusted-prune-inbound-object.js', 88], ['trusted-prune-outbound-object.js', 89], ['trusted-set-session-storage-item.js', 90], ['trusted-replace-node-text.js', 91], ['trusted-rpnt.js', 92], ['replace-node-text.js', 93], ['rpnt.js', 94]]);
6315
6319
  /**
6316
6320
  * Value map for binary deserialization. This helps to reduce the size of the serialized data,
@@ -6448,6 +6452,7 @@ class AdgScriptletInjectionBodyParser extends ParserBase {
6448
6452
  *
6449
6453
  * @note Only 256 values can be represented this way.
6450
6454
  */
6455
+ // TODO: Update this map with the actual values
6451
6456
  static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-inline-script', 0], ['abort-on-property-read', 1], ['abort-on-property-write', 2], ['abort-on-stack-trace', 3], ['adjust-setInterval', 4], ['adjust-setTimeout', 5], ['close-window', 6], ['debug-current-inline-script', 7], ['debug-on-property-read', 8], ['debug-on-property-write', 9], ['dir-string', 10], ['disable-newtab-links', 11], ['evaldata-prune', 12], ['json-prune', 13], ['log', 14], ['log-addEventListener', 15], ['log-eval', 16], ['log-on-stack-trace', 17], ['m3u-prune', 18], ['noeval', 19], ['nowebrtc', 20], ['no-topics', 21], ['prevent-addEventListener', 22], ['prevent-adfly', 23], ['prevent-bab', 24], ['prevent-eval-if', 25], ['prevent-fab-3.2.0', 26], ['prevent-fetch', 27], ['prevent-xhr', 28], ['prevent-popads-net', 29], ['prevent-refresh', 30], ['prevent-requestAnimationFrame', 31], ['prevent-setInterval', 32], ['prevent-setTimeout', 33], ['prevent-window-open', 34], ['remove-attr', 35], ['remove-class', 36], ['remove-cookie', 37], ['remove-node-text', 38], ['set-attr', 39], ['set-constant', 40], ['set-cookie', 41], ['set-cookie-reload', 42], ['set-local-storage-item', 43], ['set-popads-dummy', 44], ['set-session-storage-item', 45], ['xml-prune', 46]]);
6452
6457
  /**
6453
6458
  * Value map for binary deserialization. This helps to reduce the size of the serialized data,
@@ -21527,6 +21532,30 @@ function getScriptletName(scriptletNode) {
21527
21532
  }
21528
21533
  return scriptletNode.children[0]?.value ?? EMPTY;
21529
21534
  }
21535
+ /**
21536
+ * Transform the nth argument of the scriptlet node
21537
+ *
21538
+ * @param scriptletNode Scriptlet node to transform argument of
21539
+ * @param index Index of the argument to transform (index 0 is the scriptlet name)
21540
+ * @param transform Function to transform the argument
21541
+ */
21542
+ function transformNthScriptletArgument(scriptletNode, index, transform) {
21543
+ const child = scriptletNode.children[index];
21544
+ if (!isUndefined(child) && !isNull(child)) {
21545
+ child.value = transform(child.value);
21546
+ }
21547
+ }
21548
+ /**
21549
+ * Transform all arguments of the scriptlet node
21550
+ *
21551
+ * @param scriptletNode Scriptlet node to transform arguments of
21552
+ * @param transform Function to transform the arguments
21553
+ */
21554
+ function transformAllScriptletArguments(scriptletNode, transform) {
21555
+ for (let i = 0; i < scriptletNode.children.length; i += 1) {
21556
+ transformNthScriptletArgument(scriptletNode, i, transform);
21557
+ }
21558
+ }
21530
21559
  /**
21531
21560
  * Set name of the scriptlet.
21532
21561
  * Modifies input `scriptletNode` if needed.
@@ -21535,10 +21564,7 @@ function getScriptletName(scriptletNode) {
21535
21564
  * @param name Name to set
21536
21565
  */
21537
21566
  function setScriptletName(scriptletNode, name) {
21538
- if (scriptletNode.children.length > 0 && !isNull(scriptletNode.children[0])) {
21539
- // eslint-disable-next-line no-param-reassign
21540
- scriptletNode.children[0].value = name;
21541
- }
21567
+ transformNthScriptletArgument(scriptletNode, 0, () => name);
21542
21568
  }
21543
21569
  /**
21544
21570
  * Set quote type of the scriptlet parameters
@@ -21547,1422 +21573,1559 @@ function setScriptletName(scriptletNode, name) {
21547
21573
  * @param quoteType Preferred quote type
21548
21574
  */
21549
21575
  function setScriptletQuoteType(scriptletNode, quoteType) {
21550
- if (scriptletNode.children.length > 0) {
21551
- for (let i = 0; i < scriptletNode.children.length; i += 1) {
21552
- const child = scriptletNode.children[i];
21553
- if (isNull(child)) {
21554
- continue;
21555
- }
21556
- // eslint-disable-next-line no-param-reassign
21557
- child.value = QuoteUtils.setStringQuoteType(child.value, quoteType);
21558
- }
21559
- }
21576
+ transformAllScriptletArguments(scriptletNode, value => QuoteUtils.setStringQuoteType(value, quoteType));
21560
21577
  }
21561
21578
 
21562
21579
  /**
21563
- * @file Scriptlet injection rule converter
21580
+ * @file Compatibility tables for redirects.
21564
21581
  */
21565
- const ABP_SCRIPTLET_PREFIX = 'abp-';
21566
- const UBO_SCRIPTLET_PREFIX = 'ubo-';
21567
21582
  /**
21568
- * Scriptlet injection rule converter class
21583
+ * Prefix for resource redirection names.
21584
+ */
21585
+ const ABP_RESOURCE_PREFIX = 'abp-resource:';
21586
+ const ABP_RESOURCE_PREFIX_LENGTH = ABP_RESOURCE_PREFIX.length;
21587
+ /**
21588
+ * Transforms the name of an ABP redirect to a normalized form.
21569
21589
  *
21570
- * @todo Implement `convertToUbo` and `convertToAbp`
21590
+ * @param name Redirect name to normalize.
21591
+ *
21592
+ * @returns Normalized redirect name.
21593
+ *
21594
+ * @example
21595
+ * abpRedirectNameNormalizer('abp-resource:my-resource') // => 'my-resource'
21571
21596
  */
21572
- class ScriptletRuleConverter extends RuleConverterBase {
21597
+ const abpRedirectNameNormalizer = name => {
21598
+ if (name.startsWith(ABP_RESOURCE_PREFIX)) {
21599
+ return name.slice(ABP_RESOURCE_PREFIX_LENGTH);
21600
+ }
21601
+ return name;
21602
+ };
21603
+ /**
21604
+ * Compatibility table for redirects.
21605
+ */
21606
+ class RedirectsCompatibilityTable extends CompatibilityTableBase {
21573
21607
  /**
21574
- * Converts a scriptlet injection rule to AdGuard format, if possible.
21608
+ * Creates a new instance of the compatibility table for redirects.
21575
21609
  *
21576
- * @param rule Rule node to convert
21577
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
21578
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
21579
- * If the rule was not converted, the result array will contain the original node with the same object reference
21580
- * @throws If the rule is invalid or cannot be converted
21610
+ * @param data Compatibility table data.
21581
21611
  */
21582
- static convertToAdg(rule) {
21583
- // Ignore AdGuard rules
21584
- if (rule.syntax === exports.AdblockSyntax.Adg) {
21585
- return createNodeConversionResult([rule], false);
21586
- }
21587
- const separator = rule.separator.value;
21588
- let convertedSeparator = separator;
21589
- convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgJsInjectionException : exports.CosmeticRuleSeparator.AdgJsInjection;
21590
- const convertedScriptlets = [];
21591
- for (const scriptlet of rule.body.children) {
21592
- // Clone the node to avoid any side effects
21593
- const scriptletClone = cloneScriptletRuleNode(scriptlet);
21594
- // Remove possible quotes just to make it easier to work with the scriptlet name
21595
- const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
21596
- // Add prefix if it's not already there
21597
- let prefix;
21598
- switch (rule.syntax) {
21599
- case exports.AdblockSyntax.Abp:
21600
- prefix = ABP_SCRIPTLET_PREFIX;
21601
- break;
21602
- case exports.AdblockSyntax.Ubo:
21603
- prefix = UBO_SCRIPTLET_PREFIX;
21604
- break;
21605
- default:
21606
- prefix = EMPTY;
21607
- }
21608
- if (!scriptletName.startsWith(prefix)) {
21609
- setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
21610
- }
21611
- // ADG scriptlet parameters should be quoted, and single quoted are preferred
21612
- setScriptletQuoteType(scriptletClone, exports.QuoteType.Single);
21613
- convertedScriptlets.push(scriptletClone);
21614
- }
21615
- return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
21616
- const res = {
21617
- category: rule.category,
21618
- type: rule.type,
21619
- syntax: exports.AdblockSyntax.Adg,
21620
- exception: rule.exception,
21621
- domains: cloneDomainListNode(rule.domains),
21622
- separator: {
21623
- type: 'Value',
21624
- value: convertedSeparator
21625
- },
21626
- body: {
21627
- type: rule.body.type,
21628
- children: [scriptlet]
21629
- }
21630
- };
21631
- if (rule.modifiers) {
21632
- res.modifiers = cloneModifierListNode(rule.modifiers);
21633
- }
21634
- return res;
21635
- }), true);
21612
+ constructor(data) {
21613
+ super(data, abpRedirectNameNormalizer);
21636
21614
  }
21637
21615
  }
21616
+ /**
21617
+ * Deep freeze the compatibility table data to avoid accidental modifications.
21618
+ */
21619
+ deepFreeze(redirectsCompatibilityTableData);
21620
+ /**
21621
+ * Compatibility table instance for redirects.
21622
+ */
21623
+ const redirectsCompatibilityTable = new RedirectsCompatibilityTable(redirectsCompatibilityTableData);
21638
21624
 
21639
21625
  /**
21640
- * @file Utility functions for working with modifier nodes
21626
+ * @file Compatibility tables for scriptlets.
21641
21627
  */
21642
21628
  /**
21643
- * Creates a modifier node
21644
- *
21645
- * @param name Name of the modifier
21646
- * @param value Value of the modifier
21647
- * @param exception Whether the modifier is an exception
21648
- * @returns Modifier node
21629
+ * Compatibility table for scriptlets.
21649
21630
  */
21650
- function createModifierNode(name, value = undefined, exception = false) {
21651
- const result = {
21652
- type: 'Modifier',
21653
- exception,
21654
- name: {
21655
- type: 'Value',
21656
- value: name
21657
- }
21658
- };
21659
- if (!isUndefined(value)) {
21660
- result.value = {
21661
- type: 'Value',
21662
- value
21663
- };
21664
- }
21665
- return result;
21666
- }
21631
+ class ScriptletsCompatibilityTable extends CompatibilityTableBase {}
21667
21632
  /**
21668
- * Creates a modifier list node
21669
- *
21670
- * @param modifiers Modifiers to put in the list (optional, defaults to an empty list)
21671
- * @returns Modifier list node
21633
+ * Deep freeze the compatibility table data to avoid accidental modifications.
21672
21634
  */
21673
- function createModifierListNode(modifiers = []) {
21674
- const result = {
21675
- type: 'ModifierList',
21676
- // We need to clone the modifiers to avoid side effects
21677
- children: modifiers.length ? clone(modifiers) : []
21678
- };
21679
- return result;
21680
- }
21681
-
21635
+ deepFreeze(scriptletsCompatibilityTableData);
21682
21636
  /**
21683
- * A very simple map extension that allows to store multiple values for the same key
21684
- * by storing them in an array.
21685
- *
21686
- * @todo Add more methods if needed
21637
+ * Compatibility table instance for scriptlets.
21687
21638
  */
21688
- class MultiValueMap extends Map {
21689
- /**
21690
- * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
21691
- * otherwise a new array will be created for the key.
21692
- *
21693
- * @param key Key to add
21694
- * @param values Value(s) to add
21695
- */
21696
- add(key, ...values) {
21697
- let currentValues = super.get(key);
21698
- if (isUndefined(currentValues)) {
21699
- currentValues = [];
21700
- super.set(key, values);
21701
- }
21702
- currentValues.push(...values);
21703
- }
21704
- }
21639
+ const scriptletsCompatibilityTable = new ScriptletsCompatibilityTable(scriptletsCompatibilityTableData);
21705
21640
 
21641
+ /* eslint-disable no-bitwise */
21706
21642
  /**
21707
- * @file Cosmetic rule modifier converter from uBO to ADG
21643
+ * @file Platform schema.
21708
21644
  */
21709
- const UBO_MATCHES_PATH_OPERATOR = 'matches-path';
21710
- const ADG_PATH_MODIFIER = 'path';
21711
21645
  /**
21712
- * Special characters in modifier regexps that should be escaped
21646
+ * Platform separator, e.g. 'adg_os_any|adg_safari_any' means any AdGuard OS platform and
21647
+ * any AdGuard Safari content blocker platform.
21713
21648
  */
21714
- const SPECIAL_MODIFIER_REGEX_CHARS = new Set([OPEN_SQUARE_BRACKET, CLOSE_SQUARE_BRACKET, COMMA, ESCAPE_CHARACTER]);
21649
+ const PLATFORM_SEPARATOR = '|';
21715
21650
  /**
21716
- * Helper class for converting cosmetic rule modifiers from uBO to ADG
21651
+ * Platform negation character, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except
21652
+ * Safari content blockers.
21717
21653
  */
21718
- class AdgCosmeticRuleModifierConverter {
21719
- /**
21720
- * Converts a uBO cosmetic rule modifier list to ADG, if possible.
21721
- *
21722
- * @param modifierList Cosmetic rule modifier list node to convert
21723
- * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
21724
- * the converted node, and its `isConverted` flag indicates whether the original node was converted.
21725
- * If the node was not converted, the result will contain the original node with the same object reference
21726
- * @throws If the modifier list cannot be converted
21727
- * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
21728
- */
21729
- static convertFromUbo(modifierList) {
21730
- const conversionMap = new MultiValueMap();
21731
- modifierList.children.forEach((modifier, index) => {
21732
- // :matches-path
21733
- if (modifier.name.value === UBO_MATCHES_PATH_OPERATOR) {
21734
- if (!modifier.value) {
21735
- throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
21736
- }
21737
- const value = RegExpUtils.isRegexPattern(modifier.value.value) ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS) : modifier.value.value;
21738
- // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
21739
- conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
21740
- // We should negate the regexp if the modifier is an exception
21741
- modifier.exception
21742
- // eslint-disable-next-line max-len
21743
- ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}` : value));
21744
- }
21745
- });
21746
- // Check if we have any converted modifiers
21747
- if (conversionMap.size) {
21748
- const modifierListClone = clone(modifierList);
21749
- // Replace the original modifiers with the converted ones
21750
- modifierListClone.children = modifierListClone.children.map((modifier, index) => {
21751
- const convertedModifier = conversionMap.get(index);
21752
- return convertedModifier ?? modifier;
21753
- }).flat();
21754
- return createConversionResult(modifierListClone, true);
21654
+ const PLATFORM_NEGATION = '~';
21655
+ /**
21656
+ * Parses a raw platform string into a platform bitmask.
21657
+ *
21658
+ * @param rawPlatforms Raw platform string, e.g. 'adg_safari_any|adg_os_any'.
21659
+ *
21660
+ * @returns Platform bitmask.
21661
+ */
21662
+ const parseRawPlatforms = rawPlatforms => {
21663
+ // e.g. 'adg_safari_any|adg_os_any'
21664
+ const rawPlatformList = rawPlatforms.split(PLATFORM_SEPARATOR).map(rawPlatform => rawPlatform.trim());
21665
+ let result = 0;
21666
+ for (let rawPlatform of rawPlatformList) {
21667
+ // negation, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except Safari content blockers
21668
+ let negated = false;
21669
+ if (rawPlatform.startsWith(PLATFORM_NEGATION)) {
21670
+ negated = true;
21671
+ rawPlatform = rawPlatform.slice(1).trim();
21755
21672
  }
21756
- // Otherwise, just return the original modifier list
21757
- return createConversionResult(modifierList, false);
21758
- }
21759
- }
21760
- const ERROR_MESSAGES$1 = {
21761
- // eslint-disable-next-line max-len
21762
- INVALID_ATTRIBUTE_VALUE: `Expected '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.Ident)}' or '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.String)}' as attribute value, but got '%s' with value '%s`
21673
+ const platform = SPECIFIC_PLATFORM_MAP.get(rawPlatform) ?? GENERIC_PLATFORM_MAP.get(rawPlatform);
21674
+ if (isUndefined(platform)) {
21675
+ throw new Error(`Unknown platform: ${rawPlatform}`);
21676
+ }
21677
+ if (negated) {
21678
+ result &= ~platform;
21679
+ } else {
21680
+ result |= platform;
21681
+ }
21682
+ }
21683
+ if (result === 0) {
21684
+ throw new Error('No platforms specified');
21685
+ }
21686
+ return result;
21763
21687
  };
21764
- var PseudoClasses;
21765
- (function (PseudoClasses) {
21766
- PseudoClasses["AbpContains"] = "-abp-contains";
21767
- PseudoClasses["AbpHas"] = "-abp-has";
21768
- PseudoClasses["Contains"] = "contains";
21769
- PseudoClasses["Has"] = "has";
21770
- PseudoClasses["HasText"] = "has-text";
21771
- PseudoClasses["MatchesCss"] = "matches-css";
21772
- PseudoClasses["MatchesCssAfter"] = "matches-css-after";
21773
- PseudoClasses["MatchesCssBefore"] = "matches-css-before";
21774
- PseudoClasses["Not"] = "not";
21775
- })(PseudoClasses || (PseudoClasses = {}));
21776
- var PseudoElements;
21777
- (function (PseudoElements) {
21778
- PseudoElements["After"] = "after";
21779
- PseudoElements["Before"] = "before";
21780
- })(PseudoElements || (PseudoElements = {}));
21781
- const PSEUDO_ELEMENT_NAMES = new Set([PseudoElements.After, PseudoElements.Before]);
21782
21688
  /**
21783
- * CSS selector converter
21784
- *
21785
- * @todo Implement `convertToUbo` and `convertToAbp`
21689
+ * Platform schema.
21786
21690
  */
21787
- class CssSelectorConverter extends ConverterBase {
21788
- /**
21789
- * Converts Extended CSS elements to AdGuard-compatible ones
21790
- *
21791
- * @param selectorList Selector list to convert
21792
- * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
21793
- * the converted node, and its `isConverted` flag indicates whether the original node was converted.
21794
- * If the node was not converted, the result will contain the original node with the same object reference
21795
- * @throws If the rule is invalid or incompatible
21796
- */
21797
- static convertToAdg(selectorList) {
21798
- const stream = selectorList instanceof CssTokenStream ? selectorList : new CssTokenStream(selectorList);
21799
- const converted = [];
21800
- const convertAndPushPseudo = pseudo => {
21801
- switch (pseudo) {
21802
- case PseudoClasses.AbpContains:
21803
- case PseudoClasses.HasText:
21804
- converted.push(PseudoClasses.Contains);
21805
- converted.push(OPEN_PARENTHESIS);
21806
- break;
21807
- case PseudoClasses.AbpHas:
21808
- converted.push(PseudoClasses.Has);
21809
- converted.push(OPEN_PARENTHESIS);
21810
- break;
21811
- // a bit special case:
21812
- // - `:matches-css-before(...)` → `:matches-css(before, ...)`
21813
- // - `:matches-css-after(...)` → `:matches-css(after, ...)`
21814
- case PseudoClasses.MatchesCssBefore:
21815
- case PseudoClasses.MatchesCssAfter:
21816
- converted.push(PseudoClasses.MatchesCss);
21817
- converted.push(OPEN_PARENTHESIS);
21818
- converted.push(pseudo.substring(PseudoClasses.MatchesCss.length + 1));
21819
- converted.push(COMMA);
21820
- break;
21821
- default:
21822
- converted.push(pseudo);
21823
- converted.push(OPEN_PARENTHESIS);
21824
- break;
21825
- }
21826
- };
21827
- while (!stream.isEof()) {
21828
- const token = stream.getOrFail();
21829
- if (token.type === cssTokenizer.TokenType.Colon) {
21830
- // Advance colon
21831
- stream.advance();
21832
- converted.push(COLON);
21833
- const tempToken = stream.getOrFail();
21834
- // Double colon is a pseudo-element
21835
- if (tempToken.type === cssTokenizer.TokenType.Colon) {
21836
- stream.advance();
21837
- converted.push(COLON);
21838
- continue;
21839
- }
21840
- if (tempToken.type === cssTokenizer.TokenType.Ident) {
21841
- const name = stream.source.slice(tempToken.start, tempToken.end);
21842
- if (PSEUDO_ELEMENT_NAMES.has(name)) {
21843
- // Add an extra colon to the name
21844
- converted.push(COLON);
21845
- converted.push(name);
21846
- } else {
21847
- // Add the name as is
21848
- converted.push(name);
21849
- }
21850
- // Advance the names
21851
- stream.advance();
21852
- } else if (tempToken.type === cssTokenizer.TokenType.Function) {
21853
- const name = stream.source.slice(tempToken.start, tempToken.end - 1); // omit the last parenthesis
21854
- // :-abp-contains(...) → :contains(...)
21855
- // :has-text(...) → :contains(...)
21856
- // :-abp-has(...) → :has(...)
21857
- convertAndPushPseudo(name);
21858
- // Advance the function name
21859
- stream.advance();
21860
- }
21861
- } else if (token.type === cssTokenizer.TokenType.OpenSquareBracket) {
21862
- let tempToken;
21863
- const {
21864
- start
21865
- } = token;
21866
- stream.advance();
21867
- // Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
21868
- // For example:
21869
- // - `[-ext-has=...]` → `:has(...)`
21870
- // - `[-ext-contains=...]` → `:contains(...)`
21871
- // - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
21872
- stream.skipWhitespace();
21873
- stream.expect(cssTokenizer.TokenType.Ident);
21874
- tempToken = stream.getOrFail();
21875
- let attr = stream.source.slice(tempToken.start, tempToken.end);
21876
- // Skip if the attribute name is not a legacy Extended CSS one
21877
- if (!(attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX) || attr.startsWith(ABP_EXT_CSS_PREFIX))) {
21878
- converted.push(stream.source.slice(start, tempToken.end));
21879
- stream.advance();
21880
- continue;
21881
- }
21882
- if (attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX)) {
21883
- attr = attr.slice(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
21884
- }
21885
- stream.advance();
21886
- stream.skipWhitespace();
21887
- // Next token should be an equality operator (=), because Extended CSS attribute selectors
21888
- // do not support other operators
21889
- stream.expect(cssTokenizer.TokenType.Delim, {
21890
- value: EQUALS
21891
- });
21892
- stream.advance();
21893
- // Skip optional whitespace after the operator
21894
- stream.skipWhitespace();
21895
- // Parse attribute value
21896
- tempToken = stream.getOrFail();
21897
- // According to the spec, attribute value should be an identifier or a string
21898
- if (tempToken.type !== cssTokenizer.TokenType.Ident && tempToken.type !== cssTokenizer.TokenType.String) {
21899
- throw new Error(sprintfJs.sprintf(ERROR_MESSAGES$1.INVALID_ATTRIBUTE_VALUE, cssTokenizer.getFormattedTokenName(tempToken.type), stream.source.slice(tempToken.start, tempToken.end)));
21900
- }
21901
- const value = stream.source.slice(tempToken.start, tempToken.end);
21902
- // Advance the attribute value
21903
- stream.advance();
21904
- // Skip optional whitespace after the attribute value
21905
- stream.skipWhitespace();
21906
- // Next character should be a closing square bracket
21907
- // We don't allow flags for Extended CSS attribute selectors
21908
- stream.expect(cssTokenizer.TokenType.CloseSquareBracket);
21909
- stream.advance();
21910
- converted.push(COLON);
21911
- convertAndPushPseudo(attr);
21912
- let processedValue = value.slice(1, -1); // omit the quotes
21913
- if (attr === PseudoClasses.Has) {
21914
- // TODO: Optimize this to avoid double tokenization
21915
- processedValue = CssSelectorConverter.convertToAdg(processedValue).result;
21916
- }
21917
- converted.push(processedValue);
21918
- converted.push(CLOSE_PARENTHESIS);
21919
- } else {
21920
- converted.push(stream.source.slice(token.start, token.end));
21921
- // Advance the token
21922
- stream.advance();
21923
- }
21924
- }
21925
- const convertedSelectorList = converted.join(EMPTY);
21926
- return createConversionResult(convertedSelectorList, stream.source !== convertedSelectorList);
21927
- }
21691
+ zod.string().min(1).transform(value => parseRawPlatforms(value));
21692
+ function getDefaultExportFromCjs(x) {
21693
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
21928
21694
  }
21695
+ var mapObj$1 = {
21696
+ exports: {}
21697
+ };
21698
+ const isObject$1 = value => typeof value === 'object' && value !== null;
21699
+ const mapObjectSkip = Symbol('skip');
21929
21700
 
21930
- /**
21931
- * @file CSS injection rule converter
21932
- */
21933
- /**
21934
- * CSS injection rule converter class
21935
- *
21936
- * @todo Implement `convertToUbo` and `convertToAbp`
21937
- */
21938
- class CssInjectionRuleConverter extends RuleConverterBase {
21939
- /**
21940
- * Converts a CSS injection rule to AdGuard format, if possible.
21941
- *
21942
- * @param rule Rule node to convert
21943
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
21944
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
21945
- * If the rule was not converted, the result array will contain the original node with the same object reference
21946
- * @throws If the rule is invalid or cannot be converted
21947
- */
21948
- static convertToAdg(rule) {
21949
- const separator = rule.separator.value;
21950
- let convertedSeparator = separator;
21951
- const stream = new CssTokenStream(rule.body.selectorList.value);
21952
- const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
21953
- // Change the separator if the rule contains ExtendedCSS elements,
21954
- // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
21955
- // because sometimes we use it to force executing ExtendedCSS library.
21956
- if (stream.hasAnySelectorExtendedCssNodeStrict() || rule.body.remove) {
21957
- convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgExtendedCssInjectionException : exports.CosmeticRuleSeparator.AdgExtendedCssInjection;
21958
- } else if (rule.syntax !== exports.AdblockSyntax.Adg) {
21959
- // If the original rule syntax is not AdGuard, use the default separator
21960
- // e.g. if the input rule is from uBO, we need to convert ## to #$#.
21961
- convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgCssInjectionException : exports.CosmeticRuleSeparator.AdgCssInjection;
21962
- }
21963
- // Check if the rule needs to be converted
21964
- if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
21965
- // TODO: Replace with custom clone method
21966
- const ruleClone = clone(rule);
21967
- ruleClone.syntax = exports.AdblockSyntax.Adg;
21968
- ruleClone.separator.value = convertedSeparator;
21969
- ruleClone.body.selectorList.value = convertedSelectorList.result;
21970
- return createNodeConversionResult([ruleClone], true);
21971
- }
21972
- // Otherwise, return the original rule
21973
- return createNodeConversionResult([rule], false);
21701
+ // Customized for this use-case
21702
+ const isObjectCustom = value => isObject$1(value) && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
21703
+ const mapObject = (object, mapper, options, isSeen = new WeakMap()) => {
21704
+ options = {
21705
+ deep: false,
21706
+ target: {},
21707
+ ...options
21708
+ };
21709
+ if (isSeen.has(object)) {
21710
+ return isSeen.get(object);
21974
21711
  }
21975
- }
21712
+ isSeen.set(object, options.target);
21713
+ const {
21714
+ target
21715
+ } = options;
21716
+ delete options.target;
21717
+ const mapArray = array => array.map(element => isObjectCustom(element) ? mapObject(element, mapper, options, isSeen) : element);
21718
+ if (Array.isArray(object)) {
21719
+ return mapArray(object);
21720
+ }
21721
+ for (const [key, value] of Object.entries(object)) {
21722
+ const mapResult = mapper(key, value, object);
21723
+ if (mapResult === mapObjectSkip) {
21724
+ continue;
21725
+ }
21726
+ let [newKey, newValue, {
21727
+ shouldRecurse = true
21728
+ } = {}] = mapResult;
21976
21729
 
21977
- /**
21978
- * @file Element hiding rule converter
21979
- */
21980
- /**
21981
- * Element hiding rule converter class
21982
- *
21983
- * @todo Implement `convertToUbo` and `convertToAbp`
21984
- */
21985
- class ElementHidingRuleConverter extends RuleConverterBase {
21986
- /**
21987
- * Converts an element hiding rule to AdGuard format, if possible.
21988
- *
21989
- * @param rule Rule node to convert
21990
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
21991
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
21992
- * If the rule was not converted, the result array will contain the original node with the same object reference
21993
- * @throws If the rule is invalid or cannot be converted
21994
- */
21995
- static convertToAdg(rule) {
21996
- const separator = rule.separator.value;
21997
- let convertedSeparator = separator;
21998
- const stream = new CssTokenStream(rule.body.selectorList.value);
21999
- const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
22000
- // Change the separator if the rule contains ExtendedCSS elements,
22001
- // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
22002
- // because sometimes we use it to force executing ExtendedCSS library.
22003
- if (stream.hasAnySelectorExtendedCssNodeStrict()) {
22004
- convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.ExtendedElementHidingException : exports.CosmeticRuleSeparator.ExtendedElementHiding;
21730
+ // Drop `__proto__` keys.
21731
+ if (newKey === '__proto__') {
21732
+ continue;
22005
21733
  }
22006
- // Check if the rule needs to be converted
22007
- if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
22008
- // TODO: Replace with custom clone method
22009
- const ruleClone = clone(rule);
22010
- ruleClone.syntax = exports.AdblockSyntax.Adg;
22011
- ruleClone.separator.value = convertedSeparator;
22012
- ruleClone.body.selectorList.value = convertedSelectorList.result;
22013
- return createNodeConversionResult([ruleClone], true);
21734
+ if (options.deep && shouldRecurse && isObjectCustom(newValue)) {
21735
+ newValue = Array.isArray(newValue) ? mapArray(newValue) : mapObject(newValue, mapper, options, isSeen);
22014
21736
  }
22015
- // Otherwise, return the original rule
22016
- return createNodeConversionResult([rule], false);
21737
+ target[newKey] = newValue;
22017
21738
  }
22018
- }
22019
-
22020
- /**
22021
- * @file Utility functions for working with network rule nodes
22022
- */
22023
- /**
22024
- * Creates a network rule node
22025
- *
22026
- * @param pattern Rule pattern
22027
- * @param modifiers Rule modifiers (optional, default: undefined)
22028
- * @param exception Exception rule flag (optional, default: false)
22029
- * @param syntax Adblock syntax (optional, default: Common)
22030
- * @returns Network rule node
22031
- */
22032
- function createNetworkRuleNode(pattern, modifiers = undefined, exception = false, syntax = exports.AdblockSyntax.Common) {
22033
- const result = {
22034
- category: exports.RuleCategory.Network,
22035
- type: exports.NetworkRuleType.NetworkRule,
22036
- syntax,
22037
- exception,
22038
- pattern: {
22039
- type: 'Value',
22040
- value: pattern
21739
+ return target;
21740
+ };
21741
+ mapObj$1.exports = (object, mapper, options) => {
21742
+ if (!isObject$1(object)) {
21743
+ throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`);
21744
+ }
21745
+ return mapObject(object, mapper, options);
21746
+ };
21747
+ mapObj$1.exports.mapObjectSkip = mapObjectSkip;
21748
+ var mapObjExports = mapObj$1.exports;
21749
+ var camelcase = {
21750
+ exports: {}
21751
+ };
21752
+ const UPPERCASE = /[\p{Lu}]/u;
21753
+ const LOWERCASE = /[\p{Ll}]/u;
21754
+ const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
21755
+ const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
21756
+ const SEPARATORS = /[_.\- ]+/;
21757
+ const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
21758
+ const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
21759
+ const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
21760
+ const preserveCamelCase = (string, toLowerCase, toUpperCase) => {
21761
+ let isLastCharLower = false;
21762
+ let isLastCharUpper = false;
21763
+ let isLastLastCharUpper = false;
21764
+ for (let i = 0; i < string.length; i++) {
21765
+ const character = string[i];
21766
+ if (isLastCharLower && UPPERCASE.test(character)) {
21767
+ string = string.slice(0, i) + '-' + string.slice(i);
21768
+ isLastCharLower = false;
21769
+ isLastLastCharUpper = isLastCharUpper;
21770
+ isLastCharUpper = true;
21771
+ i++;
21772
+ } else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character)) {
21773
+ string = string.slice(0, i - 1) + '-' + string.slice(i - 1);
21774
+ isLastLastCharUpper = isLastCharUpper;
21775
+ isLastCharUpper = false;
21776
+ isLastCharLower = true;
21777
+ } else {
21778
+ isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
21779
+ isLastLastCharUpper = isLastCharUpper;
21780
+ isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
22041
21781
  }
21782
+ }
21783
+ return string;
21784
+ };
21785
+ const preserveConsecutiveUppercase = (input, toLowerCase) => {
21786
+ LEADING_CAPITAL.lastIndex = 0;
21787
+ return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1));
21788
+ };
21789
+ const postProcess = (input, toUpperCase) => {
21790
+ SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
21791
+ NUMBERS_AND_IDENTIFIER.lastIndex = 0;
21792
+ return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)).replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m));
21793
+ };
21794
+ const camelCase$1 = (input, options) => {
21795
+ if (!(typeof input === 'string' || Array.isArray(input))) {
21796
+ throw new TypeError('Expected the input to be `string | string[]`');
21797
+ }
21798
+ options = {
21799
+ pascalCase: false,
21800
+ preserveConsecutiveUppercase: false,
21801
+ ...options
22042
21802
  };
22043
- if (!isUndefined(modifiers)) {
22044
- result.modifiers = clone(modifiers);
21803
+ if (Array.isArray(input)) {
21804
+ input = input.map(x => x.trim()).filter(x => x.length).join('-');
21805
+ } else {
21806
+ input = input.trim();
22045
21807
  }
22046
- return result;
22047
- }
22048
-
22049
- /**
22050
- * @file Converter for request header removal rules
22051
- */
22052
- const UBO_RESPONSEHEADER_FN = 'responseheader';
22053
- const ADG_REMOVEHEADER_MODIFIER = 'removeheader';
22054
- const ERROR_MESSAGES = {
22055
- EMPTY_PARAMETER: `Empty parameter for '${UBO_RESPONSEHEADER_FN}' function`,
22056
- EXPECTED_END_OF_RULE: "Expected end of rule, but got '%s'",
22057
- MULTIPLE_DOMAINS_NOT_SUPPORTED: 'Multiple domains are not supported yet'
21808
+ if (input.length === 0) {
21809
+ return '';
21810
+ }
21811
+ const toLowerCase = options.locale === false ? string => string.toLowerCase() : string => string.toLocaleLowerCase(options.locale);
21812
+ const toUpperCase = options.locale === false ? string => string.toUpperCase() : string => string.toLocaleUpperCase(options.locale);
21813
+ if (input.length === 1) {
21814
+ return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
21815
+ }
21816
+ const hasUpperCase = input !== toLowerCase(input);
21817
+ if (hasUpperCase) {
21818
+ input = preserveCamelCase(input, toLowerCase, toUpperCase);
21819
+ }
21820
+ input = input.replace(LEADING_SEPARATORS, '');
21821
+ if (options.preserveConsecutiveUppercase) {
21822
+ input = preserveConsecutiveUppercase(input, toLowerCase);
21823
+ } else {
21824
+ input = toLowerCase(input);
21825
+ }
21826
+ if (options.pascalCase) {
21827
+ input = toUpperCase(input.charAt(0)) + input.slice(1);
21828
+ }
21829
+ return postProcess(input, toUpperCase);
22058
21830
  };
22059
- /**
22060
- * Converter for request header removal rules
22061
- *
22062
- * @todo Implement `convertToUbo` (ABP currently doesn't support header removal rules)
22063
- */
22064
- class HeaderRemovalRuleConverter extends RuleConverterBase {
22065
- /**
22066
- * Converts a header removal rule to AdGuard syntax, if possible.
22067
- *
22068
- * @param rule Rule node to convert
22069
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22070
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22071
- * If the rule was not converted, the result array will contain the original node with the same object reference
22072
- * @throws If the rule is invalid or cannot be converted
22073
- * @example
22074
- * If the input rule is:
22075
- * ```adblock
22076
- * example.com##^responseheader(header-name)
22077
- * ```
22078
- * The output will be:
22079
- * ```adblock
22080
- * ||example.com^$removeheader=header-name
22081
- * ```
22082
- */
22083
- static convertToAdg(rule) {
22084
- // TODO: Add support for ABP syntax once it starts supporting header removal rules
22085
- // Leave the rule as is if it's not a header removal rule
22086
- if (rule.category !== exports.RuleCategory.Cosmetic || rule.type !== exports.CosmeticRuleType.HtmlFilteringRule) {
22087
- return createNodeConversionResult([rule], false);
21831
+ camelcase.exports = camelCase$1;
21832
+ // TODO: Remove this for the next major release
21833
+ camelcase.exports.default = camelCase$1;
21834
+ var camelcaseExports = camelcase.exports;
21835
+ class QuickLRU {
21836
+ constructor(options = {}) {
21837
+ if (!(options.maxSize && options.maxSize > 0)) {
21838
+ throw new TypeError('`maxSize` must be a number greater than 0');
22088
21839
  }
22089
- const stream = new CssTokenStream(rule.body.value);
22090
- let token;
22091
- // Skip leading whitespace
22092
- stream.skipWhitespace();
22093
- // Next token should be the `^` followed by a `responseheader` function
22094
- token = stream.get();
22095
- if (!token || token.type !== cssTokenizer.TokenType.Delim || rule.body.value[token.start] !== UBO_HTML_MASK) {
22096
- return createNodeConversionResult([rule], false);
21840
+ this.maxSize = options.maxSize;
21841
+ this.onEviction = options.onEviction;
21842
+ this.cache = new Map();
21843
+ this.oldCache = new Map();
21844
+ this._size = 0;
21845
+ }
21846
+ _set(key, value) {
21847
+ this.cache.set(key, value);
21848
+ this._size++;
21849
+ if (this._size >= this.maxSize) {
21850
+ this._size = 0;
21851
+ if (typeof this.onEviction === 'function') {
21852
+ for (const [key, value] of this.oldCache.entries()) {
21853
+ this.onEviction(key, value);
21854
+ }
21855
+ }
21856
+ this.oldCache = this.cache;
21857
+ this.cache = new Map();
22097
21858
  }
22098
- stream.advance();
22099
- token = stream.get();
22100
- if (!token) {
22101
- return createNodeConversionResult([rule], false);
21859
+ }
21860
+ get(key) {
21861
+ if (this.cache.has(key)) {
21862
+ return this.cache.get(key);
21863
+ }
21864
+ if (this.oldCache.has(key)) {
21865
+ const value = this.oldCache.get(key);
21866
+ this.oldCache.delete(key);
21867
+ this._set(key, value);
21868
+ return value;
21869
+ }
21870
+ }
21871
+ set(key, value) {
21872
+ if (this.cache.has(key)) {
21873
+ this.cache.set(key, value);
21874
+ } else {
21875
+ this._set(key, value);
21876
+ }
21877
+ return this;
21878
+ }
21879
+ has(key) {
21880
+ return this.cache.has(key) || this.oldCache.has(key);
21881
+ }
21882
+ peek(key) {
21883
+ if (this.cache.has(key)) {
21884
+ return this.cache.get(key);
22102
21885
  }
22103
- const functionName = rule.body.value.slice(token.start, token.end - 1);
22104
- if (functionName !== UBO_RESPONSEHEADER_FN) {
22105
- return createNodeConversionResult([rule], false);
21886
+ if (this.oldCache.has(key)) {
21887
+ return this.oldCache.get(key);
22106
21888
  }
22107
- // Parse the parameter
22108
- const paramStart = token.end;
22109
- stream.skipUntilBalanced();
22110
- const paramEnd = stream.getOrFail().end;
22111
- const param = rule.body.value.slice(paramStart, paramEnd - 1).trim();
22112
- // Do not allow empty parameter
22113
- if (param.length === 0) {
22114
- throw new RuleConversionError(ERROR_MESSAGES.EMPTY_PARAMETER);
21889
+ }
21890
+ delete(key) {
21891
+ const deleted = this.cache.delete(key);
21892
+ if (deleted) {
21893
+ this._size--;
22115
21894
  }
22116
- stream.expect(cssTokenizer.TokenType.CloseParenthesis);
22117
- stream.advance();
22118
- // Skip trailing whitespace after the function call
22119
- stream.skipWhitespace();
22120
- // Expect the end of the rule - so nothing should be left in the stream
22121
- if (!stream.isEof()) {
22122
- token = stream.getOrFail();
22123
- throw new RuleConversionError(sprintfJs.sprintf(ERROR_MESSAGES.EXPECTED_END_OF_RULE, cssTokenizer.getFormattedTokenName(token.type)));
21895
+ return this.oldCache.delete(key) || deleted;
21896
+ }
21897
+ clear() {
21898
+ this.cache.clear();
21899
+ this.oldCache.clear();
21900
+ this._size = 0;
21901
+ }
21902
+ *keys() {
21903
+ for (const [key] of this) {
21904
+ yield key;
22124
21905
  }
22125
- // Prepare network rule pattern
22126
- const pattern = [];
22127
- if (rule.domains.children.length === 1) {
22128
- // If the rule has only one domain, we can use a simple network rule pattern:
22129
- // ||single-domain-from-the-rule^
22130
- pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
22131
- } else if (rule.domains.children.length > 1) {
22132
- // TODO: Add support for multiple domains, for example:
22133
- // example.com,example.org,example.net##^responseheader(header-name)
22134
- // We should consider allowing $domain with $removeheader modifier,
22135
- // for example:
22136
- // $removeheader=header-name,domain=example.com|example.org|example.net
22137
- throw new RuleConversionError(ERROR_MESSAGES.MULTIPLE_DOMAINS_NOT_SUPPORTED);
21906
+ }
21907
+ *values() {
21908
+ for (const [, value] of this) {
21909
+ yield value;
22138
21910
  }
22139
- // Prepare network rule modifiers
22140
- const modifiers = createModifierListNode();
22141
- modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, param));
22142
- // Construct the network rule
22143
- return createNodeConversionResult([createNetworkRuleNode(pattern.join(EMPTY), modifiers,
22144
- // Copy the exception flag
22145
- rule.exception, exports.AdblockSyntax.Adg)], true);
22146
21911
  }
22147
- }
22148
-
22149
- /**
22150
- * @file Cosmetic rule converter
22151
- */
22152
- /**
22153
- * Cosmetic rule converter class (also known as "non-basic rule converter")
22154
- *
22155
- * @todo Implement `convertToUbo` and `convertToAbp`
22156
- */
22157
- class CosmeticRuleConverter extends RuleConverterBase {
22158
- /**
22159
- * Converts a cosmetic rule to AdGuard syntax, if possible.
22160
- *
22161
- * @param rule Rule node to convert
22162
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22163
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22164
- * If the rule was not converted, the result array will contain the original node with the same object reference
22165
- * @throws If the rule is invalid or cannot be converted
22166
- */
22167
- static convertToAdg(rule) {
22168
- let subconverterResult;
22169
- // Convert cosmetic rule based on its type
22170
- switch (rule.type) {
22171
- case exports.CosmeticRuleType.ElementHidingRule:
22172
- subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
22173
- break;
22174
- case exports.CosmeticRuleType.ScriptletInjectionRule:
22175
- subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
22176
- break;
22177
- case exports.CosmeticRuleType.CssInjectionRule:
22178
- subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
22179
- break;
22180
- case exports.CosmeticRuleType.HtmlFilteringRule:
22181
- // Handle special case: uBO response header filtering rule
22182
- // TODO: Optimize double CSS tokenization here
22183
- subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
22184
- if (subconverterResult.isConverted) {
22185
- break;
22186
- }
22187
- subconverterResult = HtmlRuleConverter.convertToAdg(rule);
22188
- break;
22189
- // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
22190
- case exports.CosmeticRuleType.JsInjectionRule:
22191
- subconverterResult = createNodeConversionResult([rule], false);
22192
- break;
22193
- default:
22194
- throw new RuleConversionError('Unsupported cosmetic rule type');
21912
+ *[Symbol.iterator]() {
21913
+ for (const item of this.cache) {
21914
+ yield item;
22195
21915
  }
22196
- let convertedModifiers;
22197
- // Convert cosmetic rule modifiers, if any
22198
- if (rule.modifiers) {
22199
- if (rule.syntax === exports.AdblockSyntax.Ubo) {
22200
- // uBO doesn't support this rule:
22201
- // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
22202
- if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
22203
- throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
22204
- }
22205
- convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
22206
- } else if (rule.syntax === exports.AdblockSyntax.Abp) {
22207
- // TODO: Implement once ABP starts supporting cosmetic rule modifiers
22208
- throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
21916
+ for (const item of this.oldCache) {
21917
+ const [key] = item;
21918
+ if (!this.cache.has(key)) {
21919
+ yield item;
22209
21920
  }
22210
21921
  }
22211
- if (subconverterResult.result.length > 1 || subconverterResult.isConverted || convertedModifiers && convertedModifiers.isConverted) {
22212
- // Add modifier list to the subconverter result rules
22213
- subconverterResult.result.forEach(subconverterRule => {
22214
- if (convertedModifiers && subconverterRule.category === exports.RuleCategory.Cosmetic) {
22215
- // eslint-disable-next-line no-param-reassign
22216
- subconverterRule.modifiers = convertedModifiers.result;
22217
- }
22218
- });
22219
- return subconverterResult;
21922
+ }
21923
+ get size() {
21924
+ let oldCacheSize = 0;
21925
+ for (const key of this.oldCache.keys()) {
21926
+ if (!this.cache.has(key)) {
21927
+ oldCacheSize++;
21928
+ }
22220
21929
  }
22221
- return createNodeConversionResult([rule], false);
21930
+ return Math.min(this._size + oldCacheSize, this.maxSize);
22222
21931
  }
22223
21932
  }
21933
+ var quickLru = QuickLRU;
21934
+ const mapObj = mapObjExports;
21935
+ const camelCase = camelcaseExports;
21936
+ const QuickLru = quickLru;
21937
+ const has = (array, key) => array.some(x => {
21938
+ if (typeof x === 'string') {
21939
+ return x === key;
21940
+ }
21941
+ x.lastIndex = 0;
21942
+ return x.test(key);
21943
+ });
21944
+ const cache = new QuickLru({
21945
+ maxSize: 100000
21946
+ });
22224
21947
 
22225
- /**
22226
- * @file Compatibility tables for redirects.
22227
- */
22228
- /**
22229
- * Prefix for resource redirection names.
22230
- */
22231
- const ABP_RESOURCE_PREFIX = 'abp-resource:';
22232
- const ABP_RESOURCE_PREFIX_LENGTH = ABP_RESOURCE_PREFIX.length;
22233
- /**
22234
- * Transforms the name of an ABP redirect to a normalized form.
22235
- *
22236
- * @param name Redirect name to normalize.
22237
- *
22238
- * @returns Normalized redirect name.
22239
- *
22240
- * @example
22241
- * abpRedirectNameNormalizer('abp-resource:my-resource') // => 'my-resource'
22242
- */
22243
- const abpRedirectNameNormalizer = name => {
22244
- if (name.startsWith(ABP_RESOURCE_PREFIX)) {
22245
- return name.slice(ABP_RESOURCE_PREFIX_LENGTH);
21948
+ // Reproduces behavior from `map-obj`
21949
+ const isObject = value => typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
21950
+ const camelCaseConvert = (input, options) => {
21951
+ if (!isObject(input)) {
21952
+ return input;
22246
21953
  }
22247
- return name;
21954
+ options = {
21955
+ deep: false,
21956
+ pascalCase: false,
21957
+ ...options
21958
+ };
21959
+ const {
21960
+ exclude,
21961
+ pascalCase,
21962
+ stopPaths,
21963
+ deep
21964
+ } = options;
21965
+ const stopPathsSet = new Set(stopPaths);
21966
+ const makeMapper = parentPath => (key, value) => {
21967
+ if (deep && isObject(value)) {
21968
+ const path = parentPath === undefined ? key : `${parentPath}.${key}`;
21969
+ if (!stopPathsSet.has(path)) {
21970
+ value = mapObj(value, makeMapper(path));
21971
+ }
21972
+ }
21973
+ if (!(exclude && has(exclude, key))) {
21974
+ const cacheKey = pascalCase ? `${key}_` : key;
21975
+ if (cache.has(cacheKey)) {
21976
+ key = cache.get(cacheKey);
21977
+ } else {
21978
+ const returnValue = camelCase(key, {
21979
+ pascalCase,
21980
+ locale: false
21981
+ });
21982
+ if (key.length < 100) {
21983
+ // Prevent abuse
21984
+ cache.set(cacheKey, returnValue);
21985
+ }
21986
+ key = returnValue;
21987
+ }
21988
+ }
21989
+ return [key, value];
21990
+ };
21991
+ return mapObj(input, makeMapper(undefined));
22248
21992
  };
22249
- /**
22250
- * Compatibility table for redirects.
22251
- */
22252
- class RedirectsCompatibilityTable extends CompatibilityTableBase {
22253
- /**
22254
- * Creates a new instance of the compatibility table for redirects.
22255
- *
22256
- * @param data Compatibility table data.
22257
- */
22258
- constructor(data) {
22259
- super(data, abpRedirectNameNormalizer);
21993
+ var camelcaseKeys = (input, options) => {
21994
+ if (Array.isArray(input)) {
21995
+ return Object.keys(input).map(key => camelCaseConvert(input[key], options));
22260
21996
  }
22261
- }
22262
- /**
22263
- * Deep freeze the compatibility table data to avoid accidental modifications.
22264
- */
22265
- deepFreeze(redirectsCompatibilityTableData);
22266
- /**
22267
- * Compatibility table instance for redirects.
22268
- */
22269
- const redirectsCompatibilityTable = new RedirectsCompatibilityTable(redirectsCompatibilityTableData);
21997
+ return camelCaseConvert(input, options);
21998
+ };
21999
+ var camelCaseKeys = /*@__PURE__*/getDefaultExportFromCjs(camelcaseKeys);
22270
22000
 
22271
22001
  /**
22272
- * @file Compatibility tables for scriptlets.
22002
+ * @file Zod camelCase utility.
22273
22003
  */
22004
+ // eslint-disable-next-line import/no-extraneous-dependencies
22274
22005
  /**
22275
- * Compatibility table for scriptlets.
22006
+ * Transforms Zod schema to camelCase.
22007
+ *
22008
+ * @param zod Zod schema.
22009
+ *
22010
+ * @returns Zod schema with camelCase properties.
22011
+ *
22012
+ * @see {@link https://github.com/colinhacks/zod/issues/486#issuecomment-1501097361}
22276
22013
  */
22277
- class ScriptletsCompatibilityTable extends CompatibilityTableBase {}
22014
+ const zodToCamelCase = zod => {
22015
+ return zod.transform(val => camelCaseKeys(val));
22016
+ };
22017
+
22278
22018
  /**
22279
- * Deep freeze the compatibility table data to avoid accidental modifications.
22019
+ * @file Base compatibility data schema, which is commonly used in compatibility tables.
22280
22020
  */
22281
- deepFreeze(scriptletsCompatibilityTableData);
22282
22021
  /**
22283
- * Compatibility table instance for scriptlets.
22022
+ * Zod schema for boolean values. Accepts both boolean and string values.
22284
22023
  */
22285
- const scriptletsCompatibilityTable = new ScriptletsCompatibilityTable(scriptletsCompatibilityTableData);
22286
-
22287
- /* eslint-disable no-bitwise */
22024
+ const booleanSchema = zod.union([zod.string().transform(val => val.trim().toLowerCase() === 'true'), zod.boolean()]);
22288
22025
  /**
22289
- * @file Platform schema.
22026
+ * Zod schema for non-empty string values.
22290
22027
  */
22028
+ const nonEmptyStringSchema = zod.string().transform(val => val.trim()).pipe(zod.string().min(1));
22291
22029
  /**
22292
- * Platform separator, e.g. 'adg_os_any|adg_safari_any' means any AdGuard OS platform and
22293
- * any AdGuard Safari content blocker platform.
22030
+ * Zod schema for base compatibility data.
22031
+ * Here we use snake_case properties because the compatibility data is stored in YAML files.
22294
22032
  */
22295
- const PLATFORM_SEPARATOR = '|';
22033
+ const baseCompatibilityDataSchema = zod.object({
22034
+ /**
22035
+ * Name of the actual entity.
22036
+ */
22037
+ name: nonEmptyStringSchema,
22038
+ /**
22039
+ * List of aliases for the entity (if any).
22040
+ */
22041
+ aliases: zod.array(nonEmptyStringSchema).nullable().default(null),
22042
+ /**
22043
+ * Short description of the actual entity.
22044
+ * If not specified or it's value is `null`, then the description is not available.
22045
+ */
22046
+ description: nonEmptyStringSchema.nullable().default(null),
22047
+ /**
22048
+ * Link to the documentation. If not specified or it's value is `null`, then the documentation is not available.
22049
+ */
22050
+ docs: nonEmptyStringSchema.nullable().default(null),
22051
+ /**
22052
+ * The version of the adblocker in which the entity was added.
22053
+ * For AdGuard resources, the version of the library is specified.
22054
+ */
22055
+ version_added: nonEmptyStringSchema.nullable().default(null),
22056
+ /**
22057
+ * The version of the adblocker when the entity was removed.
22058
+ */
22059
+ version_removed: nonEmptyStringSchema.nullable().default(null),
22060
+ /**
22061
+ * Describes whether the entity is deprecated.
22062
+ */
22063
+ deprecated: booleanSchema.default(false),
22064
+ /**
22065
+ * Message that describes why the entity is deprecated.
22066
+ * If not specified or it's value is `null`, then the message is not available.
22067
+ * It's value is omitted if the entity is not marked as deprecated.
22068
+ */
22069
+ deprecation_message: nonEmptyStringSchema.nullable().default(null),
22070
+ /**
22071
+ * Describes whether the entity is removed; for *already removed* features.
22072
+ */
22073
+ removed: booleanSchema.default(false),
22074
+ /**
22075
+ * Message that describes why the entity is removed.
22076
+ * If not specified or it's value is `null`, then the message is not available.
22077
+ * It's value is omitted if the entity is not marked as deprecated.
22078
+ */
22079
+ removal_message: nonEmptyStringSchema.nullable().default(null)
22080
+ });
22296
22081
  /**
22297
- * Platform negation character, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except
22298
- * Safari content blockers.
22082
+ * Zod schema for base compatibility data with camelCase properties.
22299
22083
  */
22300
- const PLATFORM_NEGATION = '~';
22084
+ zodToCamelCase(baseCompatibilityDataSchema);
22301
22085
  /**
22302
- * Parses a raw platform string into a platform bitmask.
22303
- *
22304
- * @param rawPlatforms Raw platform string, e.g. 'adg_safari_any|adg_os_any'.
22086
+ * Refinement logic for base compatibility data.
22305
22087
  *
22306
- * @returns Platform bitmask.
22307
- */
22308
- const parseRawPlatforms = rawPlatforms => {
22309
- // e.g. 'adg_safari_any|adg_os_any'
22310
- const rawPlatformList = rawPlatforms.split(PLATFORM_SEPARATOR).map(rawPlatform => rawPlatform.trim());
22311
- let result = 0;
22312
- for (let rawPlatform of rawPlatformList) {
22313
- // negation, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except Safari content blockers
22314
- let negated = false;
22315
- if (rawPlatform.startsWith(PLATFORM_NEGATION)) {
22316
- negated = true;
22317
- rawPlatform = rawPlatform.slice(1).trim();
22318
- }
22319
- const platform = SPECIFIC_PLATFORM_MAP.get(rawPlatform) ?? GENERIC_PLATFORM_MAP.get(rawPlatform);
22320
- if (isUndefined(platform)) {
22321
- throw new Error(`Unknown platform: ${rawPlatform}`);
22322
- }
22323
- if (negated) {
22324
- result &= ~platform;
22325
- } else {
22326
- result |= platform;
22327
- }
22328
- }
22329
- if (result === 0) {
22330
- throw new Error('No platforms specified');
22331
- }
22332
- return result;
22333
- };
22334
- /**
22335
- * Platform schema.
22088
+ * @param data Base compatibility data.
22089
+ * @param ctx Refinement context.
22336
22090
  */
22337
- zod.string().min(1).transform(value => parseRawPlatforms(value));
22338
- function getDefaultExportFromCjs(x) {
22339
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
22340
- }
22341
- var mapObj$1 = {
22342
- exports: {}
22343
- };
22344
- const isObject$1 = value => typeof value === 'object' && value !== null;
22345
- const mapObjectSkip = Symbol('skip');
22346
-
22347
- // Customized for this use-case
22348
- const isObjectCustom = value => isObject$1(value) && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
22349
- const mapObject = (object, mapper, options, isSeen = new WeakMap()) => {
22350
- options = {
22351
- deep: false,
22352
- target: {},
22353
- ...options
22354
- };
22355
- if (isSeen.has(object)) {
22356
- return isSeen.get(object);
22357
- }
22358
- isSeen.set(object, options.target);
22359
- const {
22360
- target
22361
- } = options;
22362
- delete options.target;
22363
- const mapArray = array => array.map(element => isObjectCustom(element) ? mapObject(element, mapper, options, isSeen) : element);
22364
- if (Array.isArray(object)) {
22365
- return mapArray(object);
22366
- }
22367
- for (const [key, value] of Object.entries(object)) {
22368
- const mapResult = mapper(key, value, object);
22369
- if (mapResult === mapObjectSkip) {
22370
- continue;
22371
- }
22372
- let [newKey, newValue, {
22373
- shouldRecurse = true
22374
- } = {}] = mapResult;
22375
-
22376
- // Drop `__proto__` keys.
22377
- if (newKey === '__proto__') {
22378
- continue;
22379
- }
22380
- if (options.deep && shouldRecurse && isObjectCustom(newValue)) {
22381
- newValue = Array.isArray(newValue) ? mapArray(newValue) : mapObject(newValue, mapper, options, isSeen);
22382
- }
22383
- target[newKey] = newValue;
22384
- }
22385
- return target;
22386
- };
22387
- mapObj$1.exports = (object, mapper, options) => {
22388
- if (!isObject$1(object)) {
22389
- throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`);
22390
- }
22391
- return mapObject(object, mapper, options);
22392
- };
22393
- mapObj$1.exports.mapObjectSkip = mapObjectSkip;
22394
- var mapObjExports = mapObj$1.exports;
22395
- var camelcase = {
22396
- exports: {}
22397
- };
22398
- const UPPERCASE = /[\p{Lu}]/u;
22399
- const LOWERCASE = /[\p{Ll}]/u;
22400
- const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
22401
- const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
22402
- const SEPARATORS = /[_.\- ]+/;
22403
- const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
22404
- const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
22405
- const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
22406
- const preserveCamelCase = (string, toLowerCase, toUpperCase) => {
22407
- let isLastCharLower = false;
22408
- let isLastCharUpper = false;
22409
- let isLastLastCharUpper = false;
22410
- for (let i = 0; i < string.length; i++) {
22411
- const character = string[i];
22412
- if (isLastCharLower && UPPERCASE.test(character)) {
22413
- string = string.slice(0, i) + '-' + string.slice(i);
22414
- isLastCharLower = false;
22415
- isLastLastCharUpper = isLastCharUpper;
22416
- isLastCharUpper = true;
22417
- i++;
22418
- } else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character)) {
22419
- string = string.slice(0, i - 1) + '-' + string.slice(i - 1);
22420
- isLastLastCharUpper = isLastCharUpper;
22421
- isLastCharUpper = false;
22422
- isLastCharLower = true;
22423
- } else {
22424
- isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
22425
- isLastLastCharUpper = isLastCharUpper;
22426
- isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
22427
- }
22428
- }
22429
- return string;
22430
- };
22431
- const preserveConsecutiveUppercase = (input, toLowerCase) => {
22432
- LEADING_CAPITAL.lastIndex = 0;
22433
- return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1));
22434
- };
22435
- const postProcess = (input, toUpperCase) => {
22436
- SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
22437
- NUMBERS_AND_IDENTIFIER.lastIndex = 0;
22438
- return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)).replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m));
22439
- };
22440
- const camelCase$1 = (input, options) => {
22441
- if (!(typeof input === 'string' || Array.isArray(input))) {
22442
- throw new TypeError('Expected the input to be `string | string[]`');
22443
- }
22444
- options = {
22445
- pascalCase: false,
22446
- preserveConsecutiveUppercase: false,
22447
- ...options
22448
- };
22449
- if (Array.isArray(input)) {
22450
- input = input.map(x => x.trim()).filter(x => x.length).join('-');
22451
- } else {
22452
- input = input.trim();
22453
- }
22454
- if (input.length === 0) {
22455
- return '';
22456
- }
22457
- const toLowerCase = options.locale === false ? string => string.toLowerCase() : string => string.toLocaleLowerCase(options.locale);
22458
- const toUpperCase = options.locale === false ? string => string.toUpperCase() : string => string.toLocaleUpperCase(options.locale);
22459
- if (input.length === 1) {
22460
- return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
22461
- }
22462
- const hasUpperCase = input !== toLowerCase(input);
22463
- if (hasUpperCase) {
22464
- input = preserveCamelCase(input, toLowerCase, toUpperCase);
22091
+ const baseRefineLogic = (data, ctx) => {
22092
+ if (data.deprecated && !data.deprecation_message) {
22093
+ ctx.addIssue({
22094
+ code: zod.ZodIssueCode.custom,
22095
+ message: 'deprecation_message is required for deprecated modifiers'
22096
+ });
22465
22097
  }
22466
- input = input.replace(LEADING_SEPARATORS, '');
22467
- if (options.preserveConsecutiveUppercase) {
22468
- input = preserveConsecutiveUppercase(input, toLowerCase);
22469
- } else {
22470
- input = toLowerCase(input);
22098
+ if (!data.deprecated && data.deprecation_message) {
22099
+ ctx.addIssue({
22100
+ code: zod.ZodIssueCode.custom,
22101
+ message: 'deprecation_message is only allowed for deprecated modifiers'
22102
+ });
22471
22103
  }
22472
- if (options.pascalCase) {
22473
- input = toUpperCase(input.charAt(0)) + input.slice(1);
22104
+ if (data.aliases && data.aliases.length !== new Set(data.aliases).size) {
22105
+ ctx.addIssue({
22106
+ code: zod.ZodIssueCode.custom,
22107
+ message: 'Aliases must be unique'
22108
+ });
22474
22109
  }
22475
- return postProcess(input, toUpperCase);
22476
22110
  };
22477
- camelcase.exports = camelCase$1;
22478
- // TODO: Remove this for the next major release
22479
- camelcase.exports.default = camelCase$1;
22480
- var camelcaseExports = camelcase.exports;
22481
- class QuickLRU {
22482
- constructor(options = {}) {
22483
- if (!(options.maxSize && options.maxSize > 0)) {
22484
- throw new TypeError('`maxSize` must be a number greater than 0');
22485
- }
22486
- this.maxSize = options.maxSize;
22487
- this.onEviction = options.onEviction;
22488
- this.cache = new Map();
22489
- this.oldCache = new Map();
22490
- this._size = 0;
22491
- }
22492
- _set(key, value) {
22493
- this.cache.set(key, value);
22494
- this._size++;
22495
- if (this._size >= this.maxSize) {
22496
- this._size = 0;
22497
- if (typeof this.onEviction === 'function') {
22498
- for (const [key, value] of this.oldCache.entries()) {
22499
- this.onEviction(key, value);
22500
- }
22501
- }
22502
- this.oldCache = this.cache;
22503
- this.cache = new Map();
22504
- }
22111
+
22112
+ /**
22113
+ * Checks if error has message.
22114
+ *
22115
+ * @param error Error object.
22116
+ * @returns If param is error.
22117
+ */
22118
+ function isErrorWithMessage(error) {
22119
+ return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string';
22120
+ }
22121
+ /**
22122
+ * Converts error to the error with message.
22123
+ *
22124
+ * @param maybeError Possible error.
22125
+ * @returns Error with message.
22126
+ */
22127
+ function toErrorWithMessage(maybeError) {
22128
+ if (isErrorWithMessage(maybeError)) {
22129
+ return maybeError;
22505
22130
  }
22506
- get(key) {
22507
- if (this.cache.has(key)) {
22508
- return this.cache.get(key);
22509
- }
22510
- if (this.oldCache.has(key)) {
22511
- const value = this.oldCache.get(key);
22512
- this.oldCache.delete(key);
22513
- this._set(key, value);
22514
- return value;
22515
- }
22131
+ try {
22132
+ return new Error(JSON.stringify(maybeError));
22133
+ } catch {
22134
+ // fallback in case there's an error stringifying the maybeError
22135
+ // like with circular references for example.
22136
+ return new Error(String(maybeError));
22516
22137
  }
22517
- set(key, value) {
22518
- if (this.cache.has(key)) {
22519
- this.cache.set(key, value);
22520
- } else {
22521
- this._set(key, value);
22522
- }
22523
- return this;
22138
+ }
22139
+ /**
22140
+ * Converts error object to error with message. This method might be helpful to handle thrown errors.
22141
+ *
22142
+ * @param error Error object.
22143
+ *
22144
+ * @returns Message of the error.
22145
+ */
22146
+ function getErrorMessage(error) {
22147
+ return toErrorWithMessage(error).message;
22148
+ }
22149
+
22150
+ /**
22151
+ * @file Schema for modifier data.
22152
+ */
22153
+ /**
22154
+ * Known validators that don't need to be validated as regex.
22155
+ */
22156
+ const KNOWN_VALIDATORS = new Set(['domain', 'pipe_separated_domains', 'regexp', 'url']);
22157
+ /**
22158
+ * Zod schema for modifier data.
22159
+ */
22160
+ zodToCamelCase(baseCompatibilityDataSchema.extend({
22161
+ /**
22162
+ * List of modifiers that are incompatible with the actual one.
22163
+ */
22164
+ conflicts: zod.array(nonEmptyStringSchema).nullable().default(null),
22165
+ /**
22166
+ * The actual modifier is incompatible with all other modifiers, except the ones listed in `conflicts`.
22167
+ */
22168
+ inverse_conflicts: booleanSchema.default(false),
22169
+ /**
22170
+ * Describes whether the actual modifier supports value assignment. For example, `$domain` is assignable,
22171
+ * so it can be used like this: `$domain=domain.com\|~subdomain.domain.com`, where `=` is the assignment operator
22172
+ * and `domain.com\|~subdomain.domain.com` is the value.
22173
+ */
22174
+ assignable: booleanSchema.default(false),
22175
+ /**
22176
+ * Describes whether the actual modifier can be negated. For example, `$third-party` is negatable,
22177
+ * so it can be used like this: `$~third-party`.
22178
+ */
22179
+ negatable: booleanSchema.default(true),
22180
+ /**
22181
+ * The actual modifier can only be used in blocking rules, it cannot be used in exceptions.
22182
+ * If it's value is `true`, then the modifier can be used only in blocking rules.
22183
+ * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22184
+ */
22185
+ block_only: booleanSchema.default(false),
22186
+ /**
22187
+ * The actual modifier can only be used in exceptions, it cannot be used in blocking rules.
22188
+ * If it's value is `true`, then the modifier can be used only in exceptions.
22189
+ * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22190
+ */
22191
+ exception_only: booleanSchema.default(false),
22192
+ /**
22193
+ * Describes whether the *assignable* modifier value is required.
22194
+ * For example, `$cookie` is assignable but it can be used without a value in exception rules:
22195
+ * `@@\|\|example.com^$cookie`.
22196
+ * If `false`, the `value_format` is required, e.g. the value of `$app` should always be specified
22197
+ */
22198
+ value_optional: booleanSchema.default(false),
22199
+ /**
22200
+ * Describes the format of the value for the *assignable* modifier.
22201
+ * Its value can be a regex pattern or a known validator name (e.g. `domain`, `pipe_separated_domains`, etc.).
22202
+ */
22203
+ value_format: nonEmptyStringSchema.nullable().default(null)
22204
+ }).superRefine((data, ctx) => {
22205
+ // TODO: find something better, for now we can't add refine logic to the base schema:
22206
+ // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22207
+ baseRefineLogic(data, ctx);
22208
+ if (data.block_only && data.exception_only) {
22209
+ ctx.addIssue({
22210
+ code: zod.ZodIssueCode.custom,
22211
+ message: 'block_only and exception_only are mutually exclusive'
22212
+ });
22524
22213
  }
22525
- has(key) {
22526
- return this.cache.has(key) || this.oldCache.has(key);
22214
+ if (data.assignable && !data.value_format) {
22215
+ ctx.addIssue({
22216
+ code: zod.ZodIssueCode.custom,
22217
+ message: 'value_format is required for assignable modifiers'
22218
+ });
22527
22219
  }
22528
- peek(key) {
22529
- if (this.cache.has(key)) {
22530
- return this.cache.get(key);
22531
- }
22532
- if (this.oldCache.has(key)) {
22533
- return this.oldCache.get(key);
22220
+ if (data.value_format) {
22221
+ const valueFormat = data.value_format.trim();
22222
+ // if it is a known validator, we don't need to validate it further
22223
+ if (KNOWN_VALIDATORS.has(valueFormat)) {
22224
+ return;
22534
22225
  }
22535
- }
22536
- delete(key) {
22537
- const deleted = this.cache.delete(key);
22538
- if (deleted) {
22539
- this._size--;
22226
+ // otherwise, we need to validate it as a regex
22227
+ try {
22228
+ XRegExp(valueFormat);
22229
+ } catch (error) {
22230
+ ctx.addIssue({
22231
+ code: zod.ZodIssueCode.custom,
22232
+ message: getErrorMessage(error)
22233
+ });
22540
22234
  }
22541
- return this.oldCache.delete(key) || deleted;
22542
22235
  }
22543
- clear() {
22544
- this.cache.clear();
22545
- this.oldCache.clear();
22546
- this._size = 0;
22236
+ }));
22237
+
22238
+ /**
22239
+ * @file Schema for redirect data.
22240
+ */
22241
+ /**
22242
+ * Zod schema for redirect data.
22243
+ */
22244
+ zodToCamelCase(baseCompatibilityDataSchema.extend({
22245
+ /**
22246
+ * Whether the redirect is blocking.
22247
+ */
22248
+ is_blocking: booleanSchema.default(false)
22249
+ }).superRefine(baseRefineLogic));
22250
+
22251
+ /**
22252
+ * @file Schema for scriptlet data.
22253
+ */
22254
+ /**
22255
+ * Zod schema for scriptlet parameter data.
22256
+ */
22257
+ const scriptletParameterSchema = zod.object({
22258
+ /**
22259
+ * Name of the actual parameter.
22260
+ */
22261
+ name: nonEmptyStringSchema,
22262
+ /**
22263
+ * Describes whether the parameter is required. Empty parameters are not allowed.
22264
+ */
22265
+ required: booleanSchema,
22266
+ /**
22267
+ * Short description of the parameter.
22268
+ * If not specified or it's value is `null`,then the description is not available.
22269
+ */
22270
+ description: nonEmptyStringSchema.nullable().default(null),
22271
+ /**
22272
+ * Regular expression that matches the value of the parameter.
22273
+ * If it's value is `null`, then the parameter value is not checked.
22274
+ */
22275
+ pattern: nonEmptyStringSchema.nullable().default(null),
22276
+ /**
22277
+ * Default value of the parameter (if any).
22278
+ */
22279
+ default: nonEmptyStringSchema.nullable().default(null),
22280
+ /**
22281
+ * Describes whether the parameter is used only for debugging purposes.
22282
+ */
22283
+ debug: booleanSchema.default(false)
22284
+ });
22285
+ /**
22286
+ * Zod schema for scriptlet parameters.
22287
+ */
22288
+ const scriptletParametersSchema = zod.array(scriptletParameterSchema);
22289
+ /**
22290
+ * Zod schema for scriptlet data.
22291
+ */
22292
+ zodToCamelCase(baseCompatibilityDataSchema.extend({
22293
+ /**
22294
+ * List of parameters that the scriptlet accepts.
22295
+ * **Every** parameter should be listed here, because we check that the scriptlet is used correctly
22296
+ * (e.g. that the number of parameters is correct).
22297
+ */
22298
+ parameters: scriptletParametersSchema.optional()
22299
+ }).superRefine((data, ctx) => {
22300
+ // TODO: find something better, for now we can't add refine logic to the base schema:
22301
+ // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22302
+ baseRefineLogic(data, ctx);
22303
+ // we don't allow required parameters after optional ones
22304
+ if (!data.parameters) {
22305
+ return;
22547
22306
  }
22548
- *keys() {
22549
- for (const [key] of this) {
22550
- yield key;
22307
+ let optionalFound = false;
22308
+ for (const parameter of data.parameters) {
22309
+ if (optionalFound && parameter.required) {
22310
+ ctx.addIssue({
22311
+ code: zod.ZodIssueCode.custom,
22312
+ message: 'Required parameters must be before optional ones'
22313
+ });
22551
22314
  }
22552
- }
22553
- *values() {
22554
- for (const [, value] of this) {
22555
- yield value;
22315
+ if (!parameter.required) {
22316
+ optionalFound = true;
22556
22317
  }
22557
22318
  }
22558
- *[Symbol.iterator]() {
22559
- for (const item of this.cache) {
22560
- yield item;
22319
+ }));
22320
+
22321
+ /**
22322
+ * @file Scriptlet injection rule converter
22323
+ */
22324
+ const ABP_SCRIPTLET_PREFIX = 'abp-';
22325
+ const UBO_SCRIPTLET_PREFIX = 'ubo-';
22326
+ const UBO_SCRIPTLET_PREFIX_LENGTH = UBO_SCRIPTLET_PREFIX.length;
22327
+ const UBO_SCRIPTLET_JS_SUFFIX = '.js';
22328
+ const UBO_SCRIPTLET_JS_SUFFIX_LENGTH = UBO_SCRIPTLET_JS_SUFFIX.length;
22329
+ const COMMA_SEPARATOR = ',';
22330
+ const ADG_SET_CONSTANT_NAME = 'set-constant';
22331
+ const ADG_SET_CONSTANT_EMPTY_STRING = '';
22332
+ const ADG_SET_CONSTANT_EMPTY_ARRAY = 'emptyArr';
22333
+ const ADG_SET_CONSTANT_EMPTY_OBJECT = 'emptyObj';
22334
+ const UBO_SET_CONSTANT_EMPTY_STRING = '\'\'';
22335
+ const UBO_SET_CONSTANT_EMPTY_ARRAY = '[]';
22336
+ const UBO_SET_CONSTANT_EMPTY_OBJECT = '{}';
22337
+ const ADG_PREVENT_FETCH_NAME = 'prevent-fetch';
22338
+ const ADG_PREVENT_FETCH_EMPTY_STRING = '';
22339
+ const ADG_PREVENT_FETCH_WILDCARD = '*';
22340
+ const UBO_NO_FETCH_IF_WILDCARD = '/^/';
22341
+ const setConstantAdgToUboMap = {
22342
+ [ADG_SET_CONSTANT_EMPTY_STRING]: UBO_SET_CONSTANT_EMPTY_STRING,
22343
+ [ADG_SET_CONSTANT_EMPTY_ARRAY]: UBO_SET_CONSTANT_EMPTY_ARRAY,
22344
+ [ADG_SET_CONSTANT_EMPTY_OBJECT]: UBO_SET_CONSTANT_EMPTY_OBJECT
22345
+ };
22346
+ /**
22347
+ * Scriptlet injection rule converter class
22348
+ *
22349
+ * @todo Implement `convertToUbo` and `convertToAbp`
22350
+ */
22351
+ class ScriptletRuleConverter extends RuleConverterBase {
22352
+ /**
22353
+ * Converts a scriptlet injection rule to AdGuard format, if possible.
22354
+ *
22355
+ * @param rule Rule node to convert
22356
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22357
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22358
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22359
+ * @throws If the rule is invalid or cannot be converted
22360
+ */
22361
+ static convertToAdg(rule) {
22362
+ // Ignore AdGuard rules
22363
+ if (rule.syntax === exports.AdblockSyntax.Adg) {
22364
+ return createNodeConversionResult([rule], false);
22561
22365
  }
22562
- for (const item of this.oldCache) {
22563
- const [key] = item;
22564
- if (!this.cache.has(key)) {
22565
- yield item;
22366
+ const separator = rule.separator.value;
22367
+ let convertedSeparator = separator;
22368
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgJsInjectionException : exports.CosmeticRuleSeparator.AdgJsInjection;
22369
+ const convertedScriptlets = [];
22370
+ // Special case: empty uBO exception scriptlet, e.g. `example.com#@#+js()`
22371
+ if (rule.syntax === exports.AdblockSyntax.Ubo && rule.body.children.length === 1 && rule.body.children[0].children.length === 0) {
22372
+ convertedScriptlets.push(rule.body.children[0]);
22373
+ } else {
22374
+ for (const scriptlet of rule.body.children) {
22375
+ // Clone the node to avoid any side effects
22376
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
22377
+ // Remove possible quotes just to make it easier to work with the scriptlet name
22378
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
22379
+ // Add prefix if it's not already there
22380
+ let prefix;
22381
+ switch (rule.syntax) {
22382
+ case exports.AdblockSyntax.Abp:
22383
+ prefix = ABP_SCRIPTLET_PREFIX;
22384
+ break;
22385
+ case exports.AdblockSyntax.Ubo:
22386
+ prefix = UBO_SCRIPTLET_PREFIX;
22387
+ break;
22388
+ default:
22389
+ prefix = EMPTY;
22390
+ }
22391
+ if (!scriptletName.startsWith(prefix)) {
22392
+ setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
22393
+ }
22394
+ // ADG scriptlet parameters should be quoted, and single quoted are preferred
22395
+ setScriptletQuoteType(scriptletClone, exports.QuoteType.Single);
22396
+ convertedScriptlets.push(scriptletClone);
22566
22397
  }
22567
22398
  }
22568
- }
22569
- get size() {
22570
- let oldCacheSize = 0;
22571
- for (const key of this.oldCache.keys()) {
22572
- if (!this.cache.has(key)) {
22573
- oldCacheSize++;
22399
+ return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
22400
+ const res = {
22401
+ category: rule.category,
22402
+ type: rule.type,
22403
+ syntax: exports.AdblockSyntax.Adg,
22404
+ exception: rule.exception,
22405
+ domains: cloneDomainListNode(rule.domains),
22406
+ separator: {
22407
+ type: 'Value',
22408
+ value: convertedSeparator
22409
+ },
22410
+ body: {
22411
+ type: rule.body.type,
22412
+ children: [scriptlet]
22413
+ }
22414
+ };
22415
+ if (rule.modifiers) {
22416
+ res.modifiers = cloneModifierListNode(rule.modifiers);
22574
22417
  }
22575
- }
22576
- return Math.min(this._size + oldCacheSize, this.maxSize);
22577
- }
22578
- }
22579
- var quickLru = QuickLRU;
22580
- const mapObj = mapObjExports;
22581
- const camelCase = camelcaseExports;
22582
- const QuickLru = quickLru;
22583
- const has = (array, key) => array.some(x => {
22584
- if (typeof x === 'string') {
22585
- return x === key;
22586
- }
22587
- x.lastIndex = 0;
22588
- return x.test(key);
22589
- });
22590
- const cache = new QuickLru({
22591
- maxSize: 100000
22592
- });
22593
-
22594
- // Reproduces behavior from `map-obj`
22595
- const isObject = value => typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
22596
- const camelCaseConvert = (input, options) => {
22597
- if (!isObject(input)) {
22598
- return input;
22418
+ return res;
22419
+ }), true);
22599
22420
  }
22600
- options = {
22601
- deep: false,
22602
- pascalCase: false,
22603
- ...options
22604
- };
22605
- const {
22606
- exclude,
22607
- pascalCase,
22608
- stopPaths,
22609
- deep
22610
- } = options;
22611
- const stopPathsSet = new Set(stopPaths);
22612
- const makeMapper = parentPath => (key, value) => {
22613
- if (deep && isObject(value)) {
22614
- const path = parentPath === undefined ? key : `${parentPath}.${key}`;
22615
- if (!stopPathsSet.has(path)) {
22616
- value = mapObj(value, makeMapper(path));
22617
- }
22421
+ /**
22422
+ * Converts a scriptlet injection rule to uBlock format, if possible.
22423
+ *
22424
+ * @param rule Rule node to convert
22425
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22426
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22427
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22428
+ * @throws If the rule is invalid or cannot be converted
22429
+ */
22430
+ static convertToUbo(rule) {
22431
+ // Ignore uBlock rules
22432
+ if (rule.syntax === exports.AdblockSyntax.Ubo) {
22433
+ return createNodeConversionResult([rule], false);
22618
22434
  }
22619
- if (!(exclude && has(exclude, key))) {
22620
- const cacheKey = pascalCase ? `${key}_` : key;
22621
- if (cache.has(cacheKey)) {
22622
- key = cache.get(cacheKey);
22623
- } else {
22624
- const returnValue = camelCase(key, {
22625
- pascalCase,
22626
- locale: false
22435
+ const separator = rule.separator.value;
22436
+ let convertedSeparator = separator;
22437
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.ElementHidingException : exports.CosmeticRuleSeparator.ElementHiding;
22438
+ const convertedScriptlets = [];
22439
+ // Special case: empty AdGuard exception scriptlet, e.g. `example.com#%#//scriptlet()`
22440
+ if (rule.syntax === exports.AdblockSyntax.Adg && rule.body.children.length === 1 && rule.body.children[0].children.length === 0) {
22441
+ convertedScriptlets.push(rule.body.children[0]);
22442
+ } else {
22443
+ for (const scriptlet of rule.body.children) {
22444
+ // Clone the node to avoid any side effects
22445
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
22446
+ // Remove possible quotes just to make it easier to work with the scriptlet name
22447
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
22448
+ let uboScriptletName;
22449
+ if (rule.syntax === exports.AdblockSyntax.Adg && scriptletName.startsWith(UBO_SCRIPTLET_PREFIX)) {
22450
+ // Special case: AdGuard syntax 'preserves' the original scriptlet name,
22451
+ // so we need to convert it back by removing the uBO prefix
22452
+ uboScriptletName = scriptletName.slice(UBO_SCRIPTLET_PREFIX_LENGTH);
22453
+ } else {
22454
+ // Otherwise, try to find the corresponding uBO scriptlet name, or use the original one if not found
22455
+ const uboScriptlet = scriptletsCompatibilityTable.getFirst(scriptletName, exports.GenericPlatform.UboAny);
22456
+ uboScriptletName = uboScriptlet?.name ?? scriptletName;
22457
+ }
22458
+ // Remove the '.js' suffix if it's there - its presence is not mandatory
22459
+ if (uboScriptletName.endsWith(UBO_SCRIPTLET_JS_SUFFIX)) {
22460
+ uboScriptletName = uboScriptletName.slice(0, -UBO_SCRIPTLET_JS_SUFFIX_LENGTH);
22461
+ }
22462
+ setScriptletName(scriptletClone, uboScriptletName);
22463
+ setScriptletQuoteType(scriptletClone, exports.QuoteType.None);
22464
+ // Escape unescaped commas in parameters, because uBlock Origin uses them as separators.
22465
+ // For example, the following AdGuard rule:
22466
+ //
22467
+ // example.com#%#//scriptlet('spoof-css', '.adsbygoogle, #ads', 'visibility', 'visible')
22468
+ //
22469
+ // ↓↓ should be converted to ↓↓
22470
+ //
22471
+ // example.com##+js(spoof-css.js, .adsbygoogle\, #ads, visibility, visible)
22472
+ // ------------ ------------------- ---------- -------
22473
+ // arg 0 arg 1 arg 2 arg 3
22474
+ //
22475
+ // and we need to escape the comma in the second argument to prevent it from being treated
22476
+ // as two separate arguments.
22477
+ transformAllScriptletArguments(scriptletClone, value => {
22478
+ return QuoteUtils.escapeUnescapedOccurrences(value, COMMA_SEPARATOR);
22627
22479
  });
22628
- if (key.length < 100) {
22629
- // Prevent abuse
22630
- cache.set(cacheKey, returnValue);
22480
+ // Some scriptlets have special values that need to be converted
22481
+ switch (scriptletName) {
22482
+ case ADG_SET_CONSTANT_NAME:
22483
+ transformNthScriptletArgument(scriptletClone, 2, value => {
22484
+ return setConstantAdgToUboMap[value] ?? value;
22485
+ });
22486
+ break;
22487
+ case ADG_PREVENT_FETCH_NAME:
22488
+ transformNthScriptletArgument(scriptletClone, 1, value => {
22489
+ if (value === ADG_PREVENT_FETCH_EMPTY_STRING || value === ADG_PREVENT_FETCH_WILDCARD) {
22490
+ return UBO_NO_FETCH_IF_WILDCARD;
22491
+ }
22492
+ return value;
22493
+ });
22494
+ break;
22631
22495
  }
22632
- key = returnValue;
22496
+ convertedScriptlets.push(scriptletClone);
22633
22497
  }
22634
22498
  }
22635
- return [key, value];
22636
- };
22637
- return mapObj(input, makeMapper(undefined));
22638
- };
22639
- var camelcaseKeys = (input, options) => {
22640
- if (Array.isArray(input)) {
22641
- return Object.keys(input).map(key => camelCaseConvert(input[key], options));
22499
+ return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
22500
+ const res = {
22501
+ category: rule.category,
22502
+ type: rule.type,
22503
+ syntax: exports.AdblockSyntax.Ubo,
22504
+ exception: rule.exception,
22505
+ domains: cloneDomainListNode(rule.domains),
22506
+ separator: {
22507
+ type: 'Value',
22508
+ value: convertedSeparator
22509
+ },
22510
+ body: {
22511
+ type: rule.body.type,
22512
+ children: [scriptlet]
22513
+ }
22514
+ };
22515
+ if (rule.modifiers) {
22516
+ res.modifiers = cloneModifierListNode(rule.modifiers);
22517
+ }
22518
+ return res;
22519
+ }), true);
22642
22520
  }
22643
- return camelCaseConvert(input, options);
22644
- };
22645
- var camelCaseKeys = /*@__PURE__*/getDefaultExportFromCjs(camelcaseKeys);
22521
+ }
22646
22522
 
22647
22523
  /**
22648
- * @file Zod camelCase utility.
22524
+ * @file Utility functions for working with modifier nodes
22649
22525
  */
22650
- // eslint-disable-next-line import/no-extraneous-dependencies
22651
22526
  /**
22652
- * Transforms Zod schema to camelCase.
22653
- *
22654
- * @param zod Zod schema.
22527
+ * Creates a modifier node
22655
22528
  *
22656
- * @returns Zod schema with camelCase properties.
22529
+ * @param name Name of the modifier
22530
+ * @param value Value of the modifier
22531
+ * @param exception Whether the modifier is an exception
22532
+ * @returns Modifier node
22533
+ */
22534
+ function createModifierNode(name, value = undefined, exception = false) {
22535
+ const result = {
22536
+ type: 'Modifier',
22537
+ exception,
22538
+ name: {
22539
+ type: 'Value',
22540
+ value: name
22541
+ }
22542
+ };
22543
+ if (!isUndefined(value)) {
22544
+ result.value = {
22545
+ type: 'Value',
22546
+ value
22547
+ };
22548
+ }
22549
+ return result;
22550
+ }
22551
+ /**
22552
+ * Creates a modifier list node
22657
22553
  *
22658
- * @see {@link https://github.com/colinhacks/zod/issues/486#issuecomment-1501097361}
22554
+ * @param modifiers Modifiers to put in the list (optional, defaults to an empty list)
22555
+ * @returns Modifier list node
22659
22556
  */
22660
- const zodToCamelCase = zod => {
22661
- return zod.transform(val => camelCaseKeys(val));
22662
- };
22557
+ function createModifierListNode(modifiers = []) {
22558
+ const result = {
22559
+ type: 'ModifierList',
22560
+ // We need to clone the modifiers to avoid side effects
22561
+ children: modifiers.length ? clone(modifiers) : []
22562
+ };
22563
+ return result;
22564
+ }
22663
22565
 
22664
22566
  /**
22665
- * @file Base compatibility data schema, which is commonly used in compatibility tables.
22567
+ * A very simple map extension that allows to store multiple values for the same key
22568
+ * by storing them in an array.
22569
+ *
22570
+ * @todo Add more methods if needed
22666
22571
  */
22572
+ class MultiValueMap extends Map {
22573
+ /**
22574
+ * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
22575
+ * otherwise a new array will be created for the key.
22576
+ *
22577
+ * @param key Key to add
22578
+ * @param values Value(s) to add
22579
+ */
22580
+ add(key, ...values) {
22581
+ let currentValues = super.get(key);
22582
+ if (isUndefined(currentValues)) {
22583
+ currentValues = [];
22584
+ super.set(key, values);
22585
+ }
22586
+ currentValues.push(...values);
22587
+ }
22588
+ }
22589
+
22667
22590
  /**
22668
- * Zod schema for boolean values. Accepts both boolean and string values.
22591
+ * @file Cosmetic rule modifier converter from uBO to ADG
22669
22592
  */
22670
- const booleanSchema = zod.union([zod.string().transform(val => val.trim().toLowerCase() === 'true'), zod.boolean()]);
22593
+ const UBO_MATCHES_PATH_OPERATOR = 'matches-path';
22594
+ const ADG_PATH_MODIFIER = 'path';
22671
22595
  /**
22672
- * Zod schema for non-empty string values.
22596
+ * Special characters in modifier regexps that should be escaped
22673
22597
  */
22674
- const nonEmptyStringSchema = zod.string().transform(val => val.trim()).pipe(zod.string().min(1));
22598
+ const SPECIAL_MODIFIER_REGEX_CHARS = new Set([OPEN_SQUARE_BRACKET, CLOSE_SQUARE_BRACKET, COMMA, ESCAPE_CHARACTER]);
22675
22599
  /**
22676
- * Zod schema for base compatibility data.
22677
- * Here we use snake_case properties because the compatibility data is stored in YAML files.
22600
+ * Helper class for converting cosmetic rule modifiers from uBO to ADG
22678
22601
  */
22679
- const baseCompatibilityDataSchema = zod.object({
22680
- /**
22681
- * Name of the actual entity.
22682
- */
22683
- name: nonEmptyStringSchema,
22684
- /**
22685
- * List of aliases for the entity (if any).
22686
- */
22687
- aliases: zod.array(nonEmptyStringSchema).nullable().default(null),
22688
- /**
22689
- * Short description of the actual entity.
22690
- * If not specified or it's value is `null`, then the description is not available.
22691
- */
22692
- description: nonEmptyStringSchema.nullable().default(null),
22693
- /**
22694
- * Link to the documentation. If not specified or it's value is `null`, then the documentation is not available.
22695
- */
22696
- docs: nonEmptyStringSchema.nullable().default(null),
22697
- /**
22698
- * The version of the adblocker in which the entity was added.
22699
- * For AdGuard resources, the version of the library is specified.
22700
- */
22701
- version_added: nonEmptyStringSchema.nullable().default(null),
22702
- /**
22703
- * The version of the adblocker when the entity was removed.
22704
- */
22705
- version_removed: nonEmptyStringSchema.nullable().default(null),
22706
- /**
22707
- * Describes whether the entity is deprecated.
22708
- */
22709
- deprecated: booleanSchema.default(false),
22710
- /**
22711
- * Message that describes why the entity is deprecated.
22712
- * If not specified or it's value is `null`, then the message is not available.
22713
- * It's value is omitted if the entity is not marked as deprecated.
22714
- */
22715
- deprecation_message: nonEmptyStringSchema.nullable().default(null),
22602
+ class AdgCosmeticRuleModifierConverter {
22716
22603
  /**
22717
- * Describes whether the entity is removed; for *already removed* features.
22604
+ * Converts a uBO cosmetic rule modifier list to ADG, if possible.
22605
+ *
22606
+ * @param modifierList Cosmetic rule modifier list node to convert
22607
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
22608
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
22609
+ * If the node was not converted, the result will contain the original node with the same object reference
22610
+ * @throws If the modifier list cannot be converted
22611
+ * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
22718
22612
  */
22719
- removed: booleanSchema.default(false),
22613
+ static convertFromUbo(modifierList) {
22614
+ const conversionMap = new MultiValueMap();
22615
+ modifierList.children.forEach((modifier, index) => {
22616
+ // :matches-path
22617
+ if (modifier.name.value === UBO_MATCHES_PATH_OPERATOR) {
22618
+ if (!modifier.value) {
22619
+ throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
22620
+ }
22621
+ const value = RegExpUtils.isRegexPattern(modifier.value.value) ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS) : modifier.value.value;
22622
+ // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
22623
+ conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
22624
+ // We should negate the regexp if the modifier is an exception
22625
+ modifier.exception
22626
+ // eslint-disable-next-line max-len
22627
+ ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}` : value));
22628
+ }
22629
+ });
22630
+ // Check if we have any converted modifiers
22631
+ if (conversionMap.size) {
22632
+ const modifierListClone = clone(modifierList);
22633
+ // Replace the original modifiers with the converted ones
22634
+ modifierListClone.children = modifierListClone.children.map((modifier, index) => {
22635
+ const convertedModifier = conversionMap.get(index);
22636
+ return convertedModifier ?? modifier;
22637
+ }).flat();
22638
+ return createConversionResult(modifierListClone, true);
22639
+ }
22640
+ // Otherwise, just return the original modifier list
22641
+ return createConversionResult(modifierList, false);
22642
+ }
22643
+ }
22644
+ const ERROR_MESSAGES$1 = {
22645
+ // eslint-disable-next-line max-len
22646
+ INVALID_ATTRIBUTE_VALUE: `Expected '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.Ident)}' or '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.String)}' as attribute value, but got '%s' with value '%s`
22647
+ };
22648
+ var PseudoClasses;
22649
+ (function (PseudoClasses) {
22650
+ PseudoClasses["AbpContains"] = "-abp-contains";
22651
+ PseudoClasses["AbpHas"] = "-abp-has";
22652
+ PseudoClasses["Contains"] = "contains";
22653
+ PseudoClasses["Has"] = "has";
22654
+ PseudoClasses["HasText"] = "has-text";
22655
+ PseudoClasses["MatchesCss"] = "matches-css";
22656
+ PseudoClasses["MatchesCssAfter"] = "matches-css-after";
22657
+ PseudoClasses["MatchesCssBefore"] = "matches-css-before";
22658
+ PseudoClasses["Not"] = "not";
22659
+ })(PseudoClasses || (PseudoClasses = {}));
22660
+ var PseudoElements;
22661
+ (function (PseudoElements) {
22662
+ PseudoElements["After"] = "after";
22663
+ PseudoElements["Before"] = "before";
22664
+ })(PseudoElements || (PseudoElements = {}));
22665
+ const PSEUDO_ELEMENT_NAMES = new Set([PseudoElements.After, PseudoElements.Before]);
22666
+ /**
22667
+ * CSS selector converter
22668
+ *
22669
+ * @todo Implement `convertToUbo` and `convertToAbp`
22670
+ */
22671
+ class CssSelectorConverter extends ConverterBase {
22720
22672
  /**
22721
- * Message that describes why the entity is removed.
22722
- * If not specified or it's value is `null`, then the message is not available.
22723
- * It's value is omitted if the entity is not marked as deprecated.
22673
+ * Converts Extended CSS elements to AdGuard-compatible ones
22674
+ *
22675
+ * @param selectorList Selector list to convert
22676
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
22677
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
22678
+ * If the node was not converted, the result will contain the original node with the same object reference
22679
+ * @throws If the rule is invalid or incompatible
22724
22680
  */
22725
- removal_message: nonEmptyStringSchema.nullable().default(null)
22726
- });
22681
+ static convertToAdg(selectorList) {
22682
+ const stream = selectorList instanceof CssTokenStream ? selectorList : new CssTokenStream(selectorList);
22683
+ const converted = [];
22684
+ const convertAndPushPseudo = pseudo => {
22685
+ switch (pseudo) {
22686
+ case PseudoClasses.AbpContains:
22687
+ case PseudoClasses.HasText:
22688
+ converted.push(PseudoClasses.Contains);
22689
+ converted.push(OPEN_PARENTHESIS);
22690
+ break;
22691
+ case PseudoClasses.AbpHas:
22692
+ converted.push(PseudoClasses.Has);
22693
+ converted.push(OPEN_PARENTHESIS);
22694
+ break;
22695
+ // a bit special case:
22696
+ // - `:matches-css-before(...)` → `:matches-css(before, ...)`
22697
+ // - `:matches-css-after(...)` → `:matches-css(after, ...)`
22698
+ case PseudoClasses.MatchesCssBefore:
22699
+ case PseudoClasses.MatchesCssAfter:
22700
+ converted.push(PseudoClasses.MatchesCss);
22701
+ converted.push(OPEN_PARENTHESIS);
22702
+ converted.push(pseudo.substring(PseudoClasses.MatchesCss.length + 1));
22703
+ converted.push(COMMA);
22704
+ break;
22705
+ default:
22706
+ converted.push(pseudo);
22707
+ converted.push(OPEN_PARENTHESIS);
22708
+ break;
22709
+ }
22710
+ };
22711
+ while (!stream.isEof()) {
22712
+ const token = stream.getOrFail();
22713
+ if (token.type === cssTokenizer.TokenType.Colon) {
22714
+ // Advance colon
22715
+ stream.advance();
22716
+ converted.push(COLON);
22717
+ const tempToken = stream.getOrFail();
22718
+ // Double colon is a pseudo-element
22719
+ if (tempToken.type === cssTokenizer.TokenType.Colon) {
22720
+ stream.advance();
22721
+ converted.push(COLON);
22722
+ continue;
22723
+ }
22724
+ if (tempToken.type === cssTokenizer.TokenType.Ident) {
22725
+ const name = stream.source.slice(tempToken.start, tempToken.end);
22726
+ if (PSEUDO_ELEMENT_NAMES.has(name)) {
22727
+ // Add an extra colon to the name
22728
+ converted.push(COLON);
22729
+ converted.push(name);
22730
+ } else {
22731
+ // Add the name as is
22732
+ converted.push(name);
22733
+ }
22734
+ // Advance the names
22735
+ stream.advance();
22736
+ } else if (tempToken.type === cssTokenizer.TokenType.Function) {
22737
+ const name = stream.source.slice(tempToken.start, tempToken.end - 1); // omit the last parenthesis
22738
+ // :-abp-contains(...) → :contains(...)
22739
+ // :has-text(...) → :contains(...)
22740
+ // :-abp-has(...) → :has(...)
22741
+ convertAndPushPseudo(name);
22742
+ // Advance the function name
22743
+ stream.advance();
22744
+ }
22745
+ } else if (token.type === cssTokenizer.TokenType.OpenSquareBracket) {
22746
+ let tempToken;
22747
+ const {
22748
+ start
22749
+ } = token;
22750
+ stream.advance();
22751
+ // Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
22752
+ // For example:
22753
+ // - `[-ext-has=...]` → `:has(...)`
22754
+ // - `[-ext-contains=...]` → `:contains(...)`
22755
+ // - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
22756
+ stream.skipWhitespace();
22757
+ stream.expect(cssTokenizer.TokenType.Ident);
22758
+ tempToken = stream.getOrFail();
22759
+ let attr = stream.source.slice(tempToken.start, tempToken.end);
22760
+ // Skip if the attribute name is not a legacy Extended CSS one
22761
+ if (!(attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX) || attr.startsWith(ABP_EXT_CSS_PREFIX))) {
22762
+ converted.push(stream.source.slice(start, tempToken.end));
22763
+ stream.advance();
22764
+ continue;
22765
+ }
22766
+ if (attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX)) {
22767
+ attr = attr.slice(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
22768
+ }
22769
+ stream.advance();
22770
+ stream.skipWhitespace();
22771
+ // Next token should be an equality operator (=), because Extended CSS attribute selectors
22772
+ // do not support other operators
22773
+ stream.expect(cssTokenizer.TokenType.Delim, {
22774
+ value: EQUALS
22775
+ });
22776
+ stream.advance();
22777
+ // Skip optional whitespace after the operator
22778
+ stream.skipWhitespace();
22779
+ // Parse attribute value
22780
+ tempToken = stream.getOrFail();
22781
+ // According to the spec, attribute value should be an identifier or a string
22782
+ if (tempToken.type !== cssTokenizer.TokenType.Ident && tempToken.type !== cssTokenizer.TokenType.String) {
22783
+ throw new Error(sprintfJs.sprintf(ERROR_MESSAGES$1.INVALID_ATTRIBUTE_VALUE, cssTokenizer.getFormattedTokenName(tempToken.type), stream.source.slice(tempToken.start, tempToken.end)));
22784
+ }
22785
+ const value = stream.source.slice(tempToken.start, tempToken.end);
22786
+ // Advance the attribute value
22787
+ stream.advance();
22788
+ // Skip optional whitespace after the attribute value
22789
+ stream.skipWhitespace();
22790
+ // Next character should be a closing square bracket
22791
+ // We don't allow flags for Extended CSS attribute selectors
22792
+ stream.expect(cssTokenizer.TokenType.CloseSquareBracket);
22793
+ stream.advance();
22794
+ converted.push(COLON);
22795
+ convertAndPushPseudo(attr);
22796
+ let processedValue = value.slice(1, -1); // omit the quotes
22797
+ if (attr === PseudoClasses.Has) {
22798
+ // TODO: Optimize this to avoid double tokenization
22799
+ processedValue = CssSelectorConverter.convertToAdg(processedValue).result;
22800
+ }
22801
+ converted.push(processedValue);
22802
+ converted.push(CLOSE_PARENTHESIS);
22803
+ } else {
22804
+ converted.push(stream.source.slice(token.start, token.end));
22805
+ // Advance the token
22806
+ stream.advance();
22807
+ }
22808
+ }
22809
+ const convertedSelectorList = converted.join(EMPTY);
22810
+ return createConversionResult(convertedSelectorList, stream.source !== convertedSelectorList);
22811
+ }
22812
+ }
22813
+
22727
22814
  /**
22728
- * Zod schema for base compatibility data with camelCase properties.
22815
+ * @file CSS injection rule converter
22729
22816
  */
22730
- zodToCamelCase(baseCompatibilityDataSchema);
22731
22817
  /**
22732
- * Refinement logic for base compatibility data.
22818
+ * CSS injection rule converter class
22733
22819
  *
22734
- * @param data Base compatibility data.
22735
- * @param ctx Refinement context.
22820
+ * @todo Implement `convertToUbo` and `convertToAbp`
22736
22821
  */
22737
- const baseRefineLogic = (data, ctx) => {
22738
- if (data.deprecated && !data.deprecation_message) {
22739
- ctx.addIssue({
22740
- code: zod.ZodIssueCode.custom,
22741
- message: 'deprecation_message is required for deprecated modifiers'
22742
- });
22743
- }
22744
- if (!data.deprecated && data.deprecation_message) {
22745
- ctx.addIssue({
22746
- code: zod.ZodIssueCode.custom,
22747
- message: 'deprecation_message is only allowed for deprecated modifiers'
22748
- });
22749
- }
22750
- if (data.aliases && data.aliases.length !== new Set(data.aliases).size) {
22751
- ctx.addIssue({
22752
- code: zod.ZodIssueCode.custom,
22753
- message: 'Aliases must be unique'
22754
- });
22822
+ class CssInjectionRuleConverter extends RuleConverterBase {
22823
+ /**
22824
+ * Converts a CSS injection rule to AdGuard format, if possible.
22825
+ *
22826
+ * @param rule Rule node to convert
22827
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22828
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22829
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22830
+ * @throws If the rule is invalid or cannot be converted
22831
+ */
22832
+ static convertToAdg(rule) {
22833
+ const separator = rule.separator.value;
22834
+ let convertedSeparator = separator;
22835
+ const stream = new CssTokenStream(rule.body.selectorList.value);
22836
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
22837
+ // Change the separator if the rule contains ExtendedCSS elements,
22838
+ // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
22839
+ // because sometimes we use it to force executing ExtendedCSS library.
22840
+ if (stream.hasAnySelectorExtendedCssNodeStrict() || rule.body.remove) {
22841
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgExtendedCssInjectionException : exports.CosmeticRuleSeparator.AdgExtendedCssInjection;
22842
+ } else if (rule.syntax !== exports.AdblockSyntax.Adg) {
22843
+ // If the original rule syntax is not AdGuard, use the default separator
22844
+ // e.g. if the input rule is from uBO, we need to convert ## to #$#.
22845
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgCssInjectionException : exports.CosmeticRuleSeparator.AdgCssInjection;
22846
+ }
22847
+ // Check if the rule needs to be converted
22848
+ if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
22849
+ // TODO: Replace with custom clone method
22850
+ const ruleClone = clone(rule);
22851
+ ruleClone.syntax = exports.AdblockSyntax.Adg;
22852
+ ruleClone.separator.value = convertedSeparator;
22853
+ ruleClone.body.selectorList.value = convertedSelectorList.result;
22854
+ return createNodeConversionResult([ruleClone], true);
22855
+ }
22856
+ // Otherwise, return the original rule
22857
+ return createNodeConversionResult([rule], false);
22755
22858
  }
22756
- };
22859
+ }
22757
22860
 
22758
22861
  /**
22759
- * Checks if error has message.
22760
- *
22761
- * @param error Error object.
22762
- * @returns If param is error.
22862
+ * @file Element hiding rule converter
22763
22863
  */
22764
- function isErrorWithMessage(error) {
22765
- return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string';
22766
- }
22767
22864
  /**
22768
- * Converts error to the error with message.
22865
+ * Element hiding rule converter class
22769
22866
  *
22770
- * @param maybeError Possible error.
22771
- * @returns Error with message.
22867
+ * @todo Implement `convertToUbo` and `convertToAbp`
22772
22868
  */
22773
- function toErrorWithMessage(maybeError) {
22774
- if (isErrorWithMessage(maybeError)) {
22775
- return maybeError;
22776
- }
22777
- try {
22778
- return new Error(JSON.stringify(maybeError));
22779
- } catch {
22780
- // fallback in case there's an error stringifying the maybeError
22781
- // like with circular references for example.
22782
- return new Error(String(maybeError));
22869
+ class ElementHidingRuleConverter extends RuleConverterBase {
22870
+ /**
22871
+ * Converts an element hiding rule to AdGuard format, if possible.
22872
+ *
22873
+ * @param rule Rule node to convert
22874
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22875
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22876
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22877
+ * @throws If the rule is invalid or cannot be converted
22878
+ */
22879
+ static convertToAdg(rule) {
22880
+ const separator = rule.separator.value;
22881
+ let convertedSeparator = separator;
22882
+ const stream = new CssTokenStream(rule.body.selectorList.value);
22883
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
22884
+ // Change the separator if the rule contains ExtendedCSS elements,
22885
+ // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
22886
+ // because sometimes we use it to force executing ExtendedCSS library.
22887
+ if (stream.hasAnySelectorExtendedCssNodeStrict()) {
22888
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.ExtendedElementHidingException : exports.CosmeticRuleSeparator.ExtendedElementHiding;
22889
+ }
22890
+ // Check if the rule needs to be converted
22891
+ if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
22892
+ // TODO: Replace with custom clone method
22893
+ const ruleClone = clone(rule);
22894
+ ruleClone.syntax = exports.AdblockSyntax.Adg;
22895
+ ruleClone.separator.value = convertedSeparator;
22896
+ ruleClone.body.selectorList.value = convertedSelectorList.result;
22897
+ return createNodeConversionResult([ruleClone], true);
22898
+ }
22899
+ // Otherwise, return the original rule
22900
+ return createNodeConversionResult([rule], false);
22783
22901
  }
22784
22902
  }
22785
- /**
22786
- * Converts error object to error with message. This method might be helpful to handle thrown errors.
22787
- *
22788
- * @param error Error object.
22789
- *
22790
- * @returns Message of the error.
22791
- */
22792
- function getErrorMessage(error) {
22793
- return toErrorWithMessage(error).message;
22794
- }
22795
22903
 
22796
22904
  /**
22797
- * @file Schema for modifier data.
22798
- */
22799
- /**
22800
- * Known validators that don't need to be validated as regex.
22905
+ * @file Utility functions for working with network rule nodes
22801
22906
  */
22802
- const KNOWN_VALIDATORS = new Set(['domain', 'pipe_separated_domains', 'regexp', 'url']);
22803
22907
  /**
22804
- * Zod schema for modifier data.
22908
+ * Creates a network rule node
22909
+ *
22910
+ * @param pattern Rule pattern
22911
+ * @param modifiers Rule modifiers (optional, default: undefined)
22912
+ * @param exception Exception rule flag (optional, default: false)
22913
+ * @param syntax Adblock syntax (optional, default: Common)
22914
+ * @returns Network rule node
22805
22915
  */
22806
- zodToCamelCase(baseCompatibilityDataSchema.extend({
22807
- /**
22808
- * List of modifiers that are incompatible with the actual one.
22809
- */
22810
- conflicts: zod.array(nonEmptyStringSchema).nullable().default(null),
22811
- /**
22812
- * The actual modifier is incompatible with all other modifiers, except the ones listed in `conflicts`.
22813
- */
22814
- inverse_conflicts: booleanSchema.default(false),
22815
- /**
22816
- * Describes whether the actual modifier supports value assignment. For example, `$domain` is assignable,
22817
- * so it can be used like this: `$domain=domain.com\|~subdomain.domain.com`, where `=` is the assignment operator
22818
- * and `domain.com\|~subdomain.domain.com` is the value.
22819
- */
22820
- assignable: booleanSchema.default(false),
22821
- /**
22822
- * Describes whether the actual modifier can be negated. For example, `$third-party` is negatable,
22823
- * so it can be used like this: `$~third-party`.
22824
- */
22825
- negatable: booleanSchema.default(true),
22826
- /**
22827
- * The actual modifier can only be used in blocking rules, it cannot be used in exceptions.
22828
- * If it's value is `true`, then the modifier can be used only in blocking rules.
22829
- * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22830
- */
22831
- block_only: booleanSchema.default(false),
22832
- /**
22833
- * The actual modifier can only be used in exceptions, it cannot be used in blocking rules.
22834
- * If it's value is `true`, then the modifier can be used only in exceptions.
22835
- * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22836
- */
22837
- exception_only: booleanSchema.default(false),
22838
- /**
22839
- * Describes whether the *assignable* modifier value is required.
22840
- * For example, `$cookie` is assignable but it can be used without a value in exception rules:
22841
- * `@@\|\|example.com^$cookie`.
22842
- * If `false`, the `value_format` is required, e.g. the value of `$app` should always be specified
22843
- */
22844
- value_optional: booleanSchema.default(false),
22845
- /**
22846
- * Describes the format of the value for the *assignable* modifier.
22847
- * Its value can be a regex pattern or a known validator name (e.g. `domain`, `pipe_separated_domains`, etc.).
22848
- */
22849
- value_format: nonEmptyStringSchema.nullable().default(null)
22850
- }).superRefine((data, ctx) => {
22851
- // TODO: find something better, for now we can't add refine logic to the base schema:
22852
- // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22853
- baseRefineLogic(data, ctx);
22854
- if (data.block_only && data.exception_only) {
22855
- ctx.addIssue({
22856
- code: zod.ZodIssueCode.custom,
22857
- message: 'block_only and exception_only are mutually exclusive'
22858
- });
22859
- }
22860
- if (data.assignable && !data.value_format) {
22861
- ctx.addIssue({
22862
- code: zod.ZodIssueCode.custom,
22863
- message: 'value_format is required for assignable modifiers'
22864
- });
22865
- }
22866
- if (data.value_format) {
22867
- const valueFormat = data.value_format.trim();
22868
- // if it is a known validator, we don't need to validate it further
22869
- if (KNOWN_VALIDATORS.has(valueFormat)) {
22870
- return;
22871
- }
22872
- // otherwise, we need to validate it as a regex
22873
- try {
22874
- XRegExp(valueFormat);
22875
- } catch (error) {
22876
- ctx.addIssue({
22877
- code: zod.ZodIssueCode.custom,
22878
- message: getErrorMessage(error)
22879
- });
22916
+ function createNetworkRuleNode(pattern, modifiers = undefined, exception = false, syntax = exports.AdblockSyntax.Common) {
22917
+ const result = {
22918
+ category: exports.RuleCategory.Network,
22919
+ type: exports.NetworkRuleType.NetworkRule,
22920
+ syntax,
22921
+ exception,
22922
+ pattern: {
22923
+ type: 'Value',
22924
+ value: pattern
22880
22925
  }
22926
+ };
22927
+ if (!isUndefined(modifiers)) {
22928
+ result.modifiers = clone(modifiers);
22881
22929
  }
22882
- }));
22930
+ return result;
22931
+ }
22883
22932
 
22884
22933
  /**
22885
- * @file Schema for redirect data.
22934
+ * @file Converter for request header removal rules
22886
22935
  */
22936
+ const UBO_RESPONSEHEADER_FN = 'responseheader';
22937
+ const ADG_REMOVEHEADER_MODIFIER = 'removeheader';
22938
+ const ERROR_MESSAGES = {
22939
+ EMPTY_PARAMETER: `Empty parameter for '${UBO_RESPONSEHEADER_FN}' function`,
22940
+ EXPECTED_END_OF_RULE: "Expected end of rule, but got '%s'",
22941
+ MULTIPLE_DOMAINS_NOT_SUPPORTED: 'Multiple domains are not supported yet'
22942
+ };
22887
22943
  /**
22888
- * Zod schema for redirect data.
22944
+ * Converter for request header removal rules
22945
+ *
22946
+ * @todo Implement `convertToUbo` (ABP currently doesn't support header removal rules)
22889
22947
  */
22890
- zodToCamelCase(baseCompatibilityDataSchema.extend({
22948
+ class HeaderRemovalRuleConverter extends RuleConverterBase {
22891
22949
  /**
22892
- * Whether the redirect is blocking.
22950
+ * Converts a header removal rule to AdGuard syntax, if possible.
22951
+ *
22952
+ * @param rule Rule node to convert
22953
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22954
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22955
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22956
+ * @throws If the rule is invalid or cannot be converted
22957
+ * @example
22958
+ * If the input rule is:
22959
+ * ```adblock
22960
+ * example.com##^responseheader(header-name)
22961
+ * ```
22962
+ * The output will be:
22963
+ * ```adblock
22964
+ * ||example.com^$removeheader=header-name
22965
+ * ```
22893
22966
  */
22894
- is_blocking: booleanSchema.default(false)
22895
- }).superRefine(baseRefineLogic));
22967
+ static convertToAdg(rule) {
22968
+ // TODO: Add support for ABP syntax once it starts supporting header removal rules
22969
+ // Leave the rule as is if it's not a header removal rule
22970
+ if (rule.category !== exports.RuleCategory.Cosmetic || rule.type !== exports.CosmeticRuleType.HtmlFilteringRule) {
22971
+ return createNodeConversionResult([rule], false);
22972
+ }
22973
+ const stream = new CssTokenStream(rule.body.value);
22974
+ let token;
22975
+ // Skip leading whitespace
22976
+ stream.skipWhitespace();
22977
+ // Next token should be the `^` followed by a `responseheader` function
22978
+ token = stream.get();
22979
+ if (!token || token.type !== cssTokenizer.TokenType.Delim || rule.body.value[token.start] !== UBO_HTML_MASK) {
22980
+ return createNodeConversionResult([rule], false);
22981
+ }
22982
+ stream.advance();
22983
+ token = stream.get();
22984
+ if (!token) {
22985
+ return createNodeConversionResult([rule], false);
22986
+ }
22987
+ const functionName = rule.body.value.slice(token.start, token.end - 1);
22988
+ if (functionName !== UBO_RESPONSEHEADER_FN) {
22989
+ return createNodeConversionResult([rule], false);
22990
+ }
22991
+ // Parse the parameter
22992
+ const paramStart = token.end;
22993
+ stream.skipUntilBalanced();
22994
+ const paramEnd = stream.getOrFail().end;
22995
+ const param = rule.body.value.slice(paramStart, paramEnd - 1).trim();
22996
+ // Do not allow empty parameter
22997
+ if (param.length === 0) {
22998
+ throw new RuleConversionError(ERROR_MESSAGES.EMPTY_PARAMETER);
22999
+ }
23000
+ stream.expect(cssTokenizer.TokenType.CloseParenthesis);
23001
+ stream.advance();
23002
+ // Skip trailing whitespace after the function call
23003
+ stream.skipWhitespace();
23004
+ // Expect the end of the rule - so nothing should be left in the stream
23005
+ if (!stream.isEof()) {
23006
+ token = stream.getOrFail();
23007
+ throw new RuleConversionError(sprintfJs.sprintf(ERROR_MESSAGES.EXPECTED_END_OF_RULE, cssTokenizer.getFormattedTokenName(token.type)));
23008
+ }
23009
+ // Prepare network rule pattern
23010
+ const pattern = [];
23011
+ if (rule.domains.children.length === 1) {
23012
+ // If the rule has only one domain, we can use a simple network rule pattern:
23013
+ // ||single-domain-from-the-rule^
23014
+ pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
23015
+ } else if (rule.domains.children.length > 1) {
23016
+ // TODO: Add support for multiple domains, for example:
23017
+ // example.com,example.org,example.net##^responseheader(header-name)
23018
+ // We should consider allowing $domain with $removeheader modifier,
23019
+ // for example:
23020
+ // $removeheader=header-name,domain=example.com|example.org|example.net
23021
+ throw new RuleConversionError(ERROR_MESSAGES.MULTIPLE_DOMAINS_NOT_SUPPORTED);
23022
+ }
23023
+ // Prepare network rule modifiers
23024
+ const modifiers = createModifierListNode();
23025
+ modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, param));
23026
+ // Construct the network rule
23027
+ return createNodeConversionResult([createNetworkRuleNode(pattern.join(EMPTY), modifiers,
23028
+ // Copy the exception flag
23029
+ rule.exception, exports.AdblockSyntax.Adg)], true);
23030
+ }
23031
+ }
22896
23032
 
22897
23033
  /**
22898
- * @file Schema for scriptlet data.
22899
- */
22900
- /**
22901
- * Zod schema for scriptlet parameter data.
22902
- */
22903
- const scriptletParameterSchema = zod.object({
22904
- /**
22905
- * Name of the actual parameter.
22906
- */
22907
- name: nonEmptyStringSchema,
22908
- /**
22909
- * Describes whether the parameter is required. Empty parameters are not allowed.
22910
- */
22911
- required: booleanSchema,
22912
- /**
22913
- * Short description of the parameter.
22914
- * If not specified or it's value is `null`,then the description is not available.
22915
- */
22916
- description: nonEmptyStringSchema.nullable().default(null),
22917
- /**
22918
- * Regular expression that matches the value of the parameter.
22919
- * If it's value is `null`, then the parameter value is not checked.
22920
- */
22921
- pattern: nonEmptyStringSchema.nullable().default(null),
22922
- /**
22923
- * Default value of the parameter (if any).
22924
- */
22925
- default: nonEmptyStringSchema.nullable().default(null),
22926
- /**
22927
- * Describes whether the parameter is used only for debugging purposes.
22928
- */
22929
- debug: booleanSchema.default(false)
22930
- });
22931
- /**
22932
- * Zod schema for scriptlet parameters.
23034
+ * @file Cosmetic rule converter
22933
23035
  */
22934
- const scriptletParametersSchema = zod.array(scriptletParameterSchema);
22935
23036
  /**
22936
- * Zod schema for scriptlet data.
23037
+ * Cosmetic rule converter class (also known as "non-basic rule converter")
23038
+ *
23039
+ * @todo Implement `convertToUbo` and `convertToAbp`
22937
23040
  */
22938
- zodToCamelCase(baseCompatibilityDataSchema.extend({
23041
+ class CosmeticRuleConverter extends RuleConverterBase {
22939
23042
  /**
22940
- * List of parameters that the scriptlet accepts.
22941
- * **Every** parameter should be listed here, because we check that the scriptlet is used correctly
22942
- * (e.g. that the number of parameters is correct).
23043
+ * Converts a cosmetic rule to AdGuard syntax, if possible.
23044
+ *
23045
+ * @param rule Rule node to convert
23046
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23047
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23048
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23049
+ * @throws If the rule is invalid or cannot be converted
22943
23050
  */
22944
- parameters: scriptletParametersSchema.optional()
22945
- }).superRefine((data, ctx) => {
22946
- // TODO: find something better, for now we can't add refine logic to the base schema:
22947
- // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22948
- baseRefineLogic(data, ctx);
22949
- // we don't allow required parameters after optional ones
22950
- if (!data.parameters) {
22951
- return;
22952
- }
22953
- let optionalFound = false;
22954
- for (const parameter of data.parameters) {
22955
- if (optionalFound && parameter.required) {
22956
- ctx.addIssue({
22957
- code: zod.ZodIssueCode.custom,
22958
- message: 'Required parameters must be before optional ones'
23051
+ static convertToAdg(rule) {
23052
+ let subconverterResult;
23053
+ // Convert cosmetic rule based on its type
23054
+ switch (rule.type) {
23055
+ case exports.CosmeticRuleType.ElementHidingRule:
23056
+ subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
23057
+ break;
23058
+ case exports.CosmeticRuleType.ScriptletInjectionRule:
23059
+ subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
23060
+ break;
23061
+ case exports.CosmeticRuleType.CssInjectionRule:
23062
+ subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
23063
+ break;
23064
+ case exports.CosmeticRuleType.HtmlFilteringRule:
23065
+ // Handle special case: uBO response header filtering rule
23066
+ // TODO: Optimize double CSS tokenization here
23067
+ subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
23068
+ if (subconverterResult.isConverted) {
23069
+ break;
23070
+ }
23071
+ subconverterResult = HtmlRuleConverter.convertToAdg(rule);
23072
+ break;
23073
+ // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
23074
+ case exports.CosmeticRuleType.JsInjectionRule:
23075
+ subconverterResult = createNodeConversionResult([rule], false);
23076
+ break;
23077
+ default:
23078
+ throw new RuleConversionError('Unsupported cosmetic rule type');
23079
+ }
23080
+ let convertedModifiers;
23081
+ // Convert cosmetic rule modifiers, if any
23082
+ if (rule.modifiers) {
23083
+ if (rule.syntax === exports.AdblockSyntax.Ubo) {
23084
+ // uBO doesn't support this rule:
23085
+ // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
23086
+ if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
23087
+ throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
23088
+ }
23089
+ convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
23090
+ } else if (rule.syntax === exports.AdblockSyntax.Abp) {
23091
+ // TODO: Implement once ABP starts supporting cosmetic rule modifiers
23092
+ throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
23093
+ }
23094
+ }
23095
+ if (subconverterResult.result.length > 1 || subconverterResult.isConverted || convertedModifiers && convertedModifiers.isConverted) {
23096
+ // Add modifier list to the subconverter result rules
23097
+ subconverterResult.result.forEach(subconverterRule => {
23098
+ if (convertedModifiers && subconverterRule.category === exports.RuleCategory.Cosmetic) {
23099
+ // eslint-disable-next-line no-param-reassign
23100
+ subconverterRule.modifiers = convertedModifiers.result;
23101
+ }
22959
23102
  });
23103
+ return subconverterResult;
22960
23104
  }
22961
- if (!parameter.required) {
22962
- optionalFound = true;
23105
+ return createNodeConversionResult([rule], false);
23106
+ }
23107
+ /**
23108
+ * Converts a cosmetic rule to uBlock Origin syntax, if possible.
23109
+ *
23110
+ * @param rule Rule node to convert
23111
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23112
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23113
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23114
+ * @throws If the rule is invalid or cannot be converted
23115
+ */
23116
+ // TODO: Add support for other cosmetic rule types
23117
+ static convertToUbo(rule) {
23118
+ // Convert cosmetic rule based on its type
23119
+ if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
23120
+ if (rule.syntax === exports.AdblockSyntax.Adg && rule.modifiers?.children.length) {
23121
+ // e.g. example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
23122
+ throw new RuleConversionError('uBO scriptlet injection rules do not support cosmetic rule modifiers');
23123
+ }
23124
+ return ScriptletRuleConverter.convertToUbo(rule);
22963
23125
  }
23126
+ return createNodeConversionResult([rule], false);
22964
23127
  }
22965
- }));
23128
+ }
22966
23129
 
22967
23130
  /**
22968
23131
  * @file Network rule modifier list converter.
@@ -23234,6 +23397,22 @@ class RuleConverter extends RuleConverterBase {
23234
23397
  throw new RuleConversionError('Unknown rule category');
23235
23398
  }
23236
23399
  }
23400
+ /**
23401
+ * Converts an adblock filtering rule to uBlock Origin format, if possible.
23402
+ *
23403
+ * @param rule Rule node to convert
23404
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23405
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23406
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23407
+ * @throws If the rule is invalid or cannot be converted
23408
+ */
23409
+ // TODO: Add support for other rule types
23410
+ static convertToUbo(rule) {
23411
+ if (rule.category === exports.RuleCategory.Cosmetic) {
23412
+ return CosmeticRuleConverter.convertToUbo(rule);
23413
+ }
23414
+ return createConversionResult([rule], false);
23415
+ }
23237
23416
  }
23238
23417
 
23239
23418
  /**
@@ -23627,7 +23806,7 @@ class ByteBuffer {
23627
23806
  * @see {@link https://stackoverflow.com/a/62797156}
23628
23807
  */
23629
23808
  const isChromium = () => {
23630
- return typeof window !== 'undefined' && (Object.prototype.hasOwnProperty.call(window, 'chrome') || typeof window.navigator !== 'undefined' && /chrome/i.test(window.navigator.userAgent || ''));
23809
+ return typeof window !== 'undefined' && (Object.prototype.hasOwnProperty.call(window, 'chrome') || typeof window.navigator !== 'undefined' && /chrome/i.test(window.navigator.userAgent));
23631
23810
  };
23632
23811
 
23633
23812
  /* eslint-disable no-param-reassign */
@@ -23718,16 +23897,23 @@ class OutputByteBuffer extends ByteBuffer {
23718
23897
  */
23719
23898
  offset;
23720
23899
  /**
23721
- * Size of the shared buffer for encoding strings.
23900
+ * Size of the shared buffer for encoding strings in bytes.
23901
+ * This is a divisor of ByteBuffer.CHUNK_SIZE and experience shows that this value works optimally.
23902
+ * This is sufficient for most strings that occur in filter lists (we checked average string length in popular
23903
+ * filter lists).
23722
23904
  */
23723
23905
  static ENCODER_BUFFER_SIZE = 8192;
23724
23906
  /**
23725
- * Threshold for using a shared buffer for encoding strings.
23907
+ * Length threshold for using a shared buffer for encoding strings.
23908
+ * This temp buffer is needed because we write the short strings in it
23909
+ * (so there is no need to constantly allocate a new buffer).
23910
+ * The reason for dividing ENCODER_BUFFER_SIZE by 4 is to ensure that the encoded string fits in the buffer,
23911
+ * if we also take into account the worst possible case (each character is encoded with 4 bytes).
23726
23912
  */
23727
23913
  static SHORT_STRING_THRESHOLD = 2048; // 8192 / 4
23728
23914
  /**
23729
23915
  * Represents the maximum value that can be written as a 'storage optimized' unsigned integer.
23730
- * 0x1FFFFFFF means 32 bits minus 3 bits, because the last bit in each byte is a flag indicating
23916
+ * 0x1FFFFFFF means 29 bits — 32 bits minus 3 bits because the last bit in each byte is a flag indicating
23731
23917
  * if there are more bytes (except for the last byte).
23732
23918
  */
23733
23919
  static MAX_OPTIMIZED_UINT = 0x1FFFFFFF;
@@ -24274,7 +24460,7 @@ class RuleCategorizer {
24274
24460
  }
24275
24461
  }
24276
24462
  }
24277
- const version = "2.0.0-alpha.0";
24463
+ const version = "2.0.0";
24278
24464
 
24279
24465
  /**
24280
24466
  * @file AGTree version