mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 00:41:56 -06:00
XLSX: a Python script's length is measured after its escapes are decoded
A script's code was cut at the character limit before its ST_Xstring escapes were decoded, so a script whose escapes (`_x0009_`, `_x000D_`) made the file's text longer than the limit lost its end without a warning, although its code fits. The code is now decoded first and then cut one character past the limit, so a script that is too long once decoded still fails the check and its cell keeps only its value, as before. Decoding is linear and reads at most a bounded number of encoded characters, enough for any code that fits.
This commit is contained in:
@@ -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 = "<sheetData><row r=\"1\"><c r=\"A1\"><f>_xlfn._xlws.PY(0,0)</f><v>1</v></c><c r=\"B1\"><f>_xlfn._xlws.PY(1,0)</f><v>2</v></c></row></sheetData>";
|
||||
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.
|
||||
|
||||
@@ -111,15 +111,28 @@ pub fn read(cx: &mut Ctx<'_>, rels: &[crate::package::Rel]) -> Result<ScriptsIn,
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Encoded characters of a script that are decoded: enough for its first [`MAX_SCRIPT_CHARS`]
|
||||
/// characters of code, whatever its escapes. One character takes at most 14 encoded characters
|
||||
/// (a UTF-16 pair of `_xHHHH_` escapes) and a `\r\n` line break becomes one character, so
|
||||
/// `2 × (MAX_SCRIPT_CHARS + 1)` decoded characters cover the code kept, and two more escapes'
|
||||
/// worth keep the last of them, and the line break after it, from being cut.
|
||||
const MAX_ENCODED_CHARS: usize = 14 * (2 * (MAX_SCRIPT_CHARS + 1) + 2);
|
||||
|
||||
/// A `pythonScript` element's code (ST_Xstring), with `\n` line breaks, cut after
|
||||
/// [`MAX_SCRIPT_CHARS`] characters.
|
||||
/// [`MAX_SCRIPT_CHARS`] characters. The limit is measured on the decoded code, so escapes in the
|
||||
/// file (`_x000D_`, `_x0009_`) don't cut a script that fits; decoding is linear and reads at most
|
||||
/// [`MAX_ENCODED_CHARS`] characters.
|
||||
fn script_code(el: &El) -> 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
|
||||
|
||||
Reference in New Issue
Block a user