//! ODS packages assembled here, not files from another application. Adapted from GridCraft's //! `crates/xlsx/src/tests/ods_tests.rs`. use craft_ods::testing::{content, package, spreadsheet}; use craft_ods::{CellRef, Error, Options, Range, Sheet, Value, ValueType, Workbook, read}; fn ods(tables: &str) -> Vec { spreadsheet(tables) } fn row(cells: &str) -> Vec { ods(&format!(r#"{cells}"#)) } fn text(s: &str) -> Value { Value::text(s) } fn open(bytes: &[u8]) -> Result { read(bytes, &Options::default()) } /// What an application shows: the value, else the text, else the raw value. fn shown(s: &Sheet, at: CellRef) -> Value { let Some(c) = s.cells.get(&at) else { return Value::Empty }; c.value().unwrap_or_else(|| if !c.text.is_empty() { text(&c.text) } else { c.raw.as_deref().map_or(Value::Empty, text) }) } #[test] fn a_namespace_declaration_bomb_is_refused() { let bomb = |n: usize| -> String { (0..n).map(|i| format!(" xmlns:p{i}=\"urn:p{i}\"")).collect() }; let doc = |decls: &str| { content(r#""#) .replacen(" 85 #DIV/0! unavailable "#)) .unwrap(); let s = &wb.sheets[0]; let expected = [ Value::Number(42.5), Value::Bool(true), Value::Number(0.25), Value::Number(19.99), text("literal =1+1"), Value::Number(85.0), text("#DIV/0!"), Value::Empty, text("unavailable"), Value::Bool(false), ]; for (c, value) in expected.iter().enumerate() { assert_eq!(&shown(s, CellRef::new(0, c as u32)), value, "{c}"); } let at = |c| &s.cells[&CellRef::new(0, c)]; assert_eq!((at(3).value_type, at(3).currency.as_deref()), (Some(ValueType::Currency), Some("USD"))); assert_eq!(at(5).formula.as_deref(), Some("of:=A1*2")); assert_eq!((at(7).formula.as_deref(), at(7).value()), (Some("of:=UNKNOWN()"), None)); assert_eq!(at(8).value(), None); assert_eq!(at(9).style.as_deref(), Some("ce1")); } #[test] fn text_keeps_paragraphs_runs_entities_spaces_tabs_and_breaks() { let wb = open(&row( r#"one two & three fourfivesixseven!]]>ninenot cell text"#, )) .unwrap(); assert_eq!(shown(&wb.sheets[0], CellRef::new(0, 0)), text("one two & three four five\tsix\nseven!\nnine")); let wb = open(&row(r#" one two three four five "#)) .unwrap(); assert_eq!(shown(&wb.sheets[0], CellRef::new(0, 0)), text("one two three four \n five")); } #[test] fn repeats_merges_names_and_an_empty_tail_stay_sparse() { let wb = open(&ods(r#" merged "#)) .unwrap(); assert_eq!(wb.sheets.len(), 2); assert_eq!((wb.sheets[0].name.as_str(), wb.sheets[1].name.as_str()), ("First & Second", "'History'")); assert_eq!(wb.sheets[0].cells.len(), 8); assert_eq!(shown(&wb.sheets[0], CellRef::new(0, 1)), Value::Number(99.0)); assert!(wb.sheets[0].cells[&CellRef::new(0, 1)].covered); assert_eq!(wb.sheets[0].merges, vec![Range::new(CellRef::new(0, 0), CellRef::new(1, 1))]); assert_eq!(shown(&wb.sheets[0], CellRef::new(3, 2)), Value::Number(7.0)); assert_eq!(shown(&wb.sheets[1], CellRef::new(0, 0)), text("second sheet")); } #[test] fn one_sheet_can_be_read_alone() { let bytes = ods( r#""#, ); let wb = read(&bytes, &Options { only_sheet: Some(1), ..Options::default() }).unwrap(); assert_eq!(wb.sheets.iter().map(|s| (s.name.as_str(), s.cells.len())).collect::>(), [("A", 0), ("B", 1)]); } #[test] fn dates_and_durations_are_serials_and_unreadable_ones_stay_raw() { let wb = open(&row(r#" "#)) .unwrap(); let s = &wb.sheets[0]; assert_eq!(shown(s, CellRef::new(0, 0)), Value::Number(59.0)); assert_eq!(shown(s, CellRef::new(0, 1)), Value::Number(46302.0 + 66600.125 / 86400.0)); assert_eq!(shown(s, CellRef::new(0, 2)), Value::Number(1.0 + 7384.5 / 86400.0)); assert_eq!(shown(s, CellRef::new(0, 3)), Value::Number(-2.0 / 24.0)); for (c, value) in [(4, "1900-02-29"), (5, "2026-10-07T18:00:00Z"), (6, "P1M"), (7, "PT2M1H")] { assert_eq!(shown(s, CellRef::new(0, c)), text(value)); } } #[test] fn namespaces_are_resolved_not_inferred_from_prefixes() { let xml = content(r#""#) .replace("kept"#, )) .unwrap(); assert_eq!(shown(&wb.sheets[0], CellRef::new(0, 0)), text("kept")); } #[test] fn bad_packages_encryption_and_bad_xml_are_refused() { assert!(matches!(open(b"not zip"), Err(Error::Invalid(_)))); let xml = content(r#""#); let mut w = craft_odf::PackageWriter::new("application/zip"); w.file("content.xml", "text/xml", xml.as_bytes()); assert!(matches!(open(&w.finish().unwrap()), Err(Error::Invalid(_)))); assert!(craft_ods::is_ods(&package(xml.as_bytes()))); let mut w = craft_odf::PackageWriter::new(craft_odf::mime::SPREADSHEET); w.file("content.xml", "text/xml", xml.as_bytes()); if let Some(e) = w.manifest_mut().entries.iter_mut().find(|e| e.path == "content.xml") { e.encryption = Some(craft_odf::Encryption::default()); } assert_eq!(open(&w.finish().unwrap()), Err(Error::Encrypted)); for bad in [ xml.trim_end_matches("").to_owned(), xml.replace("", ""), "".to_owned(), format!("{xml}{xml}"), format!("{xml}"), xml.replace("Data", "&custom;"), content(""), ] { assert!(open(&package(bad.as_bytes())).is_err(), "{bad}"); } let deep = content(&format!("{}{}", "".repeat(300), "".repeat(300))); assert!(open(&package(deep.as_bytes())).is_err()); } #[test] fn expansion_bounds_stop_small_repeat_bombs() { for cells in [ r#""#, r#""#, r#""#, r#""#, r#""#, r#""#, r#""#, r#""#, ] { assert!(open(&row(cells)).is_err(), "{cells}"); } let cells = ods( r#""#, ); assert!(matches!(open(&cells), Err(Error::TooLarge(_)))); let text = format!( r#""#, "x".repeat(1024) ); assert!(matches!(open(&ods(&text)), Err(Error::TooLarge(_)))); // One row of cells each just under the 1 MiB cell limit, past 64 MiB in all: refused while // the row is read. The row's end is never reached. let long = format!(r#""#, "x".repeat(1024 * 1024 - 1)); let cells = package(content(&format!(r#"{}"#, long.repeat(65))).as_bytes()); assert!(matches!(open(&cells), Err(Error::TooLarge(_)))); // A row repeated once more than its cells' text allows is refused, and one that fits reads. let cell = format!(r#""#, "y".repeat(1024)); let rows = |n: u32| { ods(&format!(r#"{cell}"#)) }; assert!(open(&rows(65_536)).is_ok()); assert!(matches!(open(&rows(65_537)), Err(Error::TooLarge(_)))); // An attribute over the cell text limit. let small = Options { max_cell_text: 10, ..Options::default() }; assert!(matches!( read(&row(r#""#), &small), Err(Error::TooLarge(_)) )); let few = Options { max_cells: 3, max_merges: 1, ..Options::default() }; assert!(matches!( read(&row(r#""#), &few), Err(Error::TooLarge(_)) )); assert!(matches!( read(&row(r#""#), &few), Err(Error::TooLarge(_)) )); } #[test] fn an_error_result_never_reads_as_its_placeholder_zero() { let wb = open(&row( r#"#DIV/0!"#, )) .unwrap(); let c = &wb.sheets[0].cells[&CellRef::new(0, 0)]; assert_eq!((c.value_type, c.value(), c.text.as_str()), (Some(ValueType::Error), None, "#DIV/0!")); } #[test] fn blank_repeats_past_the_sheet_edge_are_clipped() { // Writers with larger sheets pad to their own edge; blank padding past ours carries no data. let wb = open(&ods(r#" "#)) .unwrap(); assert_eq!(wb.sheets[0].cells.len(), 1); assert_eq!(shown(&wb.sheets[0], CellRef::new(0, 0)), Value::Number(1.0)); // Data after the clipped edge is an error, never silently dropped. let late = ods( r#""#, ); assert!(matches!(open(&late), Err(Error::TooLarge(_)))); }