mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-25 03:06:53 -06:00
write_csv visited every position of the used range and appended to one growing String with no limit, so two cells far apart (A1 and XFD1048576) asked for about 16 GiB of commas. The output is now capped at 512 MiB: a lower bound (delimiters and line ends of every row) is checked before anything is allocated, and the text written so far is checked after each row. Over the cap, write_csv returns IoError::TooLarge naming the rows, columns and limit, and Save As / Export CSV and TSV report it without writing a file. Accepted exports are unchanged. write_csv now returns a Result.
46 lines
2.1 KiB
Rust
46 lines
2.1 KiB
Rust
//! CSV and TSV export is capped: two cells far apart in a sheet would otherwise make a file as
|
|
//! big as the whole rectangle between them. A refused export is an error and writes nothing.
|
|
|
|
use gridcraft_engine::Session;
|
|
use serde_json::json;
|
|
|
|
fn two_far_cells() -> Session {
|
|
let mut s = Session::new();
|
|
s.new_workbook();
|
|
s.execute("cell.set", json!({"cell": "A1", "input": "a"})).unwrap();
|
|
s.execute("cell.set", json!({"cell": "XFD1048576", "input": "b"})).unwrap();
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn a_sparse_sheet_spanning_the_grid_is_refused_not_expanded() {
|
|
let mut s = two_far_cells();
|
|
for format in ["csv", "tsv"] {
|
|
let err = s.execute("file.saveBytes", json!({"format": format})).unwrap_err().to_string();
|
|
assert!(err.contains("1048576 rows by 16384 columns") && err.contains("MiB"), "{format}: {err}");
|
|
}
|
|
// Save As and Export write no file, and the document stays as it was.
|
|
let dir = std::env::temp_dir().join(format!("gridcraft-csv-budget-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join("big.csv");
|
|
let err = s.execute("file.exportCsv", json!({"path": path.to_str().unwrap()})).unwrap_err().to_string();
|
|
assert!(err.contains("over the limit"), "{err}");
|
|
assert!(!path.exists(), "nothing is written for a refused export");
|
|
std::fs::remove_dir_all(&dir).unwrap();
|
|
// Other formats are unaffected, and so is a small export from the same session.
|
|
assert!(s.execute("file.saveBytes", json!({"format": "xlsx"})).is_ok());
|
|
s.execute("cell.set", json!({"cell": "XFD1048576", "input": ""})).unwrap();
|
|
assert_eq!(s.execute("file.saveBytes", json!({"format": "csv"})).unwrap()["bytes"], 3);
|
|
}
|
|
|
|
#[test]
|
|
fn exports_below_the_cap_keep_their_positions() {
|
|
for (corner, bytes) in [("C3", 14), ("Z10", 272), ("XFD8", 131_082)] {
|
|
let mut s = Session::new();
|
|
s.new_workbook();
|
|
s.execute("cell.set", json!({"cell": "A1", "input": "a"})).unwrap();
|
|
s.execute("cell.set", json!({"cell": corner, "input": "b"})).unwrap();
|
|
assert_eq!(s.execute("file.saveBytes", json!({"format": "csv"})).unwrap()["bytes"], bytes, "{corner}");
|
|
}
|
|
}
|