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