|
11 | 11 |
|
12 | 12 | UNICODE_REPLACEMENT_CHAR = 0xFFFD |
13 | 13 |
|
| 14 | +SELECTOR_LIMIT = 8192 |
| 15 | + |
14 | 16 | # Simple pseudo classes that take no parameters |
15 | 17 | PSEUDO_SIMPLE = { |
16 | 18 | ":any-link", |
@@ -468,6 +470,13 @@ def __init__( |
468 | 470 | self.flags = flags |
469 | 471 | self.debug = self.flags & util.DEBUG |
470 | 472 | self.custom = {} if custom is None else custom |
| 473 | + self.count = 0 |
| 474 | + |
| 475 | + def check_count(self) -> None: |
| 476 | + """Check the current selector count.""" |
| 477 | + |
| 478 | + if self.count > SELECTOR_LIMIT: |
| 479 | + raise ValueError(f'Selector exceeds pseudo-class nesting limit of {SELECTOR_LIMIT}') |
471 | 480 |
|
472 | 481 | def parse_attribute_selector(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool: |
473 | 482 | """Create attribute selector from the returned regex match.""" |
@@ -572,6 +581,9 @@ def parse_pseudo_class_custom(self, sel: _Selector, m: Match[str], has_selector: |
572 | 581 | ).process_selectors(flags=FLG_PSEUDO) |
573 | 582 | self.custom[pseudo] = selector |
574 | 583 |
|
| 584 | + self.count += selector.count |
| 585 | + self.check_count() |
| 586 | + |
575 | 587 | sel.selectors.append(selector) |
576 | 588 | has_selector = True |
577 | 589 | return has_selector |
@@ -603,34 +615,64 @@ def parse_pseudo_class( |
603 | 615 | elif pseudo == ':empty': |
604 | 616 | sel.flags |= ct.SEL_EMPTY |
605 | 617 | elif pseudo in (':link', ':any-link'): |
| 618 | + self.count += CSS_LINK.count |
| 619 | + self.check_count() |
606 | 620 | sel.selectors.append(CSS_LINK) |
607 | 621 | elif pseudo == ':checked': |
| 622 | + self.count += CSS_CHECKED.count |
| 623 | + self.check_count() |
608 | 624 | sel.selectors.append(CSS_CHECKED) |
609 | 625 | elif pseudo == ':default': |
| 626 | + self.count += CSS_DEFAULT.count |
| 627 | + self.check_count() |
610 | 628 | sel.selectors.append(CSS_DEFAULT) |
611 | 629 | elif pseudo == ':indeterminate': |
| 630 | + self.count += CSS_INDETERMINATE.count |
| 631 | + self.check_count() |
612 | 632 | sel.selectors.append(CSS_INDETERMINATE) |
613 | 633 | elif pseudo == ":disabled": |
| 634 | + self.count += CSS_DISABLED.count |
| 635 | + self.check_count() |
614 | 636 | sel.selectors.append(CSS_DISABLED) |
615 | 637 | elif pseudo == ":enabled": |
| 638 | + self.count += CSS_ENABLED.count |
| 639 | + self.check_count() |
616 | 640 | sel.selectors.append(CSS_ENABLED) |
617 | 641 | elif pseudo == ":required": |
| 642 | + self.count += CSS_REQUIRED.count |
| 643 | + self.check_count() |
618 | 644 | sel.selectors.append(CSS_REQUIRED) |
619 | 645 | elif pseudo == ":muted": |
| 646 | + self.count += CSS_MUTED.count |
| 647 | + self.check_count() |
620 | 648 | sel.selectors.append(CSS_MUTED) |
621 | 649 | elif pseudo == ":open": |
| 650 | + self.count += CSS_OPEN.count |
| 651 | + self.check_count() |
622 | 652 | sel.selectors.append(CSS_OPEN) |
623 | 653 | elif pseudo == ":optional": |
| 654 | + self.count += CSS_OPTIONAL.count |
| 655 | + self.check_count() |
624 | 656 | sel.selectors.append(CSS_OPTIONAL) |
625 | 657 | elif pseudo == ":read-only": |
| 658 | + self.count += CSS_READ_ONLY.count |
| 659 | + self.check_count() |
626 | 660 | sel.selectors.append(CSS_READ_ONLY) |
627 | 661 | elif pseudo == ":read-write": |
| 662 | + self.count += CSS_READ_WRITE.count |
| 663 | + self.check_count() |
628 | 664 | sel.selectors.append(CSS_READ_WRITE) |
629 | 665 | elif pseudo == ":in-range": |
| 666 | + self.count += CSS_IN_RANGE.count |
| 667 | + self.check_count() |
630 | 668 | sel.selectors.append(CSS_IN_RANGE) |
631 | 669 | elif pseudo == ":out-of-range": |
| 670 | + self.count += CSS_OUT_OF_RANGE.count |
| 671 | + self.check_count() |
632 | 672 | sel.selectors.append(CSS_OUT_OF_RANGE) |
633 | 673 | elif pseudo == ":placeholder-shown": |
| 674 | + self.count += CSS_PLACEHOLDER_SHOWN.count |
| 675 | + self.check_count() |
634 | 676 | sel.selectors.append(CSS_PLACEHOLDER_SHOWN) |
635 | 677 | elif pseudo == ':first-child': |
636 | 678 | sel.nth.append(ct.SelectorNth(1, False, 0, False, False, ct.SelectorList())) |
@@ -731,6 +773,8 @@ def parse_pseudo_nth( |
731 | 773 | else: |
732 | 774 | # Use default `*|*` for `of S`. |
733 | 775 | nth_sel = CSS_NTH_OF_S_DEFAULT |
| 776 | + self.count += nth_sel.count |
| 777 | + self.check_count() |
734 | 778 | if pseudo_sel == ':nth-child': |
735 | 779 | sel.nth.append(ct.SelectorNth(s1, var, s2, False, False, nth_sel)) |
736 | 780 | elif pseudo_sel == ':nth-last-child': |
@@ -937,6 +981,7 @@ def parse_selectors( |
937 | 981 | closed = False |
938 | 982 | relations = [] # type: list[_Selector] |
939 | 983 | rel_type = ":" + WS_COMBINATOR |
| 984 | + count = self.count |
940 | 985 |
|
941 | 986 | # Setup various flags |
942 | 987 | is_open = bool(flags & FLG_OPEN) |
@@ -984,6 +1029,10 @@ def parse_selectors( |
984 | 1029 | while True: |
985 | 1030 | key, m = next(iselector) |
986 | 1031 |
|
| 1032 | + if key not in ('combine', 'pseudo_close'): |
| 1033 | + self.count += 1 |
| 1034 | + self.check_count() |
| 1035 | + |
987 | 1036 | # Handle parts |
988 | 1037 | if key == "at_rule": |
989 | 1038 | raise NotImplementedError(f"At-rules found at position {m.start(0)}") |
@@ -1103,7 +1152,7 @@ def parse_selectors( |
1103 | 1152 | selectors[-1].flags = ct.SEL_PLACEHOLDER_SHOWN |
1104 | 1153 |
|
1105 | 1154 | # Return selector list |
1106 | | - return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html) |
| 1155 | + return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html, self.count - count) |
1107 | 1156 |
|
1108 | 1157 | def selector_iter(self, pattern: str) -> Iterator[tuple[str, Match[str]]]: |
1109 | 1158 | """Iterate selector tokens.""" |
|
0 commit comments