Loading src/css.cpp +26 −6 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "css.h" #include "exception.h" #include <iostream> #include <algorithm> #include <array> Loading Loading @@ -326,10 +327,30 @@ namespace { // "[data-fit=fill]" in ".fade-box[data-fit=fill] img") is a real, // checkable requirement: it's matched against each candidate frame's own // attributes (see AncestorFrame::attributes) the same way // compoundPartsMatch checks tag/class/id -- a frame with no attributes // supplied (an empty map, meaning the caller didn't populate that field) // can't refute it, so it's treated as satisfied for that frame rather // than guessed wrong. // compoundPartsMatch checks tag/class/id. Always passed as a real (never // null) map -- a PREVIOUS version of this function treated an EMPTY // attributes map as "the caller didn't populate this field, so it can't // be checked" and skipped verification entirely (auto-passing), on the // theory that every real caller already builds a complete attribute map // for every ancestor. That theory was wrong: `elemAttrs` is empty for // any genuinely attribute-less ancestor too (a bare `<main>`, a `<div>` // with no attributes at all) -- both cases produce the exact same empty // std::map, so there was no way to tell "not populated" apart from // "genuinely has nothing to match" from the map alone. On a real page, // this let a totally unrelated attribute-selector requirement (e.g. // "[data-kit-frame=solid]" several ancestors up a real selector's chain) // match against the nearest attribute-less <main>/<div> in between // instead of correctly failing, letting page-builder rules meant for a // completely different component apply to ordinary content (confirmed // on a real page: an unrelated "[data-molecule][data-kit-frame=line] // [data-atom]:first-child:last-child[data-atom=image] .con-kit- // component-image{height:100%}" rule -- gated behind ancestors this // element never had -- matched anyway via a bare attribute-less <main>, // forcing height:100% with no definite ancestor height to resolve // against, collapsing several real images to 0 height). Passing the // real (possibly empty) map instead lets attributeConditionsSatisfied // correctly fail any non-trivial condition against a truly attribute- // less frame, exactly like a real browser would. bool ancestorChainSatisfies(const std::vector<CompoundParts> &ancestorCompounds, const std::vector<libhtmlpp::AncestorFrame> &ancestors, bool *usedUnverifiableAncestor = nullptr) Loading @@ -348,8 +369,7 @@ namespace { const libhtmlpp::AncestorFrame &frame = ancestors[idx]; ++idx; if (compoundPartsMatch(compound, frame.tag, frame.classes, frame.id) && attributeConditionsSatisfied(compound.attrConditions, frame.attributes.empty() ? nullptr : &frame.attributes)) { attributeConditionsSatisfied(compound.attrConditions, &frame.attributes)) { found = true; break; } Loading test/htmlcsstest.cpp +34 −0 Original line number Diff line number Diff line Loading @@ -143,6 +143,40 @@ int main(){ }; check(!CSSStyleSheet::approximateSelectorMatch(".a .b .frame", "div", classes, "", &onlyOuterAncestor), "a missing middle ancestor compound still fails the match"); // Real bug, found on bautenschutz-wetzlar.de: an ancestor attribute- // selector condition (e.g. "[data-frame=solid]") was vacuously // satisfied against ANY ancestor with a genuinely empty attribute // map (a bare tag like <main> with no attributes at all) instead of // correctly failing -- ancestorChainSatisfies used to treat "empty // map" as "caller didn't populate this field, can't verify" and // auto-passed, but a real, attribute-less ancestor produces the // exact same empty map. This let an unrelated, specifically-gated // rule (meant for a totally different component several ancestors // over) match through the nearest bare <main>/<div> instead, // forcing height:100% with nothing definite to resolve against and // collapsing several real images on the page to 0 height. std::vector<AncestorFrame> attributelessAncestor = { {"main", {}, ""}, // attributes defaults to {} -- genuinely no attributes, not "unpopulated" }; check(!CSSStyleSheet::approximateSelectorMatch("[data-frame=solid] .frame", "div", classes, "", &attributelessAncestor), "an attribute-selector ancestor condition does not vacuously pass against a genuinely " "attribute-less ancestor (the actual bug)"); std::vector<AncestorFrame> wrongAttrAncestor = { {"div", {}, "", {{"data-frame", "line"}}}, }; check(!CSSStyleSheet::approximateSelectorMatch("[data-frame=solid] .frame", "div", classes, "", &wrongAttrAncestor), "an ancestor with a different attribute value still correctly fails"); std::vector<AncestorFrame> rightAttrAncestor = { {"div", {}, "", {{"data-frame", "solid"}}}, }; check(CSSStyleSheet::approximateSelectorMatch("[data-frame=solid] .frame", "div", classes, "", &rightAttrAncestor), "an ancestor with the matching attribute value still correctly matches"); } // --- collectApproximateMatches: cascade priority --- Loading Loading
src/css.cpp +26 −6 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "css.h" #include "exception.h" #include <iostream> #include <algorithm> #include <array> Loading Loading @@ -326,10 +327,30 @@ namespace { // "[data-fit=fill]" in ".fade-box[data-fit=fill] img") is a real, // checkable requirement: it's matched against each candidate frame's own // attributes (see AncestorFrame::attributes) the same way // compoundPartsMatch checks tag/class/id -- a frame with no attributes // supplied (an empty map, meaning the caller didn't populate that field) // can't refute it, so it's treated as satisfied for that frame rather // than guessed wrong. // compoundPartsMatch checks tag/class/id. Always passed as a real (never // null) map -- a PREVIOUS version of this function treated an EMPTY // attributes map as "the caller didn't populate this field, so it can't // be checked" and skipped verification entirely (auto-passing), on the // theory that every real caller already builds a complete attribute map // for every ancestor. That theory was wrong: `elemAttrs` is empty for // any genuinely attribute-less ancestor too (a bare `<main>`, a `<div>` // with no attributes at all) -- both cases produce the exact same empty // std::map, so there was no way to tell "not populated" apart from // "genuinely has nothing to match" from the map alone. On a real page, // this let a totally unrelated attribute-selector requirement (e.g. // "[data-kit-frame=solid]" several ancestors up a real selector's chain) // match against the nearest attribute-less <main>/<div> in between // instead of correctly failing, letting page-builder rules meant for a // completely different component apply to ordinary content (confirmed // on a real page: an unrelated "[data-molecule][data-kit-frame=line] // [data-atom]:first-child:last-child[data-atom=image] .con-kit- // component-image{height:100%}" rule -- gated behind ancestors this // element never had -- matched anyway via a bare attribute-less <main>, // forcing height:100% with no definite ancestor height to resolve // against, collapsing several real images to 0 height). Passing the // real (possibly empty) map instead lets attributeConditionsSatisfied // correctly fail any non-trivial condition against a truly attribute- // less frame, exactly like a real browser would. bool ancestorChainSatisfies(const std::vector<CompoundParts> &ancestorCompounds, const std::vector<libhtmlpp::AncestorFrame> &ancestors, bool *usedUnverifiableAncestor = nullptr) Loading @@ -348,8 +369,7 @@ namespace { const libhtmlpp::AncestorFrame &frame = ancestors[idx]; ++idx; if (compoundPartsMatch(compound, frame.tag, frame.classes, frame.id) && attributeConditionsSatisfied(compound.attrConditions, frame.attributes.empty() ? nullptr : &frame.attributes)) { attributeConditionsSatisfied(compound.attrConditions, &frame.attributes)) { found = true; break; } Loading
test/htmlcsstest.cpp +34 −0 Original line number Diff line number Diff line Loading @@ -143,6 +143,40 @@ int main(){ }; check(!CSSStyleSheet::approximateSelectorMatch(".a .b .frame", "div", classes, "", &onlyOuterAncestor), "a missing middle ancestor compound still fails the match"); // Real bug, found on bautenschutz-wetzlar.de: an ancestor attribute- // selector condition (e.g. "[data-frame=solid]") was vacuously // satisfied against ANY ancestor with a genuinely empty attribute // map (a bare tag like <main> with no attributes at all) instead of // correctly failing -- ancestorChainSatisfies used to treat "empty // map" as "caller didn't populate this field, can't verify" and // auto-passed, but a real, attribute-less ancestor produces the // exact same empty map. This let an unrelated, specifically-gated // rule (meant for a totally different component several ancestors // over) match through the nearest bare <main>/<div> instead, // forcing height:100% with nothing definite to resolve against and // collapsing several real images on the page to 0 height. std::vector<AncestorFrame> attributelessAncestor = { {"main", {}, ""}, // attributes defaults to {} -- genuinely no attributes, not "unpopulated" }; check(!CSSStyleSheet::approximateSelectorMatch("[data-frame=solid] .frame", "div", classes, "", &attributelessAncestor), "an attribute-selector ancestor condition does not vacuously pass against a genuinely " "attribute-less ancestor (the actual bug)"); std::vector<AncestorFrame> wrongAttrAncestor = { {"div", {}, "", {{"data-frame", "line"}}}, }; check(!CSSStyleSheet::approximateSelectorMatch("[data-frame=solid] .frame", "div", classes, "", &wrongAttrAncestor), "an ancestor with a different attribute value still correctly fails"); std::vector<AncestorFrame> rightAttrAncestor = { {"div", {}, "", {{"data-frame", "solid"}}}, }; check(CSSStyleSheet::approximateSelectorMatch("[data-frame=solid] .frame", "div", classes, "", &rightAttrAncestor), "an ancestor with the matching attribute value still correctly matches"); } // --- collectApproximateMatches: cascade priority --- Loading