mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-20 18:28:50 +00:00
fix(export): reject oversized page selections (#1089)
Reject page selections that cannot fit the target index type before conversion, and return an error for the largest direct PPTX page index. Validated the combined main and PR tree with all seven cargo xtask ci stages (1,967 passing tests), four actual WASM export-page regressions, and successful FreeBSD, Windows ARM64, and packaging workflows.
This commit is contained in:
1 parent
7ec5c8bc6d
commit
a184db6f8f
4 files changed
+83
-5
No files matched your search
@@ -288,14 +288,22 @@ fn pages_param(id: &str, p: &Value, page_count: usize) -> Result<Option<Vec<usiz
|
||||
Some(Value::Array(a)) => {
|
||||
let mut v = Vec::new();
|
||||
for x in a {
|
||||
let n = x.as_u64().filter(|n| *n >= 1 && (*n as usize) <= page_count).ok_or_else(|| bad(id, format!("bad page {x}")))?;
|
||||
v.push(n as usize - 1);
|
||||
let n = x
|
||||
.as_u64()
|
||||
.and_then(|n| usize::try_from(n).ok())
|
||||
.filter(|n| *n >= 1 && *n <= page_count)
|
||||
.ok_or_else(|| bad(id, format!("bad page {x}")))?;
|
||||
v.push(n - 1);
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
Some(Value::Number(n)) => {
|
||||
let n = n.as_u64().filter(|n| *n >= 1 && (*n as usize) <= page_count).ok_or_else(|| bad(id, format!("bad page {n}")))?;
|
||||
Some(vec![n as usize - 1])
|
||||
let n = n
|
||||
.as_u64()
|
||||
.and_then(|n| usize::try_from(n).ok())
|
||||
.filter(|n| *n >= 1 && *n <= page_count)
|
||||
.ok_or_else(|| bad(id, format!("bad page {n}")))?;
|
||||
Some(vec![n - 1])
|
||||
}
|
||||
Some(v) => return Err(bad(id, format!("bad `pages`: {v}"))),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Public export-command page input must be rejected before narrowing on 32-bit targets.
|
||||
use designcraft_engine::{EngineError, Session};
|
||||
use serde_json::json;
|
||||
|
||||
fn rejects(numbers: &[u64]) {
|
||||
let mut session = Session::new();
|
||||
session.execute("file.new", &json!({"pages": 1})).unwrap();
|
||||
let before = serde_json::to_value(&session.doc().unwrap().doc).unwrap();
|
||||
for command in ["file.exportPdf", "file.exportPptx"] {
|
||||
for &number in numbers {
|
||||
for pages in [json!(number), json!([number])] {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_bindgen_test::console_log!("SAFETY_REJECTION_INPUT {command} {pages}");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
println!("SAFETY_REJECTION_INPUT {command} {pages}");
|
||||
match session.execute(command, &json!({"pages": pages})) {
|
||||
Err(EngineError::BadParams { cmd, msg }) => {
|
||||
assert_eq!(cmd, command);
|
||||
assert_eq!(msg, format!("bad page {number}"));
|
||||
}
|
||||
Ok(_) => panic!("EXPECTED_BAD_PARAMS: {command} pages {pages} was accepted"),
|
||||
Err(other) => panic!("EXPECTED_BAD_PARAMS: {command} pages {pages} returned {other:?}"),
|
||||
}
|
||||
assert_eq!(before, serde_json::to_value(&session.doc().unwrap().doc).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
||||
fn zero_and_maximum_export_pages_are_bad_params() {
|
||||
rejects(&[0, u64::MAX]);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
||||
fn page_value_that_truncates_to_valid_page_is_bad_params() {
|
||||
rejects(&[(1u64 << 32) + 1]);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
||||
fn page_value_that_truncates_to_zero_is_bad_params() {
|
||||
rejects(&[1u64 << 32]);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
||||
fn page_one_exports_in_scalar_and_array_forms() {
|
||||
let mut session = Session::new();
|
||||
session.execute("file.new", &json!({"pages": 1})).unwrap();
|
||||
let before = serde_json::to_value(&session.doc().unwrap().doc).unwrap();
|
||||
for (command, count) in [("file.exportPdf", "pages"), ("file.exportPptx", "slides")] {
|
||||
for pages in [json!(1), json!([1])] {
|
||||
let result = session.execute(command, &json!({"pages": pages})).unwrap();
|
||||
assert_eq!(result[count], 1);
|
||||
assert!(!result["base64"].as_str().unwrap().is_empty());
|
||||
assert_eq!(before, serde_json::to_value(&session.doc().unwrap().doc).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ pub fn export_pptx(doc: &Document, cache: &Cache, opts: &PptxOptions) -> Result<
|
||||
return Err(PptxError::NoPages);
|
||||
}
|
||||
if let Some(bad) = pages.iter().find(|p| **p >= count) {
|
||||
return Err(PptxError::BadPage(bad + 1));
|
||||
return Err(PptxError::BadPage(bad.saturating_add(1)));
|
||||
}
|
||||
let mut ex = slide::Exporter::new(doc, cache, opts);
|
||||
let mut slides = Vec::with_capacity(pages.len());
|
||||
|
||||
@@ -228,6 +228,14 @@ fn pictures_are_cropped_to_their_frames() {
|
||||
assert!(rels.contains("../media/image1.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximal_page_index_is_an_error() {
|
||||
let d = Document::new(&NewDocument::default());
|
||||
let options = PptxOptions { pages: Some(vec![usize::MAX]), ..Default::default() };
|
||||
let result = export_pptx(&d, &Cache::new(), &options);
|
||||
assert!(matches!(result, Err(PptxError::BadPage(usize::MAX))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_pages_and_hostile_geometry_are_errors_not_crashes() {
|
||||
let mut d = Document::new(&NewDocument::default());
|
||||
|
||||
Reference in new issue
Block a user