fix(export): reject oversized page selections

This commit is contained in:
skylar taylor-barrick
2026-10-15 17:12:41 -06:00
parent a930276fd7
commit 42df4d6440
4 changed files with 83 additions and 5 deletions
+12 -4
View File
@@ -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}"))),
})
+62
View File
@@ -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());
}
}
}
+1 -1
View File
@@ -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());
+8
View File
@@ -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());