@adguard/agtree 1.1.4 → 1.1.6

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 v1.1.4 (build date: Wed, 30 Aug 2023 10:02:46 GMT)
2
+ * AGTree v1.1.6 (build date: Fri, 22 Sep 2023 13:09:45 GMT)
3
3
  * (c) 2023 AdGuard Software Ltd.
4
4
  * Released under the MIT license
5
5
  * https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
@@ -88,6 +88,9 @@ exports.AdblockSyntax = void 0;
88
88
  * @file Constant values used by all parts of the library
89
89
  */
90
90
  // General
91
+ /**
92
+ * Empty string.
93
+ */
91
94
  const EMPTY = '';
92
95
  const SPACE = ' ';
93
96
  const TAB = '\t';
@@ -278,7 +281,7 @@ const NEGATION_MARKER = '~';
278
281
  /**
279
282
  * The wildcard symbol — `*`.
280
283
  */
281
- const WILDCARD$1 = ASTERISK;
284
+ const WILDCARD = ASTERISK;
282
285
  /**
283
286
  * Classic domain separator.
284
287
  *
@@ -2877,7 +2880,7 @@ class ModifierParser {
2877
2880
  const modifierEnd = Math.max(StringUtils.skipWSBack(raw) + 1, modifierNameStart);
2878
2881
  // Modifier name can't be empty
2879
2882
  if (modifierNameStart === modifierEnd) {
2880
- throw new AdblockSyntaxError('Modifier name can\'t be empty', locRange(loc, 0, raw.length));
2883
+ throw new AdblockSyntaxError('Modifier name cannot be empty', locRange(loc, 0, raw.length));
2881
2884
  }
2882
2885
  let modifier;
2883
2886
  let value;
@@ -2901,7 +2904,7 @@ class ModifierParser {
2901
2904
  };
2902
2905
  // Value can't be empty
2903
2906
  if (assignmentIndex + 1 === modifierEnd) {
2904
- throw new AdblockSyntaxError('Modifier value can\'t be empty', locRange(loc, 0, raw.length));
2907
+ throw new AdblockSyntaxError('Modifier value cannot be empty', locRange(loc, 0, raw.length));
2905
2908
  }
2906
2909
  // Skip whitespace after the assignment operator
2907
2910
  const valueStart = StringUtils.skipWS(raw, assignmentIndex + MODIFIER_ASSIGN_OPERATOR.length);
@@ -3199,8 +3202,29 @@ const FORBIDDEN_CSS_FUNCTIONS = new Set([
3199
3202
  'url',
3200
3203
  ]);
3201
3204
 
3205
+ /**
3206
+ * @file Clone related utilities
3207
+ *
3208
+ * We should keep clone related functions in this file. Thus, we just provide
3209
+ * a simple interface for cloning values, we use it across the AGTree project,
3210
+ * and the implementation "under the hood" can be improved later, if needed.
3211
+ */
3212
+ /**
3213
+ * Clones an input value to avoid side effects. Use it only in justified cases,
3214
+ * because it can impact performance negatively.
3215
+ *
3216
+ * @param value Value to clone
3217
+ * @returns Cloned value
3218
+ */
3219
+ function clone(value) {
3220
+ // TODO: Replace cloneDeep with a more efficient implementation
3221
+ return cloneDeep(value);
3222
+ }
3223
+
3202
3224
  /**
3203
3225
  * @file Additional / helper functions for ECSSTree / CSSTree.
3226
+ *
3227
+ * @note There are no tests for some functions, but during the AGTree optimization we remove them anyway.
3204
3228
  */
3205
3229
  /**
3206
3230
  * Common CSSTree parsing options.
@@ -3336,10 +3360,10 @@ class CssTree {
3336
3360
  ast = CssTree.parse(selectorList, exports.CssTreeParserContext.selectorList);
3337
3361
  }
3338
3362
  else {
3339
- ast = cloneDeep(selectorList);
3363
+ ast = clone(selectorList);
3340
3364
  }
3341
3365
  const nodes = [];
3342
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3366
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3343
3367
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3344
3368
  ecssTree.walk(ast, (node) => {
3345
3369
  if (CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) {
@@ -3368,9 +3392,9 @@ class CssTree {
3368
3392
  ast = CssTree.parse(selectorList, exports.CssTreeParserContext.selectorList);
3369
3393
  }
3370
3394
  else {
3371
- ast = cloneDeep(selectorList);
3395
+ ast = selectorList;
3372
3396
  }
3373
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3397
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3374
3398
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3375
3399
  return ecssTree.find(ast, (node) => CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) !== null;
3376
3400
  }
@@ -3407,14 +3431,14 @@ class CssTree {
3407
3431
  ast = CssTree.parse(declarationList, exports.CssTreeParserContext.declarationList);
3408
3432
  }
3409
3433
  else {
3410
- ast = cloneDeep(declarationList);
3434
+ ast = clone(declarationList);
3411
3435
  }
3412
3436
  const nodes = [];
3413
3437
  // While walking the AST we should skip the nested functions,
3414
3438
  // for example skip url()s in cross-fade(url(), url()), since
3415
3439
  // cross-fade() itself is already a forbidden function
3416
3440
  let inForbiddenFunction = false;
3417
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3441
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3418
3442
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3419
3443
  ecssTree.walk(ast, {
3420
3444
  enter: (node) => {
@@ -3452,9 +3476,9 @@ class CssTree {
3452
3476
  ast = CssTree.parse(declarationList, exports.CssTreeParserContext.declarationList);
3453
3477
  }
3454
3478
  else {
3455
- ast = cloneDeep(declarationList);
3479
+ ast = clone(declarationList);
3456
3480
  }
3457
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3481
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3458
3482
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3459
3483
  return ecssTree.find(ast, (node) => CssTree.isForbiddenFunction(node, forbiddenFunctions)) !== null;
3460
3484
  }
@@ -3686,6 +3710,180 @@ class CssTree {
3686
3710
  });
3687
3711
  return result.trim();
3688
3712
  }
3713
+ /**
3714
+ * Generates string representation of the selector list.
3715
+ *
3716
+ * @param ast SelectorList AST
3717
+ * @returns String representation of the selector list
3718
+ */
3719
+ static generateSelectorListPlain(ast) {
3720
+ const result = [];
3721
+ if (!ast.children || ast.children.length === 0) {
3722
+ throw new Error('Selector list cannot be empty');
3723
+ }
3724
+ ast.children.forEach((selector, index, nodeList) => {
3725
+ if (selector.type !== exports.CssTreeNodeType.Selector) {
3726
+ throw new Error(`Unexpected node type: ${selector.type}`);
3727
+ }
3728
+ result.push(this.generateSelectorPlain(selector));
3729
+ // If there is a next node, add a comma and a space after the selector
3730
+ if (nodeList[index + 1]) {
3731
+ result.push(COMMA, SPACE);
3732
+ }
3733
+ });
3734
+ return result.join(EMPTY);
3735
+ }
3736
+ /**
3737
+ * Selector generation based on CSSTree's AST. This is necessary because CSSTree
3738
+ * only adds spaces in some edge cases.
3739
+ *
3740
+ * @param ast CSS Tree AST
3741
+ * @returns CSS selector as string
3742
+ */
3743
+ static generateSelectorPlain(ast) {
3744
+ let result = EMPTY;
3745
+ let inAttributeSelector = false;
3746
+ let depth = 0;
3747
+ let selectorListDepth = -1;
3748
+ let prevNode = ast;
3749
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3750
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3751
+ ecssTree.walk(ast, {
3752
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3753
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3754
+ enter: (node) => {
3755
+ depth += 1;
3756
+ // Skip attribute selector / selector list children
3757
+ if (inAttributeSelector || selectorListDepth > -1) {
3758
+ return;
3759
+ }
3760
+ switch (node.type) {
3761
+ // "Trivial" nodes
3762
+ case exports.CssTreeNodeType.TypeSelector:
3763
+ result += node.name;
3764
+ break;
3765
+ case exports.CssTreeNodeType.ClassSelector:
3766
+ result += DOT;
3767
+ result += node.name;
3768
+ break;
3769
+ case exports.CssTreeNodeType.IdSelector:
3770
+ result += HASHMARK;
3771
+ result += node.name;
3772
+ break;
3773
+ case exports.CssTreeNodeType.Identifier:
3774
+ result += node.name;
3775
+ break;
3776
+ case exports.CssTreeNodeType.Raw:
3777
+ result += node.value;
3778
+ break;
3779
+ // "Advanced" nodes
3780
+ case exports.CssTreeNodeType.Nth:
3781
+ // Default generation enough
3782
+ result += ecssTree.generate(node);
3783
+ break;
3784
+ // For example :not([id], [name])
3785
+ case exports.CssTreeNodeType.SelectorList:
3786
+ // eslint-disable-next-line no-case-declarations
3787
+ const selectors = [];
3788
+ node.children.forEach((selector) => {
3789
+ if (selector.type === exports.CssTreeNodeType.Selector) {
3790
+ selectors.push(CssTree.generateSelectorPlain(selector));
3791
+ }
3792
+ else if (selector.type === exports.CssTreeNodeType.Raw) {
3793
+ selectors.push(selector.value);
3794
+ }
3795
+ });
3796
+ // Join selector lists
3797
+ result += selectors.join(COMMA + SPACE);
3798
+ // Skip nodes here
3799
+ selectorListDepth = depth;
3800
+ break;
3801
+ case exports.CssTreeNodeType.Combinator:
3802
+ if (node.name === SPACE) {
3803
+ result += node.name;
3804
+ break;
3805
+ }
3806
+ // Prevent this case (unnecessary space): has( > .something)
3807
+ if (prevNode.type !== exports.CssTreeNodeType.Selector) {
3808
+ result += SPACE;
3809
+ }
3810
+ result += node.name;
3811
+ result += SPACE;
3812
+ break;
3813
+ case exports.CssTreeNodeType.AttributeSelector:
3814
+ result += OPEN_SQUARE_BRACKET;
3815
+ // Identifier name
3816
+ if (node.name) {
3817
+ result += node.name.name;
3818
+ }
3819
+ // Matcher operator, eg =
3820
+ if (node.matcher) {
3821
+ result += node.matcher;
3822
+ // Value can be String, Identifier or null
3823
+ if (node.value !== null) {
3824
+ // String node
3825
+ if (node.value.type === exports.CssTreeNodeType.String) {
3826
+ result += ecssTree.generate(node.value);
3827
+ }
3828
+ else if (node.value.type === exports.CssTreeNodeType.Identifier) {
3829
+ // Identifier node
3830
+ result += node.value.name;
3831
+ }
3832
+ }
3833
+ }
3834
+ // Flags
3835
+ if (node.flags) {
3836
+ // Space before flags
3837
+ result += SPACE;
3838
+ result += node.flags;
3839
+ }
3840
+ result += CLOSE_SQUARE_BRACKET;
3841
+ inAttributeSelector = true;
3842
+ break;
3843
+ case exports.CssTreeNodeType.PseudoElementSelector:
3844
+ result += COLON;
3845
+ result += COLON;
3846
+ result += node.name;
3847
+ if (node.children !== null) {
3848
+ result += OPEN_PARENTHESIS;
3849
+ }
3850
+ break;
3851
+ case exports.CssTreeNodeType.PseudoClassSelector:
3852
+ result += COLON;
3853
+ result += node.name;
3854
+ if (node.children !== null) {
3855
+ result += OPEN_PARENTHESIS;
3856
+ }
3857
+ break;
3858
+ }
3859
+ prevNode = node;
3860
+ },
3861
+ leave: (node) => {
3862
+ depth -= 1;
3863
+ if (node.type === exports.CssTreeNodeType.SelectorList && depth + 1 === selectorListDepth) {
3864
+ selectorListDepth = -1;
3865
+ }
3866
+ if (selectorListDepth > -1) {
3867
+ return;
3868
+ }
3869
+ if (node.type === exports.CssTreeNodeType.AttributeSelector) {
3870
+ inAttributeSelector = false;
3871
+ }
3872
+ if (inAttributeSelector) {
3873
+ return;
3874
+ }
3875
+ switch (node.type) {
3876
+ case exports.CssTreeNodeType.PseudoElementSelector:
3877
+ case exports.CssTreeNodeType.PseudoClassSelector:
3878
+ if (node.children) {
3879
+ result += CLOSE_PARENTHESIS;
3880
+ }
3881
+ break;
3882
+ }
3883
+ },
3884
+ });
3885
+ return result.trim();
3886
+ }
3689
3887
  /**
3690
3888
  * Block generation based on CSSTree's AST. This is necessary because CSSTree only adds spaces in some edge cases.
3691
3889
  *
@@ -3869,6 +4067,29 @@ class CssTree {
3869
4067
  });
3870
4068
  return result;
3871
4069
  }
4070
+ /**
4071
+ * Helper function to generate a raw string from a function selector's children
4072
+ *
4073
+ * @param node Function node
4074
+ * @returns Generated function value
4075
+ * @example `responseheader(name)` -> `name`
4076
+ */
4077
+ static generateFunctionPlainValue(node) {
4078
+ const result = [];
4079
+ node.children?.forEach((child) => {
4080
+ switch (child.type) {
4081
+ case exports.CssTreeNodeType.Raw:
4082
+ result.push(child.value);
4083
+ break;
4084
+ default:
4085
+ // Fallback to CSSTree's default generate function
4086
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4087
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4088
+ result.push(ecssTree.generate(child));
4089
+ }
4090
+ });
4091
+ return result.join(EMPTY);
4092
+ }
3872
4093
  }
3873
4094
 
3874
4095
  /**
@@ -3916,7 +4137,7 @@ class ElementHidingBodyParser {
3916
4137
  * @throws If the AST is invalid
3917
4138
  */
3918
4139
  static generate(ast) {
3919
- return CssTree.generateSelectorList(ecssTree.fromPlainObject(ast.selectorList));
4140
+ return CssTree.generateSelectorListPlain(ast.selectorList);
3920
4141
  }
3921
4142
  }
3922
4143
 
