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