1use super::*;
2use crate::workbook::open_options::OpenOptions;
3
4const VBA_PROJECT_REL_TYPE: &str =
6 "http://schemas.microsoft.com/office/2006/relationships/vbaProject";
7
8const VBA_PROJECT_CONTENT_TYPE: &str = "application/vnd.ms-office.vbaProject";
10
11impl Workbook {
12 pub fn new() -> Self {
14 let sst_runtime = SharedStringTable::new();
15 let mut sheet_name_index = HashMap::new();
16 sheet_name_index.insert("Sheet1".to_string(), 0);
17 Self {
18 format: WorkbookFormat::default(),
19 content_types: ContentTypes::default(),
20 package_rels: relationships::package_rels(),
21 workbook_xml: WorkbookXml::default(),
22 workbook_rels: relationships::workbook_rels(),
23 worksheets: vec![("Sheet1".to_string(), WorksheetXml::default())],
24 stylesheet: StyleSheet::default(),
25 sst_runtime,
26 sheet_comments: vec![None],
27 charts: vec![],
28 raw_charts: vec![],
29 drawings: vec![],
30 images: vec![],
31 worksheet_drawings: HashMap::new(),
32 worksheet_rels: HashMap::new(),
33 drawing_rels: HashMap::new(),
34 core_properties: None,
35 app_properties: None,
36 custom_properties: None,
37 pivot_tables: vec![],
38 pivot_cache_defs: vec![],
39 pivot_cache_records: vec![],
40 theme_xml: None,
41 theme_colors: crate::theme::default_theme_colors(),
42 sheet_name_index,
43 sheet_sparklines: vec![vec![]],
44 sheet_vml: vec![None],
45 unknown_parts: vec![],
46 vba_blob: None,
47 tables: vec![],
48 raw_sheet_xml: vec![None],
49 slicer_defs: vec![],
50 slicer_caches: vec![],
51 sheet_threaded_comments: vec![None],
52 person_list: sheetkit_xml::threaded_comment::PersonList::default(),
53 sheet_form_controls: vec![vec![]],
54 }
55 }
56
57 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
62 Self::open_with_options(path, &OpenOptions::default())
63 }
64
65 pub fn open_with_options<P: AsRef<Path>>(path: P, options: &OpenOptions) -> Result<Self> {
70 let data = std::fs::read(path.as_ref())?;
71
72 #[cfg(feature = "encryption")]
74 if data.len() >= 8 {
75 if let Ok(crate::crypt::ContainerFormat::Cfb) =
76 crate::crypt::detect_container_format(&data)
77 {
78 return Err(Error::FileEncrypted);
79 }
80 }
81
82 let cursor = std::io::Cursor::new(data);
83 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
84 Self::from_archive(&mut archive, options)
85 }
86
87 fn from_archive<R: std::io::Read + std::io::Seek>(
89 archive: &mut zip::ZipArchive<R>,
90 options: &OpenOptions,
91 ) -> Result<Self> {
92 if let Some(max_entries) = options.max_zip_entries {
94 let count = archive.len();
95 if count > max_entries {
96 return Err(Error::ZipEntryCountExceeded {
97 count,
98 limit: max_entries,
99 });
100 }
101 }
102 if let Some(max_size) = options.max_unzip_size {
103 let mut total_size: u64 = 0;
104 for i in 0..archive.len() {
105 let entry = archive.by_index(i).map_err(|e| Error::Zip(e.to_string()))?;
106 total_size = total_size.saturating_add(entry.size());
107 if total_size > max_size {
108 return Err(Error::ZipSizeExceeded {
109 size: total_size,
110 limit: max_size,
111 });
112 }
113 }
114 }
115
116 let mut known_paths: HashSet<String> = HashSet::new();
119
120 let content_types: ContentTypes = read_xml_part(archive, "[Content_Types].xml")?;
122 known_paths.insert("[Content_Types].xml".to_string());
123
124 let format = content_types
126 .overrides
127 .iter()
128 .find(|o| o.part_name == "/xl/workbook.xml")
129 .and_then(|o| WorkbookFormat::from_content_type(&o.content_type))
130 .unwrap_or_default();
131
132 let package_rels: Relationships = read_xml_part(archive, "_rels/.rels")?;
134 known_paths.insert("_rels/.rels".to_string());
135
136 let workbook_xml: WorkbookXml = read_xml_part(archive, "xl/workbook.xml")?;
138 known_paths.insert("xl/workbook.xml".to_string());
139
140 let workbook_rels: Relationships = read_xml_part(archive, "xl/_rels/workbook.xml.rels")?;
142 known_paths.insert("xl/_rels/workbook.xml.rels".to_string());
143
144 let sheet_count = workbook_xml.sheets.sheets.len();
146 let mut worksheets = Vec::with_capacity(sheet_count);
147 let mut worksheet_paths = Vec::with_capacity(sheet_count);
148 let mut raw_sheet_xml: Vec<Option<Vec<u8>>> = Vec::with_capacity(sheet_count);
149 for sheet_entry in &workbook_xml.sheets.sheets {
150 let rel = workbook_rels
152 .relationships
153 .iter()
154 .find(|r| r.id == sheet_entry.r_id && r.rel_type == rel_types::WORKSHEET);
155
156 let rel = rel.ok_or_else(|| {
157 Error::Internal(format!(
158 "missing worksheet relationship for sheet '{}'",
159 sheet_entry.name
160 ))
161 })?;
162
163 let sheet_path = resolve_relationship_target("xl/workbook.xml", &rel.target);
164
165 if options.should_parse_sheet(&sheet_entry.name) {
166 let mut ws: WorksheetXml = read_xml_part(archive, &sheet_path)?;
167 for row in &mut ws.sheet_data.rows {
168 row.cells.shrink_to_fit();
169 }
170 ws.sheet_data.rows.shrink_to_fit();
171 worksheets.push((sheet_entry.name.clone(), ws));
172 raw_sheet_xml.push(None);
173 } else {
174 let raw_bytes = read_bytes_part(archive, &sheet_path)?;
176 worksheets.push((sheet_entry.name.clone(), WorksheetXml::default()));
177 raw_sheet_xml.push(Some(raw_bytes));
178 };
179 known_paths.insert(sheet_path.clone());
180 worksheet_paths.push(sheet_path);
181 }
182
183 let stylesheet: StyleSheet = read_xml_part(archive, "xl/styles.xml")?;
185 known_paths.insert("xl/styles.xml".to_string());
186
187 let shared_strings: Sst =
189 read_xml_part(archive, "xl/sharedStrings.xml").unwrap_or_default();
190 known_paths.insert("xl/sharedStrings.xml".to_string());
191
192 let sst_runtime = SharedStringTable::from_sst(shared_strings);
193
194 let (theme_xml, theme_colors) = match read_bytes_part(archive, "xl/theme/theme1.xml") {
196 Ok(bytes) => {
197 let colors = sheetkit_xml::theme::parse_theme_colors(&bytes);
198 (Some(bytes), colors)
199 }
200 Err(_) => (None, crate::theme::default_theme_colors()),
201 };
202 known_paths.insert("xl/theme/theme1.xml".to_string());
203
204 let mut worksheet_rels: HashMap<usize, Relationships> = HashMap::with_capacity(sheet_count);
206 for (i, sheet_path) in worksheet_paths.iter().enumerate() {
207 let rels_path = relationship_part_path(sheet_path);
208 if let Ok(rels) = read_xml_part::<Relationships, _>(archive, &rels_path) {
209 worksheet_rels.insert(i, rels);
210 known_paths.insert(rels_path);
211 }
212 }
213
214 let mut sheet_comments: Vec<Option<Comments>> = vec![None; worksheets.len()];
216 let mut sheet_vml: Vec<Option<Vec<u8>>> = vec![None; worksheets.len()];
217 let mut drawings: Vec<(String, WsDr)> = Vec::new();
218 let mut worksheet_drawings: HashMap<usize, usize> = HashMap::new();
219 let mut drawing_path_to_idx: HashMap<String, usize> = HashMap::new();
220
221 for (sheet_idx, sheet_path) in worksheet_paths.iter().enumerate() {
222 let Some(rels) = worksheet_rels.get(&sheet_idx) else {
223 continue;
224 };
225
226 if let Some(comment_rel) = rels
227 .relationships
228 .iter()
229 .find(|r| r.rel_type == rel_types::COMMENTS)
230 {
231 let comment_path = resolve_relationship_target(sheet_path, &comment_rel.target);
232 if let Ok(comments) = read_xml_part::<Comments, _>(archive, &comment_path) {
233 sheet_comments[sheet_idx] = Some(comments);
234 known_paths.insert(comment_path);
235 }
236 }
237
238 if let Some(vml_rel) = rels
239 .relationships
240 .iter()
241 .find(|r| r.rel_type == rel_types::VML_DRAWING)
242 {
243 let vml_path = resolve_relationship_target(sheet_path, &vml_rel.target);
244 if let Ok(bytes) = read_bytes_part(archive, &vml_path) {
245 sheet_vml[sheet_idx] = Some(bytes);
246 known_paths.insert(vml_path);
247 }
248 }
249
250 if let Some(drawing_rel) = rels
251 .relationships
252 .iter()
253 .find(|r| r.rel_type == rel_types::DRAWING)
254 {
255 let drawing_path = resolve_relationship_target(sheet_path, &drawing_rel.target);
256 let drawing_idx = if let Some(idx) = drawing_path_to_idx.get(&drawing_path) {
257 *idx
258 } else if let Ok(drawing) = read_xml_part::<WsDr, _>(archive, &drawing_path) {
259 let idx = drawings.len();
260 drawings.push((drawing_path.clone(), drawing));
261 drawing_path_to_idx.insert(drawing_path.clone(), idx);
262 known_paths.insert(drawing_path);
263 idx
264 } else {
265 continue;
266 };
267 worksheet_drawings.insert(sheet_idx, drawing_idx);
268 }
269 }
270
271 for ovr in &content_types.overrides {
274 if ovr.content_type != mime_types::DRAWING {
275 continue;
276 }
277 let drawing_path = ovr.part_name.trim_start_matches('/').to_string();
278 if drawing_path_to_idx.contains_key(&drawing_path) {
279 continue;
280 }
281 if let Ok(drawing) = read_xml_part::<WsDr, _>(archive, &drawing_path) {
282 let idx = drawings.len();
283 drawings.push((drawing_path.clone(), drawing));
284 known_paths.insert(drawing_path.clone());
285 drawing_path_to_idx.insert(drawing_path, idx);
286 }
287 }
288
289 let mut drawing_rels: HashMap<usize, Relationships> = HashMap::new();
290 let mut charts: Vec<(String, ChartSpace)> = Vec::new();
291 let mut raw_charts: Vec<(String, Vec<u8>)> = Vec::new();
292 let mut images: Vec<(String, Vec<u8>)> = Vec::new();
293 let mut seen_chart_paths: HashSet<String> = HashSet::new();
294 let mut seen_image_paths: HashSet<String> = HashSet::new();
295
296 for (drawing_idx, (drawing_path, _)) in drawings.iter().enumerate() {
297 let drawing_rels_path = relationship_part_path(drawing_path);
298 let Ok(rels) = read_xml_part::<Relationships, _>(archive, &drawing_rels_path) else {
299 continue;
300 };
301 known_paths.insert(drawing_rels_path);
302
303 for rel in &rels.relationships {
304 if rel.rel_type == rel_types::CHART {
305 let chart_path = resolve_relationship_target(drawing_path, &rel.target);
306 if seen_chart_paths.insert(chart_path.clone()) {
307 match read_xml_part::<ChartSpace, _>(archive, &chart_path) {
308 Ok(chart) => {
309 known_paths.insert(chart_path.clone());
310 charts.push((chart_path, chart));
311 }
312 Err(_) => {
313 if let Ok(bytes) = read_bytes_part(archive, &chart_path) {
314 known_paths.insert(chart_path.clone());
315 raw_charts.push((chart_path, bytes));
316 }
317 }
318 }
319 }
320 } else if rel.rel_type == rel_types::IMAGE {
321 let image_path = resolve_relationship_target(drawing_path, &rel.target);
322 if seen_image_paths.insert(image_path.clone()) {
323 if let Ok(bytes) = read_bytes_part(archive, &image_path) {
324 known_paths.insert(image_path.clone());
325 images.push((image_path, bytes));
326 }
327 }
328 }
329 }
330
331 drawing_rels.insert(drawing_idx, rels);
332 }
333
334 for ovr in &content_types.overrides {
337 if ovr.content_type != mime_types::CHART {
338 continue;
339 }
340 let chart_path = ovr.part_name.trim_start_matches('/').to_string();
341 if seen_chart_paths.insert(chart_path.clone()) {
342 match read_xml_part::<ChartSpace, _>(archive, &chart_path) {
343 Ok(chart) => {
344 known_paths.insert(chart_path.clone());
345 charts.push((chart_path, chart));
346 }
347 Err(_) => {
348 if let Ok(bytes) = read_bytes_part(archive, &chart_path) {
349 known_paths.insert(chart_path.clone());
350 raw_charts.push((chart_path, bytes));
351 }
352 }
353 }
354 }
355 }
356
357 let core_properties = read_string_part(archive, "docProps/core.xml")
359 .ok()
360 .and_then(|xml_str| {
361 sheetkit_xml::doc_props::deserialize_core_properties(&xml_str).ok()
362 });
363 known_paths.insert("docProps/core.xml".to_string());
364
365 let app_properties: Option<sheetkit_xml::doc_props::ExtendedProperties> =
367 read_xml_part(archive, "docProps/app.xml").ok();
368 known_paths.insert("docProps/app.xml".to_string());
369
370 let custom_properties = read_string_part(archive, "docProps/custom.xml")
372 .ok()
373 .and_then(|xml_str| {
374 sheetkit_xml::doc_props::deserialize_custom_properties(&xml_str).ok()
375 });
376 known_paths.insert("docProps/custom.xml".to_string());
377
378 let mut pivot_cache_defs = Vec::new();
380 let mut pivot_tables = Vec::new();
381 let mut pivot_cache_records = Vec::new();
382 for ovr in &content_types.overrides {
383 let path = ovr.part_name.trim_start_matches('/');
384 if ovr.content_type == mime_types::PIVOT_CACHE_DEFINITION {
385 if let Ok(pcd) = read_xml_part::<sheetkit_xml::pivot_cache::PivotCacheDefinition, _>(
386 archive, path,
387 ) {
388 known_paths.insert(path.to_string());
389 pivot_cache_defs.push((path.to_string(), pcd));
390 }
391 } else if ovr.content_type == mime_types::PIVOT_TABLE {
392 if let Ok(pt) = read_xml_part::<sheetkit_xml::pivot_table::PivotTableDefinition, _>(
393 archive, path,
394 ) {
395 known_paths.insert(path.to_string());
396 pivot_tables.push((path.to_string(), pt));
397 }
398 } else if ovr.content_type == mime_types::PIVOT_CACHE_RECORDS {
399 if let Ok(pcr) =
400 read_xml_part::<sheetkit_xml::pivot_cache::PivotCacheRecords, _>(archive, path)
401 {
402 known_paths.insert(path.to_string());
403 pivot_cache_records.push((path.to_string(), pcr));
404 }
405 }
406 }
407
408 let mut slicer_defs = Vec::new();
410 let mut slicer_caches = Vec::new();
411 for ovr in &content_types.overrides {
412 let path = ovr.part_name.trim_start_matches('/');
413 if ovr.content_type == mime_types::SLICER {
414 if let Ok(sd) =
415 read_xml_part::<sheetkit_xml::slicer::SlicerDefinitions, _>(archive, path)
416 {
417 slicer_defs.push((path.to_string(), sd));
418 }
419 } else if ovr.content_type == mime_types::SLICER_CACHE {
420 if let Ok(raw) = read_string_part(archive, path) {
421 if let Some(scd) = sheetkit_xml::slicer::parse_slicer_cache(&raw) {
422 slicer_caches.push((path.to_string(), scd));
423 }
424 }
425 }
426 }
427
428 let mut sheet_threaded_comments: Vec<
430 Option<sheetkit_xml::threaded_comment::ThreadedComments>,
431 > = vec![None; worksheets.len()];
432 for (sheet_idx, sheet_path) in worksheet_paths.iter().enumerate() {
433 let Some(rels) = worksheet_rels.get(&sheet_idx) else {
434 continue;
435 };
436 if let Some(tc_rel) = rels
437 .relationships
438 .iter()
439 .find(|r| r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_THREADED_COMMENT)
440 {
441 let tc_path = resolve_relationship_target(sheet_path, &tc_rel.target);
442 if let Ok(tc) = read_xml_part::<sheetkit_xml::threaded_comment::ThreadedComments, _>(
443 archive, &tc_path,
444 ) {
445 sheet_threaded_comments[sheet_idx] = Some(tc);
446 known_paths.insert(tc_path);
447 }
448 }
449 }
450
451 let person_list: sheetkit_xml::threaded_comment::PersonList = {
453 let mut found = None;
454 if let Some(person_rel) = workbook_rels
456 .relationships
457 .iter()
458 .find(|r| r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_PERSON)
459 {
460 let person_path =
461 resolve_relationship_target("xl/workbook.xml", &person_rel.target);
462 if let Ok(pl) = read_xml_part::<sheetkit_xml::threaded_comment::PersonList, _>(
463 archive,
464 &person_path,
465 ) {
466 known_paths.insert(person_path);
467 found = Some(pl);
468 }
469 }
470 if found.is_none() {
472 if let Ok(pl) = read_xml_part::<sheetkit_xml::threaded_comment::PersonList, _>(
473 archive,
474 "xl/persons/person.xml",
475 ) {
476 known_paths.insert("xl/persons/person.xml".to_string());
477 found = Some(pl);
478 }
479 }
480 found.unwrap_or_default()
481 };
482
483 let mut sheet_sparklines: Vec<Vec<crate::sparkline::SparklineConfig>> =
485 vec![vec![]; worksheets.len()];
486 for (i, ws_path) in worksheet_paths.iter().enumerate() {
487 if let Ok(raw) = read_string_part(archive, ws_path) {
488 let parsed = parse_sparklines_from_xml(&raw);
489 if !parsed.is_empty() {
490 sheet_sparklines[i] = parsed;
491 }
492 }
493 }
494
495 let vba_blob = read_bytes_part(archive, "xl/vbaProject.bin").ok();
497 if vba_blob.is_some() {
498 known_paths.insert("xl/vbaProject.bin".to_string());
499 }
500
501 let mut tables: Vec<(String, sheetkit_xml::table::TableXml, usize)> = Vec::new();
503 for (sheet_idx, sheet_path) in worksheet_paths.iter().enumerate() {
504 let Some(rels) = worksheet_rels.get(&sheet_idx) else {
505 continue;
506 };
507 for rel in &rels.relationships {
508 if rel.rel_type != rel_types::TABLE {
509 continue;
510 }
511 let table_path = resolve_relationship_target(sheet_path, &rel.target);
512 if let Ok(table_xml) =
513 read_xml_part::<sheetkit_xml::table::TableXml, _>(archive, &table_path)
514 {
515 known_paths.insert(table_path.clone());
516 tables.push((table_path, table_xml, sheet_idx));
517 }
518 }
519 }
520 for ovr in &content_types.overrides {
522 if ovr.content_type != mime_types::TABLE {
523 continue;
524 }
525 let table_path = ovr.part_name.trim_start_matches('/').to_string();
526 if tables.iter().any(|(p, _, _)| p == &table_path) {
527 continue;
528 }
529 if let Ok(table_xml) =
530 read_xml_part::<sheetkit_xml::table::TableXml, _>(archive, &table_path)
531 {
532 known_paths.insert(table_path.clone());
533 tables.push((table_path, table_xml, 0));
534 }
535 }
536
537 let sheet_form_controls: Vec<Vec<crate::control::FormControlConfig>> =
539 vec![vec![]; worksheets.len()];
540
541 let mut sheet_name_index = HashMap::with_capacity(worksheets.len());
543 for (i, (name, _)) in worksheets.iter().enumerate() {
544 sheet_name_index.insert(name.clone(), i);
545 }
546
547 let mut unknown_parts: Vec<(String, Vec<u8>)> = Vec::new();
549 for i in 0..archive.len() {
550 let Ok(entry) = archive.by_index(i) else {
551 continue;
552 };
553 let name = entry.name().to_string();
554 drop(entry);
555 if !known_paths.contains(&name) {
556 if let Ok(bytes) = read_bytes_part(archive, &name) {
557 unknown_parts.push((name, bytes));
558 }
559 }
560 }
561
562 for (_name, ws) in &mut worksheets {
565 ws.sheet_data.rows.sort_unstable_by_key(|r| r.r);
567
568 if let Some(max_rows) = options.sheet_rows {
570 ws.sheet_data.rows.truncate(max_rows as usize);
571 }
572
573 for row in &mut ws.sheet_data.rows {
574 for cell in &mut row.cells {
575 cell.col = fast_col_number(cell.r.as_str());
576 }
577 row.cells.sort_unstable_by_key(|c| c.col);
579 }
580 }
581
582 Ok(Self {
583 format,
584 content_types,
585 package_rels,
586 workbook_xml,
587 workbook_rels,
588 worksheets,
589 stylesheet,
590 sst_runtime,
591 sheet_comments,
592 charts,
593 raw_charts,
594 drawings,
595 images,
596 worksheet_drawings,
597 worksheet_rels,
598 drawing_rels,
599 core_properties,
600 app_properties,
601 custom_properties,
602 pivot_tables,
603 pivot_cache_defs,
604 pivot_cache_records,
605 theme_xml,
606 theme_colors,
607 sheet_name_index,
608 sheet_sparklines,
609 sheet_vml,
610 unknown_parts,
611 vba_blob,
612 tables,
613 raw_sheet_xml,
614 slicer_defs,
615 slicer_caches,
616 sheet_threaded_comments,
617 person_list,
618 sheet_form_controls,
619 })
620 }
621
622 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
631 let path = path.as_ref();
632 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
633 let target_format = WorkbookFormat::from_extension(ext)
634 .ok_or_else(|| Error::UnsupportedFileExtension(ext.to_string()))?;
635
636 let file = std::fs::File::create(path)?;
637 let mut zip = zip::ZipWriter::new(file);
638 let options = SimpleFileOptions::default()
639 .compression_method(CompressionMethod::Deflated)
640 .compression_level(Some(1));
641 self.write_zip_contents(&mut zip, options, Some(target_format))?;
642 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
643 Ok(())
644 }
645
646 pub fn save_to_buffer(&self) -> Result<Vec<u8>> {
648 let estimated = self.worksheets.len() * 4000
650 + self.sst_runtime.len() * 60
651 + self.images.iter().map(|(_, d)| d.len()).sum::<usize>()
652 + 32_000;
653 let mut buf = Vec::with_capacity(estimated);
654 {
655 let cursor = std::io::Cursor::new(&mut buf);
656 let mut zip = zip::ZipWriter::new(cursor);
657 let options =
658 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
659 self.write_zip_contents(&mut zip, options, None)?;
660 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
661 }
662 Ok(buf)
663 }
664
665 pub fn open_from_buffer(data: &[u8]) -> Result<Self> {
667 Self::open_from_buffer_with_options(data, &OpenOptions::default())
668 }
669
670 pub fn open_from_buffer_with_options(data: &[u8], options: &OpenOptions) -> Result<Self> {
672 #[cfg(feature = "encryption")]
674 if data.len() >= 8 {
675 if let Ok(crate::crypt::ContainerFormat::Cfb) =
676 crate::crypt::detect_container_format(data)
677 {
678 return Err(Error::FileEncrypted);
679 }
680 }
681
682 let cursor = std::io::Cursor::new(data);
683 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
684 Self::from_archive(&mut archive, options)
685 }
686
687 #[cfg(feature = "encryption")]
693 pub fn open_with_password<P: AsRef<Path>>(path: P, password: &str) -> Result<Self> {
694 let data = std::fs::read(path.as_ref())?;
695 let decrypted_zip = crate::crypt::decrypt_xlsx(&data, password)?;
696 let cursor = std::io::Cursor::new(decrypted_zip);
697 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
698 Self::from_archive(&mut archive, &OpenOptions::default())
699 }
700
701 #[cfg(feature = "encryption")]
704 pub fn save_with_password<P: AsRef<Path>>(&self, path: P, password: &str) -> Result<()> {
705 let mut zip_buf = Vec::new();
707 {
708 let cursor = std::io::Cursor::new(&mut zip_buf);
709 let mut zip = zip::ZipWriter::new(cursor);
710 let options =
711 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
712 self.write_zip_contents(&mut zip, options, None)?;
713 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
714 }
715
716 let cfb_data = crate::crypt::encrypt_xlsx(&zip_buf, password)?;
718 std::fs::write(path.as_ref(), &cfb_data)?;
719 Ok(())
720 }
721
722 fn write_zip_contents<W: std::io::Write + std::io::Seek>(
728 &self,
729 zip: &mut zip::ZipWriter<W>,
730 options: SimpleFileOptions,
731 format_override: Option<WorkbookFormat>,
732 ) -> Result<()> {
733 let effective_format = format_override.unwrap_or(self.format);
734 let mut content_types = self.content_types.clone();
735
736 if let Some(wb_override) = content_types
738 .overrides
739 .iter_mut()
740 .find(|o| o.part_name == "/xl/workbook.xml")
741 {
742 wb_override.content_type = effective_format.content_type().to_string();
743 }
744
745 let mut workbook_rels = self.workbook_rels.clone();
748 if self.vba_blob.is_some() {
749 let vba_part_name = "/xl/vbaProject.bin";
750 if !content_types
751 .overrides
752 .iter()
753 .any(|o| o.part_name == vba_part_name)
754 {
755 content_types.overrides.push(ContentTypeOverride {
756 part_name: vba_part_name.to_string(),
757 content_type: VBA_PROJECT_CONTENT_TYPE.to_string(),
758 });
759 }
760 if !content_types.defaults.iter().any(|d| d.extension == "bin") {
761 content_types.defaults.push(ContentTypeDefault {
762 extension: "bin".to_string(),
763 content_type: VBA_PROJECT_CONTENT_TYPE.to_string(),
764 });
765 }
766 if !workbook_rels
767 .relationships
768 .iter()
769 .any(|r| r.rel_type == VBA_PROJECT_REL_TYPE)
770 {
771 let rid = crate::sheet::next_rid(&workbook_rels.relationships);
772 workbook_rels.relationships.push(Relationship {
773 id: rid,
774 rel_type: VBA_PROJECT_REL_TYPE.to_string(),
775 target: "vbaProject.bin".to_string(),
776 target_mode: None,
777 });
778 }
779 } else {
780 content_types
781 .overrides
782 .retain(|o| o.content_type != VBA_PROJECT_CONTENT_TYPE);
783 workbook_rels
784 .relationships
785 .retain(|r| r.rel_type != VBA_PROJECT_REL_TYPE);
786 }
787
788 let mut worksheet_rels = self.worksheet_rels.clone();
789
790 let mut vml_parts_to_write: Vec<(usize, String, Vec<u8>)> = Vec::new();
793 let mut legacy_drawing_rids: HashMap<usize, String> = HashMap::new();
795
796 let mut has_any_vml = false;
798
799 for sheet_idx in 0..self.worksheets.len() {
800 let has_comments = self
801 .sheet_comments
802 .get(sheet_idx)
803 .and_then(|c| c.as_ref())
804 .is_some();
805 let has_form_controls = self
806 .sheet_form_controls
807 .get(sheet_idx)
808 .map(|v| !v.is_empty())
809 .unwrap_or(false);
810 let has_preserved_vml = self
811 .sheet_vml
812 .get(sheet_idx)
813 .and_then(|v| v.as_ref())
814 .is_some();
815
816 if let Some(rels) = worksheet_rels.get_mut(&sheet_idx) {
817 rels.relationships
818 .retain(|r| r.rel_type != rel_types::COMMENTS);
819 rels.relationships
820 .retain(|r| r.rel_type != rel_types::VML_DRAWING);
821 }
822
823 let needs_vml = has_comments || has_form_controls || has_preserved_vml;
824 if !needs_vml && !has_comments {
825 continue;
826 }
827
828 if has_comments {
829 let comment_path = format!("xl/comments{}.xml", sheet_idx + 1);
830 let part_name = format!("/{}", comment_path);
831 if !content_types
832 .overrides
833 .iter()
834 .any(|o| o.part_name == part_name && o.content_type == mime_types::COMMENTS)
835 {
836 content_types.overrides.push(ContentTypeOverride {
837 part_name,
838 content_type: mime_types::COMMENTS.to_string(),
839 });
840 }
841
842 let sheet_path = self.sheet_part_path(sheet_idx);
843 let target = relative_relationship_target(&sheet_path, &comment_path);
844 let rels = worksheet_rels
845 .entry(sheet_idx)
846 .or_insert_with(default_relationships);
847 let rid = crate::sheet::next_rid(&rels.relationships);
848 rels.relationships.push(Relationship {
849 id: rid,
850 rel_type: rel_types::COMMENTS.to_string(),
851 target,
852 target_mode: None,
853 });
854 }
855
856 if !needs_vml {
857 continue;
858 }
859
860 let vml_path = format!("xl/drawings/vmlDrawing{}.vml", sheet_idx + 1);
862 let vml_bytes = if has_comments && has_form_controls {
863 let comment_vml =
865 if let Some(bytes) = self.sheet_vml.get(sheet_idx).and_then(|v| v.as_ref()) {
866 bytes.clone()
867 } else if let Some(Some(comments)) = self.sheet_comments.get(sheet_idx) {
868 let cells: Vec<&str> = comments
869 .comment_list
870 .comments
871 .iter()
872 .map(|c| c.r#ref.as_str())
873 .collect();
874 crate::vml::build_vml_drawing(&cells).into_bytes()
875 } else {
876 continue;
877 };
878 let shape_count = crate::control::count_vml_shapes(&comment_vml);
879 let start_id = 1025 + shape_count;
880 let form_controls = &self.sheet_form_controls[sheet_idx];
881 crate::control::merge_vml_controls(&comment_vml, form_controls, start_id)
882 } else if has_comments {
883 if let Some(bytes) = self.sheet_vml.get(sheet_idx).and_then(|v| v.as_ref()) {
884 bytes.clone()
885 } else if let Some(Some(comments)) = self.sheet_comments.get(sheet_idx) {
886 let cells: Vec<&str> = comments
887 .comment_list
888 .comments
889 .iter()
890 .map(|c| c.r#ref.as_str())
891 .collect();
892 crate::vml::build_vml_drawing(&cells).into_bytes()
893 } else {
894 continue;
895 }
896 } else if has_form_controls {
897 let form_controls = &self.sheet_form_controls[sheet_idx];
899 crate::control::build_form_control_vml(form_controls, 1025).into_bytes()
900 } else if let Some(Some(vml)) = self.sheet_vml.get(sheet_idx) {
901 vml.clone()
903 } else {
904 continue;
905 };
906
907 let vml_part_name = format!("/{}", vml_path);
908 if !content_types
909 .overrides
910 .iter()
911 .any(|o| o.part_name == vml_part_name && o.content_type == mime_types::VML_DRAWING)
912 {
913 content_types.overrides.push(ContentTypeOverride {
914 part_name: vml_part_name,
915 content_type: mime_types::VML_DRAWING.to_string(),
916 });
917 }
918
919 let sheet_path = self.sheet_part_path(sheet_idx);
920 let rels = worksheet_rels
921 .entry(sheet_idx)
922 .or_insert_with(default_relationships);
923 let vml_target = relative_relationship_target(&sheet_path, &vml_path);
924 let vml_rid = crate::sheet::next_rid(&rels.relationships);
925 rels.relationships.push(Relationship {
926 id: vml_rid.clone(),
927 rel_type: rel_types::VML_DRAWING.to_string(),
928 target: vml_target,
929 target_mode: None,
930 });
931
932 legacy_drawing_rids.insert(sheet_idx, vml_rid);
933 vml_parts_to_write.push((sheet_idx, vml_path, vml_bytes));
934 has_any_vml = true;
935 }
936
937 if has_any_vml && !content_types.defaults.iter().any(|d| d.extension == "vml") {
939 content_types.defaults.push(ContentTypeDefault {
940 extension: "vml".to_string(),
941 content_type: mime_types::VML_DRAWING.to_string(),
942 });
943 }
944
945 let mut table_parts_by_sheet: HashMap<usize, Vec<String>> = HashMap::new();
948 for (sheet_idx, _) in self.worksheets.iter().enumerate() {
949 if let Some(rels) = worksheet_rels.get_mut(&sheet_idx) {
950 rels.relationships
951 .retain(|r| r.rel_type != rel_types::TABLE);
952 }
953 }
954 content_types
955 .overrides
956 .retain(|o| o.content_type != mime_types::TABLE);
957 for (table_path, _table_xml, sheet_idx) in &self.tables {
958 let part_name = format!("/{table_path}");
959 content_types.overrides.push(ContentTypeOverride {
960 part_name,
961 content_type: mime_types::TABLE.to_string(),
962 });
963
964 let sheet_path = self.sheet_part_path(*sheet_idx);
965 let target = relative_relationship_target(&sheet_path, table_path);
966 let rels = worksheet_rels
967 .entry(*sheet_idx)
968 .or_insert_with(default_relationships);
969 let rid = crate::sheet::next_rid(&rels.relationships);
970 rels.relationships.push(Relationship {
971 id: rid.clone(),
972 rel_type: rel_types::TABLE.to_string(),
973 target,
974 target_mode: None,
975 });
976 table_parts_by_sheet
977 .entry(*sheet_idx)
978 .or_default()
979 .push(rid);
980 }
981
982 let has_any_threaded = self.sheet_threaded_comments.iter().any(|tc| tc.is_some());
984 if has_any_threaded {
985 for (i, tc) in self.sheet_threaded_comments.iter().enumerate() {
986 if tc.is_some() {
987 let tc_path = format!("xl/threadedComments/threadedComment{}.xml", i + 1);
988 let tc_part_name = format!("/{tc_path}");
989 if !content_types.overrides.iter().any(|o| {
990 o.part_name == tc_part_name
991 && o.content_type
992 == sheetkit_xml::threaded_comment::THREADED_COMMENTS_CONTENT_TYPE
993 }) {
994 content_types.overrides.push(ContentTypeOverride {
995 part_name: tc_part_name,
996 content_type:
997 sheetkit_xml::threaded_comment::THREADED_COMMENTS_CONTENT_TYPE
998 .to_string(),
999 });
1000 }
1001
1002 let sheet_path = self.sheet_part_path(i);
1003 let target = relative_relationship_target(&sheet_path, &tc_path);
1004 let rels = worksheet_rels
1005 .entry(i)
1006 .or_insert_with(default_relationships);
1007 if !rels.relationships.iter().any(|r| {
1008 r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_THREADED_COMMENT
1009 }) {
1010 let rid = crate::sheet::next_rid(&rels.relationships);
1011 rels.relationships.push(Relationship {
1012 id: rid,
1013 rel_type: sheetkit_xml::threaded_comment::REL_TYPE_THREADED_COMMENT
1014 .to_string(),
1015 target,
1016 target_mode: None,
1017 });
1018 }
1019 }
1020 }
1021
1022 let person_part_name = "/xl/persons/person.xml";
1023 if !content_types.overrides.iter().any(|o| {
1024 o.part_name == person_part_name
1025 && o.content_type == sheetkit_xml::threaded_comment::PERSON_LIST_CONTENT_TYPE
1026 }) {
1027 content_types.overrides.push(ContentTypeOverride {
1028 part_name: person_part_name.to_string(),
1029 content_type: sheetkit_xml::threaded_comment::PERSON_LIST_CONTENT_TYPE
1030 .to_string(),
1031 });
1032 }
1033
1034 if !workbook_rels
1036 .relationships
1037 .iter()
1038 .any(|r| r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_PERSON)
1039 {
1040 let rid = crate::sheet::next_rid(&workbook_rels.relationships);
1041 workbook_rels.relationships.push(Relationship {
1042 id: rid,
1043 rel_type: sheetkit_xml::threaded_comment::REL_TYPE_PERSON.to_string(),
1044 target: "persons/person.xml".to_string(),
1045 target_mode: None,
1046 });
1047 }
1048 }
1049
1050 write_xml_part(zip, "[Content_Types].xml", &content_types, options)?;
1052
1053 write_xml_part(zip, "_rels/.rels", &self.package_rels, options)?;
1055
1056 write_xml_part(zip, "xl/workbook.xml", &self.workbook_xml, options)?;
1058
1059 write_xml_part(zip, "xl/_rels/workbook.xml.rels", &workbook_rels, options)?;
1061
1062 for (i, (_name, ws)) in self.worksheets.iter().enumerate() {
1064 let entry_name = self.sheet_part_path(i);
1065
1066 if let Some(Some(raw_bytes)) = self.raw_sheet_xml.get(i) {
1068 zip.start_file(&entry_name, options)
1069 .map_err(|e| Error::Zip(e.to_string()))?;
1070 zip.write_all(raw_bytes)?;
1071 continue;
1072 }
1073
1074 let empty_sparklines: Vec<crate::sparkline::SparklineConfig> = vec![];
1075 let sparklines = self.sheet_sparklines.get(i).unwrap_or(&empty_sparklines);
1076 let legacy_rid = legacy_drawing_rids.get(&i).map(|s| s.as_str());
1077 let sheet_table_rids = table_parts_by_sheet.get(&i);
1078 let stale_table_parts = sheet_table_rids.is_none() && ws.table_parts.is_some();
1079 let has_extras = legacy_rid.is_some()
1080 || !sparklines.is_empty()
1081 || sheet_table_rids.is_some()
1082 || stale_table_parts;
1083
1084 if !has_extras {
1085 write_xml_part(zip, &entry_name, ws, options)?;
1086 } else {
1087 let ws_to_serialize;
1088 let ws_ref = if let Some(rids) = sheet_table_rids {
1089 ws_to_serialize = {
1090 let mut cloned = ws.clone();
1091 use sheetkit_xml::worksheet::{TablePart, TableParts};
1092 cloned.table_parts = Some(TableParts {
1093 count: Some(rids.len() as u32),
1094 table_parts: rids
1095 .iter()
1096 .map(|rid| TablePart { r_id: rid.clone() })
1097 .collect(),
1098 });
1099 cloned
1100 };
1101 &ws_to_serialize
1102 } else if stale_table_parts {
1103 ws_to_serialize = {
1104 let mut cloned = ws.clone();
1105 cloned.table_parts = None;
1106 cloned
1107 };
1108 &ws_to_serialize
1109 } else {
1110 ws
1111 };
1112 let xml = serialize_worksheet_with_extras(ws_ref, sparklines, legacy_rid)?;
1113 zip.start_file(&entry_name, options)
1114 .map_err(|e| Error::Zip(e.to_string()))?;
1115 zip.write_all(xml.as_bytes())?;
1116 }
1117 }
1118
1119 write_xml_part(zip, "xl/styles.xml", &self.stylesheet, options)?;
1121
1122 let sst_xml = self.sst_runtime.to_sst();
1124 write_xml_part(zip, "xl/sharedStrings.xml", &sst_xml, options)?;
1125
1126 for (i, comments) in self.sheet_comments.iter().enumerate() {
1128 if let Some(ref c) = comments {
1129 let entry_name = format!("xl/comments{}.xml", i + 1);
1130 write_xml_part(zip, &entry_name, c, options)?;
1131 }
1132 }
1133
1134 for (_sheet_idx, vml_path, vml_bytes) in &vml_parts_to_write {
1136 zip.start_file(vml_path, options)
1137 .map_err(|e| Error::Zip(e.to_string()))?;
1138 zip.write_all(vml_bytes)?;
1139 }
1140
1141 for (path, drawing) in &self.drawings {
1143 write_xml_part(zip, path, drawing, options)?;
1144 }
1145
1146 for (path, chart) in &self.charts {
1148 write_xml_part(zip, path, chart, options)?;
1149 }
1150 for (path, data) in &self.raw_charts {
1151 if self.charts.iter().any(|(p, _)| p == path) {
1152 continue;
1153 }
1154 zip.start_file(path, options)
1155 .map_err(|e| Error::Zip(e.to_string()))?;
1156 zip.write_all(data)?;
1157 }
1158
1159 for (path, data) in &self.images {
1161 zip.start_file(path, options)
1162 .map_err(|e| Error::Zip(e.to_string()))?;
1163 zip.write_all(data)?;
1164 }
1165
1166 for (sheet_idx, rels) in &worksheet_rels {
1168 let sheet_path = self.sheet_part_path(*sheet_idx);
1169 let path = relationship_part_path(&sheet_path);
1170 write_xml_part(zip, &path, rels, options)?;
1171 }
1172
1173 for (drawing_idx, rels) in &self.drawing_rels {
1175 if let Some((drawing_path, _)) = self.drawings.get(*drawing_idx) {
1176 let path = relationship_part_path(drawing_path);
1177 write_xml_part(zip, &path, rels, options)?;
1178 }
1179 }
1180
1181 for (path, pt) in &self.pivot_tables {
1183 write_xml_part(zip, path, pt, options)?;
1184 }
1185
1186 for (path, pcd) in &self.pivot_cache_defs {
1188 write_xml_part(zip, path, pcd, options)?;
1189 }
1190
1191 for (path, pcr) in &self.pivot_cache_records {
1193 write_xml_part(zip, path, pcr, options)?;
1194 }
1195
1196 for (path, table_xml, _sheet_idx) in &self.tables {
1198 write_xml_part(zip, path, table_xml, options)?;
1199 }
1200
1201 for (path, sd) in &self.slicer_defs {
1203 write_xml_part(zip, path, sd, options)?;
1204 }
1205
1206 for (path, scd) in &self.slicer_caches {
1208 let xml_str = format!(
1209 "{}\n{}",
1210 XML_DECLARATION,
1211 sheetkit_xml::slicer::serialize_slicer_cache(scd),
1212 );
1213 zip.start_file(path, options)
1214 .map_err(|e| Error::Zip(e.to_string()))?;
1215 zip.write_all(xml_str.as_bytes())?;
1216 }
1217
1218 {
1220 let default_theme = crate::theme::default_theme_xml();
1221 let theme_bytes = self.theme_xml.as_deref().unwrap_or(&default_theme);
1222 zip.start_file("xl/theme/theme1.xml", options)
1223 .map_err(|e| Error::Zip(e.to_string()))?;
1224 zip.write_all(theme_bytes)?;
1225 }
1226
1227 if let Some(ref blob) = self.vba_blob {
1229 zip.start_file("xl/vbaProject.bin", options)
1230 .map_err(|e| Error::Zip(e.to_string()))?;
1231 zip.write_all(blob)?;
1232 }
1233
1234 if let Some(ref props) = self.core_properties {
1236 let xml_str = sheetkit_xml::doc_props::serialize_core_properties(props);
1237 zip.start_file("docProps/core.xml", options)
1238 .map_err(|e| Error::Zip(e.to_string()))?;
1239 zip.write_all(xml_str.as_bytes())?;
1240 }
1241
1242 if let Some(ref props) = self.app_properties {
1244 write_xml_part(zip, "docProps/app.xml", props, options)?;
1245 }
1246
1247 if let Some(ref props) = self.custom_properties {
1249 let xml_str = sheetkit_xml::doc_props::serialize_custom_properties(props);
1250 zip.start_file("docProps/custom.xml", options)
1251 .map_err(|e| Error::Zip(e.to_string()))?;
1252 zip.write_all(xml_str.as_bytes())?;
1253 }
1254
1255 if has_any_threaded {
1257 for (i, tc) in self.sheet_threaded_comments.iter().enumerate() {
1258 if let Some(ref tc_data) = tc {
1259 let tc_path = format!("xl/threadedComments/threadedComment{}.xml", i + 1);
1260 write_xml_part(zip, &tc_path, tc_data, options)?;
1261 }
1262 }
1263 write_xml_part(zip, "xl/persons/person.xml", &self.person_list, options)?;
1264 }
1265
1266 for (path, data) in &self.unknown_parts {
1268 zip.start_file(path, options)
1269 .map_err(|e| Error::Zip(e.to_string()))?;
1270 zip.write_all(data)?;
1271 }
1272
1273 Ok(())
1274 }
1275}
1276
1277impl Default for Workbook {
1278 fn default() -> Self {
1279 Self::new()
1280 }
1281}
1282
1283pub(crate) fn serialize_xml<T: Serialize>(value: &T) -> Result<String> {
1285 let body = quick_xml::se::to_string(value).map_err(|e| Error::XmlParse(e.to_string()))?;
1286 let mut result = String::with_capacity(XML_DECLARATION.len() + 1 + body.len());
1287 result.push_str(XML_DECLARATION);
1288 result.push('\n');
1289 result.push_str(&body);
1290 Ok(result)
1291}
1292
1293pub(crate) fn read_xml_part<T: serde::de::DeserializeOwned, R: std::io::Read + std::io::Seek>(
1295 archive: &mut zip::ZipArchive<R>,
1296 name: &str,
1297) -> Result<T> {
1298 let mut entry = archive
1299 .by_name(name)
1300 .map_err(|e| Error::Zip(e.to_string()))?;
1301 let size_hint = entry.size() as usize;
1302 let mut content = String::with_capacity(size_hint);
1303 entry
1304 .read_to_string(&mut content)
1305 .map_err(|e| Error::Zip(e.to_string()))?;
1306 quick_xml::de::from_str(&content).map_err(|e| Error::XmlDeserialize(e.to_string()))
1307}
1308
1309pub(crate) fn read_string_part<R: std::io::Read + std::io::Seek>(
1311 archive: &mut zip::ZipArchive<R>,
1312 name: &str,
1313) -> Result<String> {
1314 let mut entry = archive
1315 .by_name(name)
1316 .map_err(|e| Error::Zip(e.to_string()))?;
1317 let size_hint = entry.size() as usize;
1318 let mut content = String::with_capacity(size_hint);
1319 entry
1320 .read_to_string(&mut content)
1321 .map_err(|e| Error::Zip(e.to_string()))?;
1322 Ok(content)
1323}
1324
1325pub(crate) fn read_bytes_part<R: std::io::Read + std::io::Seek>(
1327 archive: &mut zip::ZipArchive<R>,
1328 name: &str,
1329) -> Result<Vec<u8>> {
1330 let mut entry = archive
1331 .by_name(name)
1332 .map_err(|e| Error::Zip(e.to_string()))?;
1333 let size_hint = entry.size() as usize;
1334 let mut content = Vec::with_capacity(size_hint);
1335 entry
1336 .read_to_end(&mut content)
1337 .map_err(|e| Error::Zip(e.to_string()))?;
1338 Ok(content)
1339}
1340
1341pub(crate) fn serialize_worksheet_with_extras(
1344 ws: &WorksheetXml,
1345 sparklines: &[crate::sparkline::SparklineConfig],
1346 legacy_drawing_rid: Option<&str>,
1347) -> Result<String> {
1348 let body = quick_xml::se::to_string(ws).map_err(|e| Error::XmlParse(e.to_string()))?;
1349
1350 let closing = "</worksheet>";
1351 let ext_xml = if sparklines.is_empty() {
1352 String::new()
1353 } else {
1354 build_sparkline_ext_xml(sparklines)
1355 };
1356 let legacy_xml = if let Some(rid) = legacy_drawing_rid {
1357 format!("<legacyDrawing r:id=\"{rid}\"/>")
1358 } else {
1359 String::new()
1360 };
1361
1362 if let Some(pos) = body.rfind(closing) {
1363 let body_prefix = &body[..pos];
1366 let stripped;
1367 let prefix = if !legacy_xml.is_empty() {
1368 if let Some(ld_start) = body_prefix.find("<legacyDrawing ") {
1369 let ld_end = body_prefix[ld_start..]
1371 .find("/>")
1372 .map(|e| ld_start + e + 2)
1373 .unwrap_or(ld_start);
1374 stripped = format!("{}{}", &body_prefix[..ld_start], &body_prefix[ld_end..]);
1375 stripped.as_str()
1376 } else {
1377 body_prefix
1378 }
1379 } else {
1380 body_prefix
1381 };
1382
1383 let extra_len = ext_xml.len() + legacy_xml.len();
1384 let mut result = String::with_capacity(XML_DECLARATION.len() + 1 + body.len() + extra_len);
1385 result.push_str(XML_DECLARATION);
1386 result.push('\n');
1387 result.push_str(prefix);
1388 result.push_str(&legacy_xml);
1389 result.push_str(&ext_xml);
1390 result.push_str(closing);
1391 Ok(result)
1392 } else {
1393 Ok(format!("{XML_DECLARATION}\n{body}"))
1394 }
1395}
1396
1397pub(crate) fn build_sparkline_ext_xml(sparklines: &[crate::sparkline::SparklineConfig]) -> String {
1399 use std::fmt::Write;
1400 let mut xml = String::new();
1401 let _ = write!(
1402 xml,
1403 "<extLst>\
1404 <ext xmlns:x14=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main\" \
1405 uri=\"{{05C60535-1F16-4fd2-B633-F4F36F0B64E0}}\">\
1406 <x14:sparklineGroups \
1407 xmlns:xm=\"http://schemas.microsoft.com/office/excel/2006/main\">"
1408 );
1409 for config in sparklines {
1410 let group = crate::sparkline::config_to_xml_group(config);
1411 let _ = write!(xml, "<x14:sparklineGroup");
1412 if let Some(ref t) = group.sparkline_type {
1413 let _ = write!(xml, " type=\"{t}\"");
1414 }
1415 if group.markers == Some(true) {
1416 let _ = write!(xml, " markers=\"1\"");
1417 }
1418 if group.high == Some(true) {
1419 let _ = write!(xml, " high=\"1\"");
1420 }
1421 if group.low == Some(true) {
1422 let _ = write!(xml, " low=\"1\"");
1423 }
1424 if group.first == Some(true) {
1425 let _ = write!(xml, " first=\"1\"");
1426 }
1427 if group.last == Some(true) {
1428 let _ = write!(xml, " last=\"1\"");
1429 }
1430 if group.negative == Some(true) {
1431 let _ = write!(xml, " negative=\"1\"");
1432 }
1433 if group.display_x_axis == Some(true) {
1434 let _ = write!(xml, " displayXAxis=\"1\"");
1435 }
1436 if let Some(w) = group.line_weight {
1437 let _ = write!(xml, " lineWeight=\"{w}\"");
1438 }
1439 let _ = write!(xml, "><x14:sparklines>");
1440 for sp in &group.sparklines.items {
1441 let _ = write!(
1442 xml,
1443 "<x14:sparkline><xm:f>{}</xm:f><xm:sqref>{}</xm:sqref></x14:sparkline>",
1444 sp.formula, sp.sqref
1445 );
1446 }
1447 let _ = write!(xml, "</x14:sparklines></x14:sparklineGroup>");
1448 }
1449 let _ = write!(xml, "</x14:sparklineGroups></ext></extLst>");
1450 xml
1451}
1452
1453pub(crate) fn parse_sparklines_from_xml(xml: &str) -> Vec<crate::sparkline::SparklineConfig> {
1455 use crate::sparkline::{SparklineConfig, SparklineType};
1456
1457 let mut sparklines = Vec::new();
1458
1459 let mut search_from = 0;
1461 while let Some(group_start) = xml[search_from..].find("<x14:sparklineGroup") {
1462 let abs_start = search_from + group_start;
1463 let group_end_tag = "</x14:sparklineGroup>";
1464 let abs_end = match xml[abs_start..].find(group_end_tag) {
1465 Some(pos) => abs_start + pos + group_end_tag.len(),
1466 None => break,
1467 };
1468 let group_xml = &xml[abs_start..abs_end];
1469
1470 let sparkline_type = extract_xml_attr(group_xml, "type")
1472 .and_then(|s| SparklineType::parse(&s))
1473 .unwrap_or_default();
1474 let markers = extract_xml_bool_attr(group_xml, "markers");
1475 let high_point = extract_xml_bool_attr(group_xml, "high");
1476 let low_point = extract_xml_bool_attr(group_xml, "low");
1477 let first_point = extract_xml_bool_attr(group_xml, "first");
1478 let last_point = extract_xml_bool_attr(group_xml, "last");
1479 let negative_points = extract_xml_bool_attr(group_xml, "negative");
1480 let show_axis = extract_xml_bool_attr(group_xml, "displayXAxis");
1481 let line_weight =
1482 extract_xml_attr(group_xml, "lineWeight").and_then(|s| s.parse::<f64>().ok());
1483
1484 let mut sp_from = 0;
1486 while let Some(sp_start) = group_xml[sp_from..].find("<x14:sparkline>") {
1487 let sp_abs = sp_from + sp_start;
1488 let sp_end_tag = "</x14:sparkline>";
1489 let sp_abs_end = match group_xml[sp_abs..].find(sp_end_tag) {
1490 Some(pos) => sp_abs + pos + sp_end_tag.len(),
1491 None => break,
1492 };
1493 let sp_xml = &group_xml[sp_abs..sp_abs_end];
1494
1495 let formula = extract_xml_element(sp_xml, "xm:f").unwrap_or_default();
1496 let sqref = extract_xml_element(sp_xml, "xm:sqref").unwrap_or_default();
1497
1498 if !formula.is_empty() && !sqref.is_empty() {
1499 sparklines.push(SparklineConfig {
1500 data_range: formula,
1501 location: sqref,
1502 sparkline_type: sparkline_type.clone(),
1503 markers,
1504 high_point,
1505 low_point,
1506 first_point,
1507 last_point,
1508 negative_points,
1509 show_axis,
1510 line_weight,
1511 style: None,
1512 });
1513 }
1514 sp_from = sp_abs_end;
1515 }
1516 search_from = abs_end;
1517 }
1518 sparklines
1519}
1520
1521pub(crate) fn extract_xml_attr(xml: &str, attr: &str) -> Option<String> {
1525 for quote in ['"', '\''] {
1527 let haystack = xml.as_bytes();
1529 let attr_bytes = attr.as_bytes();
1530 let mut pos = 0;
1531 while pos + 1 + attr_bytes.len() + 2 <= haystack.len() {
1532 if haystack[pos] == b' '
1533 && haystack[pos + 1..pos + 1 + attr_bytes.len()] == *attr_bytes
1534 && haystack[pos + 1 + attr_bytes.len()] == b'='
1535 && haystack[pos + 1 + attr_bytes.len() + 1] == quote as u8
1536 {
1537 let val_start = pos + 1 + attr_bytes.len() + 2;
1538 if let Some(end) = xml[val_start..].find(quote) {
1539 return Some(xml[val_start..val_start + end].to_string());
1540 }
1541 }
1542 pos += 1;
1543 }
1544 }
1545 None
1546}
1547
1548pub(crate) fn extract_xml_bool_attr(xml: &str, attr: &str) -> bool {
1550 extract_xml_attr(xml, attr)
1551 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1552 .unwrap_or(false)
1553}
1554
1555pub(crate) fn extract_xml_element(xml: &str, tag: &str) -> Option<String> {
1557 let open = format!("<{tag}>");
1558 let close = format!("</{tag}>");
1559 let start = xml.find(&open)?;
1560 let content_start = start + open.len();
1561 let end = xml[content_start..].find(&close)?;
1562 Some(xml[content_start..content_start + end].to_string())
1563}
1564
1565pub(crate) fn write_xml_part<T: Serialize, W: std::io::Write + std::io::Seek>(
1567 zip: &mut zip::ZipWriter<W>,
1568 name: &str,
1569 value: &T,
1570 options: SimpleFileOptions,
1571) -> Result<()> {
1572 let xml = serialize_xml(value)?;
1573 zip.start_file(name, options)
1574 .map_err(|e| Error::Zip(e.to_string()))?;
1575 zip.write_all(xml.as_bytes())?;
1576 Ok(())
1577}
1578
1579fn fast_col_number(cell_ref: &str) -> u32 {
1585 let mut col: u32 = 0;
1586 for b in cell_ref.bytes() {
1587 if b.is_ascii_alphabetic() {
1588 col = col * 26 + (b.to_ascii_uppercase() - b'A') as u32 + 1;
1589 } else {
1590 break;
1591 }
1592 }
1593 col
1594}
1595
1596#[cfg(test)]
1597mod tests {
1598 use super::*;
1599 use tempfile::TempDir;
1600
1601 #[test]
1602 fn test_fast_col_number() {
1603 assert_eq!(fast_col_number("A1"), 1);
1604 assert_eq!(fast_col_number("B1"), 2);
1605 assert_eq!(fast_col_number("Z1"), 26);
1606 assert_eq!(fast_col_number("AA1"), 27);
1607 assert_eq!(fast_col_number("AZ1"), 52);
1608 assert_eq!(fast_col_number("BA1"), 53);
1609 assert_eq!(fast_col_number("XFD1"), 16384);
1610 }
1611
1612 #[test]
1613 fn test_extract_xml_attr() {
1614 let xml = r#"<tag type="column" markers="1" weight="2.5">"#;
1615 assert_eq!(extract_xml_attr(xml, "type"), Some("column".to_string()));
1616 assert_eq!(extract_xml_attr(xml, "markers"), Some("1".to_string()));
1617 assert_eq!(extract_xml_attr(xml, "weight"), Some("2.5".to_string()));
1618 assert_eq!(extract_xml_attr(xml, "missing"), None);
1619 let xml2 = "<tag name='hello'>";
1621 assert_eq!(extract_xml_attr(xml2, "name"), Some("hello".to_string()));
1622 }
1623
1624 #[test]
1625 fn test_extract_xml_bool_attr() {
1626 let xml = r#"<tag markers="1" hidden="0" visible="true">"#;
1627 assert!(extract_xml_bool_attr(xml, "markers"));
1628 assert!(!extract_xml_bool_attr(xml, "hidden"));
1629 assert!(extract_xml_bool_attr(xml, "visible"));
1630 assert!(!extract_xml_bool_attr(xml, "missing"));
1631 }
1632
1633 #[test]
1634 fn test_new_workbook_has_sheet1() {
1635 let wb = Workbook::new();
1636 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
1637 }
1638
1639 #[test]
1640 fn test_new_workbook_save_creates_file() {
1641 let dir = TempDir::new().unwrap();
1642 let path = dir.path().join("test.xlsx");
1643 let wb = Workbook::new();
1644 wb.save(&path).unwrap();
1645 assert!(path.exists());
1646 }
1647
1648 #[test]
1649 fn test_save_and_open_roundtrip() {
1650 let dir = TempDir::new().unwrap();
1651 let path = dir.path().join("roundtrip.xlsx");
1652
1653 let wb = Workbook::new();
1654 wb.save(&path).unwrap();
1655
1656 let wb2 = Workbook::open(&path).unwrap();
1657 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
1658 }
1659
1660 #[test]
1661 fn test_saved_file_is_valid_zip() {
1662 let dir = TempDir::new().unwrap();
1663 let path = dir.path().join("valid.xlsx");
1664 let wb = Workbook::new();
1665 wb.save(&path).unwrap();
1666
1667 let file = std::fs::File::open(&path).unwrap();
1669 let mut archive = zip::ZipArchive::new(file).unwrap();
1670
1671 let expected_files = [
1672 "[Content_Types].xml",
1673 "_rels/.rels",
1674 "xl/workbook.xml",
1675 "xl/_rels/workbook.xml.rels",
1676 "xl/worksheets/sheet1.xml",
1677 "xl/styles.xml",
1678 "xl/sharedStrings.xml",
1679 ];
1680
1681 for name in &expected_files {
1682 assert!(archive.by_name(name).is_ok(), "Missing ZIP entry: {}", name);
1683 }
1684 }
1685
1686 #[test]
1687 fn test_open_nonexistent_file_returns_error() {
1688 let result = Workbook::open("/nonexistent/path.xlsx");
1689 assert!(result.is_err());
1690 }
1691
1692 #[test]
1693 fn test_saved_xml_has_declarations() {
1694 let dir = TempDir::new().unwrap();
1695 let path = dir.path().join("decl.xlsx");
1696 let wb = Workbook::new();
1697 wb.save(&path).unwrap();
1698
1699 let file = std::fs::File::open(&path).unwrap();
1700 let mut archive = zip::ZipArchive::new(file).unwrap();
1701
1702 let mut content = String::new();
1703 std::io::Read::read_to_string(
1704 &mut archive.by_name("[Content_Types].xml").unwrap(),
1705 &mut content,
1706 )
1707 .unwrap();
1708 assert!(content.starts_with("<?xml"));
1709 }
1710
1711 #[test]
1712 fn test_default_trait() {
1713 let wb = Workbook::default();
1714 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
1715 }
1716
1717 #[test]
1718 fn test_serialize_xml_helper() {
1719 let ct = ContentTypes::default();
1720 let xml = serialize_xml(&ct).unwrap();
1721 assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"));
1722 assert!(xml.contains("<Types"));
1723 }
1724
1725 #[test]
1726 fn test_save_to_buffer_and_open_from_buffer_roundtrip() {
1727 let mut wb = Workbook::new();
1728 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
1729 .unwrap();
1730 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
1731 .unwrap();
1732
1733 let buf = wb.save_to_buffer().unwrap();
1734 assert!(!buf.is_empty());
1735
1736 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
1737 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
1738 assert_eq!(
1739 wb2.get_cell_value("Sheet1", "A1").unwrap(),
1740 CellValue::String("Hello".to_string())
1741 );
1742 assert_eq!(
1743 wb2.get_cell_value("Sheet1", "B2").unwrap(),
1744 CellValue::Number(42.0)
1745 );
1746 }
1747
1748 #[test]
1749 fn test_save_to_buffer_produces_valid_zip() {
1750 let wb = Workbook::new();
1751 let buf = wb.save_to_buffer().unwrap();
1752
1753 let cursor = std::io::Cursor::new(buf);
1754 let mut archive = zip::ZipArchive::new(cursor).unwrap();
1755
1756 let expected_files = [
1757 "[Content_Types].xml",
1758 "_rels/.rels",
1759 "xl/workbook.xml",
1760 "xl/_rels/workbook.xml.rels",
1761 "xl/worksheets/sheet1.xml",
1762 "xl/styles.xml",
1763 "xl/sharedStrings.xml",
1764 ];
1765
1766 for name in &expected_files {
1767 assert!(archive.by_name(name).is_ok(), "Missing ZIP entry: {}", name);
1768 }
1769 }
1770
1771 #[test]
1772 fn test_open_from_buffer_invalid_data() {
1773 let result = Workbook::open_from_buffer(b"not a zip file");
1774 assert!(result.is_err());
1775 }
1776
1777 #[cfg(feature = "encryption")]
1778 #[test]
1779 fn test_save_and_open_with_password_roundtrip() {
1780 let dir = TempDir::new().unwrap();
1781 let path = dir.path().join("encrypted.xlsx");
1782
1783 let mut wb = Workbook::new();
1785 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
1786 .unwrap();
1787 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
1788 .unwrap();
1789
1790 wb.save_with_password(&path, "test123").unwrap();
1792
1793 let data = std::fs::read(&path).unwrap();
1795 assert_eq!(
1796 &data[..8],
1797 &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]
1798 );
1799
1800 let result = Workbook::open(&path);
1802 assert!(matches!(result, Err(Error::FileEncrypted)));
1803
1804 let result = Workbook::open_with_password(&path, "wrong");
1806 assert!(matches!(result, Err(Error::IncorrectPassword)));
1807
1808 let wb2 = Workbook::open_with_password(&path, "test123").unwrap();
1810 assert_eq!(
1811 wb2.get_cell_value("Sheet1", "A1").unwrap(),
1812 CellValue::String("Hello".to_string())
1813 );
1814 assert_eq!(
1815 wb2.get_cell_value("Sheet1", "B2").unwrap(),
1816 CellValue::Number(42.0)
1817 );
1818 }
1819
1820 fn create_xlsx_with_custom_entries() -> Vec<u8> {
1823 let mut wb = Workbook::new();
1824 wb.set_cell_value("Sheet1", "A1", CellValue::String("hello".to_string()))
1825 .unwrap();
1826 let base_buf = wb.save_to_buffer().unwrap();
1827
1828 let cursor = std::io::Cursor::new(&base_buf);
1830 let mut archive = zip::ZipArchive::new(cursor).unwrap();
1831 let mut out = Vec::new();
1832 {
1833 let out_cursor = std::io::Cursor::new(&mut out);
1834 let mut zip_writer = zip::ZipWriter::new(out_cursor);
1835 let options =
1836 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
1837
1838 for i in 0..archive.len() {
1840 let mut entry = archive.by_index(i).unwrap();
1841 let name = entry.name().to_string();
1842 let mut data = Vec::new();
1843 std::io::Read::read_to_end(&mut entry, &mut data).unwrap();
1844 zip_writer.start_file(&name, options).unwrap();
1845 std::io::Write::write_all(&mut zip_writer, &data).unwrap();
1846 }
1847
1848 zip_writer
1850 .start_file("customXml/item1.xml", options)
1851 .unwrap();
1852 std::io::Write::write_all(&mut zip_writer, b"<custom>data1</custom>").unwrap();
1853
1854 zip_writer
1855 .start_file("customXml/itemProps1.xml", options)
1856 .unwrap();
1857 std::io::Write::write_all(
1858 &mut zip_writer,
1859 b"<ds:datastoreItem xmlns:ds=\"http://schemas.openxmlformats.org/officeDocument/2006/customXml\"/>",
1860 )
1861 .unwrap();
1862
1863 zip_writer
1864 .start_file("xl/printerSettings/printerSettings1.bin", options)
1865 .unwrap();
1866 std::io::Write::write_all(&mut zip_writer, b"\x00\x01\x02\x03PRINTER").unwrap();
1867
1868 zip_writer.finish().unwrap();
1869 }
1870 out
1871 }
1872
1873 #[test]
1874 fn test_unknown_zip_entries_preserved_on_roundtrip() {
1875 let buf = create_xlsx_with_custom_entries();
1876
1877 let wb = Workbook::open_from_buffer(&buf).unwrap();
1879 assert_eq!(
1880 wb.get_cell_value("Sheet1", "A1").unwrap(),
1881 CellValue::String("hello".to_string())
1882 );
1883
1884 let saved = wb.save_to_buffer().unwrap();
1886 let cursor = std::io::Cursor::new(&saved);
1887 let mut archive = zip::ZipArchive::new(cursor).unwrap();
1888
1889 let mut custom_xml = String::new();
1891 std::io::Read::read_to_string(
1892 &mut archive.by_name("customXml/item1.xml").unwrap(),
1893 &mut custom_xml,
1894 )
1895 .unwrap();
1896 assert_eq!(custom_xml, "<custom>data1</custom>");
1897
1898 let mut props_xml = String::new();
1899 std::io::Read::read_to_string(
1900 &mut archive.by_name("customXml/itemProps1.xml").unwrap(),
1901 &mut props_xml,
1902 )
1903 .unwrap();
1904 assert!(props_xml.contains("datastoreItem"));
1905
1906 let mut printer = Vec::new();
1907 std::io::Read::read_to_end(
1908 &mut archive
1909 .by_name("xl/printerSettings/printerSettings1.bin")
1910 .unwrap(),
1911 &mut printer,
1912 )
1913 .unwrap();
1914 assert_eq!(printer, b"\x00\x01\x02\x03PRINTER");
1915 }
1916
1917 #[test]
1918 fn test_unknown_entries_survive_multiple_roundtrips() {
1919 let buf = create_xlsx_with_custom_entries();
1920 let wb1 = Workbook::open_from_buffer(&buf).unwrap();
1921 let buf2 = wb1.save_to_buffer().unwrap();
1922 let wb2 = Workbook::open_from_buffer(&buf2).unwrap();
1923 let buf3 = wb2.save_to_buffer().unwrap();
1924
1925 let cursor = std::io::Cursor::new(&buf3);
1926 let mut archive = zip::ZipArchive::new(cursor).unwrap();
1927
1928 let mut custom_xml = String::new();
1929 std::io::Read::read_to_string(
1930 &mut archive.by_name("customXml/item1.xml").unwrap(),
1931 &mut custom_xml,
1932 )
1933 .unwrap();
1934 assert_eq!(custom_xml, "<custom>data1</custom>");
1935
1936 let mut printer = Vec::new();
1937 std::io::Read::read_to_end(
1938 &mut archive
1939 .by_name("xl/printerSettings/printerSettings1.bin")
1940 .unwrap(),
1941 &mut printer,
1942 )
1943 .unwrap();
1944 assert_eq!(printer, b"\x00\x01\x02\x03PRINTER");
1945 }
1946
1947 #[test]
1948 fn test_new_workbook_has_no_unknown_parts() {
1949 let wb = Workbook::new();
1950 let buf = wb.save_to_buffer().unwrap();
1951 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
1952 assert!(wb2.unknown_parts.is_empty());
1953 }
1954
1955 #[test]
1956 fn test_known_entries_not_duplicated_as_unknown() {
1957 let wb = Workbook::new();
1958 let buf = wb.save_to_buffer().unwrap();
1959 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
1960
1961 let unknown_paths: Vec<&str> = wb2.unknown_parts.iter().map(|(p, _)| p.as_str()).collect();
1963 assert!(
1964 !unknown_paths.contains(&"[Content_Types].xml"),
1965 "Content_Types should not be in unknown_parts"
1966 );
1967 assert!(
1968 !unknown_paths.contains(&"xl/workbook.xml"),
1969 "workbook.xml should not be in unknown_parts"
1970 );
1971 assert!(
1972 !unknown_paths.contains(&"xl/styles.xml"),
1973 "styles.xml should not be in unknown_parts"
1974 );
1975 }
1976
1977 #[test]
1978 fn test_modifications_preserved_alongside_unknown_parts() {
1979 let buf = create_xlsx_with_custom_entries();
1980 let mut wb = Workbook::open_from_buffer(&buf).unwrap();
1981
1982 wb.set_cell_value("Sheet1", "B1", CellValue::Number(42.0))
1984 .unwrap();
1985
1986 let saved = wb.save_to_buffer().unwrap();
1987 let wb2 = Workbook::open_from_buffer(&saved).unwrap();
1988
1989 assert_eq!(
1991 wb2.get_cell_value("Sheet1", "A1").unwrap(),
1992 CellValue::String("hello".to_string())
1993 );
1994 assert_eq!(
1996 wb2.get_cell_value("Sheet1", "B1").unwrap(),
1997 CellValue::Number(42.0)
1998 );
1999 let cursor = std::io::Cursor::new(&saved);
2001 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2002 assert!(archive.by_name("customXml/item1.xml").is_ok());
2003 }
2004
2005 #[test]
2006 fn test_threaded_comment_person_rel_in_workbook_rels() {
2007 let mut wb = Workbook::new();
2008 wb.add_threaded_comment(
2009 "Sheet1",
2010 "A1",
2011 &crate::threaded_comment::ThreadedCommentInput {
2012 author: "Alice".to_string(),
2013 text: "Test comment".to_string(),
2014 parent_id: None,
2015 },
2016 )
2017 .unwrap();
2018
2019 let buf = wb.save_to_buffer().unwrap();
2020 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2021
2022 let has_person_rel = wb2.workbook_rels.relationships.iter().any(|r| {
2024 r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_PERSON
2025 && r.target == "persons/person.xml"
2026 });
2027 assert!(
2028 has_person_rel,
2029 "workbook_rels must contain a person relationship for threaded comments"
2030 );
2031 }
2032
2033 #[test]
2034 fn test_no_person_rel_without_threaded_comments() {
2035 let wb = Workbook::new();
2036 let buf = wb.save_to_buffer().unwrap();
2037 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2038
2039 let has_person_rel = wb2
2040 .workbook_rels
2041 .relationships
2042 .iter()
2043 .any(|r| r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_PERSON);
2044 assert!(
2045 !has_person_rel,
2046 "workbook_rels must not contain a person relationship when there are no threaded comments"
2047 );
2048 }
2049
2050 #[cfg(feature = "encryption")]
2051 #[test]
2052 fn test_open_encrypted_file_without_password_returns_file_encrypted() {
2053 let dir = TempDir::new().unwrap();
2054 let path = dir.path().join("encrypted2.xlsx");
2055
2056 let wb = Workbook::new();
2057 wb.save_with_password(&path, "secret").unwrap();
2058
2059 let result = Workbook::open(&path);
2060 assert!(matches!(result, Err(Error::FileEncrypted)))
2061 }
2062
2063 #[test]
2064 fn test_workbook_format_from_content_type() {
2065 use sheetkit_xml::content_types::mime_types;
2066 assert_eq!(
2067 WorkbookFormat::from_content_type(mime_types::WORKBOOK),
2068 Some(WorkbookFormat::Xlsx)
2069 );
2070 assert_eq!(
2071 WorkbookFormat::from_content_type(mime_types::WORKBOOK_MACRO),
2072 Some(WorkbookFormat::Xlsm)
2073 );
2074 assert_eq!(
2075 WorkbookFormat::from_content_type(mime_types::WORKBOOK_TEMPLATE),
2076 Some(WorkbookFormat::Xltx)
2077 );
2078 assert_eq!(
2079 WorkbookFormat::from_content_type(mime_types::WORKBOOK_TEMPLATE_MACRO),
2080 Some(WorkbookFormat::Xltm)
2081 );
2082 assert_eq!(
2083 WorkbookFormat::from_content_type(mime_types::WORKBOOK_ADDIN_MACRO),
2084 Some(WorkbookFormat::Xlam)
2085 );
2086 assert_eq!(
2087 WorkbookFormat::from_content_type("application/unknown"),
2088 None
2089 );
2090 }
2091
2092 #[test]
2093 fn test_workbook_format_content_type_roundtrip() {
2094 for fmt in [
2095 WorkbookFormat::Xlsx,
2096 WorkbookFormat::Xlsm,
2097 WorkbookFormat::Xltx,
2098 WorkbookFormat::Xltm,
2099 WorkbookFormat::Xlam,
2100 ] {
2101 let ct = fmt.content_type();
2102 assert_eq!(WorkbookFormat::from_content_type(ct), Some(fmt));
2103 }
2104 }
2105
2106 #[test]
2107 fn test_new_workbook_defaults_to_xlsx_format() {
2108 let wb = Workbook::new();
2109 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2110 }
2111
2112 #[test]
2113 fn test_xlsx_roundtrip_preserves_format() {
2114 let dir = TempDir::new().unwrap();
2115 let path = dir.path().join("roundtrip_format.xlsx");
2116
2117 let wb = Workbook::new();
2118 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2119 wb.save(&path).unwrap();
2120
2121 let wb2 = Workbook::open(&path).unwrap();
2122 assert_eq!(wb2.format(), WorkbookFormat::Xlsx);
2123 }
2124
2125 #[test]
2126 fn test_save_writes_correct_content_type_for_each_extension() {
2127 let dir = TempDir::new().unwrap();
2128
2129 let cases = [
2130 (WorkbookFormat::Xlsx, "test.xlsx"),
2131 (WorkbookFormat::Xlsm, "test.xlsm"),
2132 (WorkbookFormat::Xltx, "test.xltx"),
2133 (WorkbookFormat::Xltm, "test.xltm"),
2134 (WorkbookFormat::Xlam, "test.xlam"),
2135 ];
2136
2137 for (expected_fmt, filename) in cases {
2138 let path = dir.path().join(filename);
2139 let wb = Workbook::new();
2140 wb.save(&path).unwrap();
2141
2142 let file = std::fs::File::open(&path).unwrap();
2143 let mut archive = zip::ZipArchive::new(file).unwrap();
2144
2145 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2146 let wb_override = ct
2147 .overrides
2148 .iter()
2149 .find(|o| o.part_name == "/xl/workbook.xml")
2150 .expect("workbook override must exist");
2151 assert_eq!(
2152 wb_override.content_type,
2153 expected_fmt.content_type(),
2154 "content type mismatch for {}",
2155 filename
2156 );
2157 }
2158 }
2159
2160 #[test]
2161 fn test_set_format_changes_workbook_format() {
2162 let mut wb = Workbook::new();
2163 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2164
2165 wb.set_format(WorkbookFormat::Xlsm);
2166 assert_eq!(wb.format(), WorkbookFormat::Xlsm);
2167 }
2168
2169 #[test]
2170 fn test_save_buffer_roundtrip_with_xlsm_format() {
2171 let mut wb = Workbook::new();
2172 wb.set_format(WorkbookFormat::Xlsm);
2173 wb.set_cell_value("Sheet1", "A1", CellValue::String("test".to_string()))
2174 .unwrap();
2175
2176 let buf = wb.save_to_buffer().unwrap();
2177 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2178 assert_eq!(wb2.format(), WorkbookFormat::Xlsm);
2179 assert_eq!(
2180 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2181 CellValue::String("test".to_string())
2182 );
2183 }
2184
2185 #[test]
2186 fn test_open_with_default_options_is_equivalent_to_open() {
2187 let dir = TempDir::new().unwrap();
2188 let path = dir.path().join("default_opts.xlsx");
2189 let mut wb = Workbook::new();
2190 wb.set_cell_value("Sheet1", "A1", CellValue::String("test".to_string()))
2191 .unwrap();
2192 wb.save(&path).unwrap();
2193
2194 let wb2 = Workbook::open_with_options(&path, &OpenOptions::default()).unwrap();
2195 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
2196 assert_eq!(
2197 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2198 CellValue::String("test".to_string())
2199 );
2200 }
2201
2202 #[test]
2203 fn test_format_inference_from_content_types_overrides() {
2204 use sheetkit_xml::content_types::mime_types;
2205
2206 let ct = ContentTypes {
2208 xmlns: "http://schemas.openxmlformats.org/package/2006/content-types".to_string(),
2209 defaults: vec![],
2210 overrides: vec![ContentTypeOverride {
2211 part_name: "/xl/workbook.xml".to_string(),
2212 content_type: mime_types::WORKBOOK_MACRO.to_string(),
2213 }],
2214 };
2215
2216 let detected = ct
2217 .overrides
2218 .iter()
2219 .find(|o| o.part_name == "/xl/workbook.xml")
2220 .and_then(|o| WorkbookFormat::from_content_type(&o.content_type))
2221 .unwrap_or_default();
2222 assert_eq!(detected, WorkbookFormat::Xlsm);
2223 }
2224
2225 #[test]
2226 fn test_workbook_format_default_is_xlsx() {
2227 assert_eq!(WorkbookFormat::default(), WorkbookFormat::Xlsx);
2228 }
2229
2230 fn build_xlsm_with_vba(vba_bytes: &[u8]) -> Vec<u8> {
2231 use std::io::Write;
2232 let mut buf = Vec::new();
2233 {
2234 let cursor = std::io::Cursor::new(&mut buf);
2235 let mut zip = zip::ZipWriter::new(cursor);
2236 let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
2237
2238 let ct_xml = format!(
2239 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2240<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
2241 <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
2242 <Default Extension="xml" ContentType="application/xml"/>
2243 <Default Extension="bin" ContentType="application/vnd.ms-office.vbaProject"/>
2244 <Override PartName="/xl/workbook.xml" ContentType="{wb_ct}"/>
2245 <Override PartName="/xl/worksheets/sheet1.xml" ContentType="{ws_ct}"/>
2246 <Override PartName="/xl/styles.xml" ContentType="{st_ct}"/>
2247 <Override PartName="/xl/sharedStrings.xml" ContentType="{sst_ct}"/>
2248 <Override PartName="/xl/vbaProject.bin" ContentType="application/vnd.ms-office.vbaProject"/>
2249</Types>"#,
2250 wb_ct = mime_types::WORKBOOK_MACRO,
2251 ws_ct = mime_types::WORKSHEET,
2252 st_ct = mime_types::STYLES,
2253 sst_ct = mime_types::SHARED_STRINGS,
2254 );
2255 zip.start_file("[Content_Types].xml", opts).unwrap();
2256 zip.write_all(ct_xml.as_bytes()).unwrap();
2257
2258 let pkg_rels = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2259<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
2260 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
2261</Relationships>"#;
2262 zip.start_file("_rels/.rels", opts).unwrap();
2263 zip.write_all(pkg_rels.as_bytes()).unwrap();
2264
2265 let wb_rels = format!(
2266 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2267<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
2268 <Relationship Id="rId1" Type="{ws_rel}" Target="worksheets/sheet1.xml"/>
2269 <Relationship Id="rId2" Type="{st_rel}" Target="styles.xml"/>
2270 <Relationship Id="rId3" Type="{sst_rel}" Target="sharedStrings.xml"/>
2271 <Relationship Id="rId4" Type="{vba_rel}" Target="vbaProject.bin"/>
2272</Relationships>"#,
2273 ws_rel = rel_types::WORKSHEET,
2274 st_rel = rel_types::STYLES,
2275 sst_rel = rel_types::SHARED_STRINGS,
2276 vba_rel = VBA_PROJECT_REL_TYPE,
2277 );
2278 zip.start_file("xl/_rels/workbook.xml.rels", opts).unwrap();
2279 zip.write_all(wb_rels.as_bytes()).unwrap();
2280
2281 let wb_xml = concat!(
2282 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
2283 r#"<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main""#,
2284 r#" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
2285 r#"<sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets>"#,
2286 r#"</workbook>"#,
2287 );
2288 zip.start_file("xl/workbook.xml", opts).unwrap();
2289 zip.write_all(wb_xml.as_bytes()).unwrap();
2290
2291 let ws_xml = concat!(
2292 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
2293 r#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main""#,
2294 r#" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
2295 r#"<sheetData/>"#,
2296 r#"</worksheet>"#,
2297 );
2298 zip.start_file("xl/worksheets/sheet1.xml", opts).unwrap();
2299 zip.write_all(ws_xml.as_bytes()).unwrap();
2300
2301 let styles_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2302<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
2303 <fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
2304 <fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
2305 <borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
2306 <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
2307 <cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
2308</styleSheet>"#;
2309 zip.start_file("xl/styles.xml", opts).unwrap();
2310 zip.write_all(styles_xml.as_bytes()).unwrap();
2311
2312 let sst_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2313<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="0" uniqueCount="0"/>"#;
2314 zip.start_file("xl/sharedStrings.xml", opts).unwrap();
2315 zip.write_all(sst_xml.as_bytes()).unwrap();
2316
2317 zip.start_file("xl/vbaProject.bin", opts).unwrap();
2318 zip.write_all(vba_bytes).unwrap();
2319
2320 zip.finish().unwrap();
2321 }
2322 buf
2323 }
2324
2325 #[test]
2326 fn test_vba_blob_loaded_when_present() {
2327 let vba_data = b"FAKE_VBA_PROJECT_BINARY_DATA_1234567890";
2328 let xlsm = build_xlsm_with_vba(vba_data);
2329 let wb = Workbook::open_from_buffer(&xlsm).unwrap();
2330 assert!(wb.vba_blob.is_some());
2331 assert_eq!(wb.vba_blob.as_deref().unwrap(), vba_data);
2332 }
2333
2334 #[test]
2335 fn test_vba_blob_none_for_plain_xlsx() {
2336 let wb = Workbook::new();
2337 assert!(wb.vba_blob.is_none());
2338
2339 let buf = wb.save_to_buffer().unwrap();
2340 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2341 assert!(wb2.vba_blob.is_none());
2342 }
2343
2344 #[test]
2345 fn test_vba_blob_survives_roundtrip_with_identical_bytes() {
2346 let vba_data: Vec<u8> = (0..=255).cycle().take(1024).collect();
2347 let xlsm = build_xlsm_with_vba(&vba_data);
2348
2349 let wb = Workbook::open_from_buffer(&xlsm).unwrap();
2350 assert_eq!(wb.vba_blob.as_deref().unwrap(), &vba_data[..]);
2351
2352 let saved = wb.save_to_buffer().unwrap();
2353 let cursor = std::io::Cursor::new(&saved);
2354 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2355
2356 let mut roundtripped = Vec::new();
2357 std::io::Read::read_to_end(
2358 &mut archive.by_name("xl/vbaProject.bin").unwrap(),
2359 &mut roundtripped,
2360 )
2361 .unwrap();
2362 assert_eq!(roundtripped, vba_data);
2363 }
2364
2365 #[test]
2366 fn test_vba_relationship_preserved_on_roundtrip() {
2367 let vba_data = b"VBA_BLOB";
2368 let xlsm = build_xlsm_with_vba(vba_data);
2369
2370 let wb = Workbook::open_from_buffer(&xlsm).unwrap();
2371 let saved = wb.save_to_buffer().unwrap();
2372
2373 let cursor = std::io::Cursor::new(&saved);
2374 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2375
2376 let rels: Relationships =
2377 read_xml_part(&mut archive, "xl/_rels/workbook.xml.rels").unwrap();
2378 let vba_rel = rels
2379 .relationships
2380 .iter()
2381 .find(|r| r.rel_type == VBA_PROJECT_REL_TYPE);
2382 assert!(vba_rel.is_some(), "VBA relationship must be preserved");
2383 assert_eq!(vba_rel.unwrap().target, "vbaProject.bin");
2384 }
2385
2386 #[test]
2387 fn test_vba_content_type_preserved_on_roundtrip() {
2388 let vba_data = b"VBA_BLOB";
2389 let xlsm = build_xlsm_with_vba(vba_data);
2390
2391 let wb = Workbook::open_from_buffer(&xlsm).unwrap();
2392 let saved = wb.save_to_buffer().unwrap();
2393
2394 let cursor = std::io::Cursor::new(&saved);
2395 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2396
2397 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2398 let vba_override = ct
2399 .overrides
2400 .iter()
2401 .find(|o| o.part_name == "/xl/vbaProject.bin");
2402 assert!(
2403 vba_override.is_some(),
2404 "VBA content type override must be preserved"
2405 );
2406 assert_eq!(vba_override.unwrap().content_type, VBA_PROJECT_CONTENT_TYPE);
2407 }
2408
2409 #[test]
2410 fn test_non_vba_save_has_no_vba_entries() {
2411 let wb = Workbook::new();
2412 let buf = wb.save_to_buffer().unwrap();
2413
2414 let cursor = std::io::Cursor::new(&buf);
2415 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2416
2417 assert!(
2418 archive.by_name("xl/vbaProject.bin").is_err(),
2419 "plain xlsx must not contain vbaProject.bin"
2420 );
2421
2422 let rels: Relationships =
2423 read_xml_part(&mut archive, "xl/_rels/workbook.xml.rels").unwrap();
2424 assert!(
2425 !rels
2426 .relationships
2427 .iter()
2428 .any(|r| r.rel_type == VBA_PROJECT_REL_TYPE),
2429 "plain xlsx must not have VBA relationship"
2430 );
2431
2432 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2433 assert!(
2434 !ct.overrides
2435 .iter()
2436 .any(|o| o.content_type == VBA_PROJECT_CONTENT_TYPE),
2437 "plain xlsx must not have VBA content type override"
2438 );
2439 }
2440
2441 #[test]
2442 fn test_xlsm_format_detected_with_vba() {
2443 let vba_data = b"VBA_BLOB";
2444 let xlsm = build_xlsm_with_vba(vba_data);
2445 let wb = Workbook::open_from_buffer(&xlsm).unwrap();
2446 assert_eq!(wb.format(), WorkbookFormat::Xlsm);
2447 }
2448
2449 #[test]
2450 fn test_from_extension_recognized() {
2451 assert_eq!(
2452 WorkbookFormat::from_extension("xlsx"),
2453 Some(WorkbookFormat::Xlsx)
2454 );
2455 assert_eq!(
2456 WorkbookFormat::from_extension("xlsm"),
2457 Some(WorkbookFormat::Xlsm)
2458 );
2459 assert_eq!(
2460 WorkbookFormat::from_extension("xltx"),
2461 Some(WorkbookFormat::Xltx)
2462 );
2463 assert_eq!(
2464 WorkbookFormat::from_extension("xltm"),
2465 Some(WorkbookFormat::Xltm)
2466 );
2467 assert_eq!(
2468 WorkbookFormat::from_extension("xlam"),
2469 Some(WorkbookFormat::Xlam)
2470 );
2471 }
2472
2473 #[test]
2474 fn test_from_extension_case_insensitive() {
2475 assert_eq!(
2476 WorkbookFormat::from_extension("XLSX"),
2477 Some(WorkbookFormat::Xlsx)
2478 );
2479 assert_eq!(
2480 WorkbookFormat::from_extension("Xlsm"),
2481 Some(WorkbookFormat::Xlsm)
2482 );
2483 assert_eq!(
2484 WorkbookFormat::from_extension("XLTX"),
2485 Some(WorkbookFormat::Xltx)
2486 );
2487 }
2488
2489 #[test]
2490 fn test_from_extension_unrecognized() {
2491 assert_eq!(WorkbookFormat::from_extension("csv"), None);
2492 assert_eq!(WorkbookFormat::from_extension("xls"), None);
2493 assert_eq!(WorkbookFormat::from_extension("txt"), None);
2494 assert_eq!(WorkbookFormat::from_extension("pdf"), None);
2495 assert_eq!(WorkbookFormat::from_extension(""), None);
2496 }
2497
2498 #[test]
2499 fn test_save_unsupported_extension_csv() {
2500 let dir = TempDir::new().unwrap();
2501 let path = dir.path().join("output.csv");
2502 let wb = Workbook::new();
2503 let result = wb.save(&path);
2504 assert!(matches!(result, Err(Error::UnsupportedFileExtension(ext)) if ext == "csv"));
2505 }
2506
2507 #[test]
2508 fn test_save_unsupported_extension_xls() {
2509 let dir = TempDir::new().unwrap();
2510 let path = dir.path().join("output.xls");
2511 let wb = Workbook::new();
2512 let result = wb.save(&path);
2513 assert!(matches!(result, Err(Error::UnsupportedFileExtension(ext)) if ext == "xls"));
2514 }
2515
2516 #[test]
2517 fn test_save_unsupported_extension_unknown() {
2518 let dir = TempDir::new().unwrap();
2519 let path = dir.path().join("output.foo");
2520 let wb = Workbook::new();
2521 let result = wb.save(&path);
2522 assert!(matches!(result, Err(Error::UnsupportedFileExtension(ext)) if ext == "foo"));
2523 }
2524
2525 #[test]
2526 fn test_save_no_extension_fails() {
2527 let dir = TempDir::new().unwrap();
2528 let path = dir.path().join("noext");
2529 let wb = Workbook::new();
2530 let result = wb.save(&path);
2531 assert!(matches!(
2532 result,
2533 Err(Error::UnsupportedFileExtension(ext)) if ext.is_empty()
2534 ));
2535 }
2536
2537 #[test]
2538 fn test_save_as_xlsm_writes_xlsm_content_type() {
2539 let dir = TempDir::new().unwrap();
2540 let path = dir.path().join("output.xlsm");
2541 let wb = Workbook::new();
2542 wb.save(&path).unwrap();
2543
2544 let file = std::fs::File::open(&path).unwrap();
2545 let mut archive = zip::ZipArchive::new(file).unwrap();
2546 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2547 let wb_ct = ct
2548 .overrides
2549 .iter()
2550 .find(|o| o.part_name == "/xl/workbook.xml")
2551 .expect("workbook override must exist");
2552 assert_eq!(wb_ct.content_type, WorkbookFormat::Xlsm.content_type());
2553 }
2554
2555 #[test]
2556 fn test_save_as_xltx_writes_template_content_type() {
2557 let dir = TempDir::new().unwrap();
2558 let path = dir.path().join("output.xltx");
2559 let wb = Workbook::new();
2560 wb.save(&path).unwrap();
2561
2562 let file = std::fs::File::open(&path).unwrap();
2563 let mut archive = zip::ZipArchive::new(file).unwrap();
2564 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2565 let wb_ct = ct
2566 .overrides
2567 .iter()
2568 .find(|o| o.part_name == "/xl/workbook.xml")
2569 .expect("workbook override must exist");
2570 assert_eq!(wb_ct.content_type, WorkbookFormat::Xltx.content_type());
2571 }
2572
2573 #[test]
2574 fn test_save_as_xltm_writes_template_macro_content_type() {
2575 let dir = TempDir::new().unwrap();
2576 let path = dir.path().join("output.xltm");
2577 let wb = Workbook::new();
2578 wb.save(&path).unwrap();
2579
2580 let file = std::fs::File::open(&path).unwrap();
2581 let mut archive = zip::ZipArchive::new(file).unwrap();
2582 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2583 let wb_ct = ct
2584 .overrides
2585 .iter()
2586 .find(|o| o.part_name == "/xl/workbook.xml")
2587 .expect("workbook override must exist");
2588 assert_eq!(wb_ct.content_type, WorkbookFormat::Xltm.content_type());
2589 }
2590
2591 #[test]
2592 fn test_save_as_xlam_writes_addin_content_type() {
2593 let dir = TempDir::new().unwrap();
2594 let path = dir.path().join("output.xlam");
2595 let wb = Workbook::new();
2596 wb.save(&path).unwrap();
2597
2598 let file = std::fs::File::open(&path).unwrap();
2599 let mut archive = zip::ZipArchive::new(file).unwrap();
2600 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2601 let wb_ct = ct
2602 .overrides
2603 .iter()
2604 .find(|o| o.part_name == "/xl/workbook.xml")
2605 .expect("workbook override must exist");
2606 assert_eq!(wb_ct.content_type, WorkbookFormat::Xlam.content_type());
2607 }
2608
2609 #[test]
2610 fn test_save_extension_overrides_stored_format() {
2611 let dir = TempDir::new().unwrap();
2612 let path = dir.path().join("output.xlsm");
2613
2614 let wb = Workbook::new();
2616 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2617 wb.save(&path).unwrap();
2618
2619 let file = std::fs::File::open(&path).unwrap();
2620 let mut archive = zip::ZipArchive::new(file).unwrap();
2621 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2622 let wb_ct = ct
2623 .overrides
2624 .iter()
2625 .find(|o| o.part_name == "/xl/workbook.xml")
2626 .expect("workbook override must exist");
2627 assert_eq!(
2628 wb_ct.content_type,
2629 WorkbookFormat::Xlsm.content_type(),
2630 "extension .xlsm must override stored Xlsx format"
2631 );
2632 }
2633
2634 #[test]
2635 fn test_save_to_buffer_preserves_stored_format() {
2636 let mut wb = Workbook::new();
2637 wb.set_format(WorkbookFormat::Xltx);
2638
2639 let buf = wb.save_to_buffer().unwrap();
2640 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2641 assert_eq!(
2642 wb2.format(),
2643 WorkbookFormat::Xltx,
2644 "save_to_buffer must use the stored format, not infer from extension"
2645 );
2646 }
2647
2648 #[test]
2649 fn test_sheet_rows_limits_rows_read() {
2650 let dir = TempDir::new().unwrap();
2651 let path = dir.path().join("sheet_rows.xlsx");
2652
2653 let mut wb = Workbook::new();
2654 for i in 1..=20 {
2655 let cell = format!("A{}", i);
2656 wb.set_cell_value("Sheet1", &cell, CellValue::Number(i as f64))
2657 .unwrap();
2658 }
2659 wb.save(&path).unwrap();
2660
2661 let opts = OpenOptions::new().sheet_rows(5);
2662 let wb2 = Workbook::open_with_options(&path, &opts).unwrap();
2663
2664 for i in 1..=5 {
2666 let cell = format!("A{}", i);
2667 assert_eq!(
2668 wb2.get_cell_value("Sheet1", &cell).unwrap(),
2669 CellValue::Number(i as f64)
2670 );
2671 }
2672
2673 for i in 6..=20 {
2675 let cell = format!("A{}", i);
2676 assert_eq!(
2677 wb2.get_cell_value("Sheet1", &cell).unwrap(),
2678 CellValue::Empty
2679 );
2680 }
2681 }
2682
2683 #[test]
2684 fn test_sheet_rows_with_buffer() {
2685 let mut wb = Workbook::new();
2686 for i in 1..=10 {
2687 let cell = format!("A{}", i);
2688 wb.set_cell_value("Sheet1", &cell, CellValue::Number(i as f64))
2689 .unwrap();
2690 }
2691 let buf = wb.save_to_buffer().unwrap();
2692
2693 let opts = OpenOptions::new().sheet_rows(3);
2694 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
2695
2696 assert_eq!(
2697 wb2.get_cell_value("Sheet1", "A3").unwrap(),
2698 CellValue::Number(3.0)
2699 );
2700 assert_eq!(
2701 wb2.get_cell_value("Sheet1", "A4").unwrap(),
2702 CellValue::Empty
2703 );
2704 }
2705
2706 #[test]
2707 fn test_save_xlsx_preserves_existing_behavior() {
2708 let dir = TempDir::new().unwrap();
2709 let path = dir.path().join("preserved.xlsx");
2710
2711 let mut wb = Workbook::new();
2712 wb.set_cell_value("Sheet1", "A1", CellValue::String("hello".to_string()))
2713 .unwrap();
2714 wb.save(&path).unwrap();
2715
2716 let wb2 = Workbook::open(&path).unwrap();
2717 assert_eq!(wb2.format(), WorkbookFormat::Xlsx);
2718 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
2719 assert_eq!(
2720 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2721 CellValue::String("hello".to_string())
2722 );
2723 }
2724
2725 #[test]
2726 fn test_selective_sheet_parsing() {
2727 let dir = TempDir::new().unwrap();
2728 let path = dir.path().join("selective.xlsx");
2729
2730 let mut wb = Workbook::new();
2731 wb.new_sheet("Sales").unwrap();
2732 wb.new_sheet("Data").unwrap();
2733 wb.set_cell_value("Sheet1", "A1", CellValue::String("Sheet1 data".to_string()))
2734 .unwrap();
2735 wb.set_cell_value("Sales", "A1", CellValue::String("Sales data".to_string()))
2736 .unwrap();
2737 wb.set_cell_value("Data", "A1", CellValue::String("Data data".to_string()))
2738 .unwrap();
2739 wb.save(&path).unwrap();
2740
2741 let opts = OpenOptions::new().sheets(vec!["Sales".to_string()]);
2742 let wb2 = Workbook::open_with_options(&path, &opts).unwrap();
2743
2744 assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Sales", "Data"]);
2746
2747 assert_eq!(
2749 wb2.get_cell_value("Sales", "A1").unwrap(),
2750 CellValue::String("Sales data".to_string())
2751 );
2752
2753 assert_eq!(
2755 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2756 CellValue::Empty
2757 );
2758 assert_eq!(wb2.get_cell_value("Data", "A1").unwrap(), CellValue::Empty);
2759 }
2760
2761 #[test]
2762 fn test_selective_sheets_multiple() {
2763 let mut wb = Workbook::new();
2764 wb.new_sheet("Alpha").unwrap();
2765 wb.new_sheet("Beta").unwrap();
2766 wb.set_cell_value("Sheet1", "A1", CellValue::Number(1.0))
2767 .unwrap();
2768 wb.set_cell_value("Alpha", "A1", CellValue::Number(2.0))
2769 .unwrap();
2770 wb.set_cell_value("Beta", "A1", CellValue::Number(3.0))
2771 .unwrap();
2772 let buf = wb.save_to_buffer().unwrap();
2773
2774 let opts = OpenOptions::new().sheets(vec!["Sheet1".to_string(), "Beta".to_string()]);
2775 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
2776
2777 assert_eq!(
2778 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2779 CellValue::Number(1.0)
2780 );
2781 assert_eq!(wb2.get_cell_value("Alpha", "A1").unwrap(), CellValue::Empty);
2782 assert_eq!(
2783 wb2.get_cell_value("Beta", "A1").unwrap(),
2784 CellValue::Number(3.0)
2785 );
2786 }
2787
2788 #[test]
2789 fn test_save_does_not_mutate_stored_format() {
2790 let dir = TempDir::new().unwrap();
2791 let path = dir.path().join("test.xlsm");
2792 let wb = Workbook::new();
2793 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2794 wb.save(&path).unwrap();
2795 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2797 }
2798
2799 #[test]
2800 fn test_max_zip_entries_exceeded() {
2801 let wb = Workbook::new();
2802 let buf = wb.save_to_buffer().unwrap();
2803
2804 let opts = OpenOptions::new().max_zip_entries(2);
2806 let result = Workbook::open_from_buffer_with_options(&buf, &opts);
2807 assert!(matches!(result, Err(Error::ZipEntryCountExceeded { .. })));
2808 }
2809
2810 #[test]
2811 fn test_max_zip_entries_within_limit() {
2812 let wb = Workbook::new();
2813 let buf = wb.save_to_buffer().unwrap();
2814
2815 let opts = OpenOptions::new().max_zip_entries(1000);
2816 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
2817 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
2818 }
2819
2820 #[test]
2821 fn test_max_unzip_size_exceeded() {
2822 let mut wb = Workbook::new();
2823 for i in 1..=100 {
2825 let cell = format!("A{}", i);
2826 wb.set_cell_value(
2827 "Sheet1",
2828 &cell,
2829 CellValue::String("long_value_for_size_check".repeat(10)),
2830 )
2831 .unwrap();
2832 }
2833 let buf = wb.save_to_buffer().unwrap();
2834
2835 let opts = OpenOptions::new().max_unzip_size(100);
2837 let result = Workbook::open_from_buffer_with_options(&buf, &opts);
2838 assert!(matches!(result, Err(Error::ZipSizeExceeded { .. })));
2839 }
2840
2841 #[test]
2842 fn test_max_unzip_size_within_limit() {
2843 let wb = Workbook::new();
2844 let buf = wb.save_to_buffer().unwrap();
2845
2846 let opts = OpenOptions::new().max_unzip_size(1_000_000_000);
2847 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
2848 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
2849 }
2850
2851 #[test]
2852 fn test_combined_options() {
2853 let mut wb = Workbook::new();
2854 wb.new_sheet("Parsed").unwrap();
2855 wb.new_sheet("Skipped").unwrap();
2856 for i in 1..=10 {
2857 let cell = format!("A{}", i);
2858 wb.set_cell_value("Parsed", &cell, CellValue::Number(i as f64))
2859 .unwrap();
2860 wb.set_cell_value("Skipped", &cell, CellValue::Number(i as f64))
2861 .unwrap();
2862 }
2863 let buf = wb.save_to_buffer().unwrap();
2864
2865 let opts = OpenOptions::new()
2866 .sheets(vec!["Parsed".to_string()])
2867 .sheet_rows(3);
2868 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
2869
2870 assert_eq!(
2872 wb2.get_cell_value("Parsed", "A3").unwrap(),
2873 CellValue::Number(3.0)
2874 );
2875 assert_eq!(
2876 wb2.get_cell_value("Parsed", "A4").unwrap(),
2877 CellValue::Empty
2878 );
2879
2880 assert_eq!(
2882 wb2.get_cell_value("Skipped", "A1").unwrap(),
2883 CellValue::Empty
2884 );
2885 }
2886
2887 #[test]
2888 fn test_sheet_rows_zero_means_no_rows() {
2889 let mut wb = Workbook::new();
2890 wb.set_cell_value("Sheet1", "A1", CellValue::Number(1.0))
2891 .unwrap();
2892 let buf = wb.save_to_buffer().unwrap();
2893
2894 let opts = OpenOptions::new().sheet_rows(0);
2895 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
2896 assert_eq!(
2897 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2898 CellValue::Empty
2899 );
2900 }
2901
2902 #[test]
2903 fn test_selective_sheet_parsing_preserves_unparsed_sheets_on_save() {
2904 let dir = TempDir::new().unwrap();
2905 let path1 = dir.path().join("original.xlsx");
2906 let path2 = dir.path().join("resaved.xlsx");
2907
2908 let mut wb = Workbook::new();
2910 wb.new_sheet("Sales").unwrap();
2911 wb.new_sheet("Data").unwrap();
2912 wb.set_cell_value(
2913 "Sheet1",
2914 "A1",
2915 CellValue::String("Sheet1 value".to_string()),
2916 )
2917 .unwrap();
2918 wb.set_cell_value("Sheet1", "B2", CellValue::Number(100.0))
2919 .unwrap();
2920 wb.set_cell_value("Sales", "A1", CellValue::String("Sales value".to_string()))
2921 .unwrap();
2922 wb.set_cell_value("Sales", "C3", CellValue::Number(200.0))
2923 .unwrap();
2924 wb.set_cell_value("Data", "A1", CellValue::String("Data value".to_string()))
2925 .unwrap();
2926 wb.set_cell_value("Data", "D4", CellValue::Bool(true))
2927 .unwrap();
2928 wb.save(&path1).unwrap();
2929
2930 let opts = OpenOptions::new().sheets(vec!["Sheet1".to_string()]);
2932 let wb2 = Workbook::open_with_options(&path1, &opts).unwrap();
2933
2934 assert_eq!(
2936 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2937 CellValue::String("Sheet1 value".to_string())
2938 );
2939
2940 wb2.save(&path2).unwrap();
2942
2943 let wb3 = Workbook::open(&path2).unwrap();
2945 assert_eq!(wb3.sheet_names(), vec!["Sheet1", "Sales", "Data"]);
2946
2947 assert_eq!(
2949 wb3.get_cell_value("Sheet1", "A1").unwrap(),
2950 CellValue::String("Sheet1 value".to_string())
2951 );
2952 assert_eq!(
2953 wb3.get_cell_value("Sheet1", "B2").unwrap(),
2954 CellValue::Number(100.0)
2955 );
2956
2957 assert_eq!(
2959 wb3.get_cell_value("Sales", "A1").unwrap(),
2960 CellValue::String("Sales value".to_string())
2961 );
2962 assert_eq!(
2963 wb3.get_cell_value("Sales", "C3").unwrap(),
2964 CellValue::Number(200.0)
2965 );
2966
2967 assert_eq!(
2969 wb3.get_cell_value("Data", "A1").unwrap(),
2970 CellValue::String("Data value".to_string())
2971 );
2972 assert_eq!(
2973 wb3.get_cell_value("Data", "D4").unwrap(),
2974 CellValue::Bool(true)
2975 );
2976 }
2977
2978 #[test]
2979 fn test_open_from_buffer_with_options_backwards_compatible() {
2980 let mut wb = Workbook::new();
2981 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
2982 .unwrap();
2983 let buf = wb.save_to_buffer().unwrap();
2984
2985 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2986 assert_eq!(
2987 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2988 CellValue::String("Hello".to_string())
2989 );
2990 }
2991}