@@ -4160,7 +4381,7 @@ class CssInjectionBodyParser {
4160
4381
  if (mediaQueryList || declarationList || remove) {
4161
4382
  throw new AdblockSyntaxError(
4162
4383
  // eslint-disable-next-line max-len
4163
- 'Invalid selector, regular selector elements can\'t be used after special pseudo-classes', {
4384
+ 'Invalid selector, regular selector elements cannot be used after special pseudo-classes', {
4164
4385
  start: node.loc?.start ?? loc,
4165
4386
  end: shiftLoc(loc, raw.length),
4166
4387
  });
@@ -4849,7 +5070,7 @@ function createModifierListNode(modifiers = []) {
4849
5070
  const result = {
4850
5071
  type: 'ModifierList',
4851
5072
  // We need to clone the modifiers to avoid side effects
4852
- children: cloneDeep(modifiers),
5073
+ children: modifiers.length ? clone(modifiers) : [],
4853
5074
  };
4854
5075
  return result;
4855
5076
  }
@@ -4889,8 +5110,9 @@ function hasUboModifierIndicator(rawSelectorList) {
4889
5110
  * @returns Linked list based selector
4890
5111
  */
4891
5112
  function convertSelectorToLinkedList(selector) {
5113
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4892
5114
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4893
- return ecssTree.fromPlainObject(cloneDeep(selector));
5115
+ return ecssTree.fromPlainObject(clone(selector));
4894
5116
  }
4895
5117
  /**
4896
5118
  * Helper function that always returns the linked list version of the
@@ -4900,8 +5122,9 @@ function convertSelectorToLinkedList(selector) {
4900
5122
  * @returns Linked list based selector list
4901
5123
  */
4902
5124
  function convertSelectorListToLinkedList(selectorList) {
5125
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4903
5126
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4904
- return ecssTree.fromPlainObject(cloneDeep(selectorList));
5127
+ return ecssTree.fromPlainObject(clone(selectorList));
4905
5128
  }
4906
5129
  /**
4907
5130
  * Helper function for checking and removing bounding combinators
@@ -5986,7 +6209,8 @@ class FilterListParser {
5986
6209
  */
5987
6210
  static generate(ast, preferRaw = false) {
5988
6211
  let result = EMPTY;
5989
- for (const rule of ast.children) {
6212
+ for (let i = 0; i < ast.children.length; i += 1) {
6213
+ const rule = ast.children[i];
5990
6214
  if (preferRaw && rule.raws?.text) {
5991
6215
  result += rule.raws.text;
5992
6216
  }
@@ -6003,6 +6227,11 @@ class FilterListParser {
6003
6227
  case 'lf':
6004
6228
  result += LF;
6005
6229
  break;
6230
+ default:
6231
+ if (i !== ast.children.length - 1) {
6232
+ result += LF;
6233
+ }
6234
+ break;
6006
6235
  }
6007
6236
  }
6008
6237
  return result;
@@ -6144,7 +6373,7 @@ var data$N = { adg_os_any:{ name:"csp",
6144
6373
  assignable:true,
6145
6374
  negatable:false,
6146
6375
  value_optional:true,
6147
- value_format:"(?xi)\n ^(\n base-uri|\n child-src|\n connect-src|\n default-src|\n font-src|\n form-action|\n frame-ancestors|\n frame-src|\n img-src|\n manifest-src|\n media-src|\n navigate-to|\n object-src|\n plugin-types|\n prefetch-src|\n report-to|\n report-uri|\n sandbox|\n script-src|\n style-src|\n upgrade-insecure-requests|\n worker-src|\n )\n \\s+\n \\S{1,}" },
6376
+ value_format:"csp_value" },
6148
6377
  adg_ext_any:{ name:"csp",
6149
6378
  description:"This modifier completely changes the rule behavior.\nIf it is applied to a rule, it will not block the matching request.\nThe response headers are going to be modified instead.",
6150
6379
  docs:"https://adguard.app/kb/general/ad-filtering/create-own-filters/#csp-modifier",
@@ -6156,7 +6385,7 @@ var data$N = { adg_os_any:{ name:"csp",
6156
6385
  assignable:true,
6157
6386
  negatable:false,
6158
6387
  value_optional:true,
6159
- value_format:"(?xi)\n ^(\n base-uri|\n child-src|\n connect-src|\n default-src|\n font-src|\n form-action|\n frame-ancestors|\n frame-src|\n img-src|\n manifest-src|\n media-src|\n navigate-to|\n object-src|\n plugin-types|\n prefetch-src|\n report-to|\n report-uri|\n sandbox|\n script-src|\n style-src|\n upgrade-insecure-requests|\n worker-src|\n )\n \\s+\n \\S{1,}" },
6388
+ value_format:"csp_value" },
6160
6389
  abp_ext_any:{ name:"csp",
6161
6390
  description:"This modifier completely changes the rule behavior.\nIf it is applied to a rule, it will not block the matching request.\nThe response headers are going to be modified instead.",
6162
6391
  docs:"https://help.adblockplus.org/hc/en-us/articles/360062733293-How-to-write-filters#content-security-policies",
@@ -6166,7 +6395,7 @@ var data$N = { adg_os_any:{ name:"csp",
6166
6395
  assignable:true,
6167
6396
  negatable:false,
6168
6397
  value_optional:true,
6169
- value_format:"(?xi)\n ^(\n base-uri|\n child-src|\n connect-src|\n default-src|\n font-src|\n form-action|\n frame-ancestors|\n frame-src|\n img-src|\n manifest-src|\n media-src|\n navigate-to|\n object-src|\n plugin-types|\n prefetch-src|\n report-to|\n report-uri|\n sandbox|\n script-src|\n style-src|\n upgrade-insecure-requests|\n worker-src|\n )\n \\s+\n \\S{1,}" },
6398
+ value_format:"csp_value" },
6170
6399
  ubo_ext_any:{ name:"csp",
6171
6400
  description:"This modifier completely changes the rule behavior.\nIf it is applied to a rule, it will not block the matching request.\nThe response headers are going to be modified instead.",
6172
6401
  docs:"https://github.com/gorhill/uBlock/wiki/Static-filter-syntax#csp",
@@ -6178,7 +6407,7 @@ var data$N = { adg_os_any:{ name:"csp",
6178
6407
  assignable:true,
6179
6408
  negatable:false,
6180
6409
  value_optional:true,
6181
- value_format:"(?xi)\n ^(\n base-uri|\n child-src|\n connect-src|\n default-src|\n font-src|\n form-action|\n frame-ancestors|\n frame-src|\n img-src|\n manifest-src|\n media-src|\n navigate-to|\n object-src|\n plugin-types|\n prefetch-src|\n report-to|\n report-uri|\n sandbox|\n script-src|\n style-src|\n upgrade-insecure-requests|\n worker-src|\n )\n \\s+\n \\S{1,}" } };
6410
+ value_format:"csp_value" } };
6182
6411
 
6183
6412
  var data$M = { adg_os_any:{ name:"denyallow",
6184
6413
  description:"The `$denyallow` modifier allows to avoid creating additional rules\nwhen it is needed to disable a certain rule for specific domains.\n`$denyallow` matches only target domains and not referrer domains.",
@@ -6685,7 +6914,8 @@ var data$l = { adg_os_any:{ name:"permissions",
6685
6914
  inverse_conflicts:true,
6686
6915
  assignable:true,
6687
6916
  negatable:false,
6688
- value_format:"(?x)\n ^\n (\n ?:(\n accelerometer|\n ambient-light-sensor|\n autoplay|\n battery|\n camera|\n display-capture|\n document-domain|\n encrypted-media|\n execution-while-not-rendered|\n execution-while-out-of-viewport|\n fullscreen|\n gamepad|\n geolocation|\n gyroscope|\n hid|\n identity-credentials-get|\n idle-detection|\n local-fonts|\n magnetometer|\n microphone|\n midi|\n payment|\n picture-in-picture|\n publickey-credentials-create|\n publickey-credentials-get|\n screen-wake-lock|\n serial|\n speaker-selection|\n storage-access|\n usb|\n web-share|\n xr-spatial-tracking\n )\n =\\(\\)\n # optional escaped comma for multiple permissions\n (\\\\,(\\s+)?)?\n )+\n $" } };
6917
+ value_optional:true,
6918
+ value_format:"permissions_value" } };
6689
6919
 
6690
6920
  var data$k = { adg_any:{ name:"ping",
6691
6921
  description:"The rule corresponds to requests caused by either navigator.sendBeacon() or the ping attribute on links.",
@@ -7346,15 +7576,104 @@ const ALLOWED_STEALTH_OPTIONS = new Set([
7346
7576
  'xclientdata',
7347
7577
  'dpi',
7348
7578
  ]);
7579
+ /**
7580
+ * Allowed CSP directives for $csp modifier.
7581
+ *
7582
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives}
7583
+ */
7584
+ const ALLOWED_CSP_DIRECTIVES = new Set([
7585
+ 'base-uri',
7586
+ 'child-src',
7587
+ 'connect-src',
7588
+ 'default-src',
7589
+ 'font-src',
7590
+ 'form-action',
7591
+ 'frame-ancestors',
7592
+ 'frame-src',
7593
+ 'img-src',
7594
+ 'manifest-src',
7595
+ 'media-src',
7596
+ 'navigate-to',
7597
+ 'object-src',
7598
+ 'plugin-types',
7599
+ 'prefetch-src',
7600
+ 'report-to',
7601
+ 'report-uri',
7602
+ 'sandbox',
7603
+ 'script-src',
7604
+ 'style-src',
7605
+ 'upgrade-insecure-requests',
7606
+ 'worker-src',
7607
+ ]);
7608
+ /**
7609
+ * Allowed stealth options for $permissions modifier.
7610
+ *
7611
+ * @see {@link https://adguard.app/kb/general/ad-filtering/create-own-filters/#permissions-modifier}
7612
+ */
7613
+ const ALLOWED_PERMISSION_DIRECTIVES = new Set([
7614
+ 'accelerometer',
7615
+ 'ambient-light-sensor',
7616
+ 'autoplay',
7617
+ 'battery',
7618
+ 'camera',
7619
+ 'display-capture',
7620
+ 'document-domain',
7621
+ 'encrypted-media',
7622
+ 'execution-while-not-rendered',
7623
+ 'execution-while-out-of-viewport',
7624
+ 'fullscreen',
7625
+ 'gamepad',
7626
+ 'geolocation',
7627
+ 'gyroscope',
7628
+ 'hid',
7629
+ 'identity-credentials-get',
7630
+ 'idle-detection',
7631
+ 'local-fonts',
7632
+ 'magnetometer',
7633
+ 'microphone',
7634
+ 'midi',
7635
+ 'payment',
7636
+ 'picture-in-picture',
7637
+ 'publickey-credentials-create',
7638
+ 'publickey-credentials-get',
7639
+ 'screen-wake-lock',
7640
+ 'serial',
7641
+ 'speaker-selection',
7642
+ 'storage-access',
7643
+ 'usb',
7644
+ 'web-share',
7645
+ 'xr-spatial-tracking',
7646
+ ]);
7647
+ /**
7648
+ * One of available tokens for $permission modifier value.
7649
+ *
7650
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization}
7651
+ */
7652
+ const PERMISSIONS_TOKEN_SELF = 'self';
7653
+ /**
7654
+ * One of allowlist values for $permissions modifier.
7655
+ *
7656
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy#allowlists}
7657
+ */
7658
+ const EMPTY_PERMISSIONS_ALLOWLIST = `${OPEN_PARENTHESIS}${CLOSE_PARENTHESIS}`;
7349
7659
  /**
7350
7660
  * Prefixes for error messages used in modifier validation.
7351
7661
  */
7352
7662
  const VALIDATION_ERROR_PREFIX = {
7353
7663
  BLOCK_ONLY: 'Only blocking rules may contain the modifier',
7354
7664
  EXCEPTION_ONLY: 'Only exception rules may contain the modifier',
7665
+ INVALID_CSP_DIRECTIVES: 'Invalid CSP directives for the modifier',
7355
7666
  INVALID_LIST_VALUES: 'Invalid values for the modifier',
7356
7667
  INVALID_NOOP: 'Invalid noop modifier',
7668
+ INVALID_PERMISSION_DIRECTIVE: 'Invalid Permissions-Policy directive for the modifier',
7669
+ INVALID_PERMISSION_ORIGINS: 'Origins in the value is invalid for the modifier and the directive',
7670
+ INVALID_PERMISSION_ORIGIN_QUOTES: 'Double quotes should be used for origins in the value of the modifier',
7357
7671
  MIXED_NEGATIONS: 'Simultaneous usage of negated and not negated values is forbidden for the modifier',
7672
+ NO_CSP_VALUE: 'No CSP value for the modifier and the directive',
7673
+ NO_CSP_DIRECTIVE_QUOTE: 'CSP directives should no be quoted for the modifier',
7674
+ NO_UNESCAPED_PERMISSION_COMMA: 'Unescaped comma in the value is not allowed for the modifier',
7675
+ // TODO: implement later for $scp and $permissions
7676
+ // NO_VALUE_ONLY_FOR_EXCEPTION: 'Modifier without value can be used only in exception rules',
7358
7677
  NOT_EXISTENT: 'Non-existent modifier',
7359
7678
  NOT_NEGATABLE_MODIFIER: 'Non-negatable modifier',
7360
7679
  NOT_NEGATABLE_VALUE: 'Values cannot be negated for the modifier',
@@ -7467,14 +7786,14 @@ const getSpecificBlockerData = (modifiersData, blockerPrefix, modifierName) => {
7467
7786
  * @example
7468
7787
  * `example.*` — matches with any TLD, e.g. `example.org`, `example.com`, etc.
7469
7788
  */
7470
- const WILDCARD_TLD = DOT + WILDCARD$1;
7789
+ const WILDCARD_TLD = DOT + WILDCARD;
7471
7790
  /**
7472
7791
  * Marker for a wildcard subdomain — `*.`.
7473
7792
  *
7474
7793
  * @example
7475
7794
  * `*.example.org` — matches with any subdomain, e.g. `foo.example.org` or `bar.example.org`
7476
7795
  */
7477
- const WILDCARD_SUBDOMAIN = WILDCARD$1 + DOT;
7796
+ const WILDCARD_SUBDOMAIN = WILDCARD + DOT;
7478
7797
  class DomainUtils {
7479
7798
  /**
7480
7799
  * Check if the input is a valid domain or hostname.
@@ -7485,7 +7804,7 @@ class DomainUtils {
7485
7804
  static isValidDomainOrHostname(domain) {
7486
7805
  let domainToCheck = domain;
7487
7806
  // Wildcard-only domain, typically a generic rule
7488
- if (domainToCheck === WILDCARD$1) {
7807
+ if (domainToCheck === WILDCARD) {
7489
7808
  return true;
7490
7809
  }
7491
7810
  // https://adguard.com/kb/general/ad-filtering/create-own-filters/#wildcard-for-tld
@@ -7666,10 +7985,12 @@ class QuoteUtils {
7666
7985
  var CustomValueFormatValidatorName;
7667
7986
  (function (CustomValueFormatValidatorName) {
7668
7987
  CustomValueFormatValidatorName["App"] = "pipe_separated_apps";
7988
+ CustomValueFormatValidatorName["Csp"] = "csp_value";
7669
7989
  // there are some differences between $domain and $denyallow
7670
7990
  CustomValueFormatValidatorName["DenyAllow"] = "pipe_separated_denyallow_domains";
7671
7991
  CustomValueFormatValidatorName["Domain"] = "pipe_separated_domains";
7672
7992
  CustomValueFormatValidatorName["Method"] = "pipe_separated_methods";
7993
+ CustomValueFormatValidatorName["Permissions"] = "permissions_value";
7673
7994
  CustomValueFormatValidatorName["StealthOption"] = "pipe_separated_stealth_options";
7674
7995
  })(CustomValueFormatValidatorName || (CustomValueFormatValidatorName = {}));
7675
7996
  /**
@@ -7703,7 +8024,7 @@ const isValidAppNameChunk = (chunk) => {
7703
8024
  const isValidAppModifierValue = (value) => {
7704
8025
  // $app modifier does not support wildcard tld
7705
8026
  // https://adguard.app/kb/general/ad-filtering/create-own-filters/#app-modifier
7706
- if (value.includes(WILDCARD$1)) {
8027
+ if (value.includes(WILDCARD)) {
7707
8028
  return false;
7708
8029
  }
7709
8030
  return value
@@ -7730,6 +8051,32 @@ const isValidMethodModifierValue = (value) => {
7730
8051
  const isValidStealthModifierValue = (value) => {
7731
8052
  return ALLOWED_STEALTH_OPTIONS.has(value);
7732
8053
  };
8054
+ /**
8055
+ * Checks whether the given `rawOrigin` is valid as Permissions Allowlist origin.
8056
+ *
8057
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
8058
+ *
8059
+ * @param rawOrigin The raw origin.
8060
+ *
8061
+ * @returns True if the origin is valid, false otherwise.
8062
+ */
8063
+ const isValidPermissionsOrigin = (rawOrigin) => {
8064
+ // origins should be quoted by double quote
8065
+ const actualQuoteType = QuoteUtils.getStringQuoteType(rawOrigin);
8066
+ if (actualQuoteType !== exports.QuoteType.Double) {
8067
+ return false;
8068
+ }
8069
+ const origin = QuoteUtils.removeQuotes(rawOrigin);
8070
+ try {
8071
+ // validate the origin by URL constructor
8072
+ // https://w3c.github.io/webappsec-permissions-policy/#algo-parse-policy-directive
8073
+ new URL(origin);
8074
+ }
8075
+ catch (e) {
8076
+ return false;
8077
+ }
8078
+ return true;
8079
+ };
7733
8080
  /**
7734
8081
  * Checks whether the given `value` is valid domain as $denyallow modifier value.
7735
8082
  * Important: wildcard tld are not supported, compared to $domain.
@@ -7742,7 +8089,7 @@ const isValidDenyAllowModifierValue = (value) => {
7742
8089
  // $denyallow modifier does not support wildcard tld
7743
8090
  // https://adguard.app/kb/general/ad-filtering/create-own-filters/#denyallow-modifier
7744
8091
  // but here we are simply checking whether the value contains wildcard `*`, not ends with `.*`
7745
- if (value.includes(WILDCARD$1)) {
8092
+ if (value.includes(WILDCARD)) {
7746
8093
  return false;
7747
8094
  }
7748
8095
  // TODO: add cache for domains validation
@@ -7921,59 +8268,241 @@ const validatePipeSeparatedStealthOptions = (modifier) => {
7921
8268
  return validateListItemsModifier(modifier, (raw) => StealthOptionListParser.parse(raw), isValidStealthModifierValue, customNoNegatedListItemsValidator);
7922
8269
  };
7923
8270
  /**
7924
- * Map of all available pre-defined validators for modifiers with custom `value_format`.
7925
- */
7926
- const CUSTOM_VALUE_FORMAT_MAP = {
7927
- [CustomValueFormatValidatorName.App]: validatePipeSeparatedApps,
7928
- [CustomValueFormatValidatorName.DenyAllow]: validatePipeSeparatedDenyAllowDomains,
7929
- [CustomValueFormatValidatorName.Domain]: validatePipeSeparatedDomains,
7930
- [CustomValueFormatValidatorName.Method]: validatePipeSeparatedMethods,
7931
- [CustomValueFormatValidatorName.StealthOption]: validatePipeSeparatedStealthOptions,
7932
- };
7933
- /**
7934
- * Returns whether the given `valueFormat` is a valid custom value format validator name.
7935
- *
7936
- * @param valueFormat Value format for the modifier.
7937
- *
7938
- * @returns True if `valueFormat` is a supported pre-defined value format validator name, false otherwise.
7939
- */
7940
- const isCustomValueFormatValidator = (valueFormat) => {
7941
- return Object.keys(CUSTOM_VALUE_FORMAT_MAP).includes(valueFormat);
7942
- };
7943
- /**
7944
- * Checks whether the value for given `modifier` is valid.
8271
+ * Validates `csp_value` custom value format.
8272
+ * Used for $csp modifier.
7945
8273
  *
7946
8274
  * @param modifier Modifier AST node.
7947
- * @param valueFormat Value format for the modifier.
7948
8275
  *
7949
8276
  * @returns Validation result.
7950
8277
  */
7951
- const validateValue = (modifier, valueFormat) => {
7952
- if (isCustomValueFormatValidator(valueFormat)) {
7953
- const validator = CUSTOM_VALUE_FORMAT_MAP[valueFormat];
7954
- return validator(modifier);
7955
- }
8278
+ const validateCspValue = (modifier) => {
7956
8279
  const modifierName = modifier.modifier.value;
7957
8280
  if (!modifier.value?.value) {
7958
8281
  return getValueRequiredValidationResult(modifierName);
7959
8282
  }
7960
- let xRegExp;
7961
- try {
7962
- xRegExp = XRegExp(valueFormat);
7963
- }
7964
- catch (e) {
7965
- throw new Error(`${SOURCE_DATA_ERROR_PREFIX.INVALID_VALUE_FORMAT_REGEXP}: '${modifierName}'`);
7966
- }
7967
- const isValid = xRegExp.test(modifier.value?.value);
7968
- if (!isValid) {
7969
- return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
8283
+ // $csp modifier value may contain multiple directives
8284
+ // e.g. "csp=child-src 'none'; frame-src 'self' *; worker-src 'none'"
8285
+ const policyDirectives = modifier.value.value
8286
+ .split(SEMICOLON)
8287
+ // rule with $csp modifier may end with semicolon
8288
+ // e.g. "$csp=sandbox allow-same-origin;"
8289
+ // TODO: add predicate helper for `(i) => !!i`
8290
+ .filter((i) => !!i);
8291
+ const invalidValueValidationResult = getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}': "${modifier.value.value}"`);
8292
+ if (policyDirectives.length === 0) {
8293
+ return invalidValueValidationResult;
8294
+ }
8295
+ const invalidDirectives = [];
8296
+ for (let i = 0; i < policyDirectives.length; i += 1) {
8297
+ const policyDirective = policyDirectives[i].trim();
8298
+ if (!policyDirective) {
8299
+ return invalidValueValidationResult;
8300
+ }
8301
+ const chunks = policyDirective.split(SPACE);
8302
+ const [directive, ...valueChunks] = chunks;
8303
+ // e.g. "csp=child-src 'none'; ; worker-src 'none'"
8304
+ // validator it here ↑
8305
+ if (!directive) {
8306
+ return invalidValueValidationResult;
8307
+ }
8308
+ if (!ALLOWED_CSP_DIRECTIVES.has(directive)) {
8309
+ // e.g. "csp='child-src' 'none'"
8310
+ if (ALLOWED_CSP_DIRECTIVES.has(QuoteUtils.removeQuotes(directive))) {
8311
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_CSP_DIRECTIVE_QUOTE}: '${modifierName}': ${directive}`);
8312
+ }
8313
+ invalidDirectives.push(directive);
8314
+ continue;
8315
+ }
8316
+ if (valueChunks.length === 0) {
8317
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_CSP_VALUE}: '${modifierName}': '${directive}'`);
8318
+ }
8319
+ }
8320
+ if (invalidDirectives.length > 0) {
8321
+ const directivesToStr = QuoteUtils.quoteAndJoinStrings(invalidDirectives, exports.QuoteType.Double);
8322
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_CSP_DIRECTIVES}: '${modifierName}': ${directivesToStr}`);
7970
8323
  }
7971
8324
  return { valid: true };
7972
8325
  };
7973
-
7974
8326
  /**
7975
- * @file Validator for modifiers.
7976
- */
8327
+ * Validates permission allowlist origins in the value of $permissions modifier.
8328
+ *
8329
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
8330
+ *
8331
+ * @param allowlistChunks Array of allowlist chunks.
8332
+ * @param directive Permission directive name.
8333
+ * @param modifierName Modifier name.
8334
+ *
8335
+ * @returns Validation result.
8336
+ */
8337
+ const validatePermissionAllowlistOrigins = (allowlistChunks, directive, modifierName) => {
8338
+ const invalidOrigins = [];
8339
+ for (let i = 0; i < allowlistChunks.length; i += 1) {
8340
+ const chunk = allowlistChunks[i].trim();
8341
+ // skip few spaces between origins (they were splitted by space)
8342
+ // e.g. 'geolocation=("https://example.com" "https://*.example.com")'
8343
+ if (chunk.length === 0) {
8344
+ continue;
8345
+ }
8346
+ /**
8347
+ * 'self' should be checked case-insensitively
8348
+ *
8349
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#algo-parse-policy-directive}
8350
+ *
8351
+ * @example 'geolocation=(self)'
8352
+ */
8353
+ if (chunk.toLowerCase() === PERMISSIONS_TOKEN_SELF) {
8354
+ continue;
8355
+ }
8356
+ if (QuoteUtils.getStringQuoteType(chunk) !== exports.QuoteType.Double) {
8357
+ return getInvalidValidationResult(
8358
+ // eslint-disable-next-line max-len
8359
+ `${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_ORIGIN_QUOTES}: '${modifierName}': '${directive}': '${QuoteUtils.removeQuotes(chunk)}'`);
8360
+ }
8361
+ if (!isValidPermissionsOrigin(chunk)) {
8362
+ invalidOrigins.push(chunk);
8363
+ }
8364
+ }
8365
+ if (invalidOrigins.length > 0) {
8366
+ const originsToStr = QuoteUtils.quoteAndJoinStrings(invalidOrigins);
8367
+ return getInvalidValidationResult(
8368
+ // eslint-disable-next-line max-len
8369
+ `${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_ORIGINS}: '${modifierName}': '${directive}': ${originsToStr}`);
8370
+ }
8371
+ return { valid: true };
8372
+ };
8373
+ /**
8374
+ * Validates permission allowlist in the modifier value.
8375
+ *
8376
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy#allowlists}
8377
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
8378
+ *
8379
+ * @param allowlist Allowlist value.
8380
+ * @param directive Permission directive name.
8381
+ * @param modifierName Modifier name.
8382
+ *
8383
+ * @returns Validation result.
8384
+ */
8385
+ const validatePermissionAllowlist = (allowlist, directive, modifierName) => {
8386
+ // `*` is one of available permissions tokens
8387
+ // e.g. 'fullscreen=*'
8388
+ // https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization
8389
+ if (allowlist === WILDCARD
8390
+ // e.g. 'autoplay=()'
8391
+ || allowlist === EMPTY_PERMISSIONS_ALLOWLIST) {
8392
+ return { valid: true };
8393
+ }
8394
+ if (!(allowlist.startsWith(OPEN_PARENTHESIS) && allowlist.endsWith(CLOSE_PARENTHESIS))) {
8395
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
8396
+ }
8397
+ const allowlistChunks = allowlist.slice(1, -1).split(SPACE);
8398
+ return validatePermissionAllowlistOrigins(allowlistChunks, directive, modifierName);
8399
+ };
8400
+ /**
8401
+ * Validates single permission in the modifier value.
8402
+ *
8403
+ * @param permission Single permission value.
8404
+ * @param modifierName Modifier name.
8405
+ * @param modifierValue Modifier value.
8406
+ *
8407
+ * @returns Validation result.
8408
+ */
8409
+ const validateSinglePermission = (permission, modifierName, modifierValue) => {
8410
+ // empty permission in the rule
8411
+ // e.g. 'permissions=storage-access=()\\, \\, camera=()'
8412
+ // the validator is here ↑
8413
+ if (!permission) {
8414
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
8415
+ }
8416
+ if (permission.includes(COMMA)) {
8417
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_UNESCAPED_PERMISSION_COMMA}: '${modifierName}': '${modifierValue}'`);
8418
+ }
8419
+ const [directive, allowlist] = permission.split(EQUALS);
8420
+ if (!ALLOWED_PERMISSION_DIRECTIVES.has(directive)) {
8421
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_DIRECTIVE}: '${modifierName}': '${directive}'`);
8422
+ }
8423
+ return validatePermissionAllowlist(allowlist, directive, modifierName);
8424
+ };
8425
+ /**
8426
+ * Validates `permissions_value` custom value format.
8427
+ * Used for $permissions modifier.
8428
+ *
8429
+ * @param modifier Modifier AST node.
8430
+ *
8431
+ * @returns Validation result.
8432
+ */
8433
+ const validatePermissions = (modifier) => {
8434
+ if (!modifier.value?.value) {
8435
+ return getValueRequiredValidationResult(modifier.modifier.value);
8436
+ }
8437
+ const modifierName = modifier.modifier.value;
8438
+ const modifierValue = modifier.value.value;
8439
+ // multiple permissions may be separated by escaped commas
8440
+ const permissions = modifier.value.value.split(`${BACKSLASH}${COMMA}`);
8441
+ for (let i = 0; i < permissions.length; i += 1) {
8442
+ const permission = permissions[i].trim();
8443
+ const singlePermissionValidationResult = validateSinglePermission(permission, modifierName, modifierValue);
8444
+ if (!singlePermissionValidationResult.valid) {
8445
+ return singlePermissionValidationResult;
8446
+ }
8447
+ }
8448
+ return { valid: true };
8449
+ };
8450
+ /**
8451
+ * Map of all available pre-defined validators for modifiers with custom `value_format`.
8452
+ */
8453
+ const CUSTOM_VALUE_FORMAT_MAP = {
8454
+ [CustomValueFormatValidatorName.App]: validatePipeSeparatedApps,
8455
+ [CustomValueFormatValidatorName.Csp]: validateCspValue,
8456
+ [CustomValueFormatValidatorName.DenyAllow]: validatePipeSeparatedDenyAllowDomains,
8457
+ [CustomValueFormatValidatorName.Domain]: validatePipeSeparatedDomains,
8458
+ [CustomValueFormatValidatorName.Method]: validatePipeSeparatedMethods,
8459
+ [CustomValueFormatValidatorName.Permissions]: validatePermissions,
8460
+ [CustomValueFormatValidatorName.StealthOption]: validatePipeSeparatedStealthOptions,
8461
+ };
8462
+ /**
8463
+ * Returns whether the given `valueFormat` is a valid custom value format validator name.
8464
+ *
8465
+ * @param valueFormat Value format for the modifier.
8466
+ *
8467
+ * @returns True if `valueFormat` is a supported pre-defined value format validator name, false otherwise.
8468
+ */
8469
+ const isCustomValueFormatValidator = (valueFormat) => {
8470
+ return Object.keys(CUSTOM_VALUE_FORMAT_MAP).includes(valueFormat);
8471
+ };
8472
+ /**
8473
+ * Checks whether the value for given `modifier` is valid.
8474
+ *
8475
+ * @param modifier Modifier AST node.
8476
+ * @param valueFormat Value format for the modifier.
8477
+ *
8478
+ * @returns Validation result.
8479
+ */
8480
+ const validateValue = (modifier, valueFormat) => {
8481
+ if (isCustomValueFormatValidator(valueFormat)) {
8482
+ const validator = CUSTOM_VALUE_FORMAT_MAP[valueFormat];
8483
+ return validator(modifier);
8484
+ }
8485
+ const modifierName = modifier.modifier.value;
8486
+ if (!modifier.value?.value) {
8487
+ return getValueRequiredValidationResult(modifierName);
8488
+ }
8489
+ let xRegExp;
8490
+ try {
8491
+ xRegExp = XRegExp(valueFormat);
8492
+ }
8493
+ catch (e) {
8494
+ throw new Error(`${SOURCE_DATA_ERROR_PREFIX.INVALID_VALUE_FORMAT_REGEXP}: '${modifierName}'`);
8495
+ }
8496
+ const isValid = xRegExp.test(modifier.value?.value);
8497
+ if (!isValid) {
8498
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
8499
+ }
8500
+ return { valid: true };
8501
+ };
8502
+
8503
+ /**
8504
+ * @file Validator for modifiers.
8505
+ */
7977
8506
  /**
7978
8507
  * Fully checks whether the given `modifier` valid for given blocker `syntax`:
7979
8508
  * is it supported by the blocker, deprecated, assignable, negatable, etc.
@@ -8030,6 +8559,10 @@ const validateForSpecificSyntax = (modifiersData, syntax, modifier, isException)
8030
8559
  // e.g. 'domain'
8031
8560
  if (specificBlockerData[SpecificKey.Assignable]) {
8032
8561
  if (!modifier.value) {
8562
+ // TODO: ditch value_optional after custom validators are implemented for value_format for all modifiers.
8563
+ // This checking should be done in each separate custom validator,
8564
+ // because $csp and $permissions without value can be used only in extension rules,
8565
+ // but $cookie with no value can be used in both blocking and exception rules.
8033
8566
  /**
8034
8567
  * Some assignable modifiers can be used without a value,
8035
8568
  * e.g. '@@||example.com^$cookie'.
@@ -8117,7 +8650,7 @@ class ModifierValidator {
8117
8650
  * @returns Result of modifier validation.
8118
8651
  */
8119
8652
  validate = (syntax, rawModifier, isException = false) => {
8120
- const modifier = cloneDeep(rawModifier);
8653
+ const modifier = clone(rawModifier);
8121
8654
  // special case: handle noop modifier which may be used as multiple underscores (not just one)
8122
8655
  // https://adguard.com/kb/general/ad-filtering/create-own-filters/#noop-modifier
8123
8656
  if (modifier.modifier.value.startsWith(UNDERSCORE)) {
@@ -8196,7 +8729,9 @@ class ConverterBase {
8196
8729
  * Converts some data to AdGuard format
8197
8730
  *
8198
8731
  * @param data Data to convert
8199
- * @returns Converted data
8732
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8733
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8734
+ * If the node was not converted, the result will contain the original node with the same object reference
8200
8735
  * @throws If the data is invalid or incompatible
8201
8736
  */
8202
8737
  static convertToAdg(data) {
@@ -8206,7 +8741,9 @@ class ConverterBase {
8206
8741
  * Converts some data to Adblock Plus format
8207
8742
  *
8208
8743
  * @param data Data to convert
8209
- * @returns Converted data
8744
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8745
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8746
+ * If the node was not converted, the result will contain the original node with the same object reference
8210
8747
  * @throws If the data is invalid or incompatible
8211
8748
  */
8212
8749
  static convertToAbp(data) {
@@ -8216,7 +8753,9 @@ class ConverterBase {
8216
8753
  * Converts some data to uBlock Origin format
8217
8754
  *
8218
8755
  * @param data Data to convert
8219
- * @returns Converted data
8756
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8757
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8758
+ * If the node was not converted, the result will contain the original node with the same object reference
8220
8759
  * @throws If the data is invalid or incompatible
8221
8760
  */
8222
8761
  static convertToUbo(data) {
@@ -8240,7 +8779,9 @@ class RuleConverterBase extends ConverterBase {
8240
8779
  * Converts an adblock filtering rule to AdGuard format, if possible.
8241
8780
  *
8242
8781
  * @param rule Rule node to convert
8243
- * @returns Array of converted rule nodes
8782
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8783
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8784
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8244
8785
  * @throws If the rule is invalid or cannot be converted
8245
8786
  */
8246
8787
  static convertToAdg(rule) {
@@ -8250,7 +8791,9 @@ class RuleConverterBase extends ConverterBase {
8250
8791
  * Converts an adblock filtering rule to Adblock Plus format, if possible.
8251
8792
  *
8252
8793
  * @param rule Rule node to convert
8253
- * @returns Array of converted rule nodes
8794
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8795
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8796
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8254
8797
  * @throws If the rule is invalid or cannot be converted
8255
8798
  */
8256
8799
  static convertToAbp(rule) {
@@ -8260,7 +8803,9 @@ class RuleConverterBase extends ConverterBase {
8260
8803
  * Converts an adblock filtering rule to uBlock Origin format, if possible.
8261
8804
  *
8262
8805
  * @param rule Rule node to convert
8263
- * @returns Array of converted rule nodes
8806
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8807
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8808
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8264
8809
  * @throws If the rule is invalid or cannot be converted
8265
8810
  */
8266
8811
  static convertToUbo(rule) {
@@ -8268,6 +8813,37 @@ class RuleConverterBase extends ConverterBase {
8268
8813
  }
8269
8814
  }
8270
8815
 
8816
+ /**
8817
+ * @file Conversion result interface and helper functions
8818
+ */
8819
+ /**
8820
+ * Helper function to create a generic conversion result.
8821
+ *
8822
+ * @param result Conversion result
8823
+ * @param isConverted Indicates whether the input item was converted
8824
+ * @template T Type of the item to convert
8825
+ * @template U Type of the conversion result (defaults to `T`, but can be `T[]` as well)
8826
+ * @returns Generic conversion result
8827
+ */
8828
+ // eslint-disable-next-line max-len
8829
+ function createConversionResult(result, isConverted) {
8830
+ return {
8831
+ result,
8832
+ isConverted,
8833
+ };
8834
+ }
8835
+ /**
8836
+ * Helper function to create a node conversion result.
8837
+ *
8838
+ * @param nodes Array of nodes
8839
+ * @param isConverted Indicates whether the input item was converted
8840
+ * @template T Type of the node (extends `Node`)
8841
+ * @returns Node conversion result
8842
+ */
8843
+ function createNodeConversionResult(nodes, isConverted) {
8844
+ return createConversionResult(nodes, isConverted);
8845
+ }
8846
+
8271
8847
  /**
8272
8848
  * @file Comment rule converter
8273
8849
  */
@@ -8281,27 +8857,30 @@ class CommentRuleConverter extends RuleConverterBase {
8281
8857
  * Converts a comment rule to AdGuard format, if possible.
8282
8858
  *
8283
8859
  * @param rule Rule node to convert
8284
- * @returns Array of converted rule nodes
8860
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8861
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8862
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8285
8863
  * @throws If the rule is invalid or cannot be converted
8286
8864
  */
8287
8865
  static convertToAdg(rule) {
8288
- // Clone the provided AST node to avoid side effects
8289
- const ruleNode = cloneDeep(rule);
8290
8866
  // TODO: Add support for other comment types, if needed
8291
8867
  // Main task is # -> ! conversion
8292
- switch (ruleNode.type) {
8868
+ switch (rule.type) {
8293
8869
  case exports.CommentRuleType.CommentRule:
8294
- // 'Comment' uBO style comments
8295
- if (ruleNode.type === exports.CommentRuleType.CommentRule
8296
- && ruleNode.marker.value === exports.CommentMarker.Hashmark) {
8297
- ruleNode.marker.value = exports.CommentMarker.Regular;
8298
- // Add the hashmark to the beginning of the comment
8299
- ruleNode.text.value = `${SPACE}${exports.CommentMarker.Hashmark}${ruleNode.text.value}`;
8870
+ // Check if the rule needs to be converted
8871
+ if (rule.type === exports.CommentRuleType.CommentRule && rule.marker.value === exports.CommentMarker.Hashmark) {
8872
+ // Add a ! to the beginning of the comment
8873
+ // TODO: Replace with custom clone method
8874
+ const ruleClone = clone(rule);
8875
+ ruleClone.marker.value = exports.CommentMarker.Regular;
8876
+ // Add the hashmark to the beginning of the comment text
8877
+ ruleClone.text.value = `${SPACE}${exports.CommentMarker.Hashmark}${ruleClone.text.value}`;
8878
+ return createNodeConversionResult([ruleClone], true);
8300
8879
  }
8301
- return [ruleNode];
8880
+ return createNodeConversionResult([rule], false);
8302
8881
  // Leave any other comment rule as is
8303
8882
  default:
8304
- return [ruleNode];
8883
+ return createNodeConversionResult([rule], false);
8305
8884
  }
8306
8885
  }
8307
8886
  }
@@ -8471,6 +9050,58 @@ class RegExpUtils {
8471
9050
  }
8472
9051
  }
8473
9052
 
9053
+ /**
9054
+ * @file Custom clone functions for AST nodes, this is probably the most efficient way to clone AST nodes.
9055
+ * @todo Maybe move them to parser classes as 'clone' methods
9056
+ */
9057
+ /**
9058
+ * Clones a scriptlet rule node.
9059
+ *
9060
+ * @param node Node to clone
9061
+ * @returns Cloned node
9062
+ */
9063
+ function cloneScriptletRuleNode(node) {
9064
+ return {
9065
+ type: node.type,
9066
+ children: node.children.map((child) => ({ ...child })),
9067
+ };
9068
+ }
9069
+ /**
9070
+ * Clones a domain list node.
9071
+ *
9072
+ * @param node Node to clone
9073
+ * @returns Cloned node
9074
+ */
9075
+ function cloneDomainListNode(node) {
9076
+ return {
9077
+ type: node.type,
9078
+ separator: node.separator,
9079
+ children: node.children.map((domain) => ({ ...domain })),
9080
+ };
9081
+ }
9082
+ /**
9083
+ * Clones a modifier list node.
9084
+ *
9085
+ * @param node Node to clone
9086
+ * @returns Cloned node
9087
+ */
9088
+ function cloneModifierListNode(node) {
9089
+ return {
9090
+ type: node.type,
9091
+ children: node.children.map((modifier) => {
9092
+ const res = {
9093
+ type: modifier.type,
9094
+ exception: modifier.exception,
9095
+ modifier: { ...modifier.modifier },
9096
+ };
9097
+ if (modifier.value) {
9098
+ res.value = { ...modifier.value };
9099
+ }
9100
+ return res;
9101
+ }),
9102
+ };
9103
+ }
9104
+
8474
9105
  /**
8475
9106
  * @file HTML filtering rule converter
8476
9107
  */
@@ -8483,16 +9114,22 @@ class RegExpUtils {
8483
9114
  *
8484
9115
  * @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#html-filtering-rules}
8485
9116
  */
8486
- const ADGUARD_HTML_DEFAULT_MAX_LENGTH = 8192;
8487
- const ADGUARD_HTML_CONVERSION_MAX_LENGTH = ADGUARD_HTML_DEFAULT_MAX_LENGTH * 32;
9117
+ const ADG_HTML_DEFAULT_MAX_LENGTH = 8192;
9118
+ const ADG_HTML_CONVERSION_MAX_LENGTH = ADG_HTML_DEFAULT_MAX_LENGTH * 32;
8488
9119
  const NOT_SPECIFIED = -1;
8489
- const CONTAINS$1 = 'contains';
8490
- const HAS_TEXT$1 = 'has-text';
8491
- const MAX_LENGTH = 'max-length';
8492
- const MIN_LENGTH = 'min-length';
8493
- const MIN_TEXT_LENGTH = 'min-text-length';
8494
- const TAG_CONTENT = 'tag-content';
8495
- const WILDCARD = 'wildcard';
9120
+ var PseudoClasses$1;
9121
+ (function (PseudoClasses) {
9122
+ PseudoClasses["Contains"] = "contains";
9123
+ PseudoClasses["HasText"] = "has-text";
9124
+ PseudoClasses["MinTextLength"] = "min-text-length";
9125
+ })(PseudoClasses$1 || (PseudoClasses$1 = {}));
9126
+ var AttributeSelectors;
9127
+ (function (AttributeSelectors) {
9128
+ AttributeSelectors["MaxLength"] = "max-length";
9129
+ AttributeSelectors["MinLength"] = "min-length";
9130
+ AttributeSelectors["TagContent"] = "tag-content";
9131
+ AttributeSelectors["Wildcard"] = "wildcard";
9132
+ })(AttributeSelectors || (AttributeSelectors = {}));
8496
9133
  /**
8497
9134
  * HTML filtering rule converter class
8498
9135
  *
@@ -8515,16 +9152,23 @@ class HtmlRuleConverter extends RuleConverterBase {
8515
9152
  * ```
8516
9153
  *
8517
9154
  * @param rule Rule node to convert
8518
- * @returns Array of converted rule nodes
9155
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9156
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9157
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8519
9158
  * @throws If the rule is invalid or cannot be converted
8520
9159
  */
8521
9160
  static convertToAdg(rule) {
8522
- // Clone the provided AST node to avoid side effects
8523
- const ruleNode = cloneDeep(rule);
9161
+ // Ignore AdGuard rules
9162
+ if (rule.syntax === exports.AdblockSyntax.Adg) {
9163
+ return createNodeConversionResult([rule], false);
9164
+ }
9165
+ if (rule.syntax === exports.AdblockSyntax.Abp) {
9166
+ throw new RuleConversionError('Invalid rule, ABP does not support HTML filtering rules');
9167
+ }
8524
9168
  // Prepare the conversion result
8525
9169
  const conversionResult = [];
8526
9170
  // Iterate over selector list
8527
- for (const selector of ruleNode.body.body.children) {
9171
+ for (const selector of rule.body.body.children) {
8528
9172
  // Check selector, just in case
8529
9173
  if (selector.type !== exports.CssTreeNodeType.Selector) {
8530
9174
  throw new RuleConversionError(`Expected selector, got '${selector.type}'`);
@@ -8551,24 +9195,24 @@ class HtmlRuleConverter extends RuleConverterBase {
8551
9195
  throw new RuleConversionError('Tag selector should be the first child, if present');
8552
9196
  }
8553
9197
  // Simply store the tag selector
8554
- convertedSelector.children.push(cloneDeep(node));
9198
+ convertedSelector.children.push(clone(node));
8555
9199
  break;
8556
9200
  case exports.CssTreeNodeType.AttributeSelector:
8557
9201
  // Check if the attribute selector is a special AdGuard attribute
8558
9202
  switch (node.name.name) {
8559
- case MIN_LENGTH:
9203
+ case AttributeSelectors.MinLength:
8560
9204
  minLength = CssTree.parseAttributeSelectorValueAsNumber(node);
8561
9205
  break;
8562
- case MAX_LENGTH:
9206
+ case AttributeSelectors.MaxLength:
8563
9207
  maxLength = CssTree.parseAttributeSelectorValueAsNumber(node);
8564
9208
  break;
8565
- case TAG_CONTENT:
8566
- case WILDCARD:
9209
+ case AttributeSelectors.TagContent:
9210
+ case AttributeSelectors.Wildcard:
8567
9211
  CssTree.assertAttributeSelectorHasStringValue(node);
8568
- convertedSelector.children.push(cloneDeep(node));
9212
+ convertedSelector.children.push(clone(node));
8569
9213
  break;
8570
9214
  default:
8571
- convertedSelector.children.push(cloneDeep(node));
9215
+ convertedSelector.children.push(clone(node));
8572
9216
  }
8573
9217
  break;
8574
9218
  case exports.CssTreeNodeType.PseudoClassSelector:
@@ -8582,18 +9226,18 @@ class HtmlRuleConverter extends RuleConverterBase {
8582
9226
  }
8583
9227
  // Process the pseudo class based on its name
8584
9228
  switch (node.name) {
8585
- case HAS_TEXT$1:
8586
- case CONTAINS$1:
9229
+ case PseudoClasses$1.HasText:
9230
+ case PseudoClasses$1.Contains:
8587
9231
  // Check if the argument is a RegExp
8588
9232
  if (RegExpUtils.isRegexPattern(arg.value)) {
8589
9233
  // TODO: Add some support for RegExp patterns later
8590
9234
  // Need to find a way to convert some RegExp patterns to glob patterns
8591
9235
  throw new RuleConversionError('Conversion of RegExp patterns is not yet supported');
8592
9236
  }
8593
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(TAG_CONTENT, arg.value));
9237
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.TagContent, arg.value));
8594
9238
  break;
8595
9239
  // https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#subjectmin-text-lengthn
8596
- case MIN_TEXT_LENGTH:
9240
+ case PseudoClasses$1.MinTextLength:
8597
9241
  minLength = CssTree.parsePseudoClassArgumentAsNumber(node);
8598
9242
  break;
8599
9243
  default:
@@ -8605,10 +9249,10 @@ class HtmlRuleConverter extends RuleConverterBase {
8605
9249
  }
8606
9250
  }
8607
9251
  if (minLength !== NOT_SPECIFIED) {
8608
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(MIN_LENGTH, String(minLength)));
9252
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MinLength, String(minLength)));
8609
9253
  }
8610
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(MAX_LENGTH, String(maxLength === NOT_SPECIFIED
8611
- ? ADGUARD_HTML_CONVERSION_MAX_LENGTH
9254
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MaxLength, String(maxLength === NOT_SPECIFIED
9255
+ ? ADG_HTML_CONVERSION_MAX_LENGTH
8612
9256
  : maxLength)));
8613
9257
  // Create the converted rule
8614
9258
  conversionResult.push({
@@ -8618,7 +9262,7 @@ class HtmlRuleConverter extends RuleConverterBase {
8618
9262
  // Convert the separator based on the exception status
8619
9263
  separator: {
8620
9264
  type: 'Value',
8621
- value: ruleNode.exception
9265
+ value: rule.exception
8622
9266
  ? exports.CosmeticRuleSeparator.AdgHtmlFilteringException
8623
9267
  : exports.CosmeticRuleSeparator.AdgHtmlFiltering,
8624
9268
  },
@@ -8633,11 +9277,11 @@ class HtmlRuleConverter extends RuleConverterBase {
8633
9277
  }],
8634
9278
  },
8635
9279
  },
8636
- exception: ruleNode.exception,
8637
- domains: ruleNode.domains,
9280
+ exception: rule.exception,
9281
+ domains: cloneDomainListNode(rule.domains),
8638
9282
  });
8639
9283
  }
8640
- return conversionResult;
9284
+ return createNodeConversionResult(conversionResult, true);
8641
9285
  }
8642
9286
  }
8643
9287
 
@@ -8658,96 +9302,38 @@ function getScriptletName(scriptletNode) {
8658
9302
  return scriptletNode.children[0].value;
8659
9303
  }
8660
9304
  /**
8661
- * Set name of the scriptlet
9305
+ * Set name of the scriptlet.
9306
+ * Modifies input `scriptletNode` if needed.
8662
9307
  *
8663
9308
  * @param scriptletNode Scriptlet node to set name of
8664
9309
  * @param name Name to set
8665
- * @returns Scriptlet node with the specified name
8666
- * @throws If the scriptlet is empty
8667
9310
  */
8668
9311
  function setScriptletName(scriptletNode, name) {
8669
- if (scriptletNode.children.length === 0) {
8670
- throw new Error('Empty scriptlet');
9312
+ if (scriptletNode.children.length > 0) {
9313
+ // eslint-disable-next-line no-param-reassign
9314
+ scriptletNode.children[0].value = name;
8671
9315
  }
8672
- const scriptletNodeClone = cloneDeep(scriptletNode);
8673
- scriptletNodeClone.children[0].value = name;
8674
- return scriptletNodeClone;
8675
9316
  }
8676
9317
  /**
8677
9318
  * Set quote type of the scriptlet parameters
8678
9319
  *
8679
9320
  * @param scriptletNode Scriptlet node to set quote type of
8680
9321
  * @param quoteType Preferred quote type
8681
- * @returns Scriptlet node with the specified quote type
8682
9322
  */
8683
9323
  function setScriptletQuoteType(scriptletNode, quoteType) {
8684
- if (scriptletNode.children.length === 0) {
8685
- throw new Error('Empty scriptlet');
8686
- }
8687
- const scriptletNodeClone = cloneDeep(scriptletNode);
8688
- for (let i = 0; i < scriptletNodeClone.children.length; i += 1) {
8689
- scriptletNodeClone.children[i].value = QuoteUtils.setStringQuoteType(scriptletNodeClone.children[i].value, quoteType);
8690
- }
8691
- return scriptletNodeClone;
8692
- }
8693
-
8694
- /**
8695
- * @file Scriptlet conversions from ABP and uBO to ADG
8696
- */
8697
- const ABP_SCRIPTLET_PREFIX = 'abp-';
8698
- const UBO_SCRIPTLET_PREFIX = 'ubo-';
8699
- /**
8700
- * Helper class for converting scriptlets from ABP and uBO to ADG
8701
- */
8702
- class AdgScriptletConverter {
8703
- /**
8704
- * Helper function to convert scriptlets to ADG. We implement the core
8705
- * logic here to avoid code duplication.
8706
- *
8707
- * @param scriptletNode Scriptlet parameter list node to convert
8708
- * @param prefix Prefix to add to the scriptlet name
8709
- * @returns Converted scriptlet parameter list node
8710
- */
8711
- static convertToAdg(scriptletNode, prefix) {
8712
- // Remove possible quotes just to make it easier to work with the scriptlet name
8713
- const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletNode), exports.QuoteType.None);
8714
- // Clone the node to avoid any side effects
8715
- let result = cloneDeep(scriptletNode);
8716
- // Only add prefix if it's not already there
8717
- if (!scriptletName.startsWith(prefix)) {
8718
- result = setScriptletName(scriptletNode, `${prefix}${scriptletName}`);
9324
+ if (scriptletNode.children.length > 0) {
9325
+ for (let i = 0; i < scriptletNode.children.length; i += 1) {
9326
+ // eslint-disable-next-line no-param-reassign
9327
+ scriptletNode.children[i].value = QuoteUtils.setStringQuoteType(scriptletNode.children[i].value, quoteType);
8719
9328
  }
8720
- // ADG scriptlet parameters should be quoted, and single quoted are preferred
8721
- result = setScriptletQuoteType(result, exports.QuoteType.Single);
8722
- return result;
8723
9329
  }
8724
- /**
8725
- * Converts an ABP snippet node to ADG scriptlet node, if possible.
8726
- *
8727
- * @param scriptletNode Scriptlet node to convert
8728
- * @returns Converted scriptlet node
8729
- * @throws If the scriptlet isn't supported by ADG or is invalid
8730
- * @see {@link https://help.adblockplus.org/hc/en-us/articles/1500002338501#snippets-ref}
8731
- */
8732
- static convertFromAbp = (scriptletNode) => {
8733
- return AdgScriptletConverter.convertToAdg(scriptletNode, ABP_SCRIPTLET_PREFIX);
8734
- };
8735
- /**
8736
- * Convert a uBO scriptlet node to ADG scriptlet node, if possible.
8737
- *
8738
- * @param scriptletNode Scriptlet node to convert
8739
- * @returns Converted scriptlet node
8740
- * @throws If the scriptlet isn't supported by ADG or is invalid
8741
- * @see {@link https://github.com/gorhill/uBlock/wiki/Resources-Library#available-general-purpose-scriptlets}
8742
- */
8743
- static convertFromUbo = (scriptletNode) => {
8744
- return AdgScriptletConverter.convertToAdg(scriptletNode, UBO_SCRIPTLET_PREFIX);
8745
- };
8746
9330
  }
8747
9331
 
8748
9332
  /**
8749
9333
  * @file Scriptlet injection rule converter
8750
9334
  */
9335
+ const ABP_SCRIPTLET_PREFIX = 'abp-';
9336
+ const UBO_SCRIPTLET_PREFIX = 'ubo-';
8751
9337
  /**
8752
9338
  * Scriptlet injection rule converter class
8753
9339
  *
@@ -8758,38 +9344,91 @@ class ScriptletRuleConverter extends RuleConverterBase {
8758
9344
  * Converts a scriptlet injection rule to AdGuard format, if possible.
8759
9345
  *
8760
9346
  * @param rule Rule node to convert
8761
- * @returns Array of converted rule nodes
9347
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9348
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9349
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8762
9350
  * @throws If the rule is invalid or cannot be converted
8763
9351
  */
8764
9352
  static convertToAdg(rule) {
8765
- // Clone the provided AST node to avoid side effects
8766
- const ruleNode = cloneDeep(rule);
9353
+ // Ignore AdGuard rules
9354
+ if (rule.syntax === exports.AdblockSyntax.Adg) {
9355
+ return createNodeConversionResult([rule], false);
9356
+ }
9357
+ const separator = rule.separator.value;
9358
+ let convertedSeparator = separator;
9359
+ convertedSeparator = rule.exception
9360
+ ? exports.CosmeticRuleSeparator.AdgJsInjectionException
9361
+ : exports.CosmeticRuleSeparator.AdgJsInjection;
8767
9362
  const convertedScriptlets = [];
8768
- for (const scriptlet of ruleNode.body.children) {
8769
- if (ruleNode.syntax === exports.AdblockSyntax.Abp) {
8770
- convertedScriptlets.push(AdgScriptletConverter.convertFromAbp(scriptlet));
8771
- }
8772
- else if (ruleNode.syntax === exports.AdblockSyntax.Ubo) {
8773
- convertedScriptlets.push(AdgScriptletConverter.convertFromUbo(scriptlet));
9363
+ for (const scriptlet of rule.body.children) {
9364
+ // Clone the node to avoid any side effects
9365
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
9366
+ // Remove possible quotes just to make it easier to work with the scriptlet name
9367
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
9368
+ // Add prefix if it's not already there
9369
+ let prefix;
9370
+ switch (rule.syntax) {
9371
+ case exports.AdblockSyntax.Abp:
9372
+ prefix = ABP_SCRIPTLET_PREFIX;
9373
+ break;
9374
+ case exports.AdblockSyntax.Ubo:
9375
+ prefix = UBO_SCRIPTLET_PREFIX;
9376
+ break;
9377
+ default:
9378
+ prefix = EMPTY;
8774
9379
  }
8775
- else if (ruleNode.syntax === exports.AdblockSyntax.Adg) {
8776
- convertedScriptlets.push(scriptlet);
9380
+ if (!scriptletName.startsWith(prefix)) {
9381
+ setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
8777
9382
  }
9383
+ // ADG scriptlet parameters should be quoted, and single quoted are preferred
9384
+ setScriptletQuoteType(scriptletClone, exports.QuoteType.Single);
9385
+ convertedScriptlets.push(scriptletClone);
8778
9386
  }
8779
- ruleNode.separator.value = ruleNode.exception
8780
- ? exports.CosmeticRuleSeparator.AdgJsInjectionException
8781
- : exports.CosmeticRuleSeparator.AdgJsInjection;
8782
- // ADG doesn't support multiple scriptlets in one rule, so we should split them
8783
- return convertedScriptlets.map((scriptlet) => {
8784
- return {
8785
- ...ruleNode,
9387
+ return createNodeConversionResult(convertedScriptlets.map((scriptlet) => {
9388
+ const res = {
9389
+ category: rule.category,
9390
+ type: rule.type,
8786
9391
  syntax: exports.AdblockSyntax.Adg,
9392
+ exception: rule.exception,
9393
+ domains: cloneDomainListNode(rule.domains),
9394
+ separator: {
9395
+ type: 'Value',
9396
+ value: convertedSeparator,
9397
+ },
8787
9398
  body: {
8788
- ...ruleNode.body,
9399
+ type: rule.body.type,
8789
9400
  children: [scriptlet],
8790
9401
  },
8791
9402
  };
8792
- });
9403
+ if (rule.modifiers) {
9404
+ res.modifiers = cloneModifierListNode(rule.modifiers);
9405
+ }
9406
+ return res;
9407
+ }), true);
9408
+ }
9409
+ }
9410
+
9411
+ /**
9412
+ * A very simple map extension that allows to store multiple values for the same key
9413
+ * by storing them in an array.
9414
+ *
9415
+ * @todo Add more methods if needed
9416
+ */
9417
+ class MultiValueMap extends Map {
9418
+ /**
9419
+ * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
9420
+ * otherwise a new array will be created for the key.
9421
+ *
9422
+ * @param key Key to add
9423
+ * @param values Value(s) to add
9424
+ */
9425
+ add(key, ...values) {
9426
+ let currentValues = super.get(key);
9427
+ if (isUndefined(currentValues)) {
9428
+ currentValues = [];
9429
+ super.set(key, values);
9430
+ }
9431
+ currentValues.push(...values);
8793
9432
  }
8794
9433
  }
8795
9434
 
@@ -8815,69 +9454,115 @@ class AdgCosmeticRuleModifierConverter {
8815
9454
  * Converts a uBO cosmetic rule modifier list to ADG, if possible.
8816
9455
  *
8817
9456
  * @param modifierList Cosmetic rule modifier list node to convert
8818
- * @returns Converted cosmetic rule modifier list node
9457
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9458
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9459
+ * If the node was not converted, the result will contain the original node with the same object reference
8819
9460
  * @throws If the modifier list cannot be converted
8820
9461
  * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
8821
9462
  */
8822
- static convertFromUbo = (modifierList) => {
8823
- const convertedModifierList = createModifierListNode();
8824
- for (const modifier of modifierList.children) {
8825
- let modifierValue;
8826
- switch (modifier.modifier.value) {
8827
- case UBO_MATCHES_PATH_OPERATOR:
8828
- // :matches-path() should have a value
8829
- if (!modifier.value) {
8830
- throw new RuleConversionError('Missing value for :matches-path(...)');
8831
- }
8832
- modifierValue = RegExpUtils.isRegexPattern(modifier.value.value)
8833
- ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
8834
- : modifier.value.value;
8835
- // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
8836
- convertedModifierList.children.push(createModifierNode(ADG_PATH_MODIFIER,
8837
- // We should negate the regexp if the modifier is an exception
8838
- modifier.exception
8839
- // eslint-disable-next-line max-len
8840
- ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(modifierValue))}${REGEX_MARKER}`
8841
- : modifierValue));
8842
- break;
8843
- default:
8844
- // Leave the modifier as-is
8845
- convertedModifierList.children.push(modifier);
9463
+ static convertFromUbo(modifierList) {
9464
+ const conversionMap = new MultiValueMap();
9465
+ modifierList.children.forEach((modifier, index) => {
9466
+ // :matches-path
9467
+ if (modifier.modifier.value === UBO_MATCHES_PATH_OPERATOR) {
9468
+ if (!modifier.value) {
9469
+ throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
9470
+ }
9471
+ const value = RegExpUtils.isRegexPattern(modifier.value.value)
9472
+ ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
9473
+ : modifier.value.value;
9474
+ // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
9475
+ conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
9476
+ // We should negate the regexp if the modifier is an exception
9477
+ modifier.exception
9478
+ // eslint-disable-next-line max-len
9479
+ ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}`
9480
+ : value));
8846
9481
  }
8847
- }
8848
- return convertedModifierList;
8849
- };
9482
+ });
9483
+ // Check if we have any converted modifiers
9484
+ if (conversionMap.size) {
9485
+ const modifierListClone = clone(modifierList);
9486
+ // Replace the original modifiers with the converted ones
9487
+ modifierListClone.children = modifierListClone.children.map((modifier, index) => {
9488
+ const convertedModifier = conversionMap.get(index);
9489
+ return convertedModifier ?? modifier;
9490
+ }).flat();
9491
+ return createConversionResult(modifierListClone, true);
9492
+ }
9493
+ // Otherwise, just return the original modifier list
9494
+ return createConversionResult(modifierList, false);
9495
+ }
8850
9496
  }
8851
9497
 
8852
- // Constants for pseudo-classes (please keep them sorted alphabetically)
8853
- const ABP_CONTAINS = '-abp-contains';
8854
- const ABP_HAS = '-abp-has';
8855
- const CONTAINS = 'contains';
8856
- const HAS = 'has';
8857
- const HAS_TEXT = 'has-text';
8858
- const MATCHES_CSS = 'matches-css';
8859
- const MATCHES_CSS_AFTER = 'matches-css-after';
8860
- const MATCHES_CSS_BEFORE = 'matches-css-before';
8861
- const NOT = 'not';
8862
- // Constants for pseudo-elements (please keep them sorted alphabetically)
8863
- const AFTER = 'after';
8864
- const BEFORE = 'before';
9498
+ var PseudoClasses;
9499
+ (function (PseudoClasses) {
9500
+ PseudoClasses["AbpContains"] = "-abp-contains";
9501
+ PseudoClasses["AbpHas"] = "-abp-has";
9502
+ PseudoClasses["Contains"] = "contains";
9503
+ PseudoClasses["Has"] = "has";
9504
+ PseudoClasses["HasText"] = "has-text";
9505
+ PseudoClasses["MatchesCss"] = "matches-css";
9506
+ PseudoClasses["MatchesCssAfter"] = "matches-css-after";
9507
+ PseudoClasses["MatchesCssBefore"] = "matches-css-before";
9508
+ PseudoClasses["Not"] = "not";
9509
+ })(PseudoClasses || (PseudoClasses = {}));
9510
+ var PseudoElements;
9511
+ (function (PseudoElements) {
9512
+ PseudoElements["After"] = "after";
9513
+ PseudoElements["Before"] = "before";
9514
+ })(PseudoElements || (PseudoElements = {}));
9515
+ const PSEUDO_ELEMENT_NAMES = new Set([
9516
+ PseudoElements.After,
9517
+ PseudoElements.Before,
9518
+ ]);
9519
+ const LEGACY_MATCHES_CSS_NAMES = new Set([
9520
+ PseudoClasses.MatchesCssAfter,
9521
+ PseudoClasses.MatchesCssBefore,
9522
+ ]);
9523
+ const LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES = new Set([
9524
+ PseudoClasses.Not,
9525
+ PseudoClasses.MatchesCssBefore,
9526
+ PseudoClasses.MatchesCssAfter,
9527
+ ]);
9528
+ const CSS_CONVERSION_INDICATOR_PSEUDO_NAMES = new Set([
9529
+ PseudoClasses.AbpContains,
9530
+ PseudoClasses.AbpHas,
9531
+ PseudoClasses.HasText,
9532
+ ]);
8865
9533
  /**
8866
9534
  * Converts some pseudo-classes to pseudo-elements. For example:
8867
9535
  * - `:before` → `::before`
8868
9536
  *
8869
9537
  * @param selectorList Selector list to convert
8870
- * @returns Converted selector list
9538
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9539
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9540
+ * If the node was not converted, the result will contain the original node with the same object reference
8871
9541
  */
8872
9542
  function convertToPseudoElements(selectorList) {
8873
- // Prepare conversion result
8874
- const selectorListClone = cloneDeep(selectorList);
9543
+ // Check conversion indications before doing any heavy work
9544
+ const hasIndicator = ecssTree.find(
9545
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9546
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9547
+ selectorList, (node) => node.type === exports.CssTreeNodeType.PseudoClassSelector && PSEUDO_ELEMENT_NAMES.has(node.name));
9548
+ if (!hasIndicator) {
9549
+ return createConversionResult(selectorList, false);
9550
+ }
9551
+ // Make a clone of the selector list to avoid modifying the original one,
9552
+ // then convert & return the cloned version
9553
+ const selectorListClone = clone(selectorList);
9554
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9555
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8875
9556
  ecssTree.walk(selectorListClone, {
9557
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9558
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8876
9559
  leave: (node) => {
8877
9560
  if (node.type === exports.CssTreeNodeType.PseudoClassSelector) {
8878
- // :after ::after
8879
- // :before ::before
8880
- if (node.name === AFTER || node.name === BEFORE) {
9561
+ // If the pseudo-class is `:before` or `:after`, then we should
9562
+ // convert the node type to pseudo-element:
9563
+ // :after → ::after
9564
+ // :before → ::before
9565
+ if (PSEUDO_ELEMENT_NAMES.has(node.name)) {
8881
9566
  Object.assign(node, {
8882
9567
  ...node,
8883
9568
  type: exports.CssTreeNodeType.PseudoElementSelector,
@@ -8886,7 +9571,7 @@ function convertToPseudoElements(selectorList) {
8886
9571
  }
8887
9572
  },
8888
9573
  });
8889
- return selectorListClone;
9574
+ return createConversionResult(selectorListClone, true);
8890
9575
  }
8891
9576
  /**
8892
9577
  * Converts legacy Extended CSS `matches-css-before` and `matches-css-after`
@@ -8895,33 +9580,36 @@ function convertToPseudoElements(selectorList) {
8895
9580
  * - `:matches-css-after(...)` → `:matches-css(after, ...)`
8896
9581
  *
8897
9582
  * @param node Node to convert
9583
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9584
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9585
+ * If the node was not converted, the result will contain the original node with the same object reference
8898
9586
  * @throws If the node is invalid
8899
9587
  */
8900
9588
  function convertLegacyMatchesCss(node) {
8901
- const nodeClone = cloneDeep(node);
8902
- if (nodeClone.type === exports.CssTreeNodeType.PseudoClassSelector
8903
- && [MATCHES_CSS_BEFORE, MATCHES_CSS_AFTER].includes(nodeClone.name)) {
8904
- if (!nodeClone.children || nodeClone.children.size < 1) {
8905
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: missing argument`);
8906
- }
8907
- // Remove the 'matches-css-' prefix to get the direction
8908
- const direction = nodeClone.name.substring(MATCHES_CSS.length + 1);
8909
- // Rename the pseudo-class
8910
- nodeClone.name = MATCHES_CSS;
8911
- // Add the direction to the first raw argument
8912
- const arg = nodeClone.children.first;
8913
- // Check argument
8914
- if (!arg) {
8915
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: argument shouldn't be null`);
8916
- }
8917
- if (arg.type !== exports.CssTreeNodeType.Raw) {
8918
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: unexpected argument type`);
8919
- }
8920
- // Add the direction as the first argument
8921
- arg.value = `${direction},${arg.value}`;
8922
- // Replace the original node with the converted one
8923
- Object.assign(node, nodeClone);
8924
- }
9589
+ // Check conversion indications before doing any heavy work
9590
+ if (node.type !== exports.CssTreeNodeType.PseudoClassSelector || !LEGACY_MATCHES_CSS_NAMES.has(node.name)) {
9591
+ return createConversionResult(node, false);
9592
+ }
9593
+ const nodeClone = clone(node);
9594
+ if (!nodeClone.children || nodeClone.children.length < 1) {
9595
+ throw new Error(`Invalid ${node.name} pseudo-class: missing argument`);
9596
+ }
9597
+ // Rename the pseudo-class
9598
+ nodeClone.name = PseudoClasses.MatchesCss;
9599
+ // Remove the 'matches-css-' prefix to get the direction
9600
+ const direction = node.name.substring(PseudoClasses.MatchesCss.length + 1);
9601
+ // Add the direction to the first raw argument
9602
+ const arg = nodeClone.children[0];
9603
+ // Check argument
9604
+ if (!arg) {
9605
+ throw new Error(`Invalid ${node.name} pseudo-class: argument shouldn't be null`);
9606
+ }
9607
+ if (arg.type !== exports.CssTreeNodeType.Raw) {
9608
+ throw new Error(`Invalid ${node.name} pseudo-class: unexpected argument type`);
9609
+ }
9610
+ // Add the direction as the first argument
9611
+ arg.value = `${direction},${arg.value}`;
9612
+ return createConversionResult(nodeClone, true);
8925
9613
  }
8926
9614
  /**
8927
9615
  * Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
@@ -8931,16 +9619,40 @@ function convertLegacyMatchesCss(node) {
8931
9619
  * - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
8932
9620
  *
8933
9621
  * @param selectorList Selector list AST to convert
8934
- * @returns Converted selector list
9622
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9623
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9624
+ * If the node was not converted, the result will contain the original node with the same object reference
8935
9625
  */
8936
9626
  function convertFromLegacyExtendedCss(selectorList) {
8937
- // Prepare conversion result
8938
- const selectorListClone = cloneDeep(selectorList);
9627
+ // Check conversion indications before doing any heavy work
9628
+ const hasIndicator = ecssTree.find(
9629
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9630
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9631
+ selectorList, (node) => {
9632
+ if (node.type === exports.CssTreeNodeType.PseudoClassSelector) {
9633
+ return LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES.has(node.name);
9634
+ }
9635
+ if (node.type === exports.CssTreeNodeType.AttributeSelector) {
9636
+ return node.name.name.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX);
9637
+ }
9638
+ return false;
9639
+ });
9640
+ if (!hasIndicator) {
9641
+ return createConversionResult(selectorList, false);
9642
+ }
9643
+ const selectorListClone = clone(selectorList);
9644
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9645
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8939
9646
  ecssTree.walk(selectorListClone, {
9647
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9648
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8940
9649
  leave: (node) => {
8941
9650
  // :matches-css-before(arg) → :matches-css(before,arg)
8942
9651
  // :matches-css-after(arg) → :matches-css(after,arg)
8943
- convertLegacyMatchesCss(node);
9652
+ const convertedLegacyExtCss = convertLegacyMatchesCss(node);
9653
+ if (convertedLegacyExtCss.isConverted) {
9654
+ Object.assign(node, convertedLegacyExtCss.result);
9655
+ }
8944
9656
  // [-ext-name=...] → :name(...)
8945
9657
  // [-ext-name='...'] → :name(...)
8946
9658
  // [-ext-name="..."] → :name(...)
@@ -8954,7 +9666,7 @@ function convertFromLegacyExtendedCss(selectorList) {
8954
9666
  // Remove the '-ext-' prefix to get the pseudo-class name
8955
9667
  const name = node.name.name.substring(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
8956
9668
  // Prepare the children list for the pseudo-class node
8957
- const children = new ecssTree.List();
9669
+ const children = [];
8958
9670
  // TODO: Change String node to Raw node to drop the quotes.
8959
9671
  // The structure of the node is the same, just the type
8960
9672
  // is different and generate() will generate the quotes
@@ -8967,7 +9679,7 @@ function convertFromLegacyExtendedCss(selectorList) {
8967
9679
  // For example, if the input is [-ext-has="> .selector"], then
8968
9680
  // we need to parse "> .selector" as a selector instead of string
8969
9681
  // it as a raw value
8970
- if ([HAS, NOT].includes(name)) {
9682
+ if ([PseudoClasses.Has, PseudoClasses.Not].includes(name)) {
8971
9683
  // Get the value of the attribute selector
8972
9684
  const { value } = node;
8973
9685
  // If the value is an identifier, then simply push it to the
@@ -8977,10 +9689,12 @@ function convertFromLegacyExtendedCss(selectorList) {
8977
9689
  }
8978
9690
  else if (value.type === exports.CssTreeNodeType.String) {
8979
9691
  // Parse the value as a selector
8980
- const parsedChildren = CssTree.parse(value.value, exports.CssTreeParserContext.selectorList);
9692
+ const parsedChildren = CssTree.parsePlain(value.value, exports.CssTreeParserContext.selectorList);
8981
9693
  // Don't forget convert the parsed AST again, because
8982
9694
  // it was a raw string before
8983
- children.push(convertFromLegacyExtendedCss(parsedChildren));
9695
+ const convertedChildren = convertFromLegacyExtendedCss(parsedChildren);
9696
+ // Push the converted children to the list
9697
+ children.push(convertedChildren.result);
8984
9698
  }
8985
9699
  }
8986
9700
  else {
@@ -9007,14 +9721,12 @@ function convertFromLegacyExtendedCss(selectorList) {
9007
9721
  children,
9008
9722
  };
9009
9723
  // Handle this case: [-ext-matches-css-before=...] → :matches-css(before,...)
9010
- convertLegacyMatchesCss(pseudoNode);
9011
- // Convert attribute selector to pseudo-class selector, but
9012
- // keep the reference to the original node
9013
- Object.assign(node, pseudoNode);
9724
+ const convertedPseudoNode = convertLegacyMatchesCss(pseudoNode);
9725
+ Object.assign(node, convertedPseudoNode.isConverted ? convertedPseudoNode.result : pseudoNode);
9014
9726
  }
9015
9727
  },
9016
9728
  });
9017
- return selectorListClone;
9729
+ return createConversionResult(selectorListClone, true);
9018
9730
  }
9019
9731
  /**
9020
9732
  * CSS selector converter
@@ -9026,32 +9738,51 @@ class CssSelectorConverter extends ConverterBase {
9026
9738
  * Converts Extended CSS elements to AdGuard-compatible ones
9027
9739
  *
9028
9740
  * @param selectorList Selector list to convert
9029
- * @returns Converted selector list
9741
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9742
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9743
+ * If the node was not converted, the result will contain the original node with the same object reference
9030
9744
  * @throws If the rule is invalid or incompatible
9031
9745
  */
9032
9746
  static convertToAdg(selectorList) {
9033
9747
  // First, convert
9034
9748
  // - legacy Extended CSS selectors to the modern Extended CSS syntax and
9035
9749
  // - some pseudo-classes to pseudo-elements
9036
- const selectorListClone = convertToPseudoElements(convertFromLegacyExtendedCss(cloneDeep(selectorList)));
9750
+ const legacyExtCssConverted = convertFromLegacyExtendedCss(selectorList);
9751
+ const pseudoElementsConverted = convertToPseudoElements(legacyExtCssConverted.result);
9752
+ const hasIndicator = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted || ecssTree.find(
9753
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9754
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9755
+ selectorList,
9756
+ // eslint-disable-next-line max-len
9757
+ (node) => node.type === exports.CssTreeNodeType.PseudoClassSelector && CSS_CONVERSION_INDICATOR_PSEUDO_NAMES.has(node.name));
9758
+ if (!hasIndicator) {
9759
+ return createConversionResult(selectorList, false);
9760
+ }
9761
+ const selectorListClone = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted
9762
+ ? pseudoElementsConverted.result
9763
+ : clone(selectorList);
9037
9764
  // Then, convert some Extended CSS pseudo-classes to AdGuard-compatible ones
9765
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9766
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9038
9767
  ecssTree.walk(selectorListClone, {
9768
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9769
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9039
9770
  leave: (node) => {
9040
9771
  if (node.type === exports.CssTreeNodeType.PseudoClassSelector) {
9041
9772
  // :-abp-contains(...) → :contains(...)
9042
9773
  // :has-text(...) → :contains(...)
9043
- if (node.name === ABP_CONTAINS || node.name === HAS_TEXT) {
9044
- CssTree.renamePseudoClass(node, CONTAINS);
9774
+ if (node.name === PseudoClasses.AbpContains || node.name === PseudoClasses.HasText) {
9775
+ CssTree.renamePseudoClass(node, PseudoClasses.Contains);
9045
9776
  }
9046
9777
  // :-abp-has(...) → :has(...)
9047
- if (node.name === ABP_HAS) {
9048
- CssTree.renamePseudoClass(node, HAS);
9778
+ if (node.name === PseudoClasses.AbpHas) {
9779
+ CssTree.renamePseudoClass(node, PseudoClasses.Has);
9049
9780
  }
9050
9781
  // TODO: check uBO's `:others()` and `:watch-attr()` pseudo-classes
9051
9782
  }
9052
9783
  },
9053
9784
  });
9054
- return selectorListClone;
9785
+ return createConversionResult(selectorListClone, true);
9055
9786
  }
9056
9787
  }
9057
9788
 
@@ -9068,27 +9799,39 @@ class CssInjectionRuleConverter extends RuleConverterBase {
9068
9799
  * Converts a CSS injection rule to AdGuard format, if possible.
9069
9800
  *
9070
9801
  * @param rule Rule node to convert
9071
- * @returns Array of converted rule nodes
9802
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9803
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9804
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9072
9805
  * @throws If the rule is invalid or cannot be converted
9073
9806
  */
9074
9807
  static convertToAdg(rule) {
9075
- // Clone the provided AST node to avoid side effects
9076
- const ruleNode = cloneDeep(rule);
9808
+ const separator = rule.separator.value;
9809
+ let convertedSeparator = separator;
9077
9810
  // Change the separator if the rule contains ExtendedCSS selectors
9078
- if (CssTree.hasAnySelectorExtendedCssNode(ruleNode.body.selectorList) || ruleNode.body.remove) {
9079
- ruleNode.separator.value = ruleNode.exception
9811
+ if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList) || rule.body.remove) {
9812
+ convertedSeparator = rule.exception
9080
9813
  ? exports.CosmeticRuleSeparator.AdgExtendedCssInjectionException
9081
9814
  : exports.CosmeticRuleSeparator.AdgExtendedCssInjection;
9082
9815
  }
9083
9816
  else {
9084
- ruleNode.separator.value = ruleNode.exception
9817
+ convertedSeparator = rule.exception
9085
9818
  ? exports.CosmeticRuleSeparator.AdgCssInjectionException
9086
9819
  : exports.CosmeticRuleSeparator.AdgCssInjection;
9087
9820
  }
9088
- // Convert CSS selector list
9089
- Object.assign(ruleNode.body.selectorList, CssSelectorConverter.convertToAdg(ecssTree.fromPlainObject(ruleNode.body.selectorList)));
9090
- ruleNode.syntax = exports.AdblockSyntax.Adg;
9091
- return [ruleNode];
9821
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
9822
+ // Check if the rule needs to be converted
9823
+ if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg)
9824
+ || separator !== convertedSeparator
9825
+ || convertedSelectorList.isConverted) {
9826
+ // TODO: Replace with custom clone method
9827
+ const ruleClone = clone(rule);
9828
+ ruleClone.syntax = exports.AdblockSyntax.Adg;
9829
+ ruleClone.separator.value = convertedSeparator;
9830
+ ruleClone.body.selectorList = convertedSelectorList.result;
9831
+ return createNodeConversionResult([ruleClone], true);
9832
+ }
9833
+ // Otherwise, return the original rule
9834
+ return createNodeConversionResult([rule], false);
9092
9835
  }
9093
9836
  }
9094
9837
 
@@ -9105,27 +9848,39 @@ class ElementHidingRuleConverter extends RuleConverterBase {
9105
9848
  * Converts an element hiding rule to AdGuard format, if possible.
9106
9849
  *
9107
9850
  * @param rule Rule node to convert
9108
- * @returns Array of converted rule nodes
9851
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9852
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9853
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9109
9854
  * @throws If the rule is invalid or cannot be converted
9110
9855
  */
9111
9856
  static convertToAdg(rule) {
9112
- // Clone the provided AST node to avoid side effects
9113
- const ruleNode = cloneDeep(rule);
9857
+ const separator = rule.separator.value;
9858
+ let convertedSeparator = separator;
9114
9859
  // Change the separator if the rule contains ExtendedCSS selectors
9115
- if (CssTree.hasAnySelectorExtendedCssNode(ruleNode.body.selectorList)) {
9116
- ruleNode.separator.value = ruleNode.exception
9860
+ if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList)) {
9861
+ convertedSeparator = rule.exception
9117
9862
  ? exports.CosmeticRuleSeparator.ExtendedElementHidingException
9118
9863
  : exports.CosmeticRuleSeparator.ExtendedElementHiding;
9119
9864
  }
9120
9865
  else {
9121
- ruleNode.separator.value = ruleNode.exception
9866
+ convertedSeparator = rule.exception
9122
9867
  ? exports.CosmeticRuleSeparator.ElementHidingException
9123
9868
  : exports.CosmeticRuleSeparator.ElementHiding;
9124
9869
  }
9125
- // Convert CSS selector list
9126
- Object.assign(ruleNode.body.selectorList, CssSelectorConverter.convertToAdg(ecssTree.fromPlainObject(ruleNode.body.selectorList)));
9127
- ruleNode.syntax = exports.AdblockSyntax.Adg;
9128
- return [ruleNode];
9870
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
9871
+ // Check if the rule needs to be converted
9872
+ if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg)
9873
+ || separator !== convertedSeparator
9874
+ || convertedSelectorList.isConverted) {
9875
+ // TODO: Replace with custom clone method
9876
+ const ruleClone = clone(rule);
9877
+ ruleClone.syntax = exports.AdblockSyntax.Adg;
9878
+ ruleClone.separator.value = convertedSeparator;
9879
+ ruleClone.body.selectorList = convertedSelectorList.result;
9880
+ return createNodeConversionResult([ruleClone], true);
9881
+ }
9882
+ // Otherwise, return the original rule
9883
+ return createNodeConversionResult([rule], false);
9129
9884
  }
9130
9885
  }
9131
9886
 
@@ -9153,7 +9908,7 @@ function createNetworkRuleNode(pattern, modifiers = undefined, exception = false
9153
9908
  },
9154
9909
  };
9155
9910
  if (!isUndefined(modifiers)) {
9156
- result.modifiers = cloneDeep(modifiers);
9911
+ result.modifiers = clone(modifiers);
9157
9912
  }
9158
9913
  return result;
9159
9914
  }
@@ -9173,32 +9928,37 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
9173
9928
  * Converts a header removal rule to AdGuard syntax, if possible.
9174
9929
  *
9175
9930
  * @param rule Rule node to convert
9176
- * @returns Array of converted rule nodes
9931
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9932
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9933
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9177
9934
  * @throws If the rule is invalid or cannot be converted
9935
+ * @example
9936
+ * If the input rule is:
9937
+ * ```adblock
9938
+ * example.com##^responseheader(header-name)
9939
+ * ```
9940
+ * The output will be:
9941
+ * ```adblock
9942
+ * ||example.com^$removeheader=header-name
9943
+ * ```
9178
9944
  */
9179
9945
  static convertToAdg(rule) {
9180
- // Clone the provided AST node to avoid side effects
9181
- const ruleNode = cloneDeep(rule);
9182
9946
  // TODO: Add support for ABP syntax once it starts supporting header removal rules
9183
- // Check the input rule
9184
- if (ruleNode.category !== exports.RuleCategory.Cosmetic
9185
- || ruleNode.type !== exports.CosmeticRuleType.HtmlFilteringRule
9186
- || ruleNode.body.body.type !== exports.CssTreeNodeType.Function
9187
- || ruleNode.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
9188
- throw new RuleConversionError('Not a response header rule');
9947
+ // Leave the rule as is if it's not a header removal rule
9948
+ if (rule.category !== exports.RuleCategory.Cosmetic
9949
+ || rule.type !== exports.CosmeticRuleType.HtmlFilteringRule
9950
+ || rule.body.body.type !== exports.CssTreeNodeType.Function
9951
+ || rule.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
9952
+ return createNodeConversionResult([rule], false);
9189
9953
  }
9190
9954
  // Prepare network rule pattern
9191
- let pattern = EMPTY;
9192
- if (ruleNode.domains.children.length === 1) {
9955
+ const pattern = [];
9956
+ if (rule.domains.children.length === 1) {
9193
9957
  // If the rule has only one domain, we can use a simple network rule pattern:
9194
9958
  // ||single-domain-from-the-rule^
9195
- pattern = [
9196
- ADBLOCK_URL_START,
9197
- ruleNode.domains.children[0].value,
9198
- ADBLOCK_URL_SEPARATOR,
9199
- ].join(EMPTY);
9959
+ pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
9200
9960
  }
9201
- else if (ruleNode.domains.children.length > 1) {
9961
+ else if (rule.domains.children.length > 1) {
9202
9962
  // TODO: Add support for multiple domains, for example:
9203
9963
  // example.com,example.org,example.net##^responseheader(header-name)
9204
9964
  // We should consider allowing $domain with $removeheader modifier,
@@ -9208,13 +9968,13 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
9208
9968
  }
9209
9969
  // Prepare network rule modifiers
9210
9970
  const modifiers = createModifierListNode();
9211
- modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionValue(ecssTree.fromPlainObject(ruleNode.body.body))));
9971
+ modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionPlainValue(rule.body.body)));
9212
9972
  // Construct the network rule
9213
- return [
9214
- createNetworkRuleNode(pattern, modifiers,
9973
+ return createNodeConversionResult([
9974
+ createNetworkRuleNode(pattern.join(EMPTY), modifiers,
9215
9975
  // Copy the exception flag
9216
- ruleNode.exception, exports.AdblockSyntax.Adg),
9217
- ];
9976
+ rule.exception, exports.AdblockSyntax.Adg),
9977
+ ], true);
9218
9978
  }
9219
9979
  }
9220
9980
 
@@ -9231,54 +9991,80 @@ class CosmeticRuleConverter extends RuleConverterBase {
9231
9991
  * Converts a cosmetic rule to AdGuard syntax, if possible.
9232
9992
  *
9233
9993
  * @param rule Rule node to convert
9234
- * @returns Array of converted rule nodes
9994
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9995
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9996
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9235
9997
  * @throws If the rule is invalid or cannot be converted
9236
9998
  */
9237
9999
  static convertToAdg(rule) {
9238
- // Clone the provided AST node to avoid side effects
9239
- const ruleNode = cloneDeep(rule);
9240
- // Convert cosmetic rule modifiers
9241
- if (ruleNode.modifiers) {
9242
- if (ruleNode.syntax === exports.AdblockSyntax.Ubo) {
9243
- // uBO doesn't support this rule:
9244
- // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
9245
- if (ruleNode.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
9246
- throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
9247
- }
9248
- ruleNode.modifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(ruleNode.modifiers);
9249
- }
9250
- else if (ruleNode.syntax === exports.AdblockSyntax.Abp) {
9251
- // TODO: Implement once ABP starts supporting cosmetic rule modifiers
9252
- throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
9253
- }
9254
- }
10000
+ let subconverterResult;
9255
10001
  // Convert cosmetic rule based on its type
9256
- switch (ruleNode.type) {
10002
+ switch (rule.type) {
9257
10003
  case exports.CosmeticRuleType.ElementHidingRule:
9258
- return ElementHidingRuleConverter.convertToAdg(ruleNode);
10004
+ subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
10005
+ break;
9259
10006
  case exports.CosmeticRuleType.ScriptletInjectionRule:
9260
- return ScriptletRuleConverter.convertToAdg(ruleNode);
10007
+ subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
10008
+ break;
9261
10009
  case exports.CosmeticRuleType.CssInjectionRule:
9262
- return CssInjectionRuleConverter.convertToAdg(ruleNode);
10010
+ subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
10011
+ break;
9263
10012
  case exports.CosmeticRuleType.HtmlFilteringRule:
9264
10013
  // Handle special case: uBO response header filtering rule
9265
- if (ruleNode.body.body.type === exports.CssTreeNodeType.Function
9266
- && ruleNode.body.body.name === UBO_RESPONSEHEADER_MARKER) {
9267
- return HeaderRemovalRuleConverter.convertToAdg(ruleNode);
10014
+ if (rule.body.body.type === exports.CssTreeNodeType.Function
10015
+ && rule.body.body.name === UBO_RESPONSEHEADER_MARKER) {
10016
+ subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
9268
10017
  }
9269
- return HtmlRuleConverter.convertToAdg(ruleNode);
9270
- // Note: Currently, only ADG supports JS injection rules
10018
+ else {
10019
+ subconverterResult = HtmlRuleConverter.convertToAdg(rule);
10020
+ }
10021
+ break;
10022
+ // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
9271
10023
  case exports.CosmeticRuleType.JsInjectionRule:
9272
- return [ruleNode];
10024
+ subconverterResult = createNodeConversionResult([rule], false);
10025
+ break;
9273
10026
  default:
9274
10027
  throw new RuleConversionError('Unsupported cosmetic rule type');
9275
10028
  }
10029
+ let convertedModifiers;
10030
+ // Convert cosmetic rule modifiers, if any
10031
+ if (rule.modifiers) {
10032
+ if (rule.syntax === exports.AdblockSyntax.Ubo) {
10033
+ // uBO doesn't support this rule:
10034
+ // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
10035
+ if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
10036
+ throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
10037
+ }
10038
+ convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
10039
+ }
10040
+ else if (rule.syntax === exports.AdblockSyntax.Abp) {
10041
+ // TODO: Implement once ABP starts supporting cosmetic rule modifiers
10042
+ throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
10043
+ }
10044
+ }
10045
+ if ((subconverterResult.result.length > 1 || subconverterResult.isConverted)
10046
+ || (convertedModifiers && convertedModifiers.isConverted)) {
10047
+ // Add modifier list to the subconverter result rules
10048
+ subconverterResult.result.forEach((subconverterRule) => {
10049
+ if (convertedModifiers && subconverterRule.category === exports.RuleCategory.Cosmetic) {
10050
+ // eslint-disable-next-line no-param-reassign
10051
+ subconverterRule.modifiers = convertedModifiers.result;
10052
+ }
10053
+ });
10054
+ return subconverterResult;
10055
+ }
10056
+ return createNodeConversionResult([rule], false);
9276
10057
  }
9277
10058
  }
9278
10059
 
9279
10060
  /**
9280
10061
  * @file Network rule modifier list converter.
9281
10062
  */
10063
+ // Since scriptlets library doesn't have ESM exports, we should import
10064
+ // the whole module and then extract the required functions from it here.
10065
+ // Otherwise importing AGTree will cause an error in ESM environment,
10066
+ // because scriptlets library doesn't support named exports.
10067
+ const { redirects } = scriptlets;
9282
10068
  /**
9283
10069
  * @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#csp-modifier}
9284
10070
  */
@@ -9339,17 +10125,16 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9339
10125
  * Converts a network rule modifier list to AdGuard format, if possible.
9340
10126
  *
9341
10127
  * @param modifierList Network rule modifier list node to convert
9342
- * @returns Converted modifier list node
10128
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10129
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
10130
+ * If the node was not converted, the result will contain the original node with the same object reference
9343
10131
  * @throws If the conversion is not possible
9344
10132
  */
9345
10133
  static convertToAdg(modifierList) {
9346
- // Clone the provided AST node to avoid side effects
9347
- const modifierListNode = cloneDeep(modifierList);
9348
- const convertedModifierList = createModifierListNode();
9349
- // We should merge $csp modifiers into one
9350
- const cspValues = [];
9351
- modifierListNode.children.forEach((modifierNode) => {
9352
- // Handle regular modifiers conversion and $csp modifiers collection
10134
+ const conversionMap = new MultiValueMap();
10135
+ // Special case: $csp modifier
10136
+ let cspCount = 0;
10137
+ modifierList.children.forEach((modifierNode, index) => {
9353
10138
  const modifierConversions = ADG_CONVERSION_MAP.get(modifierNode.modifier.value);
9354
10139
  if (modifierConversions) {
9355
10140
  for (const modifierConversion of modifierConversions) {
@@ -9362,17 +10147,14 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9362
10147
  const value = modifierConversion.value
9363
10148
  ? modifierConversion.value(modifierNode.value?.value)
9364
10149
  : modifierNode.value?.value;
9365
- if (name === CSP_MODIFIER && value) {
9366
- // Special case: collect $csp values
9367
- cspValues.push(value);
10150
+ // Check if the name or the value is different from the original modifier
10151
+ // If so, add the converted modifier to the list
10152
+ if (name !== modifierNode.modifier.value || value !== modifierNode.value?.value) {
10153
+ conversionMap.add(index, createModifierNode(name, value, exception));
9368
10154
  }
9369
- else {
9370
- // Regular case: collect the converted modifiers, if the modifier list
9371
- // not already contains the same modifier
9372
- const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === name && m.exception === exception && m.value?.value === value);
9373
- if (!existingModifier) {
9374
- convertedModifierList.children.push(createModifierNode(name, value, exception));
9375
- }
10155
+ // Special case: $csp modifier
10156
+ if (name === CSP_MODIFIER) {
10157
+ cspCount += 1;
9376
10158
  }
9377
10159
  }
9378
10160
  return;
@@ -9394,27 +10176,53 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9394
10176
  : modifierNode.modifier.value;
9395
10177
  // Try to convert the redirect resource name to ADG format
9396
10178
  // This function returns undefined if the resource name is unknown
9397
- const convertedRedirectResource = scriptlets.redirects.convertRedirectNameToAdg(redirectResource);
9398
- convertedModifierList.children.push(createModifierNode(modifierName,
9399
- // If the redirect resource name is unknown, fall back to the original one
9400
- // Later, the validator will throw an error if the resource name is invalid
9401
- convertedRedirectResource || redirectResource, modifierNode.exception));
9402
- return;
9403
- }
9404
- // In all other cases, just copy the modifier as is, if the modifier list
9405
- // not already contains the same modifier
9406
- const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === modifierNode.modifier.value
9407
- && m.exception === modifierNode.exception
9408
- && m.value?.value === modifierNode.value?.value);
9409
- if (!existingModifier) {
9410
- convertedModifierList.children.push(modifierNode);
10179
+ const convertedRedirectResource = redirects.convertRedirectNameToAdg(redirectResource);
10180
+ // Check if the modifier name or the redirect resource name is different from the original modifier
10181
+ // If so, add the converted modifier to the list
10182
+ if (modifierName !== modifierNode.modifier.value
10183
+ || (convertedRedirectResource !== undefined && convertedRedirectResource !== redirectResource)) {
10184
+ conversionMap.add(index, createModifierNode(modifierName,
10185
+ // If the redirect resource name is unknown, fall back to the original one
10186
+ // Later, the validator will throw an error if the resource name is invalid
10187
+ convertedRedirectResource || redirectResource, modifierNode.exception));
10188
+ }
9411
10189
  }
9412
10190
  });
9413
- // Merge $csp modifiers into one, then add it to the converted modifier list
9414
- if (cspValues.length > 0) {
9415
- convertedModifierList.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
10191
+ // Prepare the result if there are any converted modifiers or $csp modifiers
10192
+ if (conversionMap.size || cspCount) {
10193
+ const modifierListClone = cloneModifierListNode(modifierList);
10194
+ // Replace the original modifiers with the converted ones
10195
+ // One modifier may be replaced with multiple modifiers, so we need to flatten the array
10196
+ modifierListClone.children = modifierListClone.children.map((modifierNode, index) => {
10197
+ const conversionRecord = conversionMap.get(index);
10198
+ if (conversionRecord) {
10199
+ return conversionRecord;
10200
+ }
10201
+ return modifierNode;
10202
+ }).flat();
10203
+ // Special case: $csp modifier: merge multiple $csp modifiers into one
10204
+ // and put it at the end of the modifier list
10205
+ if (cspCount) {
10206
+ const cspValues = [];
10207
+ modifierListClone.children = modifierListClone.children.filter((modifierNode) => {
10208
+ if (modifierNode.modifier.value === CSP_MODIFIER) {
10209
+ if (!modifierNode.value?.value) {
10210
+ throw new RuleConversionError('$csp modifier value is missing');
10211
+ }
10212
+ cspValues.push(modifierNode.value?.value);
10213
+ return false;
10214
+ }
10215
+ return true;
10216
+ });
10217
+ modifierListClone.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
10218
+ }
10219
+ // Before returning the result, remove duplicated modifiers
10220
+ modifierListClone.children = modifierListClone.children.filter((modifierNode, index, self) => self.findIndex((m) => m.modifier.value === modifierNode.modifier.value
10221
+ && m.exception === modifierNode.exception
10222
+ && m.value?.value === modifierNode.value?.value) === index);
10223
+ return createConversionResult(modifierListClone, true);
9416
10224
  }
9417
- return convertedModifierList;
10225
+ return createConversionResult(modifierList, false);
9418
10226
  }
9419
10227
  }
9420
10228
 
@@ -9431,17 +10239,35 @@ class NetworkRuleConverter extends RuleConverterBase {
9431
10239
  * Converts a network rule to AdGuard format, if possible.
9432
10240
  *
9433
10241
  * @param rule Rule node to convert
9434
- * @returns Array of converted rule nodes
10242
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
10243
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
10244
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9435
10245
  * @throws If the rule is invalid or cannot be converted
9436
10246
  */
9437
10247
  static convertToAdg(rule) {
9438
- // Clone the provided AST node to avoid side effects
9439
- const ruleNode = cloneDeep(rule);
9440
- // Convert modifiers
9441
- if (ruleNode.modifiers) {
9442
- Object.assign(ruleNode.modifiers, NetworkRuleModifierListConverter.convertToAdg(ruleNode.modifiers));
10248
+ if (rule.modifiers) {
10249
+ const modifiers = NetworkRuleModifierListConverter.convertToAdg(rule.modifiers);
10250
+ // If the object reference is different, it means that the modifiers were converted
10251
+ // In this case, we should clone the entire rule and replace the modifiers with the converted ones
10252
+ if (modifiers.isConverted) {
10253
+ return {
10254
+ result: [{
10255
+ category: exports.RuleCategory.Network,
10256
+ type: 'NetworkRule',
10257
+ syntax: rule.syntax,
10258
+ exception: rule.exception,
10259
+ pattern: {
10260
+ type: 'Value',
10261
+ value: rule.pattern.value,
10262
+ },
10263
+ modifiers: modifiers.result,
10264
+ }],
10265
+ isConverted: true,
10266
+ };
10267
+ }
9443
10268
  }
9444
- return [ruleNode];
10269
+ // If the modifiers were not converted, return the original rule
10270
+ return createNodeConversionResult([rule], false);
9445
10271
  }
9446
10272
  }
9447
10273
 
@@ -9462,48 +10288,27 @@ class RuleConverter extends RuleConverterBase {
9462
10288
  * Converts an adblock filtering rule to AdGuard format, if possible.
9463
10289
  *
9464
10290
  * @param rule Rule node to convert
9465
- * @returns Array of converted rule nodes
10291
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
10292
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
10293
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9466
10294
  * @throws If the rule is invalid or cannot be converted
9467
10295
  */
9468
10296
  static convertToAdg(rule) {
9469
- // Clone the provided AST node to avoid side effects
9470
- const ruleNode = cloneDeep(rule);
9471
10297
  // Delegate conversion to the corresponding sub-converter
9472
10298
  // based on the rule category
9473
- switch (ruleNode.category) {
10299
+ switch (rule.category) {
9474
10300
  case exports.RuleCategory.Comment:
9475
- return CommentRuleConverter.convertToAdg(ruleNode);
10301
+ return CommentRuleConverter.convertToAdg(rule);
9476
10302
  case exports.RuleCategory.Cosmetic:
9477
- return CosmeticRuleConverter.convertToAdg(ruleNode);
10303
+ return CosmeticRuleConverter.convertToAdg(rule);
9478
10304
  case exports.RuleCategory.Network:
9479
- return NetworkRuleConverter.convertToAdg(ruleNode);
10305
+ return NetworkRuleConverter.convertToAdg(rule);
9480
10306
  default:
9481
- throw new RuleConversionError(`Unknown rule category: ${ruleNode.category}`);
10307
+ throw new RuleConversionError(`Unknown rule category: ${rule.category}`);
9482
10308
  }
9483
10309
  }
9484
10310
  }
9485
10311
 
9486
- /**
9487
- * @file Utility functions for working with filter list nodes
9488
- */
9489
- /**
9490
- * Creates a filter list node
9491
- *
9492
- * @param rules Rules to put in the list (optional, defaults to an empty list)
9493
- * @returns Filter list node
9494
- */
9495
- function createFilterListNode(rules = []) {
9496
- const result = {
9497
- type: 'FilterList',
9498
- children: [],
9499
- };
9500
- // We need to clone the rules to avoid side effects
9501
- if (rules.length > 0) {
9502
- result.children = cloneDeep(rules);
9503
- }
9504
- return result;
9505
- }
9506
-
9507
10312
  /**
9508
10313
  * @file Adblock filter list converter
9509
10314
  */
@@ -9522,18 +10327,133 @@ class FilterListConverter extends ConverterBase {
9522
10327
  * Converts an adblock filter list to AdGuard format, if possible.
9523
10328
  *
9524
10329
  * @param filterListNode Filter list node to convert
9525
- * @returns Converted filter list node
9526
- * @throws If the filter list is invalid or cannot be converted
10330
+ * @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
10331
+ * invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
10332
+ * Defaults to `true`.
10333
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10334
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
10335
+ * If the node was not converted, the result will contain the original node with the same object reference
10336
+ * @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
10337
+ */
10338
+ static convertToAdg(filterListNode, tolerant = true) {
10339
+ // Prepare a map to store the converted rules by their index in the filter list
10340
+ const conversionMap = new MultiValueMap();
10341
+ // Iterate over the filtering rules and convert them one by one, then add them to the result (one conversion may
10342
+ // result in multiple rules)
10343
+ for (let i = 0; i < filterListNode.children.length; i += 1) {
10344
+ try {
10345
+ const convertedRules = RuleConverter.convertToAdg(filterListNode.children[i]);
10346
+ // Add the converted rules to the map if they were converted
10347
+ if (convertedRules.isConverted) {
10348
+ conversionMap.add(i, ...convertedRules.result);
10349
+ }
10350
+ }
10351
+ catch (error) {
10352
+ // If the tolerant mode is disabled, we should throw an error, this will fail the whole filter list
10353
+ // conversion.
10354
+ // Otherwise, we just ignore the error and leave the rule as is
10355
+ if (!tolerant) {
10356
+ throw error;
10357
+ }
10358
+ }
10359
+ }
10360
+ // If the conversion map is empty, it means that no rules were converted, so we can return the original filter
10361
+ // list
10362
+ if (conversionMap.size === 0) {
10363
+ return createConversionResult(filterListNode, false);
10364
+ }
10365
+ // Otherwise, create a new filter list node with the converted rules
10366
+ const convertedFilterList = {
10367
+ type: 'FilterList',
10368
+ children: [],
10369
+ };
10370
+ // Iterate over the original rules again and add them to the converted filter list, replacing the converted
10371
+ // rules with the new ones at the specified indexes
10372
+ for (let i = 0; i < filterListNode.children.length; i += 1) {
10373
+ const rules = conversionMap.get(i);
10374
+ if (rules) {
10375
+ convertedFilterList.children.push(...rules);
10376
+ }
10377
+ else {
10378
+ // We clone the unconverted rules to avoid mutating the original filter list if we return the converted
10379
+ // one
10380
+ convertedFilterList.children.push(clone(filterListNode.children[i]));
10381
+ }
10382
+ }
10383
+ return createConversionResult(convertedFilterList, true);
10384
+ }
10385
+ }
10386
+
10387
+ /**
10388
+ * @file Filter list converter for raw filter lists
10389
+ *
10390
+ * Technically, this is a wrapper around `FilterListConverter` that works with nodes instead of strings.
10391
+ */
10392
+ /**
10393
+ * Adblock filter list converter class.
10394
+ *
10395
+ * You can use this class to convert string-based filter lists, since most of the converters work with nodes.
10396
+ * This class just provides an extra layer on top of the {@link FilterListConverter} and calls the parser/serializer
10397
+ * before/after the conversion internally.
10398
+ *
10399
+ * @todo Implement `convertToUbo` and `convertToAbp`
10400
+ */
10401
+ class RawFilterListConverter extends ConverterBase {
10402
+ /**
10403
+ * Converts an adblock filter list text to AdGuard format, if possible.
10404
+ *
10405
+ * @param rawFilterList Raw filter list text to convert
10406
+ * @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
10407
+ * invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
10408
+ * Defaults to `true`.
10409
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10410
+ * the array of converted filter list text, and its `isConverted` flag indicates whether the original rule was
10411
+ * converted. If the rule was not converted, the original filter list text will be returned
10412
+ * @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
9527
10413
  */
9528
- static convertToAdg(filterListNode) {
9529
- const result = createFilterListNode();
9530
- // Iterate over the filtering rules and convert them one by one,
9531
- // then add them to the result (one conversion may result in multiple rules)
9532
- for (const ruleNode of filterListNode.children) {
9533
- const convertedRules = RuleConverter.convertToAdg(ruleNode);
9534
- result.children.push(...convertedRules);
10414
+ static convertToAdg(rawFilterList, tolerant = true) {
10415
+ const conversionResult = FilterListConverter.convertToAdg(FilterListParser.parse(rawFilterList, tolerant), tolerant);
10416
+ // If the filter list was not converted, return the original text
10417
+ if (!conversionResult.isConverted) {
10418
+ return createConversionResult(rawFilterList, false);
9535
10419
  }
9536
- return result;
10420
+ // Otherwise, serialize the filter list and return the result
10421
+ return createConversionResult(FilterListParser.generate(conversionResult.result), true);
10422
+ }
10423
+ }
10424
+
10425
+ /**
10426
+ * @file Rule converter for raw rules
10427
+ *
10428
+ * Technically, this is a wrapper around `RuleConverter` that works with nodes instead of strings.
10429
+ */
10430
+ /**
10431
+ * Adblock filtering rule converter class.
10432
+ *
10433
+ * You can use this class to convert string-based adblock rules, since most of the converters work with nodes.
10434
+ * This class just provides an extra layer on top of the {@link RuleConverter} and calls the parser/serializer
10435
+ * before/after the conversion internally.
10436
+ *
10437
+ * @todo Implement `convertToUbo` and `convertToAbp`
10438
+ */
10439
+ class RawRuleConverter extends ConverterBase {
10440
+ /**
10441
+ * Converts an adblock filtering rule to AdGuard format, if possible.
10442
+ *
10443
+ * @param rawRule Raw rule text to convert
10444
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10445
+ * the array of converted rule texts, and its `isConverted` flag indicates whether the original rule was converted.
10446
+ * If the rule was not converted, the original rule text will be returned
10447
+ * @throws If the rule is invalid or cannot be converted
10448
+ */
10449
+ static convertToAdg(rawRule) {
10450
+ const conversionResult = RuleConverter.convertToAdg(RuleParser.parse(rawRule));
10451
+ // If the rule was not converted, return the original rule text
10452
+ if (!conversionResult.isConverted) {
10453
+ return createConversionResult([rawRule], false);
10454
+ }
10455
+ // Otherwise, serialize the converted rule nodes
10456
+ return createConversionResult(conversionResult.result.map(RuleParser.generate), true);
9537
10457
  }
9538
10458
  }
9539
10459
 
@@ -9615,7 +10535,7 @@ class LogicalExpressionUtils {
9615
10535
  }
9616
10536
  }
9617
10537
 
9618
- const version$1 = "1.1.4";
10538
+ const version$1 = "1.1.6";
9619
10539
 
9620
10540
  /**
9621
10541
  * @file AGTree version
@@ -9676,6 +10596,8 @@ exports.PREPROCESSOR_MARKER = PREPROCESSOR_MARKER;
9676
10596
  exports.ParameterListParser = ParameterListParser;
9677
10597
  exports.PreProcessorCommentRuleParser = PreProcessorCommentRuleParser;
9678
10598
  exports.QuoteUtils = QuoteUtils;
10599
+ exports.RawFilterListConverter = RawFilterListConverter;
10600
+ exports.RawRuleConverter = RawRuleConverter;
9679
10601
  exports.RegExpUtils = RegExpUtils;
9680
10602
  exports.RuleConversionError = RuleConversionError;
9681
10603
  exports.RuleConverter = RuleConverter;