Merge pull request #1675 from Silvertree2010/fix/1589-audit-dc-08-non-finite-idml-numb

Keep non-finite IDML numbers out of imported documents
This commit is contained in:
Brandon Thomas
2026-10-23 17:27:47 -04:00
committed by GitHub
2 changed files with 57 additions and 10 deletions
+21 -10
View File
@@ -387,13 +387,10 @@ fn mojikumi_row(e: &El) -> Result<designcraft_doc::cjk::MojikumiAki> {
Ok(row)
}
/// The numbers in a whitespace-separated list; empty when any number is NaN or infinite (JSON
/// can't store them, and dropping just those would shift the rest).
fn nums(s: &str) -> Vec<f64> {
s.split_whitespace().filter_map(|v| v.parse().ok()).collect()
}
/// Like [`nums`], but empty when any number is NaN or infinite.
fn finite_nums(s: &str) -> Vec<f64> {
let v = nums(s);
let v: Vec<f64> = s.split_whitespace().filter_map(|v| v.parse().ok()).collect();
if v.iter().all(|x| x.is_finite()) { v } else { Vec::new() }
}
@@ -895,6 +892,15 @@ impl<'r> Importer<'r> {
_ if v.len() >= 4 => Color::cmyk((v[0] / 100.0) as f32, (v[1] / 100.0) as f32, (v[2] / 100.0) as f32, (v[3] / 100.0) as f32),
_ => return None,
};
// Huge values (1e300) overflow `f32` to infinity.
let parts = match c {
Color::Rgb { r, g, b } => [r, g, b, 0.0],
Color::Cmyk { c, m, y, k } => [c, m, y, k],
Color::Gray { k } => [k, 0.0, 0.0, 0.0],
};
if !parts.iter().all(|v| v.is_finite()) {
return None;
}
let ty = if e.get("Model") == Some("Spot") { ColorType::Spot } else { ColorType::Process };
Some((c, ty))
}
@@ -947,14 +953,14 @@ impl<'r> Importer<'r> {
let (Some(id), Some(name)) = (e.get("Self"), e.get("Name")) else { continue };
let kind = match e.local() {
"StripedStrokeStyle" => designcraft_doc::StrokeType::Stripes {
bands: finite_nums(e.get("StripeArray").unwrap_or(""))
bands: nums(e.get("StripeArray").unwrap_or(""))
.as_chunks::<2>()
.0
.iter()
.map(|c| (c[0] / 100.0, (c[1] - c[0]) / 100.0))
.collect(),
},
"DashedStrokeStyle" => designcraft_doc::StrokeType::Dashed { pattern: finite_nums(e.get("DashArray").unwrap_or("")) },
"DashedStrokeStyle" => designcraft_doc::StrokeType::Dashed { pattern: nums(e.get("DashArray").unwrap_or("")) },
_ => designcraft_doc::StrokeType::Dotted,
};
let _ = id;
@@ -2856,7 +2862,7 @@ impl<'r> Importer<'r> {
{
it.fill.gradient_angle = Some(a);
}
let start: Option<Vec<f64>> = e.prop("GradientFillStart").map(|s| finite_nums(&s));
let start: Option<Vec<f64>> = e.prop("GradientFillStart").map(|s| nums(&s));
if let (Some([x, y]), Some(len)) = (start.as_deref().and_then(|v| <[f64; 2]>::try_from(v).ok()), e.num("GradientFillLength"))
&& len > 0.0
{
@@ -2874,7 +2880,7 @@ impl<'r> Importer<'r> {
{
it.stroke.gradient_angle = Some(a);
}
let start: Option<Vec<f64>> = e.prop("GradientStrokeStart").map(|s| finite_nums(&s));
let start: Option<Vec<f64>> = e.prop("GradientStrokeStart").map(|s| nums(&s));
if let (Some([x, y]), Some(len)) = (start.as_deref().and_then(|v| <[f64; 2]>::try_from(v).ok()), e.num("GradientStrokeLength"))
&& len > 0.0
{
@@ -3702,6 +3708,11 @@ fn path_capped(pg: &El, max: usize) -> PathData {
let closed = g.get("PathOpen") != Some("true");
let mut anchors = Vec::new();
if let Some(arr) = g.find("PathPointArray") {
// A NaN or infinite point drops its subpath (JSON can't store it).
let non_finite = |s: Option<&str>| s.is_some_and(|s| s.split_whitespace().any(|v| v.parse::<f64>().is_ok_and(|x| !x.is_finite())));
if arr.find_all("PathPointType").take(left).any(|p| ["Anchor", "LeftDirection", "RightDirection"].iter().any(|k| non_finite(p.get(k)))) {
continue;
}
for p in arr.find_all("PathPointType").take(left) {
let Some(a) = parse_point(p.get("Anchor")) else { continue };
let hin = parse_point(p.get("LeftDirection")).unwrap_or(a);
+36
View File
@@ -4402,3 +4402,39 @@ fn exports_graphic_in_table_cell() {
assert!(cell.contains("<Image "), "{story}");
assert!(cell.contains("ClipContentToCell=\"true\""), "{story}");
}
/// Non-finite numbers in colour values, page bounds and item transforms (NaN, infinities and
/// values like 1e300 that overflow `f32`) never reach the model: JSON would write them as null.
#[test]
fn non_finite_number_lists_never_reach_the_model() {
let graphic = GRAPHIC.replace(r#"ColorValue="255 0 0""#, r#"ColorValue="NaN 1e300 inf""#);
let spread = SPREAD
.replace(r#"Self="p2" AppliedMaster="m1" GeometricBounds="0 0 700 500""#, r#"Self="p2" AppliedMaster="m1" GeometricBounds="0 0 NaN 500""#)
.replace(r#"ItemTransform="1 0 0 1 50 -300""#, r#"ItemTransform="1 0 0 1 inf -300""#);
let bytes = zip_files(&[
("designmap.xml", DESIGNMAP),
("Resources/Graphic.xml", &graphic),
("Resources/Styles.xml", STYLES),
("Resources/Preferences.xml", PREFS),
("MasterSpreads/MasterSpread_m1.xml", MASTER),
("Spreads/Spread_sp1.xml", &spread),
("Stories/Story_s1.xml", STORY),
]);
let doc = import_idml(&bytes).unwrap();
for s in &doc.swatches {
if let SwatchValue::Color { color, .. } | SwatchValue::Paper { color } = &s.value {
let parts: Vec<f32> = match *color {
Color::Rgb { r, g, b } => vec![r, g, b],
Color::Cmyk { c, m, y, k } => vec![c, m, y, k],
Color::Gray { k } => vec![k],
};
assert!(parts.iter().all(|v| v.is_finite()), "swatch {} is not finite: {color:?}", s.name);
}
}
for sp in doc.spreads.iter().chain(&doc.parents) {
for p in &sp.pages {
assert!(p.width.is_finite() && p.height.is_finite() && p.x.is_finite(), "page {p:?}");
}
}
assert_eq!(doc.non_finite_item(), None);
}