Commit 214b4cc3 authored by jan.koester's avatar jan.koester
Browse files

test

parent d43cfa4f
Loading
Loading
Loading
Loading
+46 −17
Original line number Diff line number Diff line
@@ -225,28 +225,48 @@ namespace {
        return tagOk && idOk && classesOk;
    }

    // Whether `compound` is a plain bare-tag selector (no class/id/attribute
    // qualifier of its own) that only shows up as the TARGET compound
    // because a leading ancestor combinator was stripped off it -- e.g.
    // "[data-atom=header]>div" reduces to a bare "div" here. See this
    // function's callers for why that matters: on its own such a target
    // can't be trusted (confirmed against a real scraped page where exactly
    // this pattern matched ~24,000 times and blew up an HTML-import's
    // output to 36MB, back when this matcher had no way to verify the
    // ancestor side of the selector either) -- it's only safe to accept once
    // every one of this rule's ancestor compounds has ALSO been genuinely
    // verified against a real candidate ancestor, not vacuously satisfied
    // (see ancestorChainSatisfies/usedUnverifiableAncestor). A class/id/
    // attribute qualifier alongside the tag (e.g. "div.foo") is already
    // specific enough to check on its own and never counts as "bare" here.
    bool isBareTagAfterCombinator(const CompoundParts &compound, bool hadCombinator) {
        return hadCombinator && compound.classes.empty() && compound.id.empty() &&
               compound.attrConditions.empty() && !compound.tag.empty();
    }

    // A selector matches only if EVERY part it specifies is present on this
    // element -- in particular every class listed, not just one. A bare tag
    // left over after stripping a combinator's ancestor part (e.g.
    // "[data-atom=header]>div" reduces to bare "div") can't be verified when
    // no ancestor chain is available to check it against -- treating it as a
    // match would apply the rule to literally every element of that tag in
    // the whole document (confirmed against a real scraped page where
    // exactly this pattern matched ~24,000 times and blew up an HTML-import's
    // output to 36MB). A class/id qualifier alongside the tag (e.g.
    // "div.foo") is still specific enough to check, and this guard only ever
    // applies to the trailing/target compound -- ancestorChainSatisfies
    // checks leading compounds against a real candidate frame instead, so a
    // bare-tag ancestor requirement is safe there.
    // element -- in particular every class listed, not just one. Whether a
    // bare-tag target (see isBareTagAfterCombinator) should be trusted here
    // is the CALLER's decision (it depends on how the ancestor side of the
    // same rule verifies, which isn't known yet at this point) -- this
    // function only checks the target compound itself.
    bool compoundMatches(const CompoundParts &compound, bool hadCombinator,
                          const std::string &tag, const std::vector<std::string> &classes,
                          const std::string &id,
                          const std::map<std::string,std::string> *targetAttrs = nullptr)
    {
        bool bareTagAfterCombinator = hadCombinator &&
            compound.classes.empty() && compound.id.empty() && !compound.tag.empty();
        bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() || !compound.id.empty();
        if (bareTagAfterCombinator || !specifiedSomething) return false;
        (void)hadCombinator; // kept for signature/call-site symmetry with isBareTagAfterCombinator
        // An attribute condition only counts as "this compound specifies a
        // real requirement" when @p targetAttrs is actually available to
        // check it against -- a caller with no attribute map (e.g.
        // approximateSelectorMatch's own callers, which never pass one)
        // still can't verify it, so a purely-attribute compound like
        // "[data-x]" reverts to the old "nothing specified, reject" outcome
        // for them instead of permissively matching every element.
        bool specifiedSomething = !compound.tag.empty() || !compound.classes.empty() ||
                                   !compound.id.empty() ||
                                   (!compound.attrConditions.empty() && targetAttrs != nullptr);
        if (!specifiedSomething) return false;

        return compoundPartsMatch(compound, tag, classes, id) &&
               attributeConditionsSatisfied(compound.attrConditions, targetAttrs);
@@ -955,8 +975,10 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec
        if (hasUnsupportedSelectorSyntax(matchSel)) continue;

        CompoundParts compound = parseCompoundSelector(matchSel);
        compound.attrConditions = parseAttributeConditions(chain.back());
        if (!compoundMatches(compound, hadCombinator, tagLower, classes, id)) continue;

        bool usedUnverifiableAncestor = false;
        if (ancestors && chain.size() > 1) {
            std::vector<CompoundParts> ancestorCompounds;
            for (size_t i = 0; i + 1 < chain.size(); ++i) {
@@ -969,8 +991,11 @@ bool libhtmlpp::CSSStyleSheet::approximateSelectorMatch(const std::string &selec
                    ancestorCompounds.push_back(std::move(aCompound));
                }
            }
            if (!ancestorChainSatisfies(ancestorCompounds, *ancestors)) continue;
            if (!ancestorChainSatisfies(ancestorCompounds, *ancestors, &usedUnverifiableAncestor))
                continue;
        }
        if (isBareTagAfterCombinator(compound, hadCombinator) && (!ancestors || usedUnverifiableAncestor))
            continue;
        return true;
    }
    return false;
@@ -1163,6 +1188,10 @@ void libhtmlpp::CSSStyleSheet::collectApproximateMatches(
                    continue;
            }

            if (isBareTagAfterCombinator(compound, branch.hadCombinator) &&
                (!ancestors || usedUnverifiableAncestor))
                continue;

            if (branch.isAtRule) {
                // The same @media block, once present anywhere in the final
                // output, applies document-wide regardless of which element
+34 −25
Original line number Diff line number Diff line
@@ -180,32 +180,41 @@ namespace libhtmlpp {
         *  callers that can't supply ancestor context are unaffected). When
         *  @p ancestors is non-null, each leading compound must be found
         *  somewhere in it, in left-to-right order (see AncestorFrame) --
         *  same all-classes-listed/tag/id rule as the target compound, but
         *  without the "bare tag" rejection below, since a specific
         *  candidate ancestor to check it against removes that hazard.
         *  same all-classes-listed/tag/id rule as the target compound (plus
         *  its own attribute conditions, see AncestorFrame::attributes).
         *  Rejected outright for the target compound: pseudo-classes and
         *  pseudo-elements (":hover", "::before"), the universal selector
         *  ("*"), and -- after a combinator (space/">"/"+"/"~") is stripped
         *  down to its trailing compound selector -- a bare tag left with no
         *  class/id qualifier of its own (e.g. "[data-x]>div" reduces to a
         *  bare "div", which would otherwise match every element of that tag
         *  in the whole document; a qualified compound like "div.foo" is
         *  still specific enough to check). Attribute selectors ("[href]")
         *  have their "[...]" segment removed and the remaining compound (if
         *  any) checked instead of being rejected outright -- this matcher
         *  has no per-element attribute map to verify the condition itself,
         *  so the condition is simply ignored rather than guessed, for both
         *  the target and any ancestor compound. A compound selector like
         *  "div.card.featured#hero" matches only if the tag (when given),
         *  every class listed (all of them, not just one), and the id (when
         *  given) are all present on the element being checked against it.
         *  This is intentionally an approximation of real CSS selector
         *  matching, not an implementation of it -- there is no specificity
         *  calculation, and ancestor verification (when requested) doesn't
         *  distinguish child/descendant/sibling combinators or require
         *  adjacency -- built to be safe against false positives on
         *  real-world scraped markup rather than complete; treat a `false`
         *  result as "not proven to match", not "definitely doesn't". */
         *  pseudo-elements (":hover", "::before") and the universal selector
         *  ("*") -- guessing their state is worse than not matching at all.
         *  A bare tag left with no class/id/attribute qualifier of its own
         *  after a combinator (space/">"/"+"/"~") is stripped off (e.g.
         *  "[data-x]>div" reduces to a bare "div") is normally rejected too
         *  -- on its own it would otherwise match every element of that tag
         *  in the whole document -- UNLESS @p ancestors is supplied AND
         *  every one of this rule's ancestor compounds was genuinely
         *  verified against a real candidate ancestor rather than vacuously
         *  satisfied (see ancestorChainSatisfies): a rule like
         *  "header.site-nav > div" is safe to trust for a bare "div" once
         *  its ancestor requirement is a real, checked constraint and not
         *  just "ancestors exist" (a qualified compound like "div.foo" is
         *  always specific enough to check on its own and never triggers
         *  this rejection either way). An attribute-selector condition
         *  ("[href=...]") on the target compound is checked against @p
         *  collectApproximateMatches's @p targetAttributes when supplied --
         *  otherwise (or for an ancestor compound with no attributes
         *  supplied on its AncestorFrame) it's ignored rather than guessed,
         *  same "not proven to not match" bias as everywhere else here. A
         *  compound selector like "div.card.featured#hero" matches only if
         *  the tag (when given), every class listed (all of them, not just
         *  one), and the id (when given) are all present on the element
         *  being checked against it. This is intentionally an approximation
         *  of real CSS selector matching, not an implementation of it --
         *  ancestor verification (when requested) doesn't distinguish
         *  child/descendant/sibling combinators or require adjacency -- built
         *  to be safe against false positives on real-world scraped markup
         *  rather than complete; treat a `false` result as "not proven to
         *  match", not "definitely doesn't". Approximate CSS specificity
         *  (see computeSpecificity, used by collectApproximateMatches) is
         *  computed separately from this function's own true/false verdict. */
        static bool approximateSelectorMatch(const std::string &selector,
                                              const std::string &tag,
                                              const std::vector<std::string> &classes,