mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 11:13:55 +00:00
Moved from GridCraft at 5cf52c5f: the encoding detection, delimiter sniffing, record parser and field quoting of crates/xlsx/src/csv.rs, and the SYLK reader of crates/engine/src/io/sylk.rs. Fields stay text and the SYLK reader returns its own Sheet; typed values, dates and the workbook stay in GridCraft.
118 lines
5.9 KiB
Rust
118 lines
5.9 KiB
Rust
//! SYLK reading. The files are written here, the way spreadsheets export them.
|
|
|
|
use craft_csv::sylk::{ColumnWidth, Error, Value, r1c1_formula_to_a1, read};
|
|
|
|
/// `B3` → (row, column), zero-based.
|
|
fn at(a1: &str) -> (u32, u32) {
|
|
let col = u32::from(a1.as_bytes()[0] - b'A');
|
|
(a1[1..].parse::<u32>().unwrap() - 1, col)
|
|
}
|
|
|
|
/// A small SYLK file: a header, number formats, column widths, cells that carry on from the last
|
|
/// row or column, formulas in R1C1 and a shared formula.
|
|
const SYLK: &str = "ID;PTEST;N;E\r\nP;PGeneral\r\nP;P0.00\r\nP;Pyyyy\\-mm\\-dd\r\nF;W1 1 18\r\nF;W2 3 9\r\nB;Y5;X7\r\n\
|
|
C;Y1;X1;K\"Item\"\r\nC;X2;K\"Qty\"\r\nC;X4;K\"Total\"\r\n\
|
|
C;Y2;X1;K\"Bolts;; small\"\r\nC;X2;K12\r\nF;P1;F$2G;X3\r\nC;X3;K0.35\r\nC;X4;K4.2;ERC[-2]*RC[-1]\r\n\
|
|
C;X5;K52;ER2C2+R[1]C[-3]\r\nC;X6;KTRUE\r\nC;X7;K8.4;ERC[-3]*2\r\n\
|
|
C;Y3;X1;K\"Nuts \"\"hex\"\"\"\r\nC;X2;K40\r\nC;X3;K0.12\r\nC;X4;K4.8;ERC[-2]*RC[-1]\r\nC;X5;K\"big\";EIF(R4C4>5,\"big\",\"small\")\r\nC;X7;K9.6;S;R2;C7\r\n\
|
|
C;Y4;X1;K\"Sum\"\r\nC;X4;K9;ESUM(R[-2]C:R[-1]C)\r\nC;X5;K#DIV/0!;E1/0\r\nC;X7;K18;ESUM(C4)\r\n\
|
|
F;P2;FG0G;Y5;X2\r\nC;Y5;X2;K45000\r\nC;X3;K\"R2C3\";E\"R2C3\"\r\nE\r\n";
|
|
|
|
#[test]
|
|
fn reads_values_formulas_formats_and_widths() {
|
|
let sh = read(SYLK.as_bytes()).unwrap();
|
|
assert_eq!(sh.skipped, 0);
|
|
let value = |a: &str| sh.cells.get(&at(a)).map(|c| c.value.clone()).unwrap_or_default();
|
|
let formula = |a: &str| sh.cells.get(&at(a)).and_then(|c| c.formula.clone());
|
|
let text = |s: &str| Value::Text(s.into());
|
|
assert_eq!(value("A1"), text("Item"));
|
|
assert_eq!(value("A2"), text("Bolts; small"), ";; is a semicolon");
|
|
assert_eq!(value("A3"), text("Nuts \"\"hex\"\""), "text is kept between the outer quotes as written");
|
|
assert_eq!(value("B2"), Value::Number(12.0));
|
|
assert_eq!(value("F2"), Value::Bool(true));
|
|
assert_eq!(value("E4"), Value::Error("#DIV/0!".into()));
|
|
assert_eq!(value("D4"), Value::Number(9.0), "a formula keeps the value the file cached");
|
|
assert_eq!(formula("D2").as_deref(), Some("B2*C2"));
|
|
assert_eq!(formula("E2").as_deref(), Some("$B$2+B3"));
|
|
assert_eq!(formula("E3").as_deref(), Some("IF($D$4>5,\"big\",\"small\")"));
|
|
assert_eq!(formula("D4").as_deref(), Some("SUM(D2:D3)"));
|
|
assert_eq!(formula("G3").as_deref(), Some("D3*2"), "a shared formula is the other cell's, moved");
|
|
assert_eq!(formula("G4").as_deref(), Some("SUM($D:$D)"), "C4 alone is the whole column");
|
|
assert_eq!(formula("C5").as_deref(), Some("\"R2C3\""), "text in quotes isn't a reference");
|
|
assert_eq!(formula("B2"), None);
|
|
// `F;P1;X3` formats the cell on the current row only.
|
|
let format = |a: &str| sh.cells.get(&at(a)).and_then(|c| c.format.clone());
|
|
assert_eq!(format("C2").as_deref(), Some("0.00"));
|
|
assert_eq!(format("C3"), None);
|
|
assert_eq!(format("B5").as_deref(), Some("yyyy\\-mm\\-dd"));
|
|
// Widths are in characters; column D has none.
|
|
assert_eq!(sh.column_widths, [ColumnWidth { first: 0, last: 0, width: 18.0 }, ColumnWidth { first: 1, last: 2, width: 9.0 }]);
|
|
}
|
|
|
|
#[test]
|
|
fn r1c1_formulas_become_a1() {
|
|
for (r1c1, a1) in [
|
|
("RC", "B2"),
|
|
("R[1]C[-1]", "A3"),
|
|
("R1C1:R2C2", "$A$1:$B$2"),
|
|
("R3", "$3:$3"),
|
|
("R1:R[2]", "$1:4"),
|
|
("C[1]", "C:C"),
|
|
("SUM(C3:C4)", "SUM($C:$D)"),
|
|
("ROUND(RC[1],2)+RATE(1,2,3)", "ROUND(C2,2)+RATE(1,2,3)"),
|
|
("'R1C1 sheet'!R1C1", "'R1C1 sheet'!$A$1"),
|
|
("Sheet2!RC", "Sheet2!B2"),
|
|
("R2D2+rc", "R2D2+B2"),
|
|
("R[-5]C", "#REF!"),
|
|
("\"RC\"&R1C1", "\"RC\"&$A$1"),
|
|
("RC[26]+RC16384", "AB2+$XFD2"),
|
|
("RC16385", "#REF!"),
|
|
] {
|
|
assert_eq!(r1c1_formula_to_a1(r1c1, at("B2")), a1, "{r1c1}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_other_files_and_survives_damage() {
|
|
assert_eq!(read(b"A,B\n1,2\n"), Err(Error::NotSylk), "a SYLK file starts with ID");
|
|
assert_eq!(read(b""), Err(Error::NotSylk));
|
|
// A byte order mark and a single-byte code page are accepted.
|
|
assert_eq!(read("\u{feff}ID;P\nC;Y1;X1;K\"café\"\n".as_bytes()).unwrap().cells[&(0, 0)].value, Value::Text("café".into()));
|
|
assert_eq!(read(b"ID;P\nC;Y1;X1;K\"caf\xE9\"\n").unwrap().cells[&(0, 0)].value, Value::Text("café".into()));
|
|
// Positions off the sheet, broken numbers, a missing end, a format past the list and a
|
|
// formula that is no formula (kept as text for the caller to judge).
|
|
let sh = read(b"ID;P\nC;Y0;X1;K1\nC;Y2;X99999;K2\nC;Y3;X1;K1e999\nF;P7;Y3;X1\nF;W5 2 9\nF;W1 2 256\nC;Y4;X1;K5;E(((\n").unwrap();
|
|
assert_eq!(sh.cells.keys().copied().collect::<Vec<_>>(), [(2, 0), (3, 0)]);
|
|
assert_eq!(sh.cells[&(2, 0)].value, Value::Text("1e999".into()));
|
|
assert_eq!((sh.cells[&(3, 0)].value.clone(), sh.cells[&(3, 0)].formula.as_deref()), (Value::Number(5.0), Some("(((")));
|
|
assert_eq!(sh.skipped, 2, "the two cells off the sheet");
|
|
assert!(sh.column_widths.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn survives_huge_relative_offsets_and_width_floods() {
|
|
// Relative offsets at the edge of i64 fall off the sheet instead of overflowing.
|
|
let text = b"ID;P\nC;Y1;X1;K0;ER[9223372036854775807]C\nC;Y2;X1;K0;ER[-9223372036854775808]C[-9223372036854775808]\nE\n";
|
|
let sh = read(text).unwrap();
|
|
assert_eq!(sh.cells[&(0, 0)].formula.as_deref(), Some("#REF!"));
|
|
// Width records over every column, many times over: a bounded number is kept.
|
|
let mut flood = b"ID;P\n".to_vec();
|
|
for _ in 0..5000 {
|
|
flood.extend_from_slice(b"F;W1 16384 12\n");
|
|
}
|
|
flood.extend_from_slice(b"C;Y1;X1;K1\nE\n");
|
|
let sh = read(&flood).unwrap();
|
|
assert_eq!(sh.column_widths.len(), 1024);
|
|
assert_eq!(sh.cells[&(0, 0)].value, Value::Number(1.0));
|
|
// Truncation and byte damage anywhere never panic.
|
|
let good = SYLK.as_bytes().to_vec();
|
|
for cut in 0..good.len() {
|
|
let _ = read(&good[..cut]);
|
|
let mut b = good.clone();
|
|
b[cut] = b'[';
|
|
let _ = read(&b);
|
|
b[cut] = 0xFF;
|
|
let _ = read(&b);
|
|
}
|
|
}
|