From 6582b228b74857613a9da83e6e91c6b826d97b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 17 Sep 2026 19:58:54 +0200 Subject: [PATCH] Make inset length parsing portable --- .../CMakeLists.txt | 8 ++ .../native/webscene_css_ascii_number.h | 120 ++++++++++++++++++ .../native/webscene_native_dom.cpp | 1 + .../native/webscene_native_dom_scene.inc | 34 +---- .../tests/css_ascii_number_tests.cpp | 62 +++++++++ 5 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 experiments/WebScene.NativeEngine.Probe/native/webscene_css_ascii_number.h create mode 100644 experiments/WebScene.NativeEngine.Probe/tests/css_ascii_number_tests.cpp diff --git a/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt b/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt index 32a5b1ee..bec4a470 100644 --- a/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt +++ b/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt @@ -5,6 +5,14 @@ include(CTest) include(FetchContent) if(BUILD_TESTING) + add_executable(webscene_css_ascii_number_tests + tests/css_ascii_number_tests.cpp) + target_compile_features(webscene_css_ascii_number_tests PRIVATE cxx_std_20) + target_include_directories(webscene_css_ascii_number_tests PRIVATE native) + add_test(NAME webscene_css_ascii_number_tests + COMMAND webscene_css_ascii_number_tests) + set_tests_properties(webscene_css_ascii_number_tests PROPERTIES + LABELS "native;css;effects;portability;performance" TIMEOUT 10) add_executable(webscene_file_panel_v2_contract_tests tests/file_panel_v2_contract_tests.cpp) target_compile_features(webscene_file_panel_v2_contract_tests PRIVATE cxx_std_20) diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_ascii_number.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_ascii_number.h new file mode 100644 index 00000000..97f6b38e --- /dev/null +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_ascii_number.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace webscene_native::css { + +struct ascii_number_prefix final { + float value{}; + size_t consumed{}; +}; + +// Parse the locale-independent CSS prefix needed by retained paint. +// Returning the consumed prefix lets callers validate their property-specific +// unit without copying or reading past the supplied string_view. +inline std::optional parse_ascii_number_prefix( + std::string_view input) noexcept +{ + if (input.empty()) return std::nullopt; + size_t cursor = 0U; + auto negative = false; + if (input[cursor] == '+' || input[cursor] == '-') { + negative = input[cursor] == '-'; + if (++cursor == input.size()) return std::nullopt; + } + + long double significand = 0.0L; + auto fractional_digits = 0; + auto digits = 0U; + auto significand_overflow = false; + const auto append_digit = [&](unsigned digit) { + ++digits; + if (significand + > (std::numeric_limits::max() - digit) / 10.0L) { + significand_overflow = true; + return; + } + significand = significand * 10.0L + static_cast(digit); + }; + + while (cursor < input.size() && input[cursor] >= '0' && input[cursor] <= '9') { + append_digit(static_cast(input[cursor] - '0')); + ++cursor; + } + if (cursor < input.size() && input[cursor] == '.') { + ++cursor; + while (cursor < input.size() && input[cursor] >= '0' && input[cursor] <= '9') { + append_digit(static_cast(input[cursor] - '0')); + if (fractional_digits < 100000) ++fractional_digits; + else significand_overflow = true; + ++cursor; + } + } + if (digits == 0U || significand_overflow) return std::nullopt; + + auto exponent = 0; + auto exponent_negative = false; + auto exponent_overflow = false; + if (cursor < input.size() && (input[cursor] == 'e' || input[cursor] == 'E')) { + ++cursor; + if (cursor < input.size() && (input[cursor] == '+' || input[cursor] == '-')) { + exponent_negative = input[cursor] == '-'; + ++cursor; + } + const auto exponent_start = cursor; + while (cursor < input.size() && input[cursor] >= '0' && input[cursor] <= '9') { + const auto digit = static_cast(input[cursor] - '0'); + if (exponent > 10000) exponent_overflow = true; + else exponent = exponent * 10 + digit; + ++cursor; + } + if (cursor == exponent_start) return std::nullopt; + } + if (significand == 0.0L) { + return ascii_number_prefix{negative ? -0.0F : 0.0F, cursor}; + } + if (exponent_overflow) return std::nullopt; + if (exponent_negative) exponent = -exponent; + + const auto decimal_exponent = exponent - fractional_digits; + if (decimal_exponent > 10000 || decimal_exponent < -10000) return std::nullopt; + auto scaled = significand * std::pow(10.0L, static_cast(decimal_exponent)); + if (negative) scaled = -scaled; + if (!std::isfinite(scaled) + || std::abs(scaled) > std::numeric_limits::max()) return std::nullopt; + const auto result = static_cast(scaled); + if (!std::isfinite(result) || (result == 0.0F && significand != 0.0L)) { + return std::nullopt; + } + return ascii_number_prefix{result, cursor}; +} + +struct ascii_inset_length final { + float value{}; + bool percent{}; +}; + +inline std::optional parse_ascii_inset_length( + std::string_view token) noexcept +{ + const auto parsed = parse_ascii_number_prefix(token); + if (!parsed) return std::nullopt; + const auto suffix = token.substr(parsed->consumed); + if (suffix.empty()) { + if (std::abs(parsed->value) > 0.0001F) return std::nullopt; + return ascii_inset_length{0.0F, false}; + } + if (suffix.size() == 2U + && (suffix[0] == 'p' || suffix[0] == 'P') + && (suffix[1] == 'x' || suffix[1] == 'X')) { + return ascii_inset_length{parsed->value, false}; + } + if (suffix == "%") return ascii_inset_length{parsed->value, true}; + return std::nullopt; +} + +} // namespace webscene_native::css diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp index 5d6f0644..77844345 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp @@ -1,4 +1,5 @@ #include "webscene_native_dom.h" +#include "webscene_css_ascii_number.h" #include #include diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc index f6c102a8..a9d337d2 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc @@ -420,10 +420,6 @@ void native_document::append_scene( ? node.style.visibility_hidden : inherited_visibility_hidden; const auto paint_self = !visibility_hidden && paint_target == nullptr && !pseudo_only; - struct inset_clip_length final { - float value{}; - bool percent{}; - }; const auto ascii_equal = [](char left, char right) { const auto lower = [](unsigned char character) { return character >= 'A' && character <= 'Z' @@ -433,30 +429,6 @@ void native_document::append_scene( return lower(static_cast(left)) == lower(static_cast(right)); }; - const auto parse_inset_clip_length = [](std::string_view token) - -> std::optional { - if (token.empty()) return std::nullopt; - auto first = token.data(); - if (*first == '+') ++first; - if (first == token.data() + token.size()) return std::nullopt; - float number = 0.0F; - const auto parsed = std::from_chars(first, token.data() + token.size(), number); - if (parsed.ec != std::errc{} || parsed.ptr == first || !std::isfinite(number)) { - return std::nullopt; - } - const std::string_view suffix(parsed.ptr, token.data() + token.size() - parsed.ptr); - if (suffix.empty()) { - if (std::abs(number) > 0.0001F) return std::nullopt; - return inset_clip_length{0.0F, false}; - } - if (suffix.size() == 2U - && (suffix[0] == 'p' || suffix[0] == 'P') - && (suffix[1] == 'x' || suffix[1] == 'X')) { - return inset_clip_length{number, false}; - } - if (suffix == "%") return inset_clip_length{number, true}; - return std::nullopt; - }; const auto resolve_inset_clip = [&]() -> std::optional { const auto& effects = node.style.textual().effect_values; const auto known = effects.find("clip-path"); @@ -468,7 +440,7 @@ void native_document::append_scene( if (!ascii_equal(value[index], inset_prefix[index])) return std::nullopt; } const std::string_view arguments(value.data() + 6U, value.size() - 7U); - std::array parsed{}; + std::array parsed{}; size_t count = 0U; size_t cursor = 0U; while (cursor < arguments.size()) { @@ -480,7 +452,7 @@ void native_document::append_scene( cursor, end == std::string_view::npos ? arguments.size() - cursor : end - cursor); if (count == parsed.size()) return std::nullopt; - const auto length = parse_inset_clip_length(token); + const auto length = css::parse_ascii_inset_length(token); if (!length.has_value()) return std::nullopt; parsed[count++] = *length; if (end == std::string_view::npos) break; @@ -491,7 +463,7 @@ void native_document::append_scene( const auto right = count > 1U ? parsed[1] : parsed[0]; const auto bottom = count > 2U ? parsed[2] : parsed[0]; const auto left = count > 3U ? parsed[3] : count > 1U ? parsed[1] : parsed[0]; - const auto pixels = [](inset_clip_length length, float available) { + const auto pixels = [](css::ascii_inset_length length, float available) { return length.percent ? available * length.value / 100.0F : length.value; }; const auto top_pixels = pixels(top, node.layout.height); diff --git a/experiments/WebScene.NativeEngine.Probe/tests/css_ascii_number_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/css_ascii_number_tests.cpp new file mode 100644 index 00000000..3e65d7f7 --- /dev/null +++ b/experiments/WebScene.NativeEngine.Probe/tests/css_ascii_number_tests.cpp @@ -0,0 +1,62 @@ +#include "webscene_css_ascii_number.h" + +#include +#include +#include +#include +#include + +namespace { +[[noreturn]] void fail(std::string_view message) +{ + std::cerr << "webscene_css_ascii_number_tests: " << message << '\n'; + std::exit(1); +} + +void require(bool condition, std::string_view message) +{ + if (!condition) fail(message); +} + +void require_length(std::string_view source, float expected, bool percent) +{ + const auto parsed = webscene_native::css::parse_ascii_inset_length(source); + require(parsed.has_value(), "portable inset length was rejected"); + require(std::abs(parsed->value - expected) < 0.0001F, + "portable inset length value differed"); + require(parsed->percent == percent, "portable inset length unit differed"); +} +} // namespace + +int main() +{ + require_length("0", 0.0F, false); + require_length("+0PX", 0.0F, false); + require_length("0e999999px", 0.0F, false); + require_length("-0e-999999%", -0.0F, true); + require_length("-.5px", -0.5F, false); + require_length("1.25e2%", 125.0F, true); + require_length("1e-2px", 0.01F, false); + for (const auto invalid : { + std::string_view{}, std::string_view{"+"}, std::string_view{"."}, + std::string_view{"1"}, std::string_view{"1e+px"}, std::string_view{"nanpx"}, + std::string_view{"infpx"}, std::string_view{"1e9999px"}, + std::string_view{"1px "}, std::string_view{"1em"}, + std::string_view{"1\0px", 4U}}) { + require(!webscene_native::css::parse_ascii_inset_length(invalid).has_value(), + "invalid portable inset length was accepted"); + } + + const auto started = std::chrono::steady_clock::now(); + auto checksum = 0.0F; + for (auto iteration = 0U; iteration < 100000U; ++iteration) { + const auto parsed = webscene_native::css::parse_ascii_inset_length("-12.5e-1px"); + require(parsed.has_value(), "repeated portable parse failed"); + checksum += parsed->value; + } + const auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - started).count(); + require(std::abs(checksum + 125000.0F) < 0.01F, "portable parse checksum differed"); + require(elapsed < 500.0, "100000 portable parses exceeded 500 ms"); + std::cout << "css-ascii-number parses=100000 elapsed-ms=" << elapsed << '\n'; +}