diff --git a/crates/engine/src/tests/python_files.rs b/crates/engine/src/tests/python_files.rs index d540b1bba..6fa4b499c 100644 --- a/crates/engine/src/tests/python_files.rs +++ b/crates/engine/src/tests/python_files.rs @@ -317,6 +317,25 @@ fn a_python_cell_without_its_code_keeps_its_whole_cached_result_as_values() { } } +#[test] +fn a_script_is_measured_after_its_escapes_are_decoded() { + let max = gridcraft_model::python::MAX_FORMULA_CHARS; + // 2,000 tabs written as `_x0009_`: 14,000 characters in the file, 2,004 as code, which fits. + let escaped = format!("x = 1{}end", "_x0009_".repeat(2_000)); + assert!(escaped.chars().count() > max); + // Code longer than a cell may hold once decoded is still refused. + let long = format!("{}{}", "_x0009_".repeat(10), "y".repeat(max)); + let data = "_xlfn._xlws.PY(0,0)1_xlfn._xlws.PY(1,0)2"; + let (wb, warnings) = crate::io::open_bytes("escapes.xlsx", &excel_shaped(data, Some(&[&escaped, &long]))).unwrap(); + let sh = wb.sheet(0).unwrap(); + let a1 = sh.cell(CellRef::parse("A1").unwrap()).unwrap().py.as_ref().map(|p| p.code.clone()); + assert_eq!(a1, Some(format!("x = 1{}end", "\t".repeat(2_000))), "read in full"); + let b1 = sh.cell(CellRef::parse("B1").unwrap()).unwrap(); + assert!(b1.py.is_none(), "too long once decoded: only the value"); + assert_eq!(b1.value, Value::Number(2.0)); + assert!(warnings.iter().any(|w| w.contains("B1")), "{warnings:?}"); +} + #[test] fn many_cells_naming_one_long_script_take_from_one_budget() { // 20,000 cells name one script of 8,000 characters: 160 MB of code if each took a copy. diff --git a/crates/xlsx/src/python_part.rs b/crates/xlsx/src/python_part.rs index cfd47c5ca..20ac841b6 100644 --- a/crates/xlsx/src/python_part.rs +++ b/crates/xlsx/src/python_part.rs @@ -111,15 +111,28 @@ pub fn read(cx: &mut Ctx<'_>, rels: &[crate::package::Rel]) -> Result String { let text = el.child("code").map(|c| c.text.as_str()).unwrap_or_default(); - let text = match text.char_indices().nth(MAX_SCRIPT_CHARS) { + let encoded = match text.char_indices().nth(MAX_ENCODED_CHARS) { Some((at, _)) => text.get(..at).unwrap_or_default(), None => text, }; - gridcraft_model::python::normalize_code(&xml::decode_xstring(text)) + let code = gridcraft_model::python::normalize_code(&xml::decode_xstring(encoded)); + match code.char_indices().nth(MAX_SCRIPT_CHARS) { + Some((at, _)) => code.get(..at).unwrap_or_default().to_string(), + None => code, + } } /// `PY(index, 0|1, params…)`, as a file's formula holds it: the index, whether the return type is