mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(svg): enforce float distance conversion and empty-path errors
This commit is contained in:
@@ -3951,7 +3951,7 @@ pub(super) fn svg_geometry_get_total_length_callback<'s>(
|
||||
let length = svg_geometry_segments(scope, receiver)
|
||||
.iter()
|
||||
.fold(0.0, |total, segment| total + segment.length());
|
||||
rv.set(v8::Number::new(scope, length).into());
|
||||
rv.set(v8::Number::new(scope, f64::from(length as f32)).into());
|
||||
}
|
||||
|
||||
pub(super) fn svg_geometry_get_point_at_length_callback<'s>(
|
||||
@@ -3965,7 +3965,16 @@ pub(super) fn svg_geometry_get_point_at_length_callback<'s>(
|
||||
return;
|
||||
};
|
||||
let segments = svg_geometry_segments(scope, receiver);
|
||||
let point = svg_geometry::point_at_length(&segments, parsed.distance);
|
||||
if segments.is_empty() {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"InvalidStateError",
|
||||
11,
|
||||
"The SVG geometry has no point on its path.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let point = svg_geometry::point_at_length(&segments, f64::from(parsed.distance));
|
||||
rv.set(build_dom_point_like(scope, point.x, point.y).into());
|
||||
}
|
||||
|
||||
|
||||
@@ -368,8 +368,8 @@ struct SvgAngleConvertToSpecifiedUnitsArgs {
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "SVGGeometryElement.getPointAtLength")]
|
||||
struct SvgGeometryPointAtLengthArgs {
|
||||
#[webidl(required, converter = "double")]
|
||||
distance: f64,
|
||||
#[webidl(required, converter = "float")]
|
||||
distance: f32,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
(() => {
|
||||
const checks = [], ns = 'http://www.w3.org/2000/svg';
|
||||
const check = (name,fn) => {try {checks.push({name,passed:fn()===true});} catch(error) {checks.push({name,passed:false,error:String(error)});}};
|
||||
const near = (a,b) => Math.abs(a-b) <= Math.max(1e-12,Math.abs(b)*2e-14);
|
||||
const realms = [window,document.querySelector('iframe').contentWindow];
|
||||
const values = [0,-0,1,1+2**-24,1+3*2**-24,16777217,16777219,1e-46,-1,1e20,-1e20,Math.fround(3.4028234e38),null,true,'1.0000001788139343'];
|
||||
const invalid = [undefined,NaN,Infinity,-Infinity,Number.MAX_VALUE,-Number.MAX_VALUE,2**128,-(2**128),Symbol(),1n];
|
||||
const typeError = (realm,fn) => {try {fn();} catch(error) {return Object.getPrototypeOf(error)===realm.TypeError.prototype;} return false;};
|
||||
for (const [ownerIndex,owner] of realms.entries()) {
|
||||
const docs = [owner.document,owner.document.implementation.createHTMLDocument(''),owner.document.implementation.createDocument(ns,'svg'),new owner.DOMParser().parseFromString('<svg xmlns="'+ns+'"/>','image/svg+xml')];
|
||||
for (const [docIndex,doc] of docs.entries()) {
|
||||
const root=doc.createElementNS(ns,'svg');root.setAttribute('width','200');root.setAttribute('height','100');(doc.body||doc.documentElement).appendChild(root);
|
||||
for (const [calleeIndex,callee] of realms.entries()) {
|
||||
const prefix=`float/${ownerIndex}/${docIndex}/${calleeIndex}`;
|
||||
const point=callee.SVGGeometryElement.prototype.getPointAtLength, total=callee.SVGGeometryElement.prototype.getTotalLength;
|
||||
const path=doc.createElementNS(ns,'path');path.setAttribute('d','M0 0L134217728 0');root.appendChild(path);
|
||||
for (const [index,value] of values.entries()) {
|
||||
check(prefix+'/distance/'+index,()=>{const p=point.call(path,value);return near(p.x,Math.min(134217728,Math.max(0,Math.fround(Number(value)))))&&p.y===0;});
|
||||
check(prefix+'/object-distance/'+index,()=>{let n=0;const p=point.call(path,{valueOf(){n++;return value;}});return n===1&&near(p.x,Math.min(134217728,Math.max(0,Math.fround(Number(value)))))&&p.y===0;});
|
||||
}
|
||||
const empty=doc.createElementNS(ns,'path');root.appendChild(empty);
|
||||
for (const [index,value] of invalid.entries()) {
|
||||
for (const [tag,element] of [['path',path],['empty',empty]]) {
|
||||
check(prefix+'/'+tag+'/invalid/'+index,()=>typeError(callee,()=>point.call(element,value)));
|
||||
}
|
||||
}
|
||||
check(prefix+'/arity',()=>typeError(callee,()=>point.call(path)));
|
||||
check(prefix+'/conversion-exception',()=>{const sentinel={};let n=0;try {point.call(path,{valueOf(){n++;throw sentinel;}});} catch(error) {return error===sentinel&&n===1;}return false;});
|
||||
check(prefix+'/conversion-mutates-path',()=>{let n=0;const p=point.call(path,{valueOf(){n++;path.setAttribute('d','M7 9L134217735 9');return 16777217;}});return n===1&&near(p.x,16777223)&&p.y===9;});
|
||||
for (const [index,value] of [0.1,1.00000007,16777217,16777219,1000.00001].entries()) {
|
||||
const line=doc.createElementNS(ns,'line');line.setAttribute('x2',String(value));root.appendChild(line);
|
||||
check(prefix+'/total-float/'+index,()=>Object.is(total.call(line),Math.fround(value)));
|
||||
line.remove();
|
||||
}
|
||||
let conversions=0,traps=0;
|
||||
const author=new Proxy(path,{get(){traps++;throw 42;},getPrototypeOf(){traps++;throw 42;}}),revoked=Proxy.revocable(path,{});revoked.revoke();
|
||||
for (const [index,receiver] of [{},Object.create(path),Object.create(callee.SVGGeometryElement.prototype),author,revoked.proxy,doc.createElementNS(ns,'g')].entries()) {
|
||||
check(prefix+'/brand/'+index,()=>typeError(callee,()=>point.call(receiver,{valueOf(){conversions++;return Number.MAX_VALUE;}}))&&conversions===0&&traps===0);
|
||||
}
|
||||
path.remove();empty.remove();
|
||||
}
|
||||
root.remove();
|
||||
}
|
||||
}
|
||||
globalThis.__svgFloatResults={complete:true,total:checks.length,passed:checks.filter(row=>row.passed).length,checks};globalThis.__uiEventResults=globalThis.__svgFloatResults;return true;
|
||||
})()
|
||||
@@ -90,3 +90,34 @@ fn svg_geometry_methods_use_registered_proxy_geometry_without_author_traps() {
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_distance_queries_distinguish_empty_paths_from_zero_length_geometry() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://svg-empty-paths.test/");
|
||||
vm.eval("document.body.innerHTML = '<iframe></iframe>'")
|
||||
.unwrap();
|
||||
vm.eval(include_str!("svg_geometry_path_states.js"))
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval("__svgGeometryResults.complete").unwrap(), "true");
|
||||
assert_eq!(vm.eval("__svgGeometryResults.total").unwrap(), "3616");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(__svgGeometryResults.checks.filter(row => !row.passed))")
|
||||
.unwrap(),
|
||||
"[]",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_distance_queries_use_webidl_float_conversion() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://svg-geometry-float.test/");
|
||||
vm.eval("document.body.innerHTML = '<iframe></iframe>'")
|
||||
.unwrap();
|
||||
vm.eval(include_str!("svg_geometry_float.js")).unwrap();
|
||||
assert_eq!(vm.eval("__svgFloatResults.complete").unwrap(), "true");
|
||||
assert_eq!(vm.eval("__svgFloatResults.total").unwrap(), "1024");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(__svgFloatResults.checks.filter(row => !row.passed))")
|
||||
.unwrap(),
|
||||
"[]"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
(() => {
|
||||
const checks = [];
|
||||
const check = (name, fn) => { try {checks.push({name,passed:fn()===true});} catch(error) {checks.push({name,passed:false,error:String(error)});} };
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
const realms = [window, document.querySelector('iframe').contentWindow];
|
||||
const definitions = [
|
||||
['path/missing','path',{},null,0], ['path/empty','path',{d:''},null,0],
|
||||
['path/none','path',{d:'none'},null,0], ['path/invalid-first','path',{d:'L 7 9'},null,0],
|
||||
['path/moveto','path',{d:'M 7 9'},[7,9],0],
|
||||
['path/multiple-moveto','path',{d:'M 7 9 M 12 13'},[7,9],0],
|
||||
['path/zero-line','path',{d:'M 7 9 L 7 9'},[7,9],0],
|
||||
['path/invalid-tail','path',{d:'M 7 9 X 1 2'},[7,9],0],
|
||||
['line/missing','line',{},[0,0],0], ['line/zero','line',{x1:7,y1:9,x2:7,y2:9},[7,9],0],
|
||||
['rect/missing','rect',{},null,0], ['rect/zero','rect',{x:7,y:9,width:0,height:0},null,0],
|
||||
['rect/zero-width','rect',{x:7,y:9,width:0,height:10},[7,9],20],
|
||||
['rect/zero-height','rect',{x:7,y:9,width:10,height:0},[7,9],20],
|
||||
['circle/missing','circle',{},null,0], ['circle/zero','circle',{cx:7,cy:9,r:0},null,0],
|
||||
['ellipse/missing','ellipse',{},null,0], ['ellipse/zero','ellipse',{cx:7,cy:9,rx:0,ry:0},null,0],
|
||||
['ellipse/zero-rx','ellipse',{cx:7,cy:9,rx:0,ry:10},[7,9],40],
|
||||
['ellipse/zero-ry','ellipse',{cx:7,cy:9,rx:10,ry:0},[17,9],40],
|
||||
['polyline/missing','polyline',{},null,0], ['polygon/missing','polygon',{},null,0],
|
||||
['polyline/one-point','polyline',{points:'7 9'},[7,9],0],
|
||||
['polygon/one-point','polygon',{points:'7 9'},[7,9],0],
|
||||
];
|
||||
const errorIn = (realm,name,fn) => {try {fn();} catch(error) {return Object.getPrototypeOf(error) === realm[name==='TypeError'?'TypeError':'DOMException'].prototype && error.name===name && (name==='TypeError'||error.code===11);} return false;};
|
||||
for (const [ownerIndex,owner] of realms.entries()) {
|
||||
const docs=[owner.document,owner.document.implementation.createHTMLDocument(''),owner.document.implementation.createDocument(ns,'svg'),new owner.DOMParser().parseFromString('<svg xmlns="'+ns+'"/>','image/svg+xml')];
|
||||
for(const [docIndex,doc] of docs.entries()) {
|
||||
const root=doc.createElementNS(ns,'svg');(doc.body||doc.documentElement).appendChild(root);
|
||||
for(const [calleeIndex,callee] of realms.entries()) {
|
||||
const point=callee.SVGGeometryElement.prototype.getPointAtLength, length=callee.SVGGeometryElement.prototype.getTotalLength;
|
||||
for(const [name,tag,attrs,expected,total] of definitions) {
|
||||
const prefix=`${ownerIndex}/${docIndex}/${calleeIndex}/${name}`;
|
||||
const element=doc.createElementNS(ns,tag);for(const [key,value] of Object.entries(attrs))element.setAttribute(key,value);root.appendChild(element);
|
||||
for(const display of ['','none']) {
|
||||
element.style.display=display;
|
||||
check(prefix+'/length/'+display,()=>total===0?Object.is(length.call(element),0):Math.abs(length.call(element)-total)<1e-6);
|
||||
check(prefix+'/point/'+display,()=> {
|
||||
if(expected===null)return errorIn(callee,'InvalidStateError',()=>point.call(element,300));
|
||||
const value=point.call(element,300);return Object.getPrototypeOf(value)===callee.DOMPoint.prototype&&Math.abs(value.x-expected[0])<1e-7&&Math.abs(value.y-expected[1])<1e-7&&value.z===0&&value.w===1;
|
||||
});
|
||||
}
|
||||
check(prefix+'/detach',()=> {element.remove();if(expected===null)return errorIn(callee,'InvalidStateError',()=>point.call(element,300));const p=point.call(element,300);return Math.abs(p.x-expected[0])<1e-7&&Math.abs(p.y-expected[1])<1e-7;});
|
||||
if(expected===null) {
|
||||
for(const argument of [NaN,Infinity,-Infinity,Symbol(),1n])check(prefix+'/conversion/'+typeof argument+'/'+String(argument),()=>errorIn(callee,'TypeError',()=>point.call(element,argument)));
|
||||
check(prefix+'/missing-argument',()=>errorIn(callee,'TypeError',()=>point.call(element)));
|
||||
check(prefix+'/conversion-exception',()=> {const sentinel={};let n=0;try{point.call(element,{valueOf(){n++;throw sentinel;}});}catch(error){return error===sentinel&&n===1;}return false;});
|
||||
check(prefix+'/conversion-before-empty',()=> {let n=0;return errorIn(callee,'InvalidStateError',()=>point.call(element,{valueOf(){n++;return 0;}}))&&n===1;});
|
||||
}
|
||||
}
|
||||
const path=doc.createElementNS(ns,'path');root.appendChild(path);
|
||||
check(`${ownerIndex}/${docIndex}/${calleeIndex}/conversion-creates-path`,()=> {let n=0;const p=point.call(path,{valueOf(){n++;path.setAttribute('d','M 7 9 L 17 9');return 5;}});return n===1&&p.x===12&&p.y===9;});
|
||||
check(`${ownerIndex}/${docIndex}/${calleeIndex}/conversion-removes-path`,()=> {let n=0;return errorIn(callee,'InvalidStateError',()=>point.call(path,{valueOf(){n++;path.removeAttribute('d');return 5;}}))&&n===1;});
|
||||
let traps=0;const proxy=new Proxy(path,{get(){traps++;throw 42;},getPrototypeOf(){traps++;throw 42;}});const revoked=Proxy.revocable(path,{});revoked.revoke();
|
||||
for(const [index,receiver] of [{},Object.create(callee.SVGGeometryElement.prototype),Object.create(path),proxy,revoked.proxy,doc,doc.createElement('div'),doc.createElementNS(ns,'g')].entries())check(`${ownerIndex}/${docIndex}/${calleeIndex}/brand/${index}`,()=> {let n=0;return errorIn(callee,'TypeError',()=>point.call(receiver,{valueOf(){n++;return 0;}}))&&n===0&&traps===0;});
|
||||
}
|
||||
}
|
||||
}
|
||||
globalThis.__svgGeometryResults={complete:true,total:checks.length,passed:checks.filter(x=>x.passed).length,checks};globalThis.__uiEventResults=globalThis.__svgGeometryResults;return true;
|
||||
})()
|
||||
@@ -85,6 +85,8 @@ test(function () {
|
||||
test(function () {
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
rect.setAttribute("width", "10");
|
||||
rect.setAttribute("height", "10");
|
||||
svg.appendChild(rect);
|
||||
|
||||
const point = rect.getPointAtLength(0);
|
||||
|
||||
Reference in New Issue
Block a user