1mod container;
24mod error;
25mod json;
26mod model;
27mod parse;
28mod pipeline;
29mod resolve;
30
31pub use container::sanitize::SizeLimits;
32pub use error::{Error, Result};
33pub use json::{to_json_string, to_json_writer};
34pub use model::{
35 Cell, CellRef, CellValue, DateTimeValue, MergedRegion, ResolvedStyle, Sheet, SheetVisibility,
36 StyleId, Workbook,
37};
38
39use std::fs::File;
40use std::io::{Read, Seek};
41use std::path::Path;
42
43pub fn parse_workbook(path: impl AsRef<Path>) -> Result<Workbook> {
47 parse_workbook_with_limits(path, SizeLimits::default())
48}
49
50pub fn parse_workbook_with_limits(path: impl AsRef<Path>, limits: SizeLimits) -> Result<Workbook> {
59 let path = path.as_ref();
60 let file = File::open(path).map_err(|source| Error::Io {
61 path: Some(path.to_path_buf()),
62 source,
63 })?;
64 pipeline::run(file, limits).map_err(|err| fill_io_path(err, path))
65}
66
67fn fill_io_path(err: Error, path: &Path) -> Error {
75 match err {
76 Error::Io { path: None, source } => Error::Io {
77 path: Some(path.to_path_buf()),
78 source,
79 },
80 other => other,
81 }
82}
83
84pub fn parse_workbook_reader<R: Read + Seek>(reader: R) -> Result<Workbook> {
93 parse_workbook_reader_with_limits(reader, SizeLimits::default())
94}
95
96pub fn parse_workbook_reader_with_limits<R: Read + Seek>(
100 reader: R,
101 limits: SizeLimits,
102) -> Result<Workbook> {
103 pipeline::run(reader, limits)
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use std::io::{Cursor, Write};
110
111 fn build_zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
112 let mut buf = Vec::new();
113 {
114 let mut writer = zip::ZipWriter::new(Cursor::new(&mut buf));
115 let options = zip::write::SimpleFileOptions::default()
116 .compression_method(zip::CompressionMethod::Deflated);
117 for (name, data) in entries {
118 writer.start_file(*name, options).unwrap();
119 writer.write_all(data).unwrap();
120 }
121 writer.finish().unwrap();
122 }
123 buf
124 }
125
126 const RELS_XML: &[u8] = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
127 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
128 <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
129</Relationships>"#;
130
131 const WORKBOOK_XML: &[u8] = br#"<?xml version="1.0"?>
132<workbook xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
133 <sheets>
134 <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
135 </sheets>
136</workbook>"#;
137
138 const STYLES_XML: &[u8] = br#"<styleSheet><cellXfs><xf numFmtId="0"/></cellXfs></styleSheet>"#;
139
140 const WORKSHEET_XML: &[u8] =
141 br#"<worksheet><sheetData><row r="1"><c r="A1"><v>42</v></c></row></sheetData></worksheet>"#;
142
143 fn minimal_xlsx() -> Vec<u8> {
144 build_zip(&[
145 ("xl/_rels/workbook.xml.rels", RELS_XML),
146 ("xl/workbook.xml", WORKBOOK_XML),
147 ("xl/styles.xml", STYLES_XML),
148 ("xl/worksheets/sheet1.xml", WORKSHEET_XML),
149 ])
150 }
151
152 #[test]
153 fn parse_workbook_reads_a_valid_file() {
154 let dir = std::env::temp_dir();
155 let path = dir.join(format!(
156 "xlsxparser-test-{}-{}.xlsx",
157 std::process::id(),
158 "parse_workbook_reads_a_valid_file"
159 ));
160 std::fs::write(&path, minimal_xlsx()).unwrap();
161
162 let result = parse_workbook(&path);
163 std::fs::remove_file(&path).ok();
164
165 let workbook = result.unwrap();
166 assert_eq!(workbook.sheets().len(), 1);
167 }
168
169 #[test]
170 fn parse_workbook_missing_file_returns_io_error_with_path() {
171 let path = std::env::temp_dir().join("xlsxparser-test-does-not-exist.xlsx");
172 let err = parse_workbook(&path).unwrap_err();
173 match err {
174 Error::Io { path: Some(p), .. } => assert_eq!(p, path),
175 other => panic!("expected Error::Io {{ path: Some(..), .. }}, got {other:?}"),
176 }
177 }
178
179 #[test]
180 fn fill_io_path_rewrites_none_path_only() {
181 let path = Path::new("book.xlsx");
182
183 let with_none = Error::Io {
184 path: None,
185 source: std::io::Error::other("boom"),
186 };
187 match fill_io_path(with_none, path) {
188 Error::Io { path: Some(p), .. } => assert_eq!(p, path),
189 other => panic!("expected Error::Io {{ path: Some(..), .. }}, got {other:?}"),
190 }
191
192 let with_some = Error::Io {
193 path: Some(std::path::PathBuf::from("already-set.xlsx")),
194 source: std::io::Error::other("boom"),
195 };
196 match fill_io_path(with_some, path) {
197 Error::Io { path: Some(p), .. } => {
198 assert_eq!(p, std::path::PathBuf::from("already-set.xlsx"))
199 }
200 other => panic!("expected Error::Io {{ path: Some(..), .. }}, got {other:?}"),
201 }
202
203 let other_variant = Error::XmlParse {
204 path: "xl/worksheets/sheet1.xml".to_string(),
205 source: Box::new(std::io::Error::other("boom")),
206 };
207 assert!(matches!(
208 fill_io_path(other_variant, path),
209 Error::XmlParse { .. }
210 ));
211 }
212
213 #[test]
214 fn parse_workbook_reader_reads_valid_bytes() {
215 let workbook = parse_workbook_reader(Cursor::new(minimal_xlsx())).unwrap();
216 assert_eq!(workbook.sheets().len(), 1);
217 }
218
219 #[test]
220 fn parse_workbook_and_parse_workbook_reader_agree() {
221 let dir = std::env::temp_dir();
222 let path = dir.join(format!(
223 "xlsxparser-test-{}-{}.xlsx",
224 std::process::id(),
225 "parse_workbook_and_parse_workbook_reader_agree"
226 ));
227 let bytes = minimal_xlsx();
228 std::fs::write(&path, &bytes).unwrap();
229
230 let from_path = parse_workbook(&path);
231 std::fs::remove_file(&path).ok();
232 let from_path = from_path.unwrap();
233 let from_reader = parse_workbook_reader(Cursor::new(bytes)).unwrap();
234
235 assert_eq!(from_path.sheets().len(), from_reader.sheets().len());
236 assert_eq!(from_path.sheets()[0].name, from_reader.sheets()[0].name);
237 assert_eq!(
238 from_path.sheets()[0].get(CellRef { row: 1, col: 1 }),
239 from_reader.sheets()[0].get(CellRef { row: 1, col: 1 })
240 );
241 }
242
243 #[test]
244 fn with_limits_variants_match_the_default_cap_functions() {
245 let dir = std::env::temp_dir();
246 let path = dir.join(format!(
247 "xlsxparser-test-{}-{}.xlsx",
248 std::process::id(),
249 "with_limits_variants_match_the_default_cap_functions"
250 ));
251 let bytes = minimal_xlsx();
252 std::fs::write(&path, &bytes).unwrap();
253
254 let default_from_path = parse_workbook(&path).unwrap();
255 let explicit_from_path = parse_workbook_with_limits(&path, SizeLimits::default()).unwrap();
256 std::fs::remove_file(&path).ok();
257
258 let default_from_reader = parse_workbook_reader(Cursor::new(bytes.clone())).unwrap();
259 let explicit_from_reader =
260 parse_workbook_reader_with_limits(Cursor::new(bytes), SizeLimits::default()).unwrap();
261
262 assert_eq!(
263 default_from_path.sheets()[0].name,
264 explicit_from_path.sheets()[0].name
265 );
266 assert_eq!(
267 default_from_reader.sheets()[0].name,
268 explicit_from_reader.sheets()[0].name
269 );
270 }
271
272 #[test]
273 fn caller_supplied_size_limits_are_honored_by_the_public_api() {
274 parse_workbook_reader(Cursor::new(minimal_xlsx())).unwrap();
276
277 let tiny_limits = SizeLimits {
282 max_entry_size: 1,
283 max_total_size: SizeLimits::default().max_total_size,
284 };
285 let err = parse_workbook_reader_with_limits(Cursor::new(minimal_xlsx()), tiny_limits)
286 .unwrap_err();
287 assert!(matches!(err, Error::ZipBombDetected { .. }));
288 }
289
290 #[test]
291 fn parse_workbook_output_chains_into_to_json_string() {
292 let workbook = parse_workbook_reader(Cursor::new(minimal_xlsx())).unwrap();
293 let json = to_json_string(&workbook).unwrap();
294 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
295 assert_eq!(parsed["sheets"][0]["name"], "Sheet1");
296 }
297
298 #[test]
299 fn corrupt_xlsx_errors_propagate_unchanged() {
300 let err = parse_workbook_reader(Cursor::new(b"not a zip file".to_vec())).unwrap_err();
301 assert!(matches!(err, Error::InvalidPackage(_)));
302
303 let missing_rels = build_zip(&[("xl/workbook.xml", WORKBOOK_XML)]);
304 let err = parse_workbook_reader(Cursor::new(missing_rels)).unwrap_err();
305 assert!(matches!(err, Error::MissingRelationshipPart(_)));
306 }
307
308 #[test]
309 fn public_types_are_reachable_from_the_crate_root() {
310 fn assert_reachable<T>() {}
313 assert_reachable::<crate::Workbook>();
314 assert_reachable::<crate::Sheet>();
315 assert_reachable::<crate::Cell>();
316 assert_reachable::<crate::CellValue>();
317 assert_reachable::<crate::CellRef>();
318 assert_reachable::<crate::SheetVisibility>();
319 assert_reachable::<crate::MergedRegion>();
320 assert_reachable::<crate::ResolvedStyle>();
321 assert_reachable::<crate::StyleId>();
322 assert_reachable::<crate::DateTimeValue>();
323 assert_reachable::<crate::SizeLimits>();
324 assert_reachable::<crate::Error>();
325 fn _assert_result_reachable(_: crate::Result<()>) {}
326 }
327}