Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions crates/n0_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,8 @@ cargo run -p n0_cli --bin n0 -- \
The filter estate contains 26 chassis/blur cells, 60 shadow-graph, 28 native
drop-shadow, 27 color-matrix, 32 component-transfer, 38 blend, 37 morphology,
91 turbulence/displacement, 41 convolution-rung, and 71 diffuse-lighting
cells. The complete corpus contains 936 Chromium-baked cells plus 10 sampled
frames, with 169 named
cells. The complete corpus contains 936 Chromium-baked cells plus 16 sampled
frames, with 172 named
refusal rows. `feFlood`, `feComposite`,
`feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`,
`feComponentTransfer`, `feBlend`, `feMorphology`, `feConvolveMatrix`,
Expand Down Expand Up @@ -837,6 +837,20 @@ cargo run -p n0_cli --bin n0 -- \
conservative provenance patrol remains. The `<pattern>` and
`patternTransform` checklist rows therefore stay open; only `patternUnits`
and `patternContentUnits` close.
The exact-time `<animate attributeName="x">` slice may target the same direct
sharp-cornered rectangle while it carries admitted pattern paint, including
a filter inside the tile and a source-derived filter around the target. Six
committed Chromium frames cover Base and exact samples at 0, 0.25, 1, 2,
and 3 seconds; strict and best-effort pixels are exact and identical.
Pattern source geometry and tile geometry remain static. A fractional X or
Y tile phase refuses at the measured picture-shader boundary: six
discriminating witnesses differ from Chromium by 96–576 pixels at maximum
channel delta 1, including both object-box client-origin axes. Animation
endpoints use SVG-number syntax and a one-way
Chromium-normalization patrol; trailing-dot and Unicode-whitespace forms
formerly admitted only by Rust, and one valid midpoint-adjacent decimal
formerly selected the wrong binary32 neighbour. Three focused refusal rows
guard those classes. The `<pattern>` and `<animate>` rows remain open.
`<text>` is consumed (the text rung), and its font environment is the
host's: text resolves only against fonts declared with
`--font FAMILY=PATH@sha256:HEX` (repeatable), whose bytes are **verified
Expand Down
29 changes: 27 additions & 2 deletions crates/websem/src/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2906,6 +2906,12 @@ impl<'a> PatternCompiler<'a> {
active_patterns,
first.node_id(),
)?;
let is_integer = |value: f32| value.is_finite() && value == value.round();
if !items.is_empty() && (!is_integer(tile_x) || !is_integer(tile_y)) {
return Err(format!(
"pattern #{fragment} tile phase resolves to a fractional coordinate at the pinned-backend picture-shader phase precision boundary"
));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let pattern = PatternPaint::new(
width,
height,
Expand Down Expand Up @@ -6634,6 +6640,17 @@ mod filter_resource {
/// and drop shadow). Keep that operation-independent source profile here
/// so a newly admitted primitive cannot silently bypass the same raster
/// boundary.
fn has_non_animation_element_child(element: HtmlElement<'_>) -> bool {
let mut child = element.first_element_child();
while let Some(candidate) = child {
if !crate::svg_animation::is_animation_element(&candidate.local_name_string()) {
return true;
}
child = candidate.next_element_sibling();
}
false
}

fn filter_source_crosses_pattern_profile_boundary<'d>(
target: HtmlElement<'d>,
servers: &PaintServers<'d>,
Expand Down Expand Up @@ -6661,7 +6678,7 @@ mod filter_resource {
if let Some(pattern_property) = pattern_property {
if !is_target
|| element.local_name_string() != "rect"
|| element.first_element_child().is_some()
|| has_non_animation_element_child(element)
|| get_attr(element, "rx").is_some()
|| get_attr(element, "ry").is_some()
{
Expand Down Expand Up @@ -6733,7 +6750,7 @@ mod filter_resource {
if !matches!(
target.local_name_string().as_str(),
"rect" | "circle" | "ellipse" | "path" | "polygon" | "polyline"
) || target.first_element_child().is_some()
) || has_non_animation_element_child(target)
{
return Ok(false);
}
Expand Down Expand Up @@ -11221,6 +11238,14 @@ fn geometry_number_source_loses_provenance(source: &str, percentage: bool) -> bo
}
}

/// Whether a direct unitless source number's correctly rounded `f32` differs
/// from Chromium's measured CSS-number route. The shadow `f64` conversion is
/// a classifier only: callers refuse the source and never substitute it as a
/// second parser.
pub(crate) fn direct_number_source_loses_chromium_provenance(source: &str) -> bool {
geometry_number_source_loses_provenance(source, false)
}

#[cfg(test)]
mod geometry_number_resolution_tests {
use super::*;
Expand Down
22 changes: 20 additions & 2 deletions crates/websem/src/svg_animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ use csscascade::dom::{DemoNodeData, NodeId};
use style::dom::TElement;

use crate::effective_values::EffectiveValues;
use crate::svg::SourceEntry;
use crate::svg::{
SourceEntry, direct_number_source_loses_chromium_provenance, dots_carry_digits,
trim_svg_whitespace,
};

/// A deterministic rejection from the deliberately closed rect-x animation
/// slice.
Expand Down Expand Up @@ -487,7 +490,14 @@ fn required<'a>(
}

fn parse_finite_number(value: &str, name: &str, path: &str) -> Result<f32, AnimationError> {
let parsed = value.trim().parse::<f32>().map_err(|_| {
let source = trim_svg_whitespace(value);
if !dots_carry_digits(source) {
return Err(AnimationError::new(
path,
format!("{name}={value:?} is not a unitless SVG number"),
));
}
let parsed = source.parse::<f32>().map_err(|_| {
AnimationError::new(
path,
format!("{name}={value:?} is not a unitless SVG number"),
Expand All @@ -499,6 +509,14 @@ fn parse_finite_number(value: &str, name: &str, path: &str) -> Result<f32, Anima
format!("{name}={value:?} is not finite"),
));
}
if direct_number_source_loses_chromium_provenance(source) {
return Err(AnimationError::new(
path,
format!(
"{name} source number loses Chromium used-value provenance at the binary32 normalization boundary"
),
));
}
Ok(parsed)
}

Expand Down
43 changes: 43 additions & 0 deletions crates/websem/tests/filter_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,49 @@ fn filtered_patterns_refuse_curved_and_rounded_source_profiles() {
}
}

/// An admitted animation element contributes no static source draw. The
/// filtered-pattern profile must classify the animated rect itself, leaving
/// the animation inventory to decide the dynamic surface; treating the
/// `<animate>` child as source geometry over-refuses an otherwise exact P2
/// pattern/filter program.
#[test]
fn animation_child_does_not_widen_the_filtered_pattern_rect_profile() {
let source = document(
r##" <rect width="64" height="64" fill="white"/>
<defs>
<filter id="sf" filterUnits="userSpaceOnUse" x="0" y="0" width="8" height="8"
color-interpolation-filters="sRGB">
<feColorMatrix type="matrix" values=".2 0 0 0 .3 0 .7 0 0 0 0 0 .4 0 .2 0 0 0 .8 0"/>
</filter>
<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="64" height="64"
color-interpolation-filters="sRGB">
<feColorMatrix type="saturate" values=".2"/>
</filter>
<pattern id="template" patternUnits="userSpaceOnUse" width="8" height="8">
<rect width="4" height="8" fill="#16a34a" filter="url(#sf)"/>
</pattern>
<pattern id="p" href="#template"/>
</defs>
<rect x="4" y="8" width="24" height="48" fill="url(#p)" filter="url(#f)">
<animate attributeName="x" from="8" to="24" dur="2s" fill="freeze"/>
</rect>"##,
);
let strict = SvgFrameSource::from_standalone_svg(source.clone(), viewport())
.expect("the direct animated pattern rect stays inside the measured profile");
let best = SvgFrameSource::from_standalone_svg_best_effort(source, viewport())
.expect("best effort admits the same dynamic source");
assert!(best.degradations().is_empty());
assert_eq!(strict.base_frame(), best.base_frame());
for time_ns in [0, 250_000_000, 1_000_000_000, 2_000_000_000, 3_000_000_000] {
let time = animation_sampling::SampleTime::from_nanoseconds(time_ns);
assert_eq!(
strict.sample_frame(time).expect("strict sample"),
best.sample_frame(time).expect("best-effort sample"),
"admissions agree at {time_ns}ns"
);
}
}

#[test]
fn filtered_descendants_refuse_same_scope_clip_and_partial_opacity() {
let defs = r##" <defs>
Expand Down
29 changes: 28 additions & 1 deletion crates/websem/tests/pattern_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ fn invalid_and_valid_empty_patterns_select_different_fallback_outcomes() {
assert_eq!(solid_of(fallback), CGColor::from_rgb(0xef, 0x44, 0x44));

let valid_empty = admit_both(&document(
r##" <defs><pattern id="p" patternUnits="userSpaceOnUse" width="8" height="8"><title>selected local content</title></pattern></defs>
r##" <defs><pattern id="p" patternUnits="userSpaceOnUse" x=".5" y=".5" width="8" height="8"><title>selected local content</title></pattern></defs>
<rect width="64" height="64" fill="url(#p) #ef4444"/>"##,
));
let pattern = pattern_of(&valid_empty, 0);
Expand Down Expand Up @@ -368,6 +368,18 @@ fn every_measured_picture_shader_boundary_refuses_in_both_admissions() {
r##"<pattern id="p" patternUnits="userSpaceOnUse" width="7.5" height="8"><rect width="4" height="8" fill="red"/></pattern>"##,
"sampling precision boundary",
),
(
r##"<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="8" height="8" color-interpolation-filters="sRGB"><feColorMatrix type="saturate" values=".2"/></filter><pattern id="p" patternUnits="userSpaceOnUse" x=".5" width="8" height="8"><rect width="3" height="8" fill="red" filter="url(#f)"/></pattern>"##,
"phase precision boundary",
),
(
r##"<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="8" height="8" color-interpolation-filters="sRGB"><feColorMatrix type="saturate" values=".2"/></filter><pattern id="p" patternUnits="userSpaceOnUse" y=".5" width="8" height="8"><rect width="8" height="3" fill="red" filter="url(#f)"/></pattern>"##,
"phase precision boundary",
),
(
r##"<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="8" height="8" color-interpolation-filters="sRGB"><feColorMatrix type="saturate" values=".2"/></filter><pattern id="p" patternUnits="userSpaceOnUse" x="100000.0625" width="8" height="8"><rect width="3" height="8" fill="red" filter="url(#f)"/></pattern>"##,
"phase precision boundary",
),
];

for (defs, reason) in cases {
Expand All @@ -380,6 +392,21 @@ fn every_measured_picture_shader_boundary_refuses_in_both_admissions() {
reason,
);
}

let object_origin_defs = r##"<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="8" height="8" color-interpolation-filters="sRGB"><feColorMatrix type="saturate" values=".2"/></filter><pattern id="p" patternUnits="objectBoundingBox" width=".25" height=".25" viewBox="0 0 8 8"><rect width="4" height="8" fill="red" filter="url(#f)"/></pattern>"##;
for target in [
r#"<rect x="8.5" y="8" width="48" height="48" fill="url(#p)"/>"#,
r#"<rect x="8" y="8.5" width="48" height="48" fill="url(#p)"/>"#,
] {
assert_target_skip(
&document(&format!(
r##" <rect width="64" height="64" fill="white"/>
<defs>{object_origin_defs}</defs>
{target}"##
)),
"phase precision boundary",
);
}
}

#[test]
Expand Down
64 changes: 60 additions & 4 deletions crates/websem/tests/svg_animation_x.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
//! pixels. These tests consume only those artifacts; they never run the sealed
//! consolidation scoreboard or compute a similarity score.
//!
//! The slice itself is one `<animate>` on a top-level `<rect>`'s `x`. Two
//! fixtures exercise it: a bare rect on a backdrop, and `svg-scene-cub` — a
//! whole composition whose animated rect is one node among seventeen, so the
//! sampling path is gated over a scene rather than only over a single shape.
//! The slice itself is one `<animate>` on a top-level `<rect>`'s `x`. The
//! fixtures exercise a bare rect, a whole curve-heavy composition, and a
//! pattern-painted rect whose source and target both cross admitted filters.

mod support;

Expand Down Expand Up @@ -101,6 +100,7 @@ struct BakeManifest {
schema_version: u32,
kind: String,
bake_script_sha256: String,
capture_module_sha256: String,
suite: String,
suite_sha256: String,
capture: CapturePolicy,
Expand All @@ -111,6 +111,7 @@ struct BakeManifest {
struct CapturePolicy {
viewport: String,
device_scale_factor: u32,
javascript_enabled: bool,
network: String,
timeline_control: String,
comparison: String,
Expand Down Expand Up @@ -235,11 +236,16 @@ fn chromium_animation_oracle_provenance_is_current() {
manifest.bake_script_sha256,
sha256_file(&root.join("bake_chromium.ts"))
);
assert_eq!(
manifest.capture_module_sha256,
sha256_file(&root.join("../chromium_capture.ts"))
);
assert_eq!(
manifest.capture.viewport,
"per-fixture declared dims (the initial viewport)"
);
assert_eq!(manifest.capture.device_scale_factor, 1);
assert!(!manifest.capture.javascript_enabled);
assert_eq!(
manifest.capture.network,
"all http(s) requests aborted; zero attempted"
Expand Down Expand Up @@ -546,6 +552,26 @@ fn load_active_animation_refuses_at_construction_and_skips_by_declaration() {
r#"<animate attributeName="x" from="1" to="2" dur="1.5s" fill="freeze"/>"#,
"positive integer in ms or s",
),
(
r#"<animate attributeName="x" from="1." to="2" dur="1s" fill="freeze"/>"#,
"not a unitless SVG number",
),
(
r#"<animate attributeName="x" from="1.e0" to="2" dur="1s" fill="freeze"/>"#,
"not a unitless SVG number",
),
(
r#"<animate attributeName="x" from="&#160;1&#160;" to="2" dur="1s" fill="freeze"/>"#,
"not a unitless SVG number",
),
(
r#"<animate attributeName="x" from="&#12288;1&#12288;" to="2" dur="1s" fill="freeze"/>"#,
"not a unitless SVG number",
),
(
r#"<animate attributeName="x" from="1.000000059604644775390625000000000000000000000001" to="2" dur="1s" fill="freeze"/>"#,
"source number loses Chromium used-value provenance",
),
];

for (markup, expected) in CASES {
Expand Down Expand Up @@ -594,6 +620,36 @@ fn load_active_animation_refuses_at_construction_and_skips_by_declaration() {
}
}

#[test]
fn measured_svg_number_controls_remain_admitted_animation_endpoints() {
for spelling in [" 1 ", "+1", ".1e1"] {
let svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<rect x="2" y="4" width="4" height="4">
<animate attributeName="x" from="{spelling}" to="{spelling}" dur="1s" fill="freeze"/>
</rect>
</svg>"#
);
let strict = SvgFrameSource::from_standalone_svg(svg.clone(), host_viewport())
.unwrap_or_else(|error| panic!("measured control {spelling:?}: {error}"));
let best = SvgFrameSource::from_standalone_svg_best_effort(svg, host_viewport())
.unwrap_or_else(|error| panic!("best-effort control {spelling:?}: {error}"));
assert!(best.degradations().is_empty());

let strict_sample = strict
.sample_frame(SampleTime::ZERO)
.expect("strict control sample");
let best_sample = best
.sample_frame(SampleTime::ZERO)
.expect("best-effort control sample");
assert_eq!(strict_sample, best_sample);
let Geometry::Rect(rect) = &strict_sample.nodes()[0].geometry else {
panic!("measured control must remain a rectangle")
};
assert_eq!(rect.x, 1.0, "measured control {spelling:?}");
}
}

#[test]
fn sampling_refuses_dynamic_side_channels_and_unclosed_inline_html() {
for (rect_attributes, child, expected) in [
Expand Down
15 changes: 15 additions & 0 deletions crates/websem/tests/unsupported_corpus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,11 @@ const CORPUS: &[(&str, Departure, &str)] = &[
DeclaredByBestEffort,
"source cannot compile completely",
),
(
"svg-pattern-tile-phase-precision",
DeclaredByBestEffort,
"picture-shader phase precision boundary",
),
(
"svg-pattern-tile-sampling-precision",
DeclaredByBestEffort,
Expand Down Expand Up @@ -683,6 +688,16 @@ const CORPUS: &[(&str, Departure, &str)] = &[
DeclaredByBestEffort,
"animation element <animateTransform>",
),
(
"svg-smil-number-precision-alias",
DeclaredByBestEffort,
"source number loses Chromium used-value provenance",
),
(
"svg-smil-number-source-syntax",
DeclaredByBestEffort,
"not a unitless SVG number",
),
("svg-smil-retarget-href", BothRefuse, "href"),
(
"svg-smil-set-load-active",
Expand Down
Loading
Loading