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 workbook_xml = WorkbookXml {
15 file_version: Some(sheetkit_xml::workbook::FileVersion {
17 app_name: Some("xl".to_string()),
18 last_edited: Some("7".to_string()),
19 lowest_edited: Some("7".to_string()),
20 rup_build: Some("27425".to_string()),
21 }),
22 workbook_pr: Some(sheetkit_xml::workbook::WorkbookPr {
24 date1904: None,
25 filter_privacy: None,
26 default_theme_version: Some(166925),
27 show_objects: None,
28 backup_file: None,
29 code_name: None,
30 check_compatibility: None,
31 auto_compress_pictures: None,
32 save_external_link_values: None,
33 update_links: None,
34 hide_pivot_field_list: None,
35 show_pivot_chart_filter: None,
36 allow_refresh_query: None,
37 publish_items: None,
38 show_border_unselected_tables: None,
39 prompted_solutions: None,
40 show_ink_annotation: None,
41 }),
42 book_views: Some(sheetkit_xml::workbook::BookViews {
44 workbook_views: vec![sheetkit_xml::workbook::WorkbookView {
45 x_window: None,
46 y_window: None,
47 window_width: None,
48 window_height: None,
49 active_tab: Some(0),
50 }],
51 }),
52 ..WorkbookXml::default()
53 };
54
55 let sst_runtime = SharedStringTable::new();
56 let mut sheet_name_index = HashMap::new();
57 sheet_name_index.insert("Sheet1".to_string(), 0);
58 Self {
59 format: WorkbookFormat::default(),
60 content_types: ContentTypes::default(),
61 package_rels: relationships::package_rels(),
62 workbook_xml,
63 workbook_rels: relationships::workbook_rels(),
64 worksheets: vec![(
65 "Sheet1".to_string(),
66 initialized_lock(WorksheetXml::default()),
67 )],
68 stylesheet: StyleSheet::default(),
69 sst_runtime,
70 sheet_comments: vec![None],
71 charts: vec![],
72 raw_charts: vec![],
73 drawings: vec![],
74 images: vec![],
75 worksheet_drawings: HashMap::new(),
76 worksheet_rels: HashMap::new(),
77 drawing_rels: HashMap::new(),
78 core_properties: None,
79 app_properties: None,
80 custom_properties: None,
81 pivot_tables: vec![],
82 pivot_cache_defs: vec![],
83 pivot_cache_records: vec![],
84 theme_xml: None,
85 theme_colors: crate::theme::default_theme_colors(),
86 sheet_name_index,
87 sheet_sparklines: vec![vec![]],
88 sheet_vml: vec![None],
89 unknown_parts: vec![],
90 deferred_parts: crate::workbook::aux::DeferredAuxParts::new(),
91 vba_blob: None,
92 tables: vec![],
93 raw_sheet_xml: vec![None],
94 sheet_dirty: vec![true],
95 slicer_defs: vec![],
96 slicer_caches: vec![],
97 sheet_threaded_comments: vec![None],
98 person_list: sheetkit_xml::threaded_comment::PersonList::default(),
99 sheet_form_controls: vec![vec![]],
100 streamed_sheets: HashMap::new(),
101 package_source: None,
102 read_mode: ReadMode::default(),
103 sheet_rows_limit: None,
104 date_interpretation: super::DateInterpretation::default(),
105 }
106 }
107
108 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
113 Self::open_with_options(path, &OpenOptions::default())
114 }
115
116 pub fn open_with_options<P: AsRef<Path>>(path: P, options: &OpenOptions) -> Result<Self> {
124 let file_path = path.as_ref();
125
126 #[cfg(feature = "encryption")]
128 {
129 let mut header = [0u8; 8];
130 if let Ok(mut f) = std::fs::File::open(file_path) {
131 use std::io::Read as _;
132 if f.read_exact(&mut header).is_ok() {
133 if let Ok(crate::crypt::ContainerFormat::Cfb) =
134 crate::crypt::detect_container_format(&header)
135 {
136 return Err(Error::FileEncrypted);
137 }
138 }
139 }
140 }
141
142 let file = std::fs::File::open(file_path)?;
143 let mut archive = zip::ZipArchive::new(file).map_err(|e| Error::Zip(e.to_string()))?;
144 let mut wb = Self::from_archive(&mut archive, options)?;
145 wb.package_source = Some(PackageSource::Path(file_path.to_path_buf()));
146 wb.read_mode = options.read_mode;
147 Ok(wb)
148 }
149
150 fn from_archive<R: std::io::Read + std::io::Seek>(
152 archive: &mut zip::ZipArchive<R>,
153 options: &OpenOptions,
154 ) -> Result<Self> {
155 if let Some(max_entries) = options.max_zip_entries {
157 let count = archive.len();
158 if count > max_entries {
159 return Err(Error::ZipEntryCountExceeded {
160 count,
161 limit: max_entries,
162 });
163 }
164 }
165 if let Some(max_size) = options.max_unzip_size {
166 let mut total_size: u64 = 0;
167 for i in 0..archive.len() {
168 let entry = archive.by_index(i).map_err(|e| Error::Zip(e.to_string()))?;
169 total_size = total_size.saturating_add(entry.size());
170 if total_size > max_size {
171 return Err(Error::ZipSizeExceeded {
172 size: total_size,
173 limit: max_size,
174 });
175 }
176 }
177 }
178
179 let mut known_paths: HashSet<String> = HashSet::new();
182
183 let content_types: ContentTypes = read_xml_part(archive, "[Content_Types].xml")?;
185 known_paths.insert("[Content_Types].xml".to_string());
186
187 let format = content_types
189 .overrides
190 .iter()
191 .find(|o| o.part_name == "/xl/workbook.xml")
192 .and_then(|o| WorkbookFormat::from_content_type(&o.content_type))
193 .unwrap_or_default();
194
195 let package_rels: Relationships = read_xml_part(archive, "_rels/.rels")?;
197 known_paths.insert("_rels/.rels".to_string());
198
199 let workbook_xml: WorkbookXml = read_xml_part(archive, "xl/workbook.xml")?;
201 known_paths.insert("xl/workbook.xml".to_string());
202
203 let workbook_rels: Relationships = read_xml_part(archive, "xl/_rels/workbook.xml.rels")?;
205 known_paths.insert("xl/_rels/workbook.xml.rels".to_string());
206
207 let sheet_count = workbook_xml.sheets.sheets.len();
209 let mut worksheets: Vec<(String, OnceLock<WorksheetXml>)> = Vec::with_capacity(sheet_count);
210 let mut worksheet_paths = Vec::with_capacity(sheet_count);
211 let mut raw_sheet_xml: Vec<Option<Vec<u8>>> = Vec::with_capacity(sheet_count);
212
213 let defer_sheets = matches!(options.read_mode, ReadMode::Lazy | ReadMode::Stream);
214
215 for sheet_entry in &workbook_xml.sheets.sheets {
216 let rel = workbook_rels
218 .relationships
219 .iter()
220 .find(|r| r.id == sheet_entry.r_id && r.rel_type == rel_types::WORKSHEET);
221
222 let rel = rel.ok_or_else(|| {
223 Error::Internal(format!(
224 "missing worksheet relationship for sheet '{}'",
225 sheet_entry.name
226 ))
227 })?;
228
229 let sheet_path = resolve_relationship_target("xl/workbook.xml", &rel.target);
230
231 let should_parse = options.should_parse_sheet(&sheet_entry.name);
232
233 if should_parse && !defer_sheets {
234 let mut ws: WorksheetXml = read_xml_part(archive, &sheet_path)?;
236 for row in &mut ws.sheet_data.rows {
237 row.cells.shrink_to_fit();
238 }
239 ws.sheet_data.rows.shrink_to_fit();
240 worksheets.push((sheet_entry.name.clone(), initialized_lock(ws)));
241 raw_sheet_xml.push(None);
242 } else if !should_parse {
243 let raw_bytes = read_bytes_part(archive, &sheet_path)?;
247 worksheets.push((
248 sheet_entry.name.clone(),
249 initialized_lock(WorksheetXml::default()),
250 ));
251 raw_sheet_xml.push(Some(raw_bytes));
252 } else {
253 let raw_bytes = read_bytes_part(archive, &sheet_path)?;
257 worksheets.push((sheet_entry.name.clone(), OnceLock::new()));
258 raw_sheet_xml.push(Some(raw_bytes));
259 }
260 known_paths.insert(sheet_path.clone());
261 worksheet_paths.push(sheet_path);
262 }
263
264 let stylesheet: StyleSheet = read_xml_part(archive, "xl/styles.xml")?;
266 known_paths.insert("xl/styles.xml".to_string());
267
268 let shared_strings: Sst =
270 read_xml_part(archive, "xl/sharedStrings.xml").unwrap_or_default();
271 known_paths.insert("xl/sharedStrings.xml".to_string());
272
273 let sst_runtime = SharedStringTable::from_sst(shared_strings);
274
275 let (theme_xml, theme_colors) = match read_bytes_part(archive, "xl/theme/theme1.xml") {
277 Ok(bytes) => {
278 let colors = sheetkit_xml::theme::parse_theme_colors(&bytes);
279 (Some(bytes), colors)
280 }
281 Err(_) => (None, crate::theme::default_theme_colors()),
282 };
283 known_paths.insert("xl/theme/theme1.xml".to_string());
284
285 let mut worksheet_rels: HashMap<usize, Relationships> = HashMap::with_capacity(sheet_count);
288 for (i, sheet_path) in worksheet_paths.iter().enumerate() {
289 let rels_path = relationship_part_path(sheet_path);
290 if let Ok(rels) = read_xml_part::<Relationships, _>(archive, &rels_path) {
291 worksheet_rels.insert(i, rels);
292 known_paths.insert(rels_path);
293 }
294 }
295
296 let skip_aux = options.skip_aux_parts();
297
298 let mut sheet_comments: Vec<Option<Comments>> = vec![None; worksheets.len()];
300 let mut sheet_vml: Vec<Option<Vec<u8>>> = vec![None; worksheets.len()];
301 let mut drawings: Vec<(String, WsDr)> = Vec::new();
302 let mut worksheet_drawings: HashMap<usize, usize> = HashMap::new();
303 let mut drawing_rels: HashMap<usize, Relationships> = HashMap::new();
304 let mut charts: Vec<(String, ChartSpace)> = Vec::new();
305 let mut raw_charts: Vec<(String, Vec<u8>)> = Vec::new();
306 let mut images: Vec<(String, Vec<u8>)> = Vec::new();
307 let mut core_properties: Option<sheetkit_xml::doc_props::CoreProperties> = None;
308 let mut app_properties: Option<sheetkit_xml::doc_props::ExtendedProperties> = None;
309 let mut custom_properties: Option<sheetkit_xml::doc_props::CustomProperties> = None;
310 let mut pivot_cache_defs = Vec::new();
311 let mut pivot_tables = Vec::new();
312 let mut pivot_cache_records = Vec::new();
313 let mut slicer_defs = Vec::new();
314 let mut slicer_caches = Vec::new();
315 let mut sheet_threaded_comments: Vec<
316 Option<sheetkit_xml::threaded_comment::ThreadedComments>,
317 > = vec![None; worksheets.len()];
318 let mut person_list = sheetkit_xml::threaded_comment::PersonList::default();
319 let mut sheet_sparklines: Vec<Vec<crate::sparkline::SparklineConfig>> =
320 vec![vec![]; worksheets.len()];
321 let mut vba_blob: Option<Vec<u8>> = None;
322 let mut tables: Vec<(String, sheetkit_xml::table::TableXml, usize)> = Vec::new();
323
324 if !skip_aux {
325 let mut drawing_path_to_idx: HashMap<String, usize> = HashMap::new();
326
327 for (sheet_idx, sheet_path) in worksheet_paths.iter().enumerate() {
328 let Some(rels) = worksheet_rels.get(&sheet_idx) else {
329 continue;
330 };
331
332 if let Some(comment_rel) = rels
333 .relationships
334 .iter()
335 .find(|r| r.rel_type == rel_types::COMMENTS)
336 {
337 let comment_path = resolve_relationship_target(sheet_path, &comment_rel.target);
338 if let Ok(comments) = read_xml_part::<Comments, _>(archive, &comment_path) {
339 sheet_comments[sheet_idx] = Some(comments);
340 known_paths.insert(comment_path);
341 }
342 }
343
344 if let Some(vml_rel) = rels
345 .relationships
346 .iter()
347 .find(|r| r.rel_type == rel_types::VML_DRAWING)
348 {
349 let vml_path = resolve_relationship_target(sheet_path, &vml_rel.target);
350 if let Ok(bytes) = read_bytes_part(archive, &vml_path) {
351 sheet_vml[sheet_idx] = Some(bytes);
352 known_paths.insert(vml_path);
353 }
354 }
355
356 if let Some(drawing_rel) = rels
357 .relationships
358 .iter()
359 .find(|r| r.rel_type == rel_types::DRAWING)
360 {
361 let drawing_path = resolve_relationship_target(sheet_path, &drawing_rel.target);
362 let drawing_idx = if let Some(idx) = drawing_path_to_idx.get(&drawing_path) {
363 *idx
364 } else if let Ok(drawing) = read_xml_part::<WsDr, _>(archive, &drawing_path) {
365 let idx = drawings.len();
366 drawings.push((drawing_path.clone(), drawing));
367 drawing_path_to_idx.insert(drawing_path.clone(), idx);
368 known_paths.insert(drawing_path);
369 idx
370 } else {
371 continue;
372 };
373 worksheet_drawings.insert(sheet_idx, drawing_idx);
374 }
375 }
376
377 for ovr in &content_types.overrides {
380 if ovr.content_type != mime_types::DRAWING {
381 continue;
382 }
383 let drawing_path = ovr.part_name.trim_start_matches('/').to_string();
384 if drawing_path_to_idx.contains_key(&drawing_path) {
385 continue;
386 }
387 if let Ok(drawing) = read_xml_part::<WsDr, _>(archive, &drawing_path) {
388 let idx = drawings.len();
389 drawings.push((drawing_path.clone(), drawing));
390 known_paths.insert(drawing_path.clone());
391 drawing_path_to_idx.insert(drawing_path, idx);
392 }
393 }
394
395 let mut seen_chart_paths: HashSet<String> = HashSet::new();
396 let mut seen_image_paths: HashSet<String> = HashSet::new();
397
398 for (drawing_idx, (drawing_path, _)) in drawings.iter().enumerate() {
399 let drawing_rels_path = relationship_part_path(drawing_path);
400 let Ok(rels) = read_xml_part::<Relationships, _>(archive, &drawing_rels_path)
401 else {
402 continue;
403 };
404 known_paths.insert(drawing_rels_path);
405
406 for rel in &rels.relationships {
407 if rel.rel_type == rel_types::CHART {
408 let chart_path = resolve_relationship_target(drawing_path, &rel.target);
409 if seen_chart_paths.insert(chart_path.clone()) {
410 match read_xml_part::<ChartSpace, _>(archive, &chart_path) {
411 Ok(chart) => {
412 known_paths.insert(chart_path.clone());
413 charts.push((chart_path, chart));
414 }
415 Err(_) => {
416 if let Ok(bytes) = read_bytes_part(archive, &chart_path) {
417 known_paths.insert(chart_path.clone());
418 raw_charts.push((chart_path, bytes));
419 }
420 }
421 }
422 }
423 } else if rel.rel_type == rel_types::IMAGE {
424 let image_path = resolve_relationship_target(drawing_path, &rel.target);
425 if seen_image_paths.insert(image_path.clone()) {
426 if let Ok(bytes) = read_bytes_part(archive, &image_path) {
427 known_paths.insert(image_path.clone());
428 images.push((image_path, bytes));
429 }
430 }
431 }
432 }
433
434 drawing_rels.insert(drawing_idx, rels);
435 }
436
437 for ovr in &content_types.overrides {
440 if ovr.content_type != mime_types::CHART {
441 continue;
442 }
443 let chart_path = ovr.part_name.trim_start_matches('/').to_string();
444 if seen_chart_paths.insert(chart_path.clone()) {
445 match read_xml_part::<ChartSpace, _>(archive, &chart_path) {
446 Ok(chart) => {
447 known_paths.insert(chart_path.clone());
448 charts.push((chart_path, chart));
449 }
450 Err(_) => {
451 if let Ok(bytes) = read_bytes_part(archive, &chart_path) {
452 known_paths.insert(chart_path.clone());
453 raw_charts.push((chart_path, bytes));
454 }
455 }
456 }
457 }
458 }
459
460 core_properties = read_string_part(archive, "docProps/core.xml")
462 .ok()
463 .and_then(|xml_str| {
464 sheetkit_xml::doc_props::deserialize_core_properties(&xml_str).ok()
465 });
466 known_paths.insert("docProps/core.xml".to_string());
467
468 app_properties = read_xml_part(archive, "docProps/app.xml").ok();
470 known_paths.insert("docProps/app.xml".to_string());
471
472 custom_properties = read_string_part(archive, "docProps/custom.xml")
474 .ok()
475 .and_then(|xml_str| {
476 sheetkit_xml::doc_props::deserialize_custom_properties(&xml_str).ok()
477 });
478 known_paths.insert("docProps/custom.xml".to_string());
479
480 for ovr in &content_types.overrides {
482 let path = ovr.part_name.trim_start_matches('/');
483 if ovr.content_type == mime_types::PIVOT_CACHE_DEFINITION {
484 if let Ok(pcd) = read_xml_part::<
485 sheetkit_xml::pivot_cache::PivotCacheDefinition,
486 _,
487 >(archive, path)
488 {
489 known_paths.insert(path.to_string());
490 pivot_cache_defs.push((path.to_string(), pcd));
491 }
492 } else if ovr.content_type == mime_types::PIVOT_TABLE {
493 if let Ok(pt) = read_xml_part::<
494 sheetkit_xml::pivot_table::PivotTableDefinition,
495 _,
496 >(archive, path)
497 {
498 known_paths.insert(path.to_string());
499 pivot_tables.push((path.to_string(), pt));
500 }
501 } else if ovr.content_type == mime_types::PIVOT_CACHE_RECORDS {
502 if let Ok(pcr) = read_xml_part::<sheetkit_xml::pivot_cache::PivotCacheRecords, _>(
503 archive, path,
504 ) {
505 known_paths.insert(path.to_string());
506 pivot_cache_records.push((path.to_string(), pcr));
507 }
508 }
509 }
510
511 for ovr in &content_types.overrides {
513 let path = ovr.part_name.trim_start_matches('/');
514 if ovr.content_type == mime_types::SLICER {
515 if let Ok(sd) =
516 read_xml_part::<sheetkit_xml::slicer::SlicerDefinitions, _>(archive, path)
517 {
518 slicer_defs.push((path.to_string(), sd));
519 }
520 } else if ovr.content_type == mime_types::SLICER_CACHE {
521 if let Ok(raw) = read_string_part(archive, path) {
522 if let Some(scd) = sheetkit_xml::slicer::parse_slicer_cache(&raw) {
523 slicer_caches.push((path.to_string(), scd));
524 }
525 }
526 }
527 }
528
529 for (sheet_idx, sheet_path) in worksheet_paths.iter().enumerate() {
531 let Some(rels) = worksheet_rels.get(&sheet_idx) else {
532 continue;
533 };
534 if let Some(tc_rel) = rels.relationships.iter().find(|r| {
535 r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_THREADED_COMMENT
536 }) {
537 let tc_path = resolve_relationship_target(sheet_path, &tc_rel.target);
538 if let Ok(tc) = read_xml_part::<
539 sheetkit_xml::threaded_comment::ThreadedComments,
540 _,
541 >(archive, &tc_path)
542 {
543 sheet_threaded_comments[sheet_idx] = Some(tc);
544 known_paths.insert(tc_path);
545 }
546 }
547 }
548
549 person_list = {
551 let mut found = None;
552 if let Some(person_rel) = workbook_rels
553 .relationships
554 .iter()
555 .find(|r| r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_PERSON)
556 {
557 let person_path =
558 resolve_relationship_target("xl/workbook.xml", &person_rel.target);
559 if let Ok(pl) = read_xml_part::<sheetkit_xml::threaded_comment::PersonList, _>(
560 archive,
561 &person_path,
562 ) {
563 known_paths.insert(person_path);
564 found = Some(pl);
565 }
566 }
567 if found.is_none() {
568 if let Ok(pl) = read_xml_part::<sheetkit_xml::threaded_comment::PersonList, _>(
569 archive,
570 "xl/persons/person.xml",
571 ) {
572 known_paths.insert("xl/persons/person.xml".to_string());
573 found = Some(pl);
574 }
575 }
576 found.unwrap_or_default()
577 };
578
579 for (i, ws_path) in worksheet_paths.iter().enumerate() {
581 if let Ok(raw) = read_string_part(archive, ws_path) {
582 let parsed = parse_sparklines_from_xml(&raw);
583 if !parsed.is_empty() {
584 sheet_sparklines[i] = parsed;
585 }
586 }
587 }
588
589 vba_blob = read_bytes_part(archive, "xl/vbaProject.bin").ok();
591 if vba_blob.is_some() {
592 known_paths.insert("xl/vbaProject.bin".to_string());
593 }
594
595 for (sheet_idx, sheet_path) in worksheet_paths.iter().enumerate() {
597 let Some(rels) = worksheet_rels.get(&sheet_idx) else {
598 continue;
599 };
600 for rel in &rels.relationships {
601 if rel.rel_type != rel_types::TABLE {
602 continue;
603 }
604 let table_path = resolve_relationship_target(sheet_path, &rel.target);
605 if let Ok(table_xml) =
606 read_xml_part::<sheetkit_xml::table::TableXml, _>(archive, &table_path)
607 {
608 known_paths.insert(table_path.clone());
609 tables.push((table_path, table_xml, sheet_idx));
610 }
611 }
612 }
613 for ovr in &content_types.overrides {
615 if ovr.content_type != mime_types::TABLE {
616 continue;
617 }
618 let table_path = ovr.part_name.trim_start_matches('/').to_string();
619 if tables.iter().any(|(p, _, _)| p == &table_path) {
620 continue;
621 }
622 if let Ok(table_xml) =
623 read_xml_part::<sheetkit_xml::table::TableXml, _>(archive, &table_path)
624 {
625 known_paths.insert(table_path.clone());
626 tables.push((table_path, table_xml, 0));
627 }
628 }
629 }
630
631 let sheet_form_controls: Vec<Vec<crate::control::FormControlConfig>> =
632 vec![vec![]; worksheets.len()];
633
634 let mut sheet_name_index = HashMap::with_capacity(worksheets.len());
636 for (i, (name, _)) in worksheets.iter().enumerate() {
637 sheet_name_index.insert(name.clone(), i);
638 }
639
640 let mut unknown_parts: Vec<(String, Vec<u8>)> = Vec::new();
644 let mut deferred_parts = crate::workbook::aux::DeferredAuxParts::new();
645 for i in 0..archive.len() {
646 let Ok(entry) = archive.by_index(i) else {
647 continue;
648 };
649 let name = entry.name().to_string();
650 drop(entry);
651 if !known_paths.contains(&name) {
652 if let Ok(bytes) = read_bytes_part(archive, &name) {
653 if skip_aux && crate::workbook::aux::classify_deferred_path(&name).is_some() {
654 deferred_parts.insert(name, bytes);
655 } else {
656 unknown_parts.push((name, bytes));
657 }
658 }
659 }
660 }
661
662 for (_name, ws_lock) in &mut worksheets {
667 let Some(ws) = ws_lock.get_mut() else {
668 continue;
669 };
670 ws.sheet_data.rows.sort_unstable_by_key(|r| r.r);
672
673 if let Some(max_rows) = options.sheet_rows {
675 ws.sheet_data.rows.truncate(max_rows as usize);
676 }
677
678 for row in &mut ws.sheet_data.rows {
679 for cell in &mut row.cells {
680 cell.col = fast_col_number(cell.r.as_str());
681 }
682 row.cells.sort_unstable_by_key(|c| c.col);
684 }
685 }
686
687 Ok(Self {
688 format,
689 content_types,
690 package_rels,
691 workbook_xml,
692 workbook_rels,
693 worksheets,
694 stylesheet,
695 sst_runtime,
696 sheet_comments,
697 charts,
698 raw_charts,
699 drawings,
700 images,
701 worksheet_drawings,
702 worksheet_rels,
703 drawing_rels,
704 core_properties,
705 app_properties,
706 custom_properties,
707 pivot_tables,
708 pivot_cache_defs,
709 pivot_cache_records,
710 theme_xml,
711 theme_colors,
712 sheet_name_index,
713 sheet_sparklines,
714 sheet_vml,
715 unknown_parts,
716 deferred_parts,
717 vba_blob,
718 tables,
719 sheet_dirty: raw_sheet_xml.iter().map(|raw| raw.is_none()).collect(),
723 raw_sheet_xml,
724 slicer_defs,
725 slicer_caches,
726 sheet_threaded_comments,
727 person_list,
728 sheet_form_controls,
729 streamed_sheets: HashMap::new(),
730 package_source: None,
731 read_mode: options.read_mode,
732 sheet_rows_limit: options.sheet_rows,
733 date_interpretation: options.date_interpretation,
734 })
735 }
736
737 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
746 let path = path.as_ref();
747 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
748 let target_format = WorkbookFormat::from_extension(ext)
749 .ok_or_else(|| Error::UnsupportedFileExtension(ext.to_string()))?;
750
751 let file = std::fs::File::create(path)?;
752 let mut zip = zip::ZipWriter::new(file);
753 let options = SimpleFileOptions::default()
754 .compression_method(CompressionMethod::Deflated)
755 .compression_level(Some(1));
756 self.write_zip_contents(&mut zip, options, Some(target_format))?;
757 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
758 Ok(())
759 }
760
761 pub fn save_to_buffer(&self) -> Result<Vec<u8>> {
763 let estimated = self.worksheets.len() * 4000
765 + self.sst_runtime.len() * 60
766 + self.images.iter().map(|(_, d)| d.len()).sum::<usize>()
767 + 32_000;
768 let mut buf = Vec::with_capacity(estimated);
769 {
770 let cursor = std::io::Cursor::new(&mut buf);
771 let mut zip = zip::ZipWriter::new(cursor);
772 let options =
773 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
774 self.write_zip_contents(&mut zip, options, None)?;
775 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
776 }
777 Ok(buf)
778 }
779
780 pub fn open_from_buffer(data: &[u8]) -> Result<Self> {
782 Self::open_from_buffer_with_options(data, &OpenOptions::default())
783 }
784
785 pub fn open_from_buffer_with_options(data: &[u8], options: &OpenOptions) -> Result<Self> {
787 #[cfg(feature = "encryption")]
789 if data.len() >= 8 {
790 if let Ok(crate::crypt::ContainerFormat::Cfb) =
791 crate::crypt::detect_container_format(data)
792 {
793 return Err(Error::FileEncrypted);
794 }
795 }
796
797 let cursor = std::io::Cursor::new(data);
798 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
799 let mut wb = Self::from_archive(&mut archive, options)?;
800 wb.package_source = Some(PackageSource::Buffer(data.into()));
801 wb.read_mode = options.read_mode;
802 Ok(wb)
803 }
804
805 #[cfg(feature = "encryption")]
811 pub fn open_with_password<P: AsRef<Path>>(path: P, password: &str) -> Result<Self> {
812 let data = std::fs::read(path.as_ref())?;
813 let decrypted_zip = crate::crypt::decrypt_xlsx(&data, password)?;
814 let cursor = std::io::Cursor::new(decrypted_zip);
815 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
816 Self::from_archive(&mut archive, &OpenOptions::default())
817 }
818
819 #[cfg(feature = "encryption")]
822 pub fn save_with_password<P: AsRef<Path>>(&self, path: P, password: &str) -> Result<()> {
823 let mut zip_buf = Vec::new();
825 {
826 let cursor = std::io::Cursor::new(&mut zip_buf);
827 let mut zip = zip::ZipWriter::new(cursor);
828 let options =
829 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
830 self.write_zip_contents(&mut zip, options, None)?;
831 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
832 }
833
834 let cfb_data = crate::crypt::encrypt_xlsx(&zip_buf, password)?;
836 std::fs::write(path.as_ref(), &cfb_data)?;
837 Ok(())
838 }
839
840 fn write_zip_contents<W: std::io::Write + std::io::Seek>(
846 &self,
847 zip: &mut zip::ZipWriter<W>,
848 options: SimpleFileOptions,
849 format_override: Option<WorkbookFormat>,
850 ) -> Result<()> {
851 let effective_format = format_override.unwrap_or(self.format);
852 let mut content_types = self.content_types.clone();
853
854 if let Some(wb_override) = content_types
856 .overrides
857 .iter_mut()
858 .find(|o| o.part_name == "/xl/workbook.xml")
859 {
860 wb_override.content_type = effective_format.content_type().to_string();
861 }
862
863 let has_deferred = self.deferred_parts.has_any();
867 let mut workbook_rels = self.workbook_rels.clone();
868 if self.vba_blob.is_some() {
869 let vba_part_name = "/xl/vbaProject.bin";
870 if !content_types
871 .overrides
872 .iter()
873 .any(|o| o.part_name == vba_part_name)
874 {
875 content_types.overrides.push(ContentTypeOverride {
876 part_name: vba_part_name.to_string(),
877 content_type: VBA_PROJECT_CONTENT_TYPE.to_string(),
878 });
879 }
880 if !content_types.defaults.iter().any(|d| d.extension == "bin") {
881 content_types.defaults.push(ContentTypeDefault {
882 extension: "bin".to_string(),
883 content_type: VBA_PROJECT_CONTENT_TYPE.to_string(),
884 });
885 }
886 if !workbook_rels
887 .relationships
888 .iter()
889 .any(|r| r.rel_type == VBA_PROJECT_REL_TYPE)
890 {
891 let rid = crate::sheet::next_rid(&workbook_rels.relationships);
892 workbook_rels.relationships.push(Relationship {
893 id: rid,
894 rel_type: VBA_PROJECT_REL_TYPE.to_string(),
895 target: "vbaProject.bin".to_string(),
896 target_mode: None,
897 });
898 }
899 } else if !has_deferred {
900 content_types
901 .overrides
902 .retain(|o| o.content_type != VBA_PROJECT_CONTENT_TYPE);
903 workbook_rels
904 .relationships
905 .retain(|r| r.rel_type != VBA_PROJECT_REL_TYPE);
906 }
907
908 let mut worksheet_rels = self.worksheet_rels.clone();
909
910 let mut vml_parts_to_write: Vec<(usize, String, Vec<u8>)> = Vec::new();
913 let mut legacy_drawing_rids: HashMap<usize, String> = HashMap::new();
915
916 let mut has_any_vml = false;
918
919 for sheet_idx in 0..self.worksheets.len() {
923 let has_comments = self
924 .sheet_comments
925 .get(sheet_idx)
926 .and_then(|c| c.as_ref())
927 .is_some();
928 let has_form_controls = self
929 .sheet_form_controls
930 .get(sheet_idx)
931 .map(|v| !v.is_empty())
932 .unwrap_or(false);
933 let has_preserved_vml = self
934 .sheet_vml
935 .get(sheet_idx)
936 .and_then(|v| v.as_ref())
937 .is_some();
938
939 if has_deferred && !has_comments && !has_form_controls && !has_preserved_vml {
943 continue;
944 }
945
946 if let Some(rels) = worksheet_rels.get_mut(&sheet_idx) {
947 rels.relationships
948 .retain(|r| r.rel_type != rel_types::COMMENTS);
949 rels.relationships
950 .retain(|r| r.rel_type != rel_types::VML_DRAWING);
951 }
952
953 let needs_vml = has_comments || has_form_controls || has_preserved_vml;
954 if !needs_vml && !has_comments {
955 continue;
956 }
957
958 if has_comments {
959 let comment_path = format!("xl/comments{}.xml", sheet_idx + 1);
960 let part_name = format!("/{}", comment_path);
961 if !content_types
962 .overrides
963 .iter()
964 .any(|o| o.part_name == part_name && o.content_type == mime_types::COMMENTS)
965 {
966 content_types.overrides.push(ContentTypeOverride {
967 part_name,
968 content_type: mime_types::COMMENTS.to_string(),
969 });
970 }
971
972 let sheet_path = self.sheet_part_path(sheet_idx);
973 let target = relative_relationship_target(&sheet_path, &comment_path);
974 let rels = worksheet_rels
975 .entry(sheet_idx)
976 .or_insert_with(default_relationships);
977 let rid = crate::sheet::next_rid(&rels.relationships);
978 rels.relationships.push(Relationship {
979 id: rid,
980 rel_type: rel_types::COMMENTS.to_string(),
981 target,
982 target_mode: None,
983 });
984 }
985
986 if !needs_vml {
987 continue;
988 }
989
990 let vml_path = format!("xl/drawings/vmlDrawing{}.vml", sheet_idx + 1);
992 let vml_bytes = if has_comments && has_form_controls {
993 let comment_vml =
995 if let Some(bytes) = self.sheet_vml.get(sheet_idx).and_then(|v| v.as_ref()) {
996 bytes.clone()
997 } else if let Some(Some(comments)) = self.sheet_comments.get(sheet_idx) {
998 let cells: Vec<&str> = comments
999 .comment_list
1000 .comments
1001 .iter()
1002 .map(|c| c.r#ref.as_str())
1003 .collect();
1004 crate::vml::build_vml_drawing(&cells).into_bytes()
1005 } else {
1006 continue;
1007 };
1008 let shape_count = crate::control::count_vml_shapes(&comment_vml);
1009 let start_id = 1025 + shape_count;
1010 let form_controls = &self.sheet_form_controls[sheet_idx];
1011 crate::control::merge_vml_controls(&comment_vml, form_controls, start_id)
1012 } else if has_comments {
1013 if let Some(bytes) = self.sheet_vml.get(sheet_idx).and_then(|v| v.as_ref()) {
1014 bytes.clone()
1015 } else if let Some(Some(comments)) = self.sheet_comments.get(sheet_idx) {
1016 let cells: Vec<&str> = comments
1017 .comment_list
1018 .comments
1019 .iter()
1020 .map(|c| c.r#ref.as_str())
1021 .collect();
1022 crate::vml::build_vml_drawing(&cells).into_bytes()
1023 } else {
1024 continue;
1025 }
1026 } else if has_form_controls {
1027 let form_controls = &self.sheet_form_controls[sheet_idx];
1029 crate::control::build_form_control_vml(form_controls, 1025).into_bytes()
1030 } else if let Some(Some(vml)) = self.sheet_vml.get(sheet_idx) {
1031 vml.clone()
1033 } else {
1034 continue;
1035 };
1036
1037 let vml_part_name = format!("/{}", vml_path);
1038 if !content_types
1039 .overrides
1040 .iter()
1041 .any(|o| o.part_name == vml_part_name && o.content_type == mime_types::VML_DRAWING)
1042 {
1043 content_types.overrides.push(ContentTypeOverride {
1044 part_name: vml_part_name,
1045 content_type: mime_types::VML_DRAWING.to_string(),
1046 });
1047 }
1048
1049 let sheet_path = self.sheet_part_path(sheet_idx);
1050 let rels = worksheet_rels
1051 .entry(sheet_idx)
1052 .or_insert_with(default_relationships);
1053 let vml_target = relative_relationship_target(&sheet_path, &vml_path);
1054 let vml_rid = crate::sheet::next_rid(&rels.relationships);
1055 rels.relationships.push(Relationship {
1056 id: vml_rid.clone(),
1057 rel_type: rel_types::VML_DRAWING.to_string(),
1058 target: vml_target,
1059 target_mode: None,
1060 });
1061
1062 legacy_drawing_rids.insert(sheet_idx, vml_rid);
1063 vml_parts_to_write.push((sheet_idx, vml_path, vml_bytes));
1064 has_any_vml = true;
1065 }
1066
1067 if has_any_vml && !content_types.defaults.iter().any(|d| d.extension == "vml") {
1069 content_types.defaults.push(ContentTypeDefault {
1070 extension: "vml".to_string(),
1071 content_type: mime_types::VML_DRAWING.to_string(),
1072 });
1073 }
1074
1075 use crate::workbook::aux::AuxCategory;
1081 let mut table_parts_by_sheet: HashMap<usize, Vec<String>> = HashMap::new();
1082 let should_sync_tables = !has_deferred
1083 || self.deferred_parts.is_dirty(AuxCategory::Tables)
1084 || !self.tables.is_empty();
1085 if should_sync_tables {
1086 for (sheet_idx, _) in self.worksheets.iter().enumerate() {
1087 if let Some(rels) = worksheet_rels.get_mut(&sheet_idx) {
1088 rels.relationships
1089 .retain(|r| r.rel_type != rel_types::TABLE);
1090 }
1091 }
1092 content_types
1093 .overrides
1094 .retain(|o| o.content_type != mime_types::TABLE);
1095 }
1096 for (table_path, _table_xml, sheet_idx) in &self.tables {
1097 let part_name = format!("/{table_path}");
1098 content_types.overrides.push(ContentTypeOverride {
1099 part_name,
1100 content_type: mime_types::TABLE.to_string(),
1101 });
1102
1103 let sheet_path = self.sheet_part_path(*sheet_idx);
1104 let target = relative_relationship_target(&sheet_path, table_path);
1105 let rels = worksheet_rels
1106 .entry(*sheet_idx)
1107 .or_insert_with(default_relationships);
1108 let rid = crate::sheet::next_rid(&rels.relationships);
1109 rels.relationships.push(Relationship {
1110 id: rid.clone(),
1111 rel_type: rel_types::TABLE.to_string(),
1112 target,
1113 target_mode: None,
1114 });
1115 table_parts_by_sheet
1116 .entry(*sheet_idx)
1117 .or_default()
1118 .push(rid);
1119 }
1120
1121 let has_any_threaded = self.sheet_threaded_comments.iter().any(|tc| tc.is_some());
1123 if has_any_threaded {
1124 for (i, tc) in self.sheet_threaded_comments.iter().enumerate() {
1125 if tc.is_some() {
1126 let tc_path = format!("xl/threadedComments/threadedComment{}.xml", i + 1);
1127 let tc_part_name = format!("/{tc_path}");
1128 if !content_types.overrides.iter().any(|o| {
1129 o.part_name == tc_part_name
1130 && o.content_type
1131 == sheetkit_xml::threaded_comment::THREADED_COMMENTS_CONTENT_TYPE
1132 }) {
1133 content_types.overrides.push(ContentTypeOverride {
1134 part_name: tc_part_name,
1135 content_type:
1136 sheetkit_xml::threaded_comment::THREADED_COMMENTS_CONTENT_TYPE
1137 .to_string(),
1138 });
1139 }
1140
1141 let sheet_path = self.sheet_part_path(i);
1142 let target = relative_relationship_target(&sheet_path, &tc_path);
1143 let rels = worksheet_rels
1144 .entry(i)
1145 .or_insert_with(default_relationships);
1146 if !rels.relationships.iter().any(|r| {
1147 r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_THREADED_COMMENT
1148 }) {
1149 let rid = crate::sheet::next_rid(&rels.relationships);
1150 rels.relationships.push(Relationship {
1151 id: rid,
1152 rel_type: sheetkit_xml::threaded_comment::REL_TYPE_THREADED_COMMENT
1153 .to_string(),
1154 target,
1155 target_mode: None,
1156 });
1157 }
1158 }
1159 }
1160
1161 let person_part_name = "/xl/persons/person.xml";
1162 if !content_types.overrides.iter().any(|o| {
1163 o.part_name == person_part_name
1164 && o.content_type == sheetkit_xml::threaded_comment::PERSON_LIST_CONTENT_TYPE
1165 }) {
1166 content_types.overrides.push(ContentTypeOverride {
1167 part_name: person_part_name.to_string(),
1168 content_type: sheetkit_xml::threaded_comment::PERSON_LIST_CONTENT_TYPE
1169 .to_string(),
1170 });
1171 }
1172
1173 if !workbook_rels
1175 .relationships
1176 .iter()
1177 .any(|r| r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_PERSON)
1178 {
1179 let rid = crate::sheet::next_rid(&workbook_rels.relationships);
1180 workbook_rels.relationships.push(Relationship {
1181 id: rid,
1182 rel_type: sheetkit_xml::threaded_comment::REL_TYPE_PERSON.to_string(),
1183 target: "persons/person.xml".to_string(),
1184 target_mode: None,
1185 });
1186 }
1187 }
1188
1189 write_xml_part(zip, "[Content_Types].xml", &content_types, options)?;
1191
1192 write_xml_part(zip, "_rels/.rels", &self.package_rels, options)?;
1194
1195 write_xml_part(zip, "xl/workbook.xml", &self.workbook_xml, options)?;
1197
1198 write_xml_part(zip, "xl/_rels/workbook.xml.rels", &workbook_rels, options)?;
1200
1201 for (i, (_name, ws_lock)) in self.worksheets.iter().enumerate() {
1203 let entry_name = self.sheet_part_path(i);
1204 let dirty = self.sheet_dirty.get(i).copied().unwrap_or(true);
1205
1206 if let Some(streamed) = self.streamed_sheets.get(&i) {
1208 crate::stream::write_streamed_sheet(zip, &entry_name, streamed, options)?;
1209 continue;
1210 }
1211
1212 let needs_aux_injection =
1222 legacy_drawing_rids.contains_key(&i) || table_parts_by_sheet.contains_key(&i);
1223 if !dirty && !needs_aux_injection {
1224 if let Some(Some(raw_bytes)) = self.raw_sheet_xml.get(i) {
1225 zip.start_file(&entry_name, options)
1226 .map_err(|e| Error::Zip(e.to_string()))?;
1227 zip.write_all(raw_bytes)?;
1228 continue;
1229 }
1230 }
1231
1232 let hydrated_for_save: WorksheetXml;
1242 let ws = if !dirty {
1243 if let Some(Some(raw_bytes)) = self.raw_sheet_xml.get(i) {
1244 hydrated_for_save = deserialize_worksheet_xml(raw_bytes)?;
1245 &hydrated_for_save
1246 } else {
1247 match ws_lock.get() {
1248 Some(ws) => ws,
1249 None => continue,
1250 }
1251 }
1252 } else {
1253 match ws_lock.get() {
1254 Some(ws) => ws,
1255 None => {
1256 if let Some(Some(raw_bytes)) = self.raw_sheet_xml.get(i) {
1257 hydrated_for_save = deserialize_worksheet_xml(raw_bytes)?;
1258 &hydrated_for_save
1259 } else {
1260 continue;
1261 }
1262 }
1263 }
1264 };
1265
1266 let empty_sparklines: Vec<crate::sparkline::SparklineConfig> = vec![];
1267 let sparklines = self.sheet_sparklines.get(i).unwrap_or(&empty_sparklines);
1268 let legacy_rid = legacy_drawing_rids.get(&i).map(|s| s.as_str());
1269 let sheet_table_rids = table_parts_by_sheet.get(&i);
1270 let stale_table_parts =
1271 should_sync_tables && sheet_table_rids.is_none() && ws.table_parts.is_some();
1272 let has_extras = legacy_rid.is_some()
1273 || !sparklines.is_empty()
1274 || sheet_table_rids.is_some()
1275 || stale_table_parts;
1276
1277 if !has_extras {
1278 write_xml_part(zip, &entry_name, ws, options)?;
1279 } else {
1280 let ws_to_serialize;
1281 let ws_ref = if let Some(rids) = sheet_table_rids {
1282 ws_to_serialize = {
1283 let mut cloned = ws.clone();
1284 use sheetkit_xml::worksheet::{TablePart, TableParts};
1285 cloned.table_parts = Some(TableParts {
1286 count: Some(rids.len() as u32),
1287 table_parts: rids
1288 .iter()
1289 .map(|rid| TablePart { r_id: rid.clone() })
1290 .collect(),
1291 });
1292 cloned
1293 };
1294 &ws_to_serialize
1295 } else if stale_table_parts {
1296 ws_to_serialize = {
1297 let mut cloned = ws.clone();
1298 cloned.table_parts = None;
1299 cloned
1300 };
1301 &ws_to_serialize
1302 } else {
1303 ws
1304 };
1305 let xml = serialize_worksheet_with_extras(ws_ref, sparklines, legacy_rid)?;
1306 zip.start_file(&entry_name, options)
1307 .map_err(|e| Error::Zip(e.to_string()))?;
1308 zip.write_all(xml.as_bytes())?;
1309 }
1310 }
1311
1312 write_xml_part(zip, "xl/styles.xml", &self.stylesheet, options)?;
1314
1315 let sst_xml = self.sst_runtime.to_sst();
1317 write_xml_part(zip, "xl/sharedStrings.xml", &sst_xml, options)?;
1318
1319 for (i, comments) in self.sheet_comments.iter().enumerate() {
1321 if let Some(ref c) = comments {
1322 let entry_name = format!("xl/comments{}.xml", i + 1);
1323 write_xml_part(zip, &entry_name, c, options)?;
1324 }
1325 }
1326
1327 for (_sheet_idx, vml_path, vml_bytes) in &vml_parts_to_write {
1329 zip.start_file(vml_path, options)
1330 .map_err(|e| Error::Zip(e.to_string()))?;
1331 zip.write_all(vml_bytes)?;
1332 }
1333
1334 for (path, drawing) in &self.drawings {
1336 write_xml_part(zip, path, drawing, options)?;
1337 }
1338
1339 for (path, chart) in &self.charts {
1341 write_xml_part(zip, path, chart, options)?;
1342 }
1343 for (path, data) in &self.raw_charts {
1344 if self.charts.iter().any(|(p, _)| p == path) {
1345 continue;
1346 }
1347 zip.start_file(path, options)
1348 .map_err(|e| Error::Zip(e.to_string()))?;
1349 zip.write_all(data)?;
1350 }
1351
1352 for (path, data) in &self.images {
1354 zip.start_file(path, options)
1355 .map_err(|e| Error::Zip(e.to_string()))?;
1356 zip.write_all(data)?;
1357 }
1358
1359 for (sheet_idx, rels) in &worksheet_rels {
1361 let sheet_path = self.sheet_part_path(*sheet_idx);
1362 let path = relationship_part_path(&sheet_path);
1363 write_xml_part(zip, &path, rels, options)?;
1364 }
1365
1366 for (drawing_idx, rels) in &self.drawing_rels {
1368 if let Some((drawing_path, _)) = self.drawings.get(*drawing_idx) {
1369 let path = relationship_part_path(drawing_path);
1370 write_xml_part(zip, &path, rels, options)?;
1371 }
1372 }
1373
1374 for (path, pt) in &self.pivot_tables {
1376 write_xml_part(zip, path, pt, options)?;
1377 }
1378
1379 for (path, pcd) in &self.pivot_cache_defs {
1381 write_xml_part(zip, path, pcd, options)?;
1382 }
1383
1384 for (path, pcr) in &self.pivot_cache_records {
1386 write_xml_part(zip, path, pcr, options)?;
1387 }
1388
1389 for (path, table_xml, _sheet_idx) in &self.tables {
1391 write_xml_part(zip, path, table_xml, options)?;
1392 }
1393
1394 for (path, sd) in &self.slicer_defs {
1396 write_xml_part(zip, path, sd, options)?;
1397 }
1398
1399 for (path, scd) in &self.slicer_caches {
1401 let xml_str = format!(
1402 "{}\n{}",
1403 XML_DECLARATION,
1404 sheetkit_xml::slicer::serialize_slicer_cache(scd),
1405 );
1406 zip.start_file(path, options)
1407 .map_err(|e| Error::Zip(e.to_string()))?;
1408 zip.write_all(xml_str.as_bytes())?;
1409 }
1410
1411 {
1413 let default_theme = crate::theme::default_theme_xml();
1414 let theme_bytes = self.theme_xml.as_deref().unwrap_or(&default_theme);
1415 zip.start_file("xl/theme/theme1.xml", options)
1416 .map_err(|e| Error::Zip(e.to_string()))?;
1417 zip.write_all(theme_bytes)?;
1418 }
1419
1420 if let Some(ref blob) = self.vba_blob {
1422 zip.start_file("xl/vbaProject.bin", options)
1423 .map_err(|e| Error::Zip(e.to_string()))?;
1424 zip.write_all(blob)?;
1425 }
1426
1427 if let Some(ref props) = self.core_properties {
1429 let xml_str = sheetkit_xml::doc_props::serialize_core_properties(props);
1430 zip.start_file("docProps/core.xml", options)
1431 .map_err(|e| Error::Zip(e.to_string()))?;
1432 zip.write_all(xml_str.as_bytes())?;
1433 }
1434
1435 if let Some(ref props) = self.app_properties {
1437 write_xml_part(zip, "docProps/app.xml", props, options)?;
1438 }
1439
1440 if let Some(ref props) = self.custom_properties {
1442 let xml_str = sheetkit_xml::doc_props::serialize_custom_properties(props);
1443 zip.start_file("docProps/custom.xml", options)
1444 .map_err(|e| Error::Zip(e.to_string()))?;
1445 zip.write_all(xml_str.as_bytes())?;
1446 }
1447
1448 if has_any_threaded {
1450 for (i, tc) in self.sheet_threaded_comments.iter().enumerate() {
1451 if let Some(ref tc_data) = tc {
1452 let tc_path = format!("xl/threadedComments/threadedComment{}.xml", i + 1);
1453 write_xml_part(zip, &tc_path, tc_data, options)?;
1454 }
1455 }
1456 write_xml_part(zip, "xl/persons/person.xml", &self.person_list, options)?;
1457 }
1458
1459 for (path, data) in &self.unknown_parts {
1461 zip.start_file(path, options)
1462 .map_err(|e| Error::Zip(e.to_string()))?;
1463 zip.write_all(data)?;
1464 }
1465
1466 if self.deferred_parts.has_any() {
1471 let mut emitted_owned: HashSet<String> = HashSet::new();
1472 emitted_owned.insert("[Content_Types].xml".to_string());
1474 emitted_owned.insert("_rels/.rels".to_string());
1475 emitted_owned.insert("xl/workbook.xml".to_string());
1476 emitted_owned.insert("xl/_rels/workbook.xml.rels".to_string());
1477 emitted_owned.insert("xl/styles.xml".to_string());
1478 emitted_owned.insert("xl/sharedStrings.xml".to_string());
1479 emitted_owned.insert("xl/theme/theme1.xml".to_string());
1480 for i in 0..self.worksheets.len() {
1482 emitted_owned.insert(self.sheet_part_path(i));
1483 }
1484 for (i, comments) in self.sheet_comments.iter().enumerate() {
1485 if comments.is_some() {
1486 emitted_owned.insert(format!("xl/comments{}.xml", i + 1));
1487 }
1488 }
1489 for (_sheet_idx, vml_path, _) in &vml_parts_to_write {
1490 emitted_owned.insert(vml_path.clone());
1491 }
1492 for (path, _) in &self.drawings {
1493 emitted_owned.insert(path.clone());
1494 }
1495 for (path, _) in &self.charts {
1496 emitted_owned.insert(path.clone());
1497 }
1498 for (path, _) in &self.raw_charts {
1499 emitted_owned.insert(path.clone());
1500 }
1501 for (path, _) in &self.images {
1502 emitted_owned.insert(path.clone());
1503 }
1504 for sheet_idx in worksheet_rels.keys() {
1505 let sheet_path = self.sheet_part_path(*sheet_idx);
1506 emitted_owned.insert(relationship_part_path(&sheet_path));
1507 }
1508 for drawing_idx in self.drawing_rels.keys() {
1509 if let Some((drawing_path, _)) = self.drawings.get(*drawing_idx) {
1510 emitted_owned.insert(relationship_part_path(drawing_path));
1511 }
1512 }
1513 for (path, _) in &self.pivot_tables {
1514 emitted_owned.insert(path.clone());
1515 }
1516 for (path, _) in &self.pivot_cache_defs {
1517 emitted_owned.insert(path.clone());
1518 }
1519 for (path, _) in &self.pivot_cache_records {
1520 emitted_owned.insert(path.clone());
1521 }
1522 for (path, _, _) in &self.tables {
1523 emitted_owned.insert(path.clone());
1524 }
1525 for (path, _) in &self.slicer_defs {
1526 emitted_owned.insert(path.clone());
1527 }
1528 for (path, _) in &self.slicer_caches {
1529 emitted_owned.insert(path.clone());
1530 }
1531 if self.vba_blob.is_some() {
1532 emitted_owned.insert("xl/vbaProject.bin".to_string());
1533 }
1534 if self.core_properties.is_some() {
1535 emitted_owned.insert("docProps/core.xml".to_string());
1536 }
1537 if self.app_properties.is_some() {
1538 emitted_owned.insert("docProps/app.xml".to_string());
1539 }
1540 if self.custom_properties.is_some() {
1541 emitted_owned.insert("docProps/custom.xml".to_string());
1542 }
1543 if has_any_threaded {
1544 for (i, tc) in self.sheet_threaded_comments.iter().enumerate() {
1545 if tc.is_some() {
1546 emitted_owned
1547 .insert(format!("xl/threadedComments/threadedComment{}.xml", i + 1));
1548 }
1549 }
1550 emitted_owned.insert("xl/persons/person.xml".to_string());
1551 }
1552 for (path, _) in &self.unknown_parts {
1553 emitted_owned.insert(path.clone());
1554 }
1555
1556 for (path, data) in self.deferred_parts.remaining_parts() {
1557 if emitted_owned.contains(path) {
1558 continue;
1559 }
1560 zip.start_file(path, options)
1561 .map_err(|e| Error::Zip(e.to_string()))?;
1562 zip.write_all(data)?;
1563 }
1564 }
1565
1566 Ok(())
1567 }
1568}
1569
1570impl Default for Workbook {
1571 fn default() -> Self {
1572 Self::new()
1573 }
1574}
1575
1576pub(crate) fn serialize_xml<T: Serialize>(value: &T) -> Result<String> {
1578 let body = quick_xml::se::to_string(value).map_err(|e| Error::XmlParse(e.to_string()))?;
1579 let mut result = String::with_capacity(XML_DECLARATION.len() + 1 + body.len());
1580 result.push_str(XML_DECLARATION);
1581 result.push('\n');
1582 result.push_str(&body);
1583 Ok(result)
1584}
1585
1586pub(super) fn deserialize_worksheet_xml(bytes: &[u8]) -> Result<WorksheetXml> {
1593 let buf_cap = bytes.len().clamp(8192, LARGE_BUF_CAPACITY);
1594 let reader = std::io::BufReader::with_capacity(buf_cap, bytes);
1595 let mut ws: WorksheetXml =
1596 quick_xml::de::from_reader(reader).map_err(|e| Error::XmlDeserialize(e.to_string()))?;
1597 ws.sheet_data.rows.sort_unstable_by_key(|r| r.r);
1599 for row in &mut ws.sheet_data.rows {
1600 for cell in &mut row.cells {
1601 cell.col = fast_col_number(cell.r.as_str());
1602 }
1603 row.cells.sort_unstable_by_key(|c| c.col);
1604 row.cells.shrink_to_fit();
1605 }
1606 ws.sheet_data.rows.shrink_to_fit();
1607 Ok(ws)
1608}
1609
1610const LARGE_BUF_CAPACITY: usize = 64 * 1024;
1613
1614pub(crate) fn read_xml_part<T: serde::de::DeserializeOwned, R: std::io::Read + std::io::Seek>(
1622 archive: &mut zip::ZipArchive<R>,
1623 name: &str,
1624) -> Result<T> {
1625 let entry = archive
1626 .by_name(name)
1627 .map_err(|e| Error::Zip(e.to_string()))?;
1628 let size = entry.size() as usize;
1629 let buf_cap = size.clamp(8192, LARGE_BUF_CAPACITY);
1630 let reader = std::io::BufReader::with_capacity(buf_cap, entry);
1631 quick_xml::de::from_reader(reader).map_err(|e| Error::XmlDeserialize(e.to_string()))
1632}
1633
1634pub(crate) fn read_string_part<R: std::io::Read + std::io::Seek>(
1636 archive: &mut zip::ZipArchive<R>,
1637 name: &str,
1638) -> Result<String> {
1639 let mut entry = archive
1640 .by_name(name)
1641 .map_err(|e| Error::Zip(e.to_string()))?;
1642 let size_hint = entry.size() as usize;
1643 let mut content = String::with_capacity(size_hint);
1644 entry
1645 .read_to_string(&mut content)
1646 .map_err(|e| Error::Zip(e.to_string()))?;
1647 Ok(content)
1648}
1649
1650pub(crate) fn read_bytes_part<R: std::io::Read + std::io::Seek>(
1652 archive: &mut zip::ZipArchive<R>,
1653 name: &str,
1654) -> Result<Vec<u8>> {
1655 let mut entry = archive
1656 .by_name(name)
1657 .map_err(|e| Error::Zip(e.to_string()))?;
1658 let size_hint = entry.size() as usize;
1659 let mut content = Vec::with_capacity(size_hint);
1660 entry
1661 .read_to_end(&mut content)
1662 .map_err(|e| Error::Zip(e.to_string()))?;
1663 Ok(content)
1664}
1665
1666pub(crate) fn serialize_worksheet_with_extras(
1669 ws: &WorksheetXml,
1670 sparklines: &[crate::sparkline::SparklineConfig],
1671 legacy_drawing_rid: Option<&str>,
1672) -> Result<String> {
1673 let body = quick_xml::se::to_string(ws).map_err(|e| Error::XmlParse(e.to_string()))?;
1674
1675 let closing = "</worksheet>";
1676 let ext_xml = if sparklines.is_empty() {
1677 String::new()
1678 } else {
1679 build_sparkline_ext_xml(sparklines)
1680 };
1681 let legacy_xml = if let Some(rid) = legacy_drawing_rid {
1682 format!("<legacyDrawing r:id=\"{rid}\"/>")
1683 } else {
1684 String::new()
1685 };
1686
1687 if let Some(pos) = body.rfind(closing) {
1688 let body_prefix = &body[..pos];
1691 let stripped;
1692 let prefix = if !legacy_xml.is_empty() {
1693 if let Some(ld_start) = body_prefix.find("<legacyDrawing ") {
1694 let ld_end = body_prefix[ld_start..]
1696 .find("/>")
1697 .map(|e| ld_start + e + 2)
1698 .unwrap_or(ld_start);
1699 stripped = format!("{}{}", &body_prefix[..ld_start], &body_prefix[ld_end..]);
1700 stripped.as_str()
1701 } else {
1702 body_prefix
1703 }
1704 } else {
1705 body_prefix
1706 };
1707
1708 let extra_len = ext_xml.len() + legacy_xml.len();
1709 let mut result = String::with_capacity(XML_DECLARATION.len() + 1 + body.len() + extra_len);
1710 result.push_str(XML_DECLARATION);
1711 result.push('\n');
1712 result.push_str(prefix);
1713 result.push_str(&legacy_xml);
1714 result.push_str(&ext_xml);
1715 result.push_str(closing);
1716 Ok(result)
1717 } else {
1718 Ok(format!("{XML_DECLARATION}\n{body}"))
1719 }
1720}
1721
1722pub(crate) fn build_sparkline_ext_xml(sparklines: &[crate::sparkline::SparklineConfig]) -> String {
1724 use std::fmt::Write;
1725 let mut xml = String::new();
1726 let _ = write!(
1727 xml,
1728 "<extLst>\
1729 <ext xmlns:x14=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main\" \
1730 uri=\"{{05C60535-1F16-4fd2-B633-F4F36F0B64E0}}\">\
1731 <x14:sparklineGroups \
1732 xmlns:xm=\"http://schemas.microsoft.com/office/excel/2006/main\">"
1733 );
1734 for config in sparklines {
1735 let group = crate::sparkline::config_to_xml_group(config);
1736 let _ = write!(xml, "<x14:sparklineGroup");
1737 if let Some(ref t) = group.sparkline_type {
1738 let _ = write!(xml, " type=\"{t}\"");
1739 }
1740 if group.markers == Some(true) {
1741 let _ = write!(xml, " markers=\"1\"");
1742 }
1743 if group.high == Some(true) {
1744 let _ = write!(xml, " high=\"1\"");
1745 }
1746 if group.low == Some(true) {
1747 let _ = write!(xml, " low=\"1\"");
1748 }
1749 if group.first == Some(true) {
1750 let _ = write!(xml, " first=\"1\"");
1751 }
1752 if group.last == Some(true) {
1753 let _ = write!(xml, " last=\"1\"");
1754 }
1755 if group.negative == Some(true) {
1756 let _ = write!(xml, " negative=\"1\"");
1757 }
1758 if group.display_x_axis == Some(true) {
1759 let _ = write!(xml, " displayXAxis=\"1\"");
1760 }
1761 if let Some(w) = group.line_weight {
1762 let _ = write!(xml, " lineWeight=\"{w}\"");
1763 }
1764 let _ = write!(xml, "><x14:sparklines>");
1765 for sp in &group.sparklines.items {
1766 let _ = write!(
1767 xml,
1768 "<x14:sparkline><xm:f>{}</xm:f><xm:sqref>{}</xm:sqref></x14:sparkline>",
1769 sp.formula, sp.sqref
1770 );
1771 }
1772 let _ = write!(xml, "</x14:sparklines></x14:sparklineGroup>");
1773 }
1774 let _ = write!(xml, "</x14:sparklineGroups></ext></extLst>");
1775 xml
1776}
1777
1778pub(crate) fn parse_sparklines_from_xml(xml: &str) -> Vec<crate::sparkline::SparklineConfig> {
1780 use crate::sparkline::{SparklineConfig, SparklineType};
1781
1782 let mut sparklines = Vec::new();
1783
1784 let mut search_from = 0;
1786 while let Some(group_start) = xml[search_from..].find("<x14:sparklineGroup") {
1787 let abs_start = search_from + group_start;
1788 let group_end_tag = "</x14:sparklineGroup>";
1789 let abs_end = match xml[abs_start..].find(group_end_tag) {
1790 Some(pos) => abs_start + pos + group_end_tag.len(),
1791 None => break,
1792 };
1793 let group_xml = &xml[abs_start..abs_end];
1794
1795 let sparkline_type = extract_xml_attr(group_xml, "type")
1797 .and_then(|s| SparklineType::parse(&s))
1798 .unwrap_or_default();
1799 let markers = extract_xml_bool_attr(group_xml, "markers");
1800 let high_point = extract_xml_bool_attr(group_xml, "high");
1801 let low_point = extract_xml_bool_attr(group_xml, "low");
1802 let first_point = extract_xml_bool_attr(group_xml, "first");
1803 let last_point = extract_xml_bool_attr(group_xml, "last");
1804 let negative_points = extract_xml_bool_attr(group_xml, "negative");
1805 let show_axis = extract_xml_bool_attr(group_xml, "displayXAxis");
1806 let line_weight =
1807 extract_xml_attr(group_xml, "lineWeight").and_then(|s| s.parse::<f64>().ok());
1808
1809 let mut sp_from = 0;
1811 while let Some(sp_start) = group_xml[sp_from..].find("<x14:sparkline>") {
1812 let sp_abs = sp_from + sp_start;
1813 let sp_end_tag = "</x14:sparkline>";
1814 let sp_abs_end = match group_xml[sp_abs..].find(sp_end_tag) {
1815 Some(pos) => sp_abs + pos + sp_end_tag.len(),
1816 None => break,
1817 };
1818 let sp_xml = &group_xml[sp_abs..sp_abs_end];
1819
1820 let formula = extract_xml_element(sp_xml, "xm:f").unwrap_or_default();
1821 let sqref = extract_xml_element(sp_xml, "xm:sqref").unwrap_or_default();
1822
1823 if !formula.is_empty() && !sqref.is_empty() {
1824 sparklines.push(SparklineConfig {
1825 data_range: formula,
1826 location: sqref,
1827 sparkline_type: sparkline_type.clone(),
1828 markers,
1829 high_point,
1830 low_point,
1831 first_point,
1832 last_point,
1833 negative_points,
1834 show_axis,
1835 line_weight,
1836 style: None,
1837 });
1838 }
1839 sp_from = sp_abs_end;
1840 }
1841 search_from = abs_end;
1842 }
1843 sparklines
1844}
1845
1846pub(crate) fn extract_xml_attr(xml: &str, attr: &str) -> Option<String> {
1850 for quote in ['"', '\''] {
1852 let haystack = xml.as_bytes();
1854 let attr_bytes = attr.as_bytes();
1855 let mut pos = 0;
1856 while pos + 1 + attr_bytes.len() + 2 <= haystack.len() {
1857 if haystack[pos] == b' '
1858 && haystack[pos + 1..pos + 1 + attr_bytes.len()] == *attr_bytes
1859 && haystack[pos + 1 + attr_bytes.len()] == b'='
1860 && haystack[pos + 1 + attr_bytes.len() + 1] == quote as u8
1861 {
1862 let val_start = pos + 1 + attr_bytes.len() + 2;
1863 if let Some(end) = xml[val_start..].find(quote) {
1864 return Some(xml[val_start..val_start + end].to_string());
1865 }
1866 }
1867 pos += 1;
1868 }
1869 }
1870 None
1871}
1872
1873pub(crate) fn extract_xml_bool_attr(xml: &str, attr: &str) -> bool {
1875 extract_xml_attr(xml, attr)
1876 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1877 .unwrap_or(false)
1878}
1879
1880pub(crate) fn extract_xml_element(xml: &str, tag: &str) -> Option<String> {
1882 let open = format!("<{tag}>");
1883 let close = format!("</{tag}>");
1884 let start = xml.find(&open)?;
1885 let content_start = start + open.len();
1886 let end = xml[content_start..].find(&close)?;
1887 Some(xml[content_start..content_start + end].to_string())
1888}
1889
1890pub(crate) fn write_xml_part<T: Serialize, W: std::io::Write + std::io::Seek>(
1892 zip: &mut zip::ZipWriter<W>,
1893 name: &str,
1894 value: &T,
1895 options: SimpleFileOptions,
1896) -> Result<()> {
1897 let xml = serialize_xml(value)?;
1898 zip.start_file(name, options)
1899 .map_err(|e| Error::Zip(e.to_string()))?;
1900 zip.write_all(xml.as_bytes())?;
1901 Ok(())
1902}
1903
1904fn fast_col_number(cell_ref: &str) -> u32 {
1910 let mut col: u32 = 0;
1911 for b in cell_ref.bytes() {
1912 if b.is_ascii_alphabetic() {
1913 col = col * 26 + (b.to_ascii_uppercase() - b'A') as u32 + 1;
1914 } else {
1915 break;
1916 }
1917 }
1918 col
1919}
1920
1921#[cfg(test)]
1922#[allow(clippy::unnecessary_map_or)]
1923mod tests {
1924 use super::*;
1925 use tempfile::TempDir;
1926
1927 #[test]
1928 fn test_fast_col_number() {
1929 assert_eq!(fast_col_number("A1"), 1);
1930 assert_eq!(fast_col_number("B1"), 2);
1931 assert_eq!(fast_col_number("Z1"), 26);
1932 assert_eq!(fast_col_number("AA1"), 27);
1933 assert_eq!(fast_col_number("AZ1"), 52);
1934 assert_eq!(fast_col_number("BA1"), 53);
1935 assert_eq!(fast_col_number("XFD1"), 16384);
1936 }
1937
1938 #[test]
1939 fn test_extract_xml_attr() {
1940 let xml = r#"<tag type="column" markers="1" weight="2.5">"#;
1941 assert_eq!(extract_xml_attr(xml, "type"), Some("column".to_string()));
1942 assert_eq!(extract_xml_attr(xml, "markers"), Some("1".to_string()));
1943 assert_eq!(extract_xml_attr(xml, "weight"), Some("2.5".to_string()));
1944 assert_eq!(extract_xml_attr(xml, "missing"), None);
1945 let xml2 = "<tag name='hello'>";
1947 assert_eq!(extract_xml_attr(xml2, "name"), Some("hello".to_string()));
1948 }
1949
1950 #[test]
1951 fn test_extract_xml_bool_attr() {
1952 let xml = r#"<tag markers="1" hidden="0" visible="true">"#;
1953 assert!(extract_xml_bool_attr(xml, "markers"));
1954 assert!(!extract_xml_bool_attr(xml, "hidden"));
1955 assert!(extract_xml_bool_attr(xml, "visible"));
1956 assert!(!extract_xml_bool_attr(xml, "missing"));
1957 }
1958
1959 #[test]
1960 fn test_new_workbook_has_sheet1() {
1961 let wb = Workbook::new();
1962 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
1963 }
1964
1965 #[test]
1966 fn test_new_workbook_writes_interop_workbook_defaults() {
1967 let wb = Workbook::new();
1968 let buf = wb.save_to_buffer().unwrap();
1969
1970 let cursor = std::io::Cursor::new(buf);
1971 let mut archive = zip::ZipArchive::new(cursor).unwrap();
1972 let mut workbook_xml = String::new();
1973 std::io::Read::read_to_string(
1974 &mut archive.by_name("xl/workbook.xml").unwrap(),
1975 &mut workbook_xml,
1976 )
1977 .unwrap();
1978
1979 assert!(workbook_xml.contains("<fileVersion"));
1980 assert!(workbook_xml.contains("appName=\"xl\""));
1981 assert!(workbook_xml.contains("lastEdited=\"7\""));
1982 assert!(workbook_xml.contains("lowestEdited=\"7\""));
1983 assert!(workbook_xml.contains("rupBuild=\"27425\""));
1984
1985 assert!(workbook_xml.contains("<workbookPr"));
1986 assert!(workbook_xml.contains("defaultThemeVersion=\"166925\""));
1987
1988 assert!(workbook_xml.contains("<bookViews>"));
1989 assert!(workbook_xml.contains("<workbookView"));
1990 assert!(workbook_xml.contains("activeTab=\"0\""));
1991 assert!(!workbook_xml.contains("xWindow="));
1992 assert!(!workbook_xml.contains("yWindow="));
1993 assert!(!workbook_xml.contains("windowWidth="));
1994 assert!(!workbook_xml.contains("windowHeight="));
1995 }
1996
1997 #[test]
1998 fn test_new_workbook_save_creates_file() {
1999 let dir = TempDir::new().unwrap();
2000 let path = dir.path().join("test.xlsx");
2001 let wb = Workbook::new();
2002 wb.save(&path).unwrap();
2003 assert!(path.exists());
2004 }
2005
2006 #[test]
2007 fn test_save_and_open_roundtrip() {
2008 let dir = TempDir::new().unwrap();
2009 let path = dir.path().join("roundtrip.xlsx");
2010
2011 let wb = Workbook::new();
2012 wb.save(&path).unwrap();
2013
2014 let wb2 = Workbook::open(&path).unwrap();
2015 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
2016 }
2017
2018 #[test]
2019 fn test_saved_file_is_valid_zip() {
2020 let dir = TempDir::new().unwrap();
2021 let path = dir.path().join("valid.xlsx");
2022 let wb = Workbook::new();
2023 wb.save(&path).unwrap();
2024
2025 let file = std::fs::File::open(&path).unwrap();
2027 let mut archive = zip::ZipArchive::new(file).unwrap();
2028
2029 let expected_files = [
2030 "[Content_Types].xml",
2031 "_rels/.rels",
2032 "xl/workbook.xml",
2033 "xl/_rels/workbook.xml.rels",
2034 "xl/worksheets/sheet1.xml",
2035 "xl/styles.xml",
2036 "xl/sharedStrings.xml",
2037 ];
2038
2039 for name in &expected_files {
2040 assert!(archive.by_name(name).is_ok(), "Missing ZIP entry: {}", name);
2041 }
2042 }
2043
2044 #[test]
2045 fn test_open_nonexistent_file_returns_error() {
2046 let result = Workbook::open("/nonexistent/path.xlsx");
2047 assert!(result.is_err());
2048 }
2049
2050 #[test]
2051 fn test_saved_xml_has_declarations() {
2052 let dir = TempDir::new().unwrap();
2053 let path = dir.path().join("decl.xlsx");
2054 let wb = Workbook::new();
2055 wb.save(&path).unwrap();
2056
2057 let file = std::fs::File::open(&path).unwrap();
2058 let mut archive = zip::ZipArchive::new(file).unwrap();
2059
2060 let mut content = String::new();
2061 std::io::Read::read_to_string(
2062 &mut archive.by_name("[Content_Types].xml").unwrap(),
2063 &mut content,
2064 )
2065 .unwrap();
2066 assert!(content.starts_with("<?xml"));
2067 }
2068
2069 #[test]
2070 fn test_default_trait() {
2071 let wb = Workbook::default();
2072 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
2073 }
2074
2075 #[test]
2076 fn test_serialize_xml_helper() {
2077 let ct = ContentTypes::default();
2078 let xml = serialize_xml(&ct).unwrap();
2079 assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"));
2080 assert!(xml.contains("<Types"));
2081 }
2082
2083 #[test]
2084 fn test_save_to_buffer_and_open_from_buffer_roundtrip() {
2085 let mut wb = Workbook::new();
2086 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
2087 .unwrap();
2088 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
2089 .unwrap();
2090
2091 let buf = wb.save_to_buffer().unwrap();
2092 assert!(!buf.is_empty());
2093
2094 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2095 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
2096 assert_eq!(
2097 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2098 CellValue::String("Hello".to_string())
2099 );
2100 assert_eq!(
2101 wb2.get_cell_value("Sheet1", "B2").unwrap(),
2102 CellValue::Number(42.0)
2103 );
2104 }
2105
2106 #[test]
2107 fn test_save_to_buffer_produces_valid_zip() {
2108 let wb = Workbook::new();
2109 let buf = wb.save_to_buffer().unwrap();
2110
2111 let cursor = std::io::Cursor::new(buf);
2112 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2113
2114 let expected_files = [
2115 "[Content_Types].xml",
2116 "_rels/.rels",
2117 "xl/workbook.xml",
2118 "xl/_rels/workbook.xml.rels",
2119 "xl/worksheets/sheet1.xml",
2120 "xl/styles.xml",
2121 "xl/sharedStrings.xml",
2122 ];
2123
2124 for name in &expected_files {
2125 assert!(archive.by_name(name).is_ok(), "Missing ZIP entry: {}", name);
2126 }
2127 }
2128
2129 #[test]
2130 fn test_open_from_buffer_invalid_data() {
2131 let result = Workbook::open_from_buffer(b"not a zip file");
2132 assert!(result.is_err());
2133 }
2134
2135 #[cfg(feature = "encryption")]
2136 #[test]
2137 fn test_save_and_open_with_password_roundtrip() {
2138 let dir = TempDir::new().unwrap();
2139 let path = dir.path().join("encrypted.xlsx");
2140
2141 let mut wb = Workbook::new();
2143 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
2144 .unwrap();
2145 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
2146 .unwrap();
2147
2148 wb.save_with_password(&path, "test123").unwrap();
2150
2151 let data = std::fs::read(&path).unwrap();
2153 assert_eq!(
2154 &data[..8],
2155 &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]
2156 );
2157
2158 let result = Workbook::open(&path);
2160 assert!(matches!(result, Err(Error::FileEncrypted)));
2161
2162 let result = Workbook::open_with_password(&path, "wrong");
2164 assert!(matches!(result, Err(Error::IncorrectPassword)));
2165
2166 let wb2 = Workbook::open_with_password(&path, "test123").unwrap();
2168 assert_eq!(
2169 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2170 CellValue::String("Hello".to_string())
2171 );
2172 assert_eq!(
2173 wb2.get_cell_value("Sheet1", "B2").unwrap(),
2174 CellValue::Number(42.0)
2175 );
2176 }
2177
2178 fn create_xlsx_with_custom_entries() -> Vec<u8> {
2181 let mut wb = Workbook::new();
2182 wb.set_cell_value("Sheet1", "A1", CellValue::String("hello".to_string()))
2183 .unwrap();
2184 let base_buf = wb.save_to_buffer().unwrap();
2185
2186 let cursor = std::io::Cursor::new(&base_buf);
2188 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2189 let mut out = Vec::new();
2190 {
2191 let out_cursor = std::io::Cursor::new(&mut out);
2192 let mut zip_writer = zip::ZipWriter::new(out_cursor);
2193 let options =
2194 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
2195
2196 for i in 0..archive.len() {
2198 let mut entry = archive.by_index(i).unwrap();
2199 let name = entry.name().to_string();
2200 let mut data = Vec::new();
2201 std::io::Read::read_to_end(&mut entry, &mut data).unwrap();
2202 zip_writer.start_file(&name, options).unwrap();
2203 std::io::Write::write_all(&mut zip_writer, &data).unwrap();
2204 }
2205
2206 zip_writer
2208 .start_file("customXml/item1.xml", options)
2209 .unwrap();
2210 std::io::Write::write_all(&mut zip_writer, b"<custom>data1</custom>").unwrap();
2211
2212 zip_writer
2213 .start_file("customXml/itemProps1.xml", options)
2214 .unwrap();
2215 std::io::Write::write_all(
2216 &mut zip_writer,
2217 b"<ds:datastoreItem xmlns:ds=\"http://schemas.openxmlformats.org/officeDocument/2006/customXml\"/>",
2218 )
2219 .unwrap();
2220
2221 zip_writer
2222 .start_file("xl/printerSettings/printerSettings1.bin", options)
2223 .unwrap();
2224 std::io::Write::write_all(&mut zip_writer, b"\x00\x01\x02\x03PRINTER").unwrap();
2225
2226 zip_writer.finish().unwrap();
2227 }
2228 out
2229 }
2230
2231 #[test]
2232 fn test_unknown_zip_entries_preserved_on_roundtrip() {
2233 let buf = create_xlsx_with_custom_entries();
2234
2235 let wb = Workbook::open_from_buffer(&buf).unwrap();
2237 assert_eq!(
2238 wb.get_cell_value("Sheet1", "A1").unwrap(),
2239 CellValue::String("hello".to_string())
2240 );
2241
2242 let saved = wb.save_to_buffer().unwrap();
2244 let cursor = std::io::Cursor::new(&saved);
2245 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2246
2247 let mut custom_xml = String::new();
2249 std::io::Read::read_to_string(
2250 &mut archive.by_name("customXml/item1.xml").unwrap(),
2251 &mut custom_xml,
2252 )
2253 .unwrap();
2254 assert_eq!(custom_xml, "<custom>data1</custom>");
2255
2256 let mut props_xml = String::new();
2257 std::io::Read::read_to_string(
2258 &mut archive.by_name("customXml/itemProps1.xml").unwrap(),
2259 &mut props_xml,
2260 )
2261 .unwrap();
2262 assert!(props_xml.contains("datastoreItem"));
2263
2264 let mut printer = Vec::new();
2265 std::io::Read::read_to_end(
2266 &mut archive
2267 .by_name("xl/printerSettings/printerSettings1.bin")
2268 .unwrap(),
2269 &mut printer,
2270 )
2271 .unwrap();
2272 assert_eq!(printer, b"\x00\x01\x02\x03PRINTER");
2273 }
2274
2275 #[test]
2276 fn test_unknown_entries_survive_multiple_roundtrips() {
2277 let buf = create_xlsx_with_custom_entries();
2278 let wb1 = Workbook::open_from_buffer(&buf).unwrap();
2279 let buf2 = wb1.save_to_buffer().unwrap();
2280 let wb2 = Workbook::open_from_buffer(&buf2).unwrap();
2281 let buf3 = wb2.save_to_buffer().unwrap();
2282
2283 let cursor = std::io::Cursor::new(&buf3);
2284 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2285
2286 let mut custom_xml = String::new();
2287 std::io::Read::read_to_string(
2288 &mut archive.by_name("customXml/item1.xml").unwrap(),
2289 &mut custom_xml,
2290 )
2291 .unwrap();
2292 assert_eq!(custom_xml, "<custom>data1</custom>");
2293
2294 let mut printer = Vec::new();
2295 std::io::Read::read_to_end(
2296 &mut archive
2297 .by_name("xl/printerSettings/printerSettings1.bin")
2298 .unwrap(),
2299 &mut printer,
2300 )
2301 .unwrap();
2302 assert_eq!(printer, b"\x00\x01\x02\x03PRINTER");
2303 }
2304
2305 #[test]
2306 fn test_new_workbook_has_no_unknown_parts() {
2307 let wb = Workbook::new();
2308 let buf = wb.save_to_buffer().unwrap();
2309 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2310 assert!(wb2.unknown_parts.is_empty());
2311 }
2312
2313 #[test]
2314 fn test_known_entries_not_duplicated_as_unknown() {
2315 let wb = Workbook::new();
2316 let buf = wb.save_to_buffer().unwrap();
2317 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2318
2319 let unknown_paths: Vec<&str> = wb2.unknown_parts.iter().map(|(p, _)| p.as_str()).collect();
2321 assert!(
2322 !unknown_paths.contains(&"[Content_Types].xml"),
2323 "Content_Types should not be in unknown_parts"
2324 );
2325 assert!(
2326 !unknown_paths.contains(&"xl/workbook.xml"),
2327 "workbook.xml should not be in unknown_parts"
2328 );
2329 assert!(
2330 !unknown_paths.contains(&"xl/styles.xml"),
2331 "styles.xml should not be in unknown_parts"
2332 );
2333 }
2334
2335 #[test]
2336 fn test_modifications_preserved_alongside_unknown_parts() {
2337 let buf = create_xlsx_with_custom_entries();
2338 let mut wb = Workbook::open_from_buffer(&buf).unwrap();
2339
2340 wb.set_cell_value("Sheet1", "B1", CellValue::Number(42.0))
2342 .unwrap();
2343
2344 let saved = wb.save_to_buffer().unwrap();
2345 let wb2 = Workbook::open_from_buffer(&saved).unwrap();
2346
2347 assert_eq!(
2349 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2350 CellValue::String("hello".to_string())
2351 );
2352 assert_eq!(
2354 wb2.get_cell_value("Sheet1", "B1").unwrap(),
2355 CellValue::Number(42.0)
2356 );
2357 let cursor = std::io::Cursor::new(&saved);
2359 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2360 assert!(archive.by_name("customXml/item1.xml").is_ok());
2361 }
2362
2363 #[test]
2364 fn test_threaded_comment_person_rel_in_workbook_rels() {
2365 let mut wb = Workbook::new();
2366 wb.add_threaded_comment(
2367 "Sheet1",
2368 "A1",
2369 &crate::threaded_comment::ThreadedCommentInput {
2370 author: "Alice".to_string(),
2371 text: "Test comment".to_string(),
2372 parent_id: None,
2373 },
2374 )
2375 .unwrap();
2376
2377 let buf = wb.save_to_buffer().unwrap();
2378 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2379
2380 let has_person_rel = wb2.workbook_rels.relationships.iter().any(|r| {
2382 r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_PERSON
2383 && r.target == "persons/person.xml"
2384 });
2385 assert!(
2386 has_person_rel,
2387 "workbook_rels must contain a person relationship for threaded comments"
2388 );
2389 }
2390
2391 #[test]
2392 fn test_no_person_rel_without_threaded_comments() {
2393 let wb = Workbook::new();
2394 let buf = wb.save_to_buffer().unwrap();
2395 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2396
2397 let has_person_rel = wb2
2398 .workbook_rels
2399 .relationships
2400 .iter()
2401 .any(|r| r.rel_type == sheetkit_xml::threaded_comment::REL_TYPE_PERSON);
2402 assert!(
2403 !has_person_rel,
2404 "workbook_rels must not contain a person relationship when there are no threaded comments"
2405 );
2406 }
2407
2408 #[cfg(feature = "encryption")]
2409 #[test]
2410 fn test_open_encrypted_file_without_password_returns_file_encrypted() {
2411 let dir = TempDir::new().unwrap();
2412 let path = dir.path().join("encrypted2.xlsx");
2413
2414 let wb = Workbook::new();
2415 wb.save_with_password(&path, "secret").unwrap();
2416
2417 let result = Workbook::open(&path);
2418 assert!(matches!(result, Err(Error::FileEncrypted)))
2419 }
2420
2421 #[test]
2422 fn test_workbook_format_from_content_type() {
2423 use sheetkit_xml::content_types::mime_types;
2424 assert_eq!(
2425 WorkbookFormat::from_content_type(mime_types::WORKBOOK),
2426 Some(WorkbookFormat::Xlsx)
2427 );
2428 assert_eq!(
2429 WorkbookFormat::from_content_type(mime_types::WORKBOOK_MACRO),
2430 Some(WorkbookFormat::Xlsm)
2431 );
2432 assert_eq!(
2433 WorkbookFormat::from_content_type(mime_types::WORKBOOK_TEMPLATE),
2434 Some(WorkbookFormat::Xltx)
2435 );
2436 assert_eq!(
2437 WorkbookFormat::from_content_type(mime_types::WORKBOOK_TEMPLATE_MACRO),
2438 Some(WorkbookFormat::Xltm)
2439 );
2440 assert_eq!(
2441 WorkbookFormat::from_content_type(mime_types::WORKBOOK_ADDIN_MACRO),
2442 Some(WorkbookFormat::Xlam)
2443 );
2444 assert_eq!(
2445 WorkbookFormat::from_content_type("application/unknown"),
2446 None
2447 );
2448 }
2449
2450 #[test]
2451 fn test_workbook_format_content_type_roundtrip() {
2452 for fmt in [
2453 WorkbookFormat::Xlsx,
2454 WorkbookFormat::Xlsm,
2455 WorkbookFormat::Xltx,
2456 WorkbookFormat::Xltm,
2457 WorkbookFormat::Xlam,
2458 ] {
2459 let ct = fmt.content_type();
2460 assert_eq!(WorkbookFormat::from_content_type(ct), Some(fmt));
2461 }
2462 }
2463
2464 #[test]
2465 fn test_new_workbook_defaults_to_xlsx_format() {
2466 let wb = Workbook::new();
2467 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2468 }
2469
2470 #[test]
2471 fn test_xlsx_roundtrip_preserves_format() {
2472 let dir = TempDir::new().unwrap();
2473 let path = dir.path().join("roundtrip_format.xlsx");
2474
2475 let wb = Workbook::new();
2476 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2477 wb.save(&path).unwrap();
2478
2479 let wb2 = Workbook::open(&path).unwrap();
2480 assert_eq!(wb2.format(), WorkbookFormat::Xlsx);
2481 }
2482
2483 #[test]
2484 fn test_save_writes_correct_content_type_for_each_extension() {
2485 let dir = TempDir::new().unwrap();
2486
2487 let cases = [
2488 (WorkbookFormat::Xlsx, "test.xlsx"),
2489 (WorkbookFormat::Xlsm, "test.xlsm"),
2490 (WorkbookFormat::Xltx, "test.xltx"),
2491 (WorkbookFormat::Xltm, "test.xltm"),
2492 (WorkbookFormat::Xlam, "test.xlam"),
2493 ];
2494
2495 for (expected_fmt, filename) in cases {
2496 let path = dir.path().join(filename);
2497 let wb = Workbook::new();
2498 wb.save(&path).unwrap();
2499
2500 let file = std::fs::File::open(&path).unwrap();
2501 let mut archive = zip::ZipArchive::new(file).unwrap();
2502
2503 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2504 let wb_override = ct
2505 .overrides
2506 .iter()
2507 .find(|o| o.part_name == "/xl/workbook.xml")
2508 .expect("workbook override must exist");
2509 assert_eq!(
2510 wb_override.content_type,
2511 expected_fmt.content_type(),
2512 "content type mismatch for {}",
2513 filename
2514 );
2515 }
2516 }
2517
2518 #[test]
2519 fn test_set_format_changes_workbook_format() {
2520 let mut wb = Workbook::new();
2521 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2522
2523 wb.set_format(WorkbookFormat::Xlsm);
2524 assert_eq!(wb.format(), WorkbookFormat::Xlsm);
2525 }
2526
2527 #[test]
2528 fn test_save_buffer_roundtrip_with_xlsm_format() {
2529 let mut wb = Workbook::new();
2530 wb.set_format(WorkbookFormat::Xlsm);
2531 wb.set_cell_value("Sheet1", "A1", CellValue::String("test".to_string()))
2532 .unwrap();
2533
2534 let buf = wb.save_to_buffer().unwrap();
2535 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2536 assert_eq!(wb2.format(), WorkbookFormat::Xlsm);
2537 assert_eq!(
2538 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2539 CellValue::String("test".to_string())
2540 );
2541 }
2542
2543 #[test]
2544 fn test_open_with_default_options_is_equivalent_to_open() {
2545 let dir = TempDir::new().unwrap();
2546 let path = dir.path().join("default_opts.xlsx");
2547 let mut wb = Workbook::new();
2548 wb.set_cell_value("Sheet1", "A1", CellValue::String("test".to_string()))
2549 .unwrap();
2550 wb.save(&path).unwrap();
2551
2552 let wb2 = Workbook::open_with_options(&path, &OpenOptions::default()).unwrap();
2553 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
2554 assert_eq!(
2555 wb2.get_cell_value("Sheet1", "A1").unwrap(),
2556 CellValue::String("test".to_string())
2557 );
2558 }
2559
2560 #[test]
2561 fn test_format_inference_from_content_types_overrides() {
2562 use sheetkit_xml::content_types::mime_types;
2563
2564 let ct = ContentTypes {
2566 xmlns: "http://schemas.openxmlformats.org/package/2006/content-types".to_string(),
2567 defaults: vec![],
2568 overrides: vec![ContentTypeOverride {
2569 part_name: "/xl/workbook.xml".to_string(),
2570 content_type: mime_types::WORKBOOK_MACRO.to_string(),
2571 }],
2572 };
2573
2574 let detected = ct
2575 .overrides
2576 .iter()
2577 .find(|o| o.part_name == "/xl/workbook.xml")
2578 .and_then(|o| WorkbookFormat::from_content_type(&o.content_type))
2579 .unwrap_or_default();
2580 assert_eq!(detected, WorkbookFormat::Xlsm);
2581 }
2582
2583 #[test]
2584 fn test_workbook_format_default_is_xlsx() {
2585 assert_eq!(WorkbookFormat::default(), WorkbookFormat::Xlsx);
2586 }
2587
2588 fn build_xlsm_with_vba(vba_bytes: &[u8]) -> Vec<u8> {
2589 use std::io::Write;
2590 let mut buf = Vec::new();
2591 {
2592 let cursor = std::io::Cursor::new(&mut buf);
2593 let mut zip = zip::ZipWriter::new(cursor);
2594 let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
2595
2596 let ct_xml = format!(
2597 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2598<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
2599 <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
2600 <Default Extension="xml" ContentType="application/xml"/>
2601 <Default Extension="bin" ContentType="application/vnd.ms-office.vbaProject"/>
2602 <Override PartName="/xl/workbook.xml" ContentType="{wb_ct}"/>
2603 <Override PartName="/xl/worksheets/sheet1.xml" ContentType="{ws_ct}"/>
2604 <Override PartName="/xl/styles.xml" ContentType="{st_ct}"/>
2605 <Override PartName="/xl/sharedStrings.xml" ContentType="{sst_ct}"/>
2606 <Override PartName="/xl/vbaProject.bin" ContentType="application/vnd.ms-office.vbaProject"/>
2607</Types>"#,
2608 wb_ct = mime_types::WORKBOOK_MACRO,
2609 ws_ct = mime_types::WORKSHEET,
2610 st_ct = mime_types::STYLES,
2611 sst_ct = mime_types::SHARED_STRINGS,
2612 );
2613 zip.start_file("[Content_Types].xml", opts).unwrap();
2614 zip.write_all(ct_xml.as_bytes()).unwrap();
2615
2616 let pkg_rels = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2617<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
2618 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
2619</Relationships>"#;
2620 zip.start_file("_rels/.rels", opts).unwrap();
2621 zip.write_all(pkg_rels.as_bytes()).unwrap();
2622
2623 let wb_rels = format!(
2624 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2625<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
2626 <Relationship Id="rId1" Type="{ws_rel}" Target="worksheets/sheet1.xml"/>
2627 <Relationship Id="rId2" Type="{st_rel}" Target="styles.xml"/>
2628 <Relationship Id="rId3" Type="{sst_rel}" Target="sharedStrings.xml"/>
2629 <Relationship Id="rId4" Type="{vba_rel}" Target="vbaProject.bin"/>
2630</Relationships>"#,
2631 ws_rel = rel_types::WORKSHEET,
2632 st_rel = rel_types::STYLES,
2633 sst_rel = rel_types::SHARED_STRINGS,
2634 vba_rel = VBA_PROJECT_REL_TYPE,
2635 );
2636 zip.start_file("xl/_rels/workbook.xml.rels", opts).unwrap();
2637 zip.write_all(wb_rels.as_bytes()).unwrap();
2638
2639 let wb_xml = concat!(
2640 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
2641 r#"<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main""#,
2642 r#" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
2643 r#"<sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets>"#,
2644 r#"</workbook>"#,
2645 );
2646 zip.start_file("xl/workbook.xml", opts).unwrap();
2647 zip.write_all(wb_xml.as_bytes()).unwrap();
2648
2649 let ws_xml = concat!(
2650 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
2651 r#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main""#,
2652 r#" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
2653 r#"<sheetData/>"#,
2654 r#"</worksheet>"#,
2655 );
2656 zip.start_file("xl/worksheets/sheet1.xml", opts).unwrap();
2657 zip.write_all(ws_xml.as_bytes()).unwrap();
2658
2659 let styles_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2660<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
2661 <fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
2662 <fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
2663 <borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
2664 <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
2665 <cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
2666</styleSheet>"#;
2667 zip.start_file("xl/styles.xml", opts).unwrap();
2668 zip.write_all(styles_xml.as_bytes()).unwrap();
2669
2670 let sst_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2671<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="0" uniqueCount="0"/>"#;
2672 zip.start_file("xl/sharedStrings.xml", opts).unwrap();
2673 zip.write_all(sst_xml.as_bytes()).unwrap();
2674
2675 zip.start_file("xl/vbaProject.bin", opts).unwrap();
2676 zip.write_all(vba_bytes).unwrap();
2677
2678 zip.finish().unwrap();
2679 }
2680 buf
2681 }
2682
2683 #[test]
2684 fn test_vba_blob_loaded_when_present() {
2685 use crate::workbook::open_options::{AuxParts, OpenOptions, ReadMode};
2686
2687 let vba_data = b"FAKE_VBA_PROJECT_BINARY_DATA_1234567890";
2688 let xlsm = build_xlsm_with_vba(vba_data);
2689 let opts = OpenOptions::new()
2690 .read_mode(ReadMode::Eager)
2691 .aux_parts(AuxParts::EagerLoad);
2692 let wb = Workbook::open_from_buffer_with_options(&xlsm, &opts).unwrap();
2693 assert!(wb.vba_blob.is_some());
2694 assert_eq!(wb.vba_blob.as_deref().unwrap(), vba_data);
2695 }
2696
2697 #[test]
2698 fn test_vba_blob_none_for_plain_xlsx() {
2699 let wb = Workbook::new();
2700 assert!(wb.vba_blob.is_none());
2701
2702 let buf = wb.save_to_buffer().unwrap();
2703 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2704 assert!(wb2.vba_blob.is_none());
2705 }
2706
2707 #[test]
2708 fn test_vba_blob_survives_roundtrip_with_identical_bytes() {
2709 use crate::workbook::open_options::{AuxParts, OpenOptions, ReadMode};
2710
2711 let vba_data: Vec<u8> = (0..=255).cycle().take(1024).collect();
2712 let xlsm = build_xlsm_with_vba(&vba_data);
2713
2714 let opts = OpenOptions::new()
2715 .read_mode(ReadMode::Eager)
2716 .aux_parts(AuxParts::EagerLoad);
2717 let wb = Workbook::open_from_buffer_with_options(&xlsm, &opts).unwrap();
2718 assert_eq!(wb.vba_blob.as_deref().unwrap(), &vba_data[..]);
2719
2720 let saved = wb.save_to_buffer().unwrap();
2721 let cursor = std::io::Cursor::new(&saved);
2722 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2723
2724 let mut roundtripped = Vec::new();
2725 std::io::Read::read_to_end(
2726 &mut archive.by_name("xl/vbaProject.bin").unwrap(),
2727 &mut roundtripped,
2728 )
2729 .unwrap();
2730 assert_eq!(roundtripped, vba_data);
2731 }
2732
2733 #[test]
2734 fn test_vba_relationship_preserved_on_roundtrip() {
2735 let vba_data = b"VBA_BLOB";
2736 let xlsm = build_xlsm_with_vba(vba_data);
2737
2738 let wb = Workbook::open_from_buffer(&xlsm).unwrap();
2739 let saved = wb.save_to_buffer().unwrap();
2740
2741 let cursor = std::io::Cursor::new(&saved);
2742 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2743
2744 let rels: Relationships =
2745 read_xml_part(&mut archive, "xl/_rels/workbook.xml.rels").unwrap();
2746 let vba_rel = rels
2747 .relationships
2748 .iter()
2749 .find(|r| r.rel_type == VBA_PROJECT_REL_TYPE);
2750 assert!(vba_rel.is_some(), "VBA relationship must be preserved");
2751 assert_eq!(vba_rel.unwrap().target, "vbaProject.bin");
2752 }
2753
2754 #[test]
2755 fn test_vba_content_type_preserved_on_roundtrip() {
2756 let vba_data = b"VBA_BLOB";
2757 let xlsm = build_xlsm_with_vba(vba_data);
2758
2759 let wb = Workbook::open_from_buffer(&xlsm).unwrap();
2760 let saved = wb.save_to_buffer().unwrap();
2761
2762 let cursor = std::io::Cursor::new(&saved);
2763 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2764
2765 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2766 let vba_override = ct
2767 .overrides
2768 .iter()
2769 .find(|o| o.part_name == "/xl/vbaProject.bin");
2770 assert!(
2771 vba_override.is_some(),
2772 "VBA content type override must be preserved"
2773 );
2774 assert_eq!(vba_override.unwrap().content_type, VBA_PROJECT_CONTENT_TYPE);
2775 }
2776
2777 #[test]
2778 fn test_non_vba_save_has_no_vba_entries() {
2779 let wb = Workbook::new();
2780 let buf = wb.save_to_buffer().unwrap();
2781
2782 let cursor = std::io::Cursor::new(&buf);
2783 let mut archive = zip::ZipArchive::new(cursor).unwrap();
2784
2785 assert!(
2786 archive.by_name("xl/vbaProject.bin").is_err(),
2787 "plain xlsx must not contain vbaProject.bin"
2788 );
2789
2790 let rels: Relationships =
2791 read_xml_part(&mut archive, "xl/_rels/workbook.xml.rels").unwrap();
2792 assert!(
2793 !rels
2794 .relationships
2795 .iter()
2796 .any(|r| r.rel_type == VBA_PROJECT_REL_TYPE),
2797 "plain xlsx must not have VBA relationship"
2798 );
2799
2800 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2801 assert!(
2802 !ct.overrides
2803 .iter()
2804 .any(|o| o.content_type == VBA_PROJECT_CONTENT_TYPE),
2805 "plain xlsx must not have VBA content type override"
2806 );
2807 }
2808
2809 #[test]
2810 fn test_xlsm_format_detected_with_vba() {
2811 let vba_data = b"VBA_BLOB";
2812 let xlsm = build_xlsm_with_vba(vba_data);
2813 let wb = Workbook::open_from_buffer(&xlsm).unwrap();
2814 assert_eq!(wb.format(), WorkbookFormat::Xlsm);
2815 }
2816
2817 #[test]
2818 fn test_from_extension_recognized() {
2819 assert_eq!(
2820 WorkbookFormat::from_extension("xlsx"),
2821 Some(WorkbookFormat::Xlsx)
2822 );
2823 assert_eq!(
2824 WorkbookFormat::from_extension("xlsm"),
2825 Some(WorkbookFormat::Xlsm)
2826 );
2827 assert_eq!(
2828 WorkbookFormat::from_extension("xltx"),
2829 Some(WorkbookFormat::Xltx)
2830 );
2831 assert_eq!(
2832 WorkbookFormat::from_extension("xltm"),
2833 Some(WorkbookFormat::Xltm)
2834 );
2835 assert_eq!(
2836 WorkbookFormat::from_extension("xlam"),
2837 Some(WorkbookFormat::Xlam)
2838 );
2839 }
2840
2841 #[test]
2842 fn test_from_extension_case_insensitive() {
2843 assert_eq!(
2844 WorkbookFormat::from_extension("XLSX"),
2845 Some(WorkbookFormat::Xlsx)
2846 );
2847 assert_eq!(
2848 WorkbookFormat::from_extension("Xlsm"),
2849 Some(WorkbookFormat::Xlsm)
2850 );
2851 assert_eq!(
2852 WorkbookFormat::from_extension("XLTX"),
2853 Some(WorkbookFormat::Xltx)
2854 );
2855 }
2856
2857 #[test]
2858 fn test_from_extension_unrecognized() {
2859 assert_eq!(WorkbookFormat::from_extension("csv"), None);
2860 assert_eq!(WorkbookFormat::from_extension("xls"), None);
2861 assert_eq!(WorkbookFormat::from_extension("txt"), None);
2862 assert_eq!(WorkbookFormat::from_extension("pdf"), None);
2863 assert_eq!(WorkbookFormat::from_extension(""), None);
2864 }
2865
2866 #[test]
2867 fn test_save_unsupported_extension_csv() {
2868 let dir = TempDir::new().unwrap();
2869 let path = dir.path().join("output.csv");
2870 let wb = Workbook::new();
2871 let result = wb.save(&path);
2872 assert!(matches!(result, Err(Error::UnsupportedFileExtension(ext)) if ext == "csv"));
2873 }
2874
2875 #[test]
2876 fn test_save_unsupported_extension_xls() {
2877 let dir = TempDir::new().unwrap();
2878 let path = dir.path().join("output.xls");
2879 let wb = Workbook::new();
2880 let result = wb.save(&path);
2881 assert!(matches!(result, Err(Error::UnsupportedFileExtension(ext)) if ext == "xls"));
2882 }
2883
2884 #[test]
2885 fn test_save_unsupported_extension_unknown() {
2886 let dir = TempDir::new().unwrap();
2887 let path = dir.path().join("output.foo");
2888 let wb = Workbook::new();
2889 let result = wb.save(&path);
2890 assert!(matches!(result, Err(Error::UnsupportedFileExtension(ext)) if ext == "foo"));
2891 }
2892
2893 #[test]
2894 fn test_save_no_extension_fails() {
2895 let dir = TempDir::new().unwrap();
2896 let path = dir.path().join("noext");
2897 let wb = Workbook::new();
2898 let result = wb.save(&path);
2899 assert!(matches!(
2900 result,
2901 Err(Error::UnsupportedFileExtension(ext)) if ext.is_empty()
2902 ));
2903 }
2904
2905 #[test]
2906 fn test_save_as_xlsm_writes_xlsm_content_type() {
2907 let dir = TempDir::new().unwrap();
2908 let path = dir.path().join("output.xlsm");
2909 let wb = Workbook::new();
2910 wb.save(&path).unwrap();
2911
2912 let file = std::fs::File::open(&path).unwrap();
2913 let mut archive = zip::ZipArchive::new(file).unwrap();
2914 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2915 let wb_ct = ct
2916 .overrides
2917 .iter()
2918 .find(|o| o.part_name == "/xl/workbook.xml")
2919 .expect("workbook override must exist");
2920 assert_eq!(wb_ct.content_type, WorkbookFormat::Xlsm.content_type());
2921 }
2922
2923 #[test]
2924 fn test_save_as_xltx_writes_template_content_type() {
2925 let dir = TempDir::new().unwrap();
2926 let path = dir.path().join("output.xltx");
2927 let wb = Workbook::new();
2928 wb.save(&path).unwrap();
2929
2930 let file = std::fs::File::open(&path).unwrap();
2931 let mut archive = zip::ZipArchive::new(file).unwrap();
2932 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2933 let wb_ct = ct
2934 .overrides
2935 .iter()
2936 .find(|o| o.part_name == "/xl/workbook.xml")
2937 .expect("workbook override must exist");
2938 assert_eq!(wb_ct.content_type, WorkbookFormat::Xltx.content_type());
2939 }
2940
2941 #[test]
2942 fn test_save_as_xltm_writes_template_macro_content_type() {
2943 let dir = TempDir::new().unwrap();
2944 let path = dir.path().join("output.xltm");
2945 let wb = Workbook::new();
2946 wb.save(&path).unwrap();
2947
2948 let file = std::fs::File::open(&path).unwrap();
2949 let mut archive = zip::ZipArchive::new(file).unwrap();
2950 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2951 let wb_ct = ct
2952 .overrides
2953 .iter()
2954 .find(|o| o.part_name == "/xl/workbook.xml")
2955 .expect("workbook override must exist");
2956 assert_eq!(wb_ct.content_type, WorkbookFormat::Xltm.content_type());
2957 }
2958
2959 #[test]
2960 fn test_save_as_xlam_writes_addin_content_type() {
2961 let dir = TempDir::new().unwrap();
2962 let path = dir.path().join("output.xlam");
2963 let wb = Workbook::new();
2964 wb.save(&path).unwrap();
2965
2966 let file = std::fs::File::open(&path).unwrap();
2967 let mut archive = zip::ZipArchive::new(file).unwrap();
2968 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2969 let wb_ct = ct
2970 .overrides
2971 .iter()
2972 .find(|o| o.part_name == "/xl/workbook.xml")
2973 .expect("workbook override must exist");
2974 assert_eq!(wb_ct.content_type, WorkbookFormat::Xlam.content_type());
2975 }
2976
2977 #[test]
2978 fn test_save_extension_overrides_stored_format() {
2979 let dir = TempDir::new().unwrap();
2980 let path = dir.path().join("output.xlsm");
2981
2982 let wb = Workbook::new();
2984 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
2985 wb.save(&path).unwrap();
2986
2987 let file = std::fs::File::open(&path).unwrap();
2988 let mut archive = zip::ZipArchive::new(file).unwrap();
2989 let ct: ContentTypes = read_xml_part(&mut archive, "[Content_Types].xml").unwrap();
2990 let wb_ct = ct
2991 .overrides
2992 .iter()
2993 .find(|o| o.part_name == "/xl/workbook.xml")
2994 .expect("workbook override must exist");
2995 assert_eq!(
2996 wb_ct.content_type,
2997 WorkbookFormat::Xlsm.content_type(),
2998 "extension .xlsm must override stored Xlsx format"
2999 );
3000 }
3001
3002 #[test]
3003 fn test_save_to_buffer_preserves_stored_format() {
3004 let mut wb = Workbook::new();
3005 wb.set_format(WorkbookFormat::Xltx);
3006
3007 let buf = wb.save_to_buffer().unwrap();
3008 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
3009 assert_eq!(
3010 wb2.format(),
3011 WorkbookFormat::Xltx,
3012 "save_to_buffer must use the stored format, not infer from extension"
3013 );
3014 }
3015
3016 #[test]
3017 fn test_sheet_rows_limits_rows_read() {
3018 let dir = TempDir::new().unwrap();
3019 let path = dir.path().join("sheet_rows.xlsx");
3020
3021 let mut wb = Workbook::new();
3022 for i in 1..=20 {
3023 let cell = format!("A{}", i);
3024 wb.set_cell_value("Sheet1", &cell, CellValue::Number(i as f64))
3025 .unwrap();
3026 }
3027 wb.save(&path).unwrap();
3028
3029 let opts = OpenOptions::new().sheet_rows(5);
3030 let wb2 = Workbook::open_with_options(&path, &opts).unwrap();
3031
3032 for i in 1..=5 {
3034 let cell = format!("A{}", i);
3035 assert_eq!(
3036 wb2.get_cell_value("Sheet1", &cell).unwrap(),
3037 CellValue::Number(i as f64)
3038 );
3039 }
3040
3041 for i in 6..=20 {
3043 let cell = format!("A{}", i);
3044 assert_eq!(
3045 wb2.get_cell_value("Sheet1", &cell).unwrap(),
3046 CellValue::Empty
3047 );
3048 }
3049 }
3050
3051 #[test]
3052 fn test_sheet_rows_with_buffer() {
3053 let mut wb = Workbook::new();
3054 for i in 1..=10 {
3055 let cell = format!("A{}", i);
3056 wb.set_cell_value("Sheet1", &cell, CellValue::Number(i as f64))
3057 .unwrap();
3058 }
3059 let buf = wb.save_to_buffer().unwrap();
3060
3061 let opts = OpenOptions::new().sheet_rows(3);
3062 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3063
3064 assert_eq!(
3065 wb2.get_cell_value("Sheet1", "A3").unwrap(),
3066 CellValue::Number(3.0)
3067 );
3068 assert_eq!(
3069 wb2.get_cell_value("Sheet1", "A4").unwrap(),
3070 CellValue::Empty
3071 );
3072 }
3073
3074 #[test]
3075 fn test_save_xlsx_preserves_existing_behavior() {
3076 let dir = TempDir::new().unwrap();
3077 let path = dir.path().join("preserved.xlsx");
3078
3079 let mut wb = Workbook::new();
3080 wb.set_cell_value("Sheet1", "A1", CellValue::String("hello".to_string()))
3081 .unwrap();
3082 wb.save(&path).unwrap();
3083
3084 let wb2 = Workbook::open(&path).unwrap();
3085 assert_eq!(wb2.format(), WorkbookFormat::Xlsx);
3086 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
3087 assert_eq!(
3088 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3089 CellValue::String("hello".to_string())
3090 );
3091 }
3092
3093 #[test]
3094 fn test_selective_sheet_parsing() {
3095 let dir = TempDir::new().unwrap();
3096 let path = dir.path().join("selective.xlsx");
3097
3098 let mut wb = Workbook::new();
3099 wb.new_sheet("Sales").unwrap();
3100 wb.new_sheet("Data").unwrap();
3101 wb.set_cell_value("Sheet1", "A1", CellValue::String("Sheet1 data".to_string()))
3102 .unwrap();
3103 wb.set_cell_value("Sales", "A1", CellValue::String("Sales data".to_string()))
3104 .unwrap();
3105 wb.set_cell_value("Data", "A1", CellValue::String("Data data".to_string()))
3106 .unwrap();
3107 wb.save(&path).unwrap();
3108
3109 let opts = OpenOptions::new().sheets(vec!["Sales".to_string()]);
3110 let wb2 = Workbook::open_with_options(&path, &opts).unwrap();
3111
3112 assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Sales", "Data"]);
3114
3115 assert_eq!(
3117 wb2.get_cell_value("Sales", "A1").unwrap(),
3118 CellValue::String("Sales data".to_string())
3119 );
3120
3121 assert_eq!(
3123 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3124 CellValue::Empty
3125 );
3126 assert_eq!(wb2.get_cell_value("Data", "A1").unwrap(), CellValue::Empty);
3127 }
3128
3129 #[test]
3130 fn test_selective_sheets_multiple() {
3131 let mut wb = Workbook::new();
3132 wb.new_sheet("Alpha").unwrap();
3133 wb.new_sheet("Beta").unwrap();
3134 wb.set_cell_value("Sheet1", "A1", CellValue::Number(1.0))
3135 .unwrap();
3136 wb.set_cell_value("Alpha", "A1", CellValue::Number(2.0))
3137 .unwrap();
3138 wb.set_cell_value("Beta", "A1", CellValue::Number(3.0))
3139 .unwrap();
3140 let buf = wb.save_to_buffer().unwrap();
3141
3142 let opts = OpenOptions::new().sheets(vec!["Sheet1".to_string(), "Beta".to_string()]);
3143 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3144
3145 assert_eq!(
3146 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3147 CellValue::Number(1.0)
3148 );
3149 assert_eq!(wb2.get_cell_value("Alpha", "A1").unwrap(), CellValue::Empty);
3150 assert_eq!(
3151 wb2.get_cell_value("Beta", "A1").unwrap(),
3152 CellValue::Number(3.0)
3153 );
3154 }
3155
3156 #[test]
3157 fn test_save_does_not_mutate_stored_format() {
3158 let dir = TempDir::new().unwrap();
3159 let path = dir.path().join("test.xlsm");
3160 let wb = Workbook::new();
3161 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
3162 wb.save(&path).unwrap();
3163 assert_eq!(wb.format(), WorkbookFormat::Xlsx);
3165 }
3166
3167 #[test]
3168 fn test_max_zip_entries_exceeded() {
3169 let wb = Workbook::new();
3170 let buf = wb.save_to_buffer().unwrap();
3171
3172 let opts = OpenOptions::new().max_zip_entries(2);
3174 let result = Workbook::open_from_buffer_with_options(&buf, &opts);
3175 assert!(matches!(result, Err(Error::ZipEntryCountExceeded { .. })));
3176 }
3177
3178 #[test]
3179 fn test_max_zip_entries_within_limit() {
3180 let wb = Workbook::new();
3181 let buf = wb.save_to_buffer().unwrap();
3182
3183 let opts = OpenOptions::new().max_zip_entries(1000);
3184 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3185 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
3186 }
3187
3188 #[test]
3189 fn test_max_unzip_size_exceeded() {
3190 let mut wb = Workbook::new();
3191 for i in 1..=100 {
3193 let cell = format!("A{}", i);
3194 wb.set_cell_value(
3195 "Sheet1",
3196 &cell,
3197 CellValue::String("long_value_for_size_check".repeat(10)),
3198 )
3199 .unwrap();
3200 }
3201 let buf = wb.save_to_buffer().unwrap();
3202
3203 let opts = OpenOptions::new().max_unzip_size(100);
3205 let result = Workbook::open_from_buffer_with_options(&buf, &opts);
3206 assert!(matches!(result, Err(Error::ZipSizeExceeded { .. })));
3207 }
3208
3209 #[test]
3210 fn test_max_unzip_size_within_limit() {
3211 let wb = Workbook::new();
3212 let buf = wb.save_to_buffer().unwrap();
3213
3214 let opts = OpenOptions::new().max_unzip_size(1_000_000_000);
3215 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3216 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
3217 }
3218
3219 #[test]
3220 fn test_combined_options() {
3221 let mut wb = Workbook::new();
3222 wb.new_sheet("Parsed").unwrap();
3223 wb.new_sheet("Skipped").unwrap();
3224 for i in 1..=10 {
3225 let cell = format!("A{}", i);
3226 wb.set_cell_value("Parsed", &cell, CellValue::Number(i as f64))
3227 .unwrap();
3228 wb.set_cell_value("Skipped", &cell, CellValue::Number(i as f64))
3229 .unwrap();
3230 }
3231 let buf = wb.save_to_buffer().unwrap();
3232
3233 let opts = OpenOptions::new()
3234 .sheets(vec!["Parsed".to_string()])
3235 .sheet_rows(3);
3236 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3237
3238 assert_eq!(
3240 wb2.get_cell_value("Parsed", "A3").unwrap(),
3241 CellValue::Number(3.0)
3242 );
3243 assert_eq!(
3244 wb2.get_cell_value("Parsed", "A4").unwrap(),
3245 CellValue::Empty
3246 );
3247
3248 assert_eq!(
3250 wb2.get_cell_value("Skipped", "A1").unwrap(),
3251 CellValue::Empty
3252 );
3253 }
3254
3255 #[test]
3256 fn test_sheet_rows_zero_means_no_rows() {
3257 let mut wb = Workbook::new();
3258 wb.set_cell_value("Sheet1", "A1", CellValue::Number(1.0))
3259 .unwrap();
3260 let buf = wb.save_to_buffer().unwrap();
3261
3262 let opts = OpenOptions::new().sheet_rows(0);
3263 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3264 assert_eq!(
3265 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3266 CellValue::Empty
3267 );
3268 }
3269
3270 #[test]
3271 fn test_selective_sheet_parsing_preserves_unparsed_sheets_on_save() {
3272 let dir = TempDir::new().unwrap();
3273 let path1 = dir.path().join("original.xlsx");
3274 let path2 = dir.path().join("resaved.xlsx");
3275
3276 let mut wb = Workbook::new();
3278 wb.new_sheet("Sales").unwrap();
3279 wb.new_sheet("Data").unwrap();
3280 wb.set_cell_value(
3281 "Sheet1",
3282 "A1",
3283 CellValue::String("Sheet1 value".to_string()),
3284 )
3285 .unwrap();
3286 wb.set_cell_value("Sheet1", "B2", CellValue::Number(100.0))
3287 .unwrap();
3288 wb.set_cell_value("Sales", "A1", CellValue::String("Sales value".to_string()))
3289 .unwrap();
3290 wb.set_cell_value("Sales", "C3", CellValue::Number(200.0))
3291 .unwrap();
3292 wb.set_cell_value("Data", "A1", CellValue::String("Data value".to_string()))
3293 .unwrap();
3294 wb.set_cell_value("Data", "D4", CellValue::Bool(true))
3295 .unwrap();
3296 wb.save(&path1).unwrap();
3297
3298 let opts = OpenOptions::new().sheets(vec!["Sheet1".to_string()]);
3300 let wb2 = Workbook::open_with_options(&path1, &opts).unwrap();
3301
3302 assert_eq!(
3304 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3305 CellValue::String("Sheet1 value".to_string())
3306 );
3307
3308 wb2.save(&path2).unwrap();
3310
3311 let wb3 = Workbook::open(&path2).unwrap();
3313 assert_eq!(wb3.sheet_names(), vec!["Sheet1", "Sales", "Data"]);
3314
3315 assert_eq!(
3317 wb3.get_cell_value("Sheet1", "A1").unwrap(),
3318 CellValue::String("Sheet1 value".to_string())
3319 );
3320 assert_eq!(
3321 wb3.get_cell_value("Sheet1", "B2").unwrap(),
3322 CellValue::Number(100.0)
3323 );
3324
3325 assert_eq!(
3327 wb3.get_cell_value("Sales", "A1").unwrap(),
3328 CellValue::String("Sales value".to_string())
3329 );
3330 assert_eq!(
3331 wb3.get_cell_value("Sales", "C3").unwrap(),
3332 CellValue::Number(200.0)
3333 );
3334
3335 assert_eq!(
3337 wb3.get_cell_value("Data", "A1").unwrap(),
3338 CellValue::String("Data value".to_string())
3339 );
3340 assert_eq!(
3341 wb3.get_cell_value("Data", "D4").unwrap(),
3342 CellValue::Bool(true)
3343 );
3344 }
3345
3346 #[test]
3347 fn test_open_from_buffer_with_options_backwards_compatible() {
3348 let mut wb = Workbook::new();
3349 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
3350 .unwrap();
3351 let buf = wb.save_to_buffer().unwrap();
3352
3353 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
3354 assert_eq!(
3355 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3356 CellValue::String("Hello".to_string())
3357 );
3358 }
3359
3360 use crate::workbook::open_options::ReadMode;
3361
3362 #[test]
3363 fn test_readfast_open_reads_cell_data() {
3364 let mut wb = Workbook::new();
3365 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
3366 .unwrap();
3367 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
3368 .unwrap();
3369 wb.set_cell_value("Sheet1", "C3", CellValue::Bool(true))
3370 .unwrap();
3371 let buf = wb.save_to_buffer().unwrap();
3372
3373 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3374 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3375 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
3376 assert_eq!(
3377 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3378 CellValue::String("Hello".to_string())
3379 );
3380 assert_eq!(
3381 wb2.get_cell_value("Sheet1", "B2").unwrap(),
3382 CellValue::Number(42.0)
3383 );
3384 assert_eq!(
3385 wb2.get_cell_value("Sheet1", "C3").unwrap(),
3386 CellValue::Bool(true)
3387 );
3388 }
3389
3390 #[test]
3391 fn test_readfast_open_multi_sheet() {
3392 let mut wb = Workbook::new();
3393 wb.new_sheet("Sheet2").unwrap();
3394 wb.set_cell_value("Sheet1", "A1", CellValue::String("S1".to_string()))
3395 .unwrap();
3396 wb.set_cell_value("Sheet2", "A1", CellValue::String("S2".to_string()))
3397 .unwrap();
3398 let buf = wb.save_to_buffer().unwrap();
3399
3400 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3401 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3402 assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Sheet2"]);
3403 assert_eq!(
3404 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3405 CellValue::String("S1".to_string())
3406 );
3407 assert_eq!(
3408 wb2.get_cell_value("Sheet2", "A1").unwrap(),
3409 CellValue::String("S2".to_string())
3410 );
3411 }
3412
3413 #[test]
3414 fn test_readfast_skips_comments() {
3415 let mut wb = Workbook::new();
3416 wb.set_cell_value("Sheet1", "A1", CellValue::String("data".to_string()))
3417 .unwrap();
3418 wb.add_comment(
3419 "Sheet1",
3420 &crate::comment::CommentConfig {
3421 cell: "A1".to_string(),
3422 author: "Tester".to_string(),
3423 text: "A test comment".to_string(),
3424 },
3425 )
3426 .unwrap();
3427 let buf = wb.save_to_buffer().unwrap();
3428
3429 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3430 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3431
3432 assert_eq!(
3434 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3435 CellValue::String("data".to_string())
3436 );
3437 let comments = wb2.get_comments("Sheet1").unwrap();
3439 assert_eq!(comments.len(), 1);
3440 assert_eq!(comments[0].text, "A test comment");
3441 }
3442
3443 #[test]
3444 fn test_readfast_get_doc_properties_without_mutation() {
3445 let mut wb = Workbook::new();
3446 wb.set_cell_value("Sheet1", "A1", CellValue::Number(1.0))
3447 .unwrap();
3448 wb.set_doc_props(crate::doc_props::DocProperties {
3449 title: Some("Test Title".to_string()),
3450 ..Default::default()
3451 });
3452 let buf = wb.save_to_buffer().unwrap();
3453
3454 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3455 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3456
3457 assert_eq!(
3459 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3460 CellValue::Number(1.0)
3461 );
3462 let props = wb2.get_doc_props();
3464 assert_eq!(props.title.as_deref(), Some("Test Title"));
3465 }
3466
3467 #[test]
3468 fn test_readfast_save_roundtrip_preserves_all_parts() {
3469 let mut wb = Workbook::new();
3470 wb.set_cell_value("Sheet1", "A1", CellValue::String("data".to_string()))
3471 .unwrap();
3472 wb.add_comment(
3473 "Sheet1",
3474 &crate::comment::CommentConfig {
3475 cell: "A1".to_string(),
3476 author: "Tester".to_string(),
3477 text: "A comment".to_string(),
3478 },
3479 )
3480 .unwrap();
3481 wb.set_doc_props(crate::doc_props::DocProperties {
3482 title: Some("Title".to_string()),
3483 ..Default::default()
3484 });
3485 let buf = wb.save_to_buffer().unwrap();
3486
3487 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3489 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3490 let saved = wb2.save_to_buffer().unwrap();
3491
3492 let mut wb3 = Workbook::open_from_buffer(&saved).unwrap();
3494 assert_eq!(
3495 wb3.get_cell_value("Sheet1", "A1").unwrap(),
3496 CellValue::String("data".to_string())
3497 );
3498 let comments = wb3.get_comments("Sheet1").unwrap();
3499 assert_eq!(comments.len(), 1);
3500 assert_eq!(comments[0].text, "A comment");
3501 let props = wb3.get_doc_props();
3502 assert_eq!(props.title, Some("Title".to_string()));
3503 }
3504
3505 #[test]
3506 fn test_readfast_with_sheet_rows_limit() {
3507 let mut wb = Workbook::new();
3508 for i in 1..=100 {
3509 wb.set_cell_value("Sheet1", &format!("A{}", i), CellValue::Number(i as f64))
3510 .unwrap();
3511 }
3512 let buf = wb.save_to_buffer().unwrap();
3513
3514 let opts = OpenOptions::new().read_mode(ReadMode::Lazy).sheet_rows(10);
3515 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3516 let rows = wb2.get_rows("Sheet1").unwrap();
3517 assert_eq!(rows.len(), 10);
3518 }
3519
3520 #[test]
3521 fn test_readfast_with_sheets_filter() {
3522 let mut wb = Workbook::new();
3523 wb.new_sheet("Sheet2").unwrap();
3524 wb.set_cell_value("Sheet1", "A1", CellValue::String("S1".to_string()))
3525 .unwrap();
3526 wb.set_cell_value("Sheet2", "A1", CellValue::String("S2".to_string()))
3527 .unwrap();
3528 let buf = wb.save_to_buffer().unwrap();
3529
3530 let opts = OpenOptions::new()
3531 .read_mode(ReadMode::Lazy)
3532 .sheets(vec!["Sheet2".to_string()]);
3533 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3534 assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Sheet2"]);
3535 assert_eq!(
3536 wb2.get_cell_value("Sheet2", "A1").unwrap(),
3537 CellValue::String("S2".to_string())
3538 );
3539 assert_eq!(
3541 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3542 CellValue::Empty
3543 );
3544 }
3545
3546 #[test]
3547 fn test_readfast_preserves_styles() {
3548 let mut wb = Workbook::new();
3549 let style_id = wb
3550 .add_style(&crate::style::Style {
3551 font: Some(crate::style::FontStyle {
3552 bold: true,
3553 ..Default::default()
3554 }),
3555 ..Default::default()
3556 })
3557 .unwrap();
3558 wb.set_cell_value("Sheet1", "A1", CellValue::String("bold".to_string()))
3559 .unwrap();
3560 wb.set_cell_style("Sheet1", "A1", style_id).unwrap();
3561 let buf = wb.save_to_buffer().unwrap();
3562
3563 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3564 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3565 let sid = wb2.get_cell_style("Sheet1", "A1").unwrap();
3566 assert!(sid.is_some());
3567 let style = crate::style::get_style(&wb2.stylesheet, sid.unwrap());
3568 assert!(style.is_some());
3569 assert!(style.unwrap().font.map_or(false, |f| f.bold));
3570 }
3571
3572 #[test]
3573 fn test_readfast_full_mode_unchanged() {
3574 let mut wb = Workbook::new();
3575 wb.set_cell_value("Sheet1", "A1", CellValue::String("test".to_string()))
3576 .unwrap();
3577 wb.add_comment(
3578 "Sheet1",
3579 &crate::comment::CommentConfig {
3580 cell: "A1".to_string(),
3581 author: "Author".to_string(),
3582 text: "comment text".to_string(),
3583 },
3584 )
3585 .unwrap();
3586 let buf = wb.save_to_buffer().unwrap();
3587
3588 let opts = OpenOptions::new().read_mode(ReadMode::Eager);
3590 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3591 let comments = wb2.get_comments("Sheet1").unwrap();
3592 assert_eq!(comments.len(), 1);
3593 }
3594
3595 #[test]
3596 fn test_readfast_open_from_file() {
3597 let dir = TempDir::new().unwrap();
3598 let path = dir.path().join("readfast_test.xlsx");
3599
3600 let mut wb = Workbook::new();
3601 wb.set_cell_value("Sheet1", "A1", CellValue::String("file test".to_string()))
3602 .unwrap();
3603 wb.save(&path).unwrap();
3604
3605 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3606 let wb2 = Workbook::open_with_options(&path, &opts).unwrap();
3607 assert_eq!(
3608 wb2.get_cell_value("Sheet1", "A1").unwrap(),
3609 CellValue::String("file test".to_string())
3610 );
3611 }
3612
3613 #[test]
3614 fn test_readfast_roundtrip_with_custom_zip_entries() {
3615 let buf = create_xlsx_with_custom_entries();
3616
3617 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3618 let wb = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3619 assert_eq!(
3620 wb.get_cell_value("Sheet1", "A1").unwrap(),
3621 CellValue::String("hello".to_string())
3622 );
3623
3624 let saved = wb.save_to_buffer().unwrap();
3625 let cursor = std::io::Cursor::new(&saved);
3626 let mut archive = zip::ZipArchive::new(cursor).unwrap();
3627
3628 let mut custom_xml = String::new();
3630 std::io::Read::read_to_string(
3631 &mut archive.by_name("customXml/item1.xml").unwrap(),
3632 &mut custom_xml,
3633 )
3634 .unwrap();
3635 assert_eq!(custom_xml, "<custom>data1</custom>");
3636
3637 let mut printer = Vec::new();
3638 std::io::Read::read_to_end(
3639 &mut archive
3640 .by_name("xl/printerSettings/printerSettings1.bin")
3641 .unwrap(),
3642 &mut printer,
3643 )
3644 .unwrap();
3645 assert_eq!(printer, b"\x00\x01\x02\x03PRINTER");
3646 }
3647
3648 #[test]
3649 fn test_readfast_deferred_parts_not_empty_when_auxiliary_exist() {
3650 let mut wb = Workbook::new();
3651 wb.set_cell_value("Sheet1", "A1", CellValue::String("data".to_string()))
3652 .unwrap();
3653 wb.add_comment(
3654 "Sheet1",
3655 &crate::comment::CommentConfig {
3656 cell: "A1".to_string(),
3657 author: "Tester".to_string(),
3658 text: "comment".to_string(),
3659 },
3660 )
3661 .unwrap();
3662 let buf = wb.save_to_buffer().unwrap();
3663
3664 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3665 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3666 assert!(
3668 wb2.deferred_parts.has_any(),
3669 "deferred_parts should contain skipped auxiliary parts"
3670 );
3671 }
3672
3673 #[test]
3674 fn test_readfast_eager_mode_has_no_deferred_parts() {
3675 use crate::workbook::open_options::{AuxParts, OpenOptions, ReadMode};
3676
3677 let mut wb = Workbook::new();
3678 wb.set_cell_value("Sheet1", "A1", CellValue::String("data".to_string()))
3679 .unwrap();
3680 wb.add_comment(
3681 "Sheet1",
3682 &crate::comment::CommentConfig {
3683 cell: "A1".to_string(),
3684 author: "Tester".to_string(),
3685 text: "comment".to_string(),
3686 },
3687 )
3688 .unwrap();
3689 let buf = wb.save_to_buffer().unwrap();
3690
3691 let opts = OpenOptions::new()
3693 .read_mode(ReadMode::Eager)
3694 .aux_parts(AuxParts::EagerLoad);
3695 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3696 assert!(
3697 !wb2.deferred_parts.has_any(),
3698 "Eager mode should not have deferred parts"
3699 );
3700 }
3701
3702 #[test]
3703 fn test_readfast_table_parts_preserved_on_roundtrip() {
3704 let mut wb = Workbook::new();
3705 wb.set_cell_value("Sheet1", "A1", CellValue::String("Name".to_string()))
3706 .unwrap();
3707 wb.set_cell_value("Sheet1", "B1", CellValue::String("Value".to_string()))
3708 .unwrap();
3709 wb.set_cell_value("Sheet1", "A2", CellValue::String("Alice".to_string()))
3710 .unwrap();
3711 wb.set_cell_value("Sheet1", "B2", CellValue::Number(10.0))
3712 .unwrap();
3713 wb.add_table(
3714 "Sheet1",
3715 &crate::table::TableConfig {
3716 name: "Table1".to_string(),
3717 display_name: "Table1".to_string(),
3718 range: "A1:B2".to_string(),
3719 columns: vec![
3720 crate::table::TableColumn {
3721 name: "Name".to_string(),
3722 totals_row_function: None,
3723 totals_row_label: None,
3724 },
3725 crate::table::TableColumn {
3726 name: "Value".to_string(),
3727 totals_row_function: None,
3728 totals_row_label: None,
3729 },
3730 ],
3731 ..Default::default()
3732 },
3733 )
3734 .unwrap();
3735 let buf = wb.save_to_buffer().unwrap();
3736
3737 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3739 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3740 let saved = wb2.save_to_buffer().unwrap();
3741
3742 let wb3 = Workbook::open_from_buffer(&saved).unwrap();
3744 let tables = wb3.get_tables("Sheet1").unwrap();
3745 assert_eq!(tables.len(), 1);
3746 assert_eq!(tables[0].name, "Table1");
3747 }
3748
3749 #[test]
3750 fn test_readfast_delete_table_with_other_deferred_cleans_references() {
3751 use std::io::Read as _;
3752
3753 let mut wb = Workbook::new();
3754 wb.set_cell_value("Sheet1", "A1", CellValue::String("Name".to_string()))
3755 .unwrap();
3756 wb.set_cell_value("Sheet1", "B1", CellValue::String("Value".to_string()))
3757 .unwrap();
3758 wb.set_cell_value("Sheet1", "A2", CellValue::String("Alice".to_string()))
3759 .unwrap();
3760 wb.set_cell_value("Sheet1", "B2", CellValue::Number(10.0))
3761 .unwrap();
3762 wb.add_table(
3763 "Sheet1",
3764 &crate::table::TableConfig {
3765 name: "Table1".to_string(),
3766 display_name: "Table1".to_string(),
3767 range: "A1:B2".to_string(),
3768 columns: vec![
3769 crate::table::TableColumn {
3770 name: "Name".to_string(),
3771 totals_row_function: None,
3772 totals_row_label: None,
3773 },
3774 crate::table::TableColumn {
3775 name: "Value".to_string(),
3776 totals_row_function: None,
3777 totals_row_label: None,
3778 },
3779 ],
3780 ..Default::default()
3781 },
3782 )
3783 .unwrap();
3784 wb.set_doc_props(crate::doc_props::DocProperties {
3786 title: Some("Keep deferred".to_string()),
3787 ..Default::default()
3788 });
3789 let buf = wb.save_to_buffer().unwrap();
3790
3791 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3792 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3793 wb2.delete_table("Sheet1", "Table1").unwrap();
3794 let saved = wb2.save_to_buffer().unwrap();
3795
3796 let wb3 = Workbook::open_from_buffer(&saved).unwrap();
3797 assert!(wb3.get_tables("Sheet1").unwrap().is_empty());
3798
3799 let cursor = std::io::Cursor::new(saved);
3800 let mut archive = zip::ZipArchive::new(cursor).unwrap();
3801
3802 let mut ct_xml = String::new();
3803 archive
3804 .by_name("[Content_Types].xml")
3805 .unwrap()
3806 .read_to_string(&mut ct_xml)
3807 .unwrap();
3808 assert!(
3809 !ct_xml.contains("/xl/tables/table1.xml"),
3810 "content types must not reference the deleted table part"
3811 );
3812 assert!(
3813 !ct_xml.contains(mime_types::TABLE),
3814 "content types must not keep table override after deletion"
3815 );
3816
3817 let mut rels_xml = String::new();
3818 archive
3819 .by_name("xl/worksheets/_rels/sheet1.xml.rels")
3820 .unwrap()
3821 .read_to_string(&mut rels_xml)
3822 .unwrap();
3823 assert!(
3824 !rels_xml.contains(rel_types::TABLE),
3825 "worksheet rels must not contain table relationship after deletion"
3826 );
3827
3828 let mut sheet_xml = String::new();
3829 archive
3830 .by_name("xl/worksheets/sheet1.xml")
3831 .unwrap()
3832 .read_to_string(&mut sheet_xml)
3833 .unwrap();
3834 assert!(
3835 !sheet_xml.contains("tableParts"),
3836 "worksheet XML must not contain tableParts after deletion"
3837 );
3838 }
3839
3840 #[test]
3841 fn test_readfast_add_comment_then_save_no_duplicate() {
3842 let mut wb = Workbook::new();
3843 wb.set_cell_value("Sheet1", "A1", CellValue::String("data".to_string()))
3844 .unwrap();
3845 wb.add_comment(
3846 "Sheet1",
3847 &crate::comment::CommentConfig {
3848 cell: "A1".to_string(),
3849 author: "Tester".to_string(),
3850 text: "Original comment".to_string(),
3851 },
3852 )
3853 .unwrap();
3854 let buf = wb.save_to_buffer().unwrap();
3855
3856 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3858 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3859 wb2.add_comment(
3860 "Sheet1",
3861 &crate::comment::CommentConfig {
3862 cell: "B1".to_string(),
3863 author: "Tester".to_string(),
3864 text: "New comment".to_string(),
3865 },
3866 )
3867 .unwrap();
3868 let saved = wb2.save_to_buffer().unwrap();
3870
3871 let mut wb3 = Workbook::open_from_buffer(&saved).unwrap();
3873 let comments = wb3.get_comments("Sheet1").unwrap();
3874 assert!(
3875 comments.iter().any(|c| c.text == "New comment"),
3876 "New comment should be present after Lazy + add_comment round-trip"
3877 );
3878 assert!(
3879 comments.iter().any(|c| c.text == "Original comment"),
3880 "Original comment must be preserved after Lazy + add_comment round-trip"
3881 );
3882 assert_eq!(
3883 comments.len(),
3884 2,
3885 "Both original and new comments must survive"
3886 );
3887 }
3888
3889 #[test]
3890 fn test_readfast_add_comment_preserves_existing_comments() {
3891 let mut wb = Workbook::new();
3894 wb.set_cell_value("Sheet1", "A1", CellValue::String("data".to_string()))
3895 .unwrap();
3896 wb.add_comment(
3897 "Sheet1",
3898 &crate::comment::CommentConfig {
3899 cell: "A1".to_string(),
3900 author: "Alice".to_string(),
3901 text: "First comment".to_string(),
3902 },
3903 )
3904 .unwrap();
3905 wb.add_comment(
3906 "Sheet1",
3907 &crate::comment::CommentConfig {
3908 cell: "B2".to_string(),
3909 author: "Bob".to_string(),
3910 text: "Second comment".to_string(),
3911 },
3912 )
3913 .unwrap();
3914 let buf = wb.save_to_buffer().unwrap();
3915
3916 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3917 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3918
3919 wb2.add_comment(
3921 "Sheet1",
3922 &crate::comment::CommentConfig {
3923 cell: "C3".to_string(),
3924 author: "Charlie".to_string(),
3925 text: "Third comment".to_string(),
3926 },
3927 )
3928 .unwrap();
3929 let saved = wb2.save_to_buffer().unwrap();
3930
3931 let mut wb3 = Workbook::open_from_buffer(&saved).unwrap();
3932 let comments = wb3.get_comments("Sheet1").unwrap();
3933 assert_eq!(comments.len(), 3, "All three comments must be present");
3934 assert!(comments
3935 .iter()
3936 .any(|c| c.cell == "A1" && c.text == "First comment"));
3937 assert!(comments
3938 .iter()
3939 .any(|c| c.cell == "B2" && c.text == "Second comment"));
3940 assert!(comments
3941 .iter()
3942 .any(|c| c.cell == "C3" && c.text == "Third comment"));
3943 }
3944
3945 #[test]
3946 fn test_readfast_get_comments_hydrates_deferred() {
3947 let mut wb = Workbook::new();
3949 wb.add_comment(
3950 "Sheet1",
3951 &crate::comment::CommentConfig {
3952 cell: "A1".to_string(),
3953 author: "Author".to_string(),
3954 text: "Deferred comment".to_string(),
3955 },
3956 )
3957 .unwrap();
3958 let buf = wb.save_to_buffer().unwrap();
3959
3960 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3961 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3962
3963 let comments = wb2.get_comments("Sheet1").unwrap();
3965 assert_eq!(comments.len(), 1);
3966 assert_eq!(comments[0].cell, "A1");
3967 assert_eq!(comments[0].text, "Deferred comment");
3968 }
3969
3970 #[test]
3971 fn test_readfast_remove_comment_hydrates_first() {
3972 let mut wb = Workbook::new();
3975 wb.add_comment(
3976 "Sheet1",
3977 &crate::comment::CommentConfig {
3978 cell: "A1".to_string(),
3979 author: "Alice".to_string(),
3980 text: "Keep me".to_string(),
3981 },
3982 )
3983 .unwrap();
3984 wb.add_comment(
3985 "Sheet1",
3986 &crate::comment::CommentConfig {
3987 cell: "B2".to_string(),
3988 author: "Bob".to_string(),
3989 text: "Remove me".to_string(),
3990 },
3991 )
3992 .unwrap();
3993 let buf = wb.save_to_buffer().unwrap();
3994
3995 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
3996 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
3997 wb2.remove_comment("Sheet1", "B2").unwrap();
3998
3999 let saved = wb2.save_to_buffer().unwrap();
4000 let mut wb3 = Workbook::open_from_buffer(&saved).unwrap();
4001 let comments = wb3.get_comments("Sheet1").unwrap();
4002 assert_eq!(comments.len(), 1);
4003 assert_eq!(comments[0].cell, "A1");
4004 assert_eq!(comments[0].text, "Keep me");
4005 }
4006
4007 #[test]
4008 fn test_readfast_add_comment_no_preexisting_comments() {
4009 let mut wb = Workbook::new();
4013 wb.set_cell_value("Sheet1", "A1", CellValue::String("data".to_string()))
4014 .unwrap();
4015 wb.set_doc_props(crate::doc_props::DocProperties {
4017 title: Some("Trigger deferred".to_string()),
4018 ..Default::default()
4019 });
4020 let buf = wb.save_to_buffer().unwrap();
4021
4022 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
4023 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4024 wb2.add_comment(
4025 "Sheet1",
4026 &crate::comment::CommentConfig {
4027 cell: "A1".to_string(),
4028 author: "Newcomer".to_string(),
4029 text: "Brand new comment".to_string(),
4030 },
4031 )
4032 .unwrap();
4033
4034 let saved = wb2.save_to_buffer().unwrap();
4035
4036 let mut wb3 = Workbook::open_from_buffer(&saved).unwrap();
4038 let comments = wb3.get_comments("Sheet1").unwrap();
4039 assert_eq!(comments.len(), 1);
4040 assert_eq!(comments[0].text, "Brand new comment");
4041
4042 let reader = std::io::Cursor::new(&saved);
4044 let mut archive = zip::ZipArchive::new(reader).unwrap();
4045 assert!(
4046 archive.by_name("xl/comments1.xml").is_ok(),
4047 "comments1.xml must be present"
4048 );
4049 assert!(
4050 archive.by_name("xl/drawings/vmlDrawing1.vml").is_ok(),
4051 "vmlDrawing1.vml must be present for the comment"
4052 );
4053 }
4054
4055 #[test]
4056 fn test_readfast_add_comment_vml_roundtrip() {
4057 let mut wb = Workbook::new();
4059 wb.add_comment(
4060 "Sheet1",
4061 &crate::comment::CommentConfig {
4062 cell: "A1".to_string(),
4063 author: "Original".to_string(),
4064 text: "Has VML".to_string(),
4065 },
4066 )
4067 .unwrap();
4068 let buf = wb.save_to_buffer().unwrap();
4069
4070 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
4071 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4072 wb2.add_comment(
4073 "Sheet1",
4074 &crate::comment::CommentConfig {
4075 cell: "B2".to_string(),
4076 author: "New".to_string(),
4077 text: "Also has VML".to_string(),
4078 },
4079 )
4080 .unwrap();
4081 let saved = wb2.save_to_buffer().unwrap();
4082
4083 let reader = std::io::Cursor::new(&saved);
4085 let mut archive = zip::ZipArchive::new(reader).unwrap();
4086 assert!(archive.by_name("xl/drawings/vmlDrawing1.vml").is_ok());
4087
4088 let mut wb3 = Workbook::open_from_buffer(&saved).unwrap();
4090 let comments = wb3.get_comments("Sheet1").unwrap();
4091 assert_eq!(comments.len(), 2);
4092 }
4093
4094 #[test]
4095 fn test_readfast_set_doc_props_then_save_no_duplicate() {
4096 let mut wb = Workbook::new();
4097 wb.set_cell_value("Sheet1", "A1", CellValue::Number(1.0))
4098 .unwrap();
4099 wb.set_doc_props(crate::doc_props::DocProperties {
4100 title: Some("Original Title".to_string()),
4101 ..Default::default()
4102 });
4103 let buf = wb.save_to_buffer().unwrap();
4104
4105 let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
4107 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4108 wb2.set_doc_props(crate::doc_props::DocProperties {
4109 title: Some("Updated Title".to_string()),
4110 ..Default::default()
4111 });
4112 let saved = wb2.save_to_buffer().unwrap();
4114
4115 let wb3 = Workbook::open_from_buffer(&saved).unwrap();
4117 let props = wb3.get_doc_props();
4118 assert_eq!(props.title, Some("Updated Title".to_string()));
4119 }
4120
4121 #[test]
4122 fn test_read_xml_part_from_reader_worksheet() {
4123 use sheetkit_xml::worksheet::WorksheetXml;
4124 let ws_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4125<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
4126 <sheetData>
4127 <row r="1"><c r="A1" t="s"><v>0</v></c></row>
4128 <row r="2"><c r="A2"><v>42</v></c></row>
4129 </sheetData>
4130</worksheet>"#;
4131 let mut buf = Vec::new();
4132 {
4133 let cursor = std::io::Cursor::new(&mut buf);
4134 let mut zip = zip::ZipWriter::new(cursor);
4135 let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
4136 zip.start_file("test.xml", opts).unwrap();
4137 use std::io::Write;
4138 zip.write_all(ws_xml.as_bytes()).unwrap();
4139 zip.finish().unwrap();
4140 }
4141 let cursor = std::io::Cursor::new(&buf);
4142 let mut archive = zip::ZipArchive::new(cursor).unwrap();
4143 let ws: WorksheetXml = read_xml_part(&mut archive, "test.xml").unwrap();
4144 assert_eq!(ws.sheet_data.rows.len(), 2);
4145 assert_eq!(ws.sheet_data.rows[0].r, 1);
4146 assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
4147 assert_eq!(ws.sheet_data.rows[1].r, 2);
4148 assert_eq!(ws.sheet_data.rows[1].cells[0].v, Some("42".to_string()));
4149 }
4150
4151 #[test]
4152 fn test_read_xml_part_from_reader_sst() {
4153 use sheetkit_xml::shared_strings::Sst;
4154 let sst_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4155<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2">
4156 <si><t>Hello</t></si>
4157 <si><t>World</t></si>
4158</sst>"#;
4159 let mut buf = Vec::new();
4160 {
4161 let cursor = std::io::Cursor::new(&mut buf);
4162 let mut zip = zip::ZipWriter::new(cursor);
4163 let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
4164 zip.start_file("sst.xml", opts).unwrap();
4165 use std::io::Write;
4166 zip.write_all(sst_xml.as_bytes()).unwrap();
4167 zip.finish().unwrap();
4168 }
4169 let cursor = std::io::Cursor::new(&buf);
4170 let mut archive = zip::ZipArchive::new(cursor).unwrap();
4171 let sst: Sst = read_xml_part(&mut archive, "sst.xml").unwrap();
4172 assert_eq!(sst.count, Some(2));
4173 assert_eq!(sst.unique_count, Some(2));
4174 assert_eq!(sst.items.len(), 2);
4175 assert_eq!(sst.items[0].t.as_ref().unwrap().value, "Hello");
4176 assert_eq!(sst.items[1].t.as_ref().unwrap().value, "World");
4177 }
4178
4179 #[test]
4180 fn test_read_xml_part_from_reader_large_worksheet() {
4181 use sheetkit_xml::worksheet::WorksheetXml;
4182 let mut ws_xml = String::from(
4183 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4184<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
4185 <sheetData>"#,
4186 );
4187 for i in 1..=500 {
4188 ws_xml.push_str(&format!(
4189 "<row r=\"{i}\"><c r=\"A{i}\"><v>{}</v></c><c r=\"B{i}\"><v>{}</v></c></row>",
4190 i * 10,
4191 i * 20,
4192 ));
4193 }
4194 ws_xml.push_str("</sheetData></worksheet>");
4195
4196 let mut buf = Vec::new();
4197 {
4198 let cursor = std::io::Cursor::new(&mut buf);
4199 let mut zip = zip::ZipWriter::new(cursor);
4200 let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
4201 zip.start_file("sheet.xml", opts).unwrap();
4202 use std::io::Write;
4203 zip.write_all(ws_xml.as_bytes()).unwrap();
4204 zip.finish().unwrap();
4205 }
4206 let cursor = std::io::Cursor::new(&buf);
4207 let mut archive = zip::ZipArchive::new(cursor).unwrap();
4208 let ws: WorksheetXml = read_xml_part(&mut archive, "sheet.xml").unwrap();
4209 assert_eq!(ws.sheet_data.rows.len(), 500);
4210 assert_eq!(ws.sheet_data.rows[0].r, 1);
4211 assert_eq!(ws.sheet_data.rows[0].cells[0].v, Some("10".to_string()));
4212 assert_eq!(ws.sheet_data.rows[499].r, 500);
4213 assert_eq!(
4214 ws.sheet_data.rows[499].cells[1].v,
4215 Some("10000".to_string())
4216 );
4217 }
4218
4219 #[test]
4222 fn test_lazy_open_save_without_modification_roundtrips() {
4223 let mut wb = Workbook::new();
4224 wb.set_cell_value("Sheet1", "A1", "Hello").unwrap();
4225 wb.set_cell_value("Sheet1", "B1", 42.0f64).unwrap();
4226 let buf = wb.save_to_buffer().unwrap();
4227
4228 let opts = crate::workbook::open_options::OpenOptions::new()
4229 .read_mode(crate::workbook::open_options::ReadMode::Lazy);
4230 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4231
4232 let saved = wb2.save_to_buffer().unwrap();
4234
4235 let wb3 = Workbook::open_from_buffer(&saved).unwrap();
4237 assert_eq!(
4238 wb3.get_cell_value("Sheet1", "A1").unwrap(),
4239 CellValue::String("Hello".to_string())
4240 );
4241 assert_eq!(
4242 wb3.get_cell_value("Sheet1", "B1").unwrap(),
4243 CellValue::Number(42.0)
4244 );
4245 }
4246
4247 #[test]
4248 fn test_lazy_open_modify_one_sheet_passthroughs_others() {
4249 let mut wb = Workbook::new();
4250 wb.set_cell_value("Sheet1", "A1", "First sheet").unwrap();
4251 wb.new_sheet("Sheet2").unwrap();
4252 wb.set_cell_value("Sheet2", "A1", "Second sheet").unwrap();
4253 wb.new_sheet("Sheet3").unwrap();
4254 wb.set_cell_value("Sheet3", "A1", "Third sheet").unwrap();
4255 let buf = wb.save_to_buffer().unwrap();
4256
4257 let opts = crate::workbook::open_options::OpenOptions::new()
4258 .read_mode(crate::workbook::open_options::ReadMode::Lazy);
4259 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4260
4261 wb2.set_cell_value("Sheet2", "B1", "Modified").unwrap();
4263
4264 assert!(!wb2.is_sheet_dirty(0), "Sheet1 should not be dirty");
4266 assert!(wb2.is_sheet_dirty(1), "Sheet2 should be dirty");
4267 assert!(!wb2.is_sheet_dirty(2), "Sheet3 should not be dirty");
4268
4269 let saved = wb2.save_to_buffer().unwrap();
4270
4271 let wb3 = Workbook::open_from_buffer(&saved).unwrap();
4272 assert_eq!(
4273 wb3.get_cell_value("Sheet1", "A1").unwrap(),
4274 CellValue::String("First sheet".to_string())
4275 );
4276 assert_eq!(
4277 wb3.get_cell_value("Sheet2", "A1").unwrap(),
4278 CellValue::String("Second sheet".to_string())
4279 );
4280 assert_eq!(
4281 wb3.get_cell_value("Sheet2", "B1").unwrap(),
4282 CellValue::String("Modified".to_string())
4283 );
4284 assert_eq!(
4285 wb3.get_cell_value("Sheet3", "A1").unwrap(),
4286 CellValue::String("Third sheet".to_string())
4287 );
4288 }
4289
4290 #[test]
4291 fn test_lazy_open_deferred_aux_parts_preserved() {
4292 let mut wb = Workbook::new();
4293 wb.set_cell_value("Sheet1", "A1", "data").unwrap();
4294 wb.set_doc_props(crate::doc_props::DocProperties {
4295 title: Some("Test Title".to_string()),
4296 creator: Some("Test Author".to_string()),
4297 ..Default::default()
4298 });
4299 wb.add_comment(
4300 "Sheet1",
4301 &crate::comment::CommentConfig {
4302 cell: "A1".to_string(),
4303 author: "Tester".to_string(),
4304 text: "A comment".to_string(),
4305 },
4306 )
4307 .unwrap();
4308 let buf = wb.save_to_buffer().unwrap();
4309
4310 let opts = crate::workbook::open_options::OpenOptions::new()
4311 .read_mode(crate::workbook::open_options::ReadMode::Lazy);
4312 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4313
4314 let saved = wb2.save_to_buffer().unwrap();
4316
4317 let mut wb3 = Workbook::open_from_buffer(&saved).unwrap();
4318 assert_eq!(
4319 wb3.get_cell_value("Sheet1", "A1").unwrap(),
4320 CellValue::String("data".to_string())
4321 );
4322 let props = wb3.get_doc_props();
4323 assert_eq!(props.title.as_deref(), Some("Test Title"));
4324 assert_eq!(props.creator.as_deref(), Some("Test Author"));
4325 let comments = wb3.get_comments("Sheet1").unwrap();
4326 assert_eq!(comments.len(), 1);
4327 assert_eq!(comments[0].text, "A comment");
4328 }
4329
4330 #[test]
4331 fn test_eager_open_save_preserves_all_data() {
4332 let mut wb = Workbook::new();
4333 wb.set_cell_value("Sheet1", "A1", "data").unwrap();
4334 wb.set_cell_value("Sheet1", "B1", 42.0f64).unwrap();
4335 wb.new_sheet("Sheet2").unwrap();
4336 wb.set_cell_value("Sheet2", "A1", "sheet2").unwrap();
4337 let buf = wb.save_to_buffer().unwrap();
4338
4339 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
4341 let saved = wb2.save_to_buffer().unwrap();
4342
4343 let wb3 = Workbook::open_from_buffer(&saved).unwrap();
4344 assert_eq!(
4345 wb3.get_cell_value("Sheet1", "A1").unwrap(),
4346 CellValue::String("data".to_string())
4347 );
4348 assert_eq!(
4349 wb3.get_cell_value("Sheet1", "B1").unwrap(),
4350 CellValue::Number(42.0)
4351 );
4352 assert_eq!(
4353 wb3.get_cell_value("Sheet2", "A1").unwrap(),
4354 CellValue::String("sheet2".to_string())
4355 );
4356 }
4357
4358 #[test]
4359 fn test_lazy_read_then_save_passthrough() {
4360 let mut wb = Workbook::new();
4361 wb.set_cell_value("Sheet1", "A1", "value").unwrap();
4362 let buf = wb.save_to_buffer().unwrap();
4363
4364 let opts = crate::workbook::open_options::OpenOptions::new()
4365 .read_mode(crate::workbook::open_options::ReadMode::Lazy);
4366 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4367
4368 let val = wb2.get_cell_value("Sheet1", "A1").unwrap();
4370 assert_eq!(val, CellValue::String("value".to_string()));
4371
4372 assert!(!wb2.is_sheet_dirty(0));
4374
4375 let saved = wb2.save_to_buffer().unwrap();
4377 let wb3 = Workbook::open_from_buffer(&saved).unwrap();
4378 assert_eq!(
4379 wb3.get_cell_value("Sheet1", "A1").unwrap(),
4380 CellValue::String("value".to_string())
4381 );
4382 }
4383
4384 #[test]
4385 fn test_cow_passthrough_with_styles_and_formulas() {
4386 let mut wb = Workbook::new();
4387 let style_id = wb
4388 .add_style(&crate::style::Style {
4389 font: Some(crate::style::FontStyle {
4390 bold: true,
4391 ..Default::default()
4392 }),
4393 ..Default::default()
4394 })
4395 .unwrap();
4396 wb.set_cell_value("Sheet1", "A1", "styled").unwrap();
4397 wb.set_cell_style("Sheet1", "A1", style_id).unwrap();
4398 wb.set_cell_formula("Sheet1", "B1", "LEN(A1)").unwrap();
4399 wb.new_sheet("Sheet2").unwrap();
4400 wb.set_cell_value("Sheet2", "A1", "other").unwrap();
4401 let buf = wb.save_to_buffer().unwrap();
4402
4403 let opts = crate::workbook::open_options::OpenOptions::new()
4404 .read_mode(crate::workbook::open_options::ReadMode::Lazy);
4405 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4406 let saved = wb2.save_to_buffer().unwrap();
4407
4408 let wb3 = Workbook::open_from_buffer(&saved).unwrap();
4409 assert_eq!(
4410 wb3.get_cell_value("Sheet1", "A1").unwrap(),
4411 CellValue::String("styled".to_string())
4412 );
4413 assert_eq!(wb3.get_cell_style("Sheet1", "A1").unwrap(), Some(style_id));
4414 match wb3.get_cell_value("Sheet1", "B1").unwrap() {
4415 CellValue::Formula { expr, .. } => assert_eq!(expr, "LEN(A1)"),
4416 other => panic!("expected formula, got {:?}", other),
4417 }
4418 assert_eq!(
4419 wb3.get_cell_value("Sheet2", "A1").unwrap(),
4420 CellValue::String("other".to_string())
4421 );
4422 }
4423
4424 #[test]
4425 fn test_new_workbook_sheets_are_dirty() {
4426 let wb = Workbook::new();
4427 assert!(wb.is_sheet_dirty(0), "new workbook sheet should be dirty");
4428 }
4429
4430 #[test]
4431 fn test_eager_open_sheets_are_dirty() {
4432 use crate::workbook::open_options::{AuxParts, OpenOptions, ReadMode};
4433
4434 let mut wb = Workbook::new();
4435 wb.set_cell_value("Sheet1", "A1", "test").unwrap();
4436 let buf = wb.save_to_buffer().unwrap();
4437
4438 let opts = OpenOptions::new()
4439 .read_mode(ReadMode::Eager)
4440 .aux_parts(AuxParts::EagerLoad);
4441 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4442 assert!(
4443 wb2.is_sheet_dirty(0),
4444 "eagerly parsed sheet should be dirty"
4445 );
4446 }
4447
4448 #[test]
4449 fn test_lazy_open_sheets_start_clean() {
4450 let mut wb = Workbook::new();
4451 wb.set_cell_value("Sheet1", "A1", "test").unwrap();
4452 let buf = wb.save_to_buffer().unwrap();
4453
4454 let opts = crate::workbook::open_options::OpenOptions::new()
4455 .read_mode(crate::workbook::open_options::ReadMode::Lazy);
4456 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4457 assert!(
4458 !wb2.is_sheet_dirty(0),
4459 "lazily deferred sheet should start clean"
4460 );
4461 }
4462
4463 #[test]
4464 fn test_lazy_mutation_marks_dirty() {
4465 let mut wb = Workbook::new();
4466 wb.set_cell_value("Sheet1", "A1", "test").unwrap();
4467 let buf = wb.save_to_buffer().unwrap();
4468
4469 let opts = crate::workbook::open_options::OpenOptions::new()
4470 .read_mode(crate::workbook::open_options::ReadMode::Lazy);
4471 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4472 assert!(!wb2.is_sheet_dirty(0));
4473
4474 wb2.set_cell_value("Sheet1", "B1", "new").unwrap();
4475 assert!(
4476 wb2.is_sheet_dirty(0),
4477 "sheet should be dirty after mutation"
4478 );
4479 }
4480
4481 #[test]
4482 fn test_lazy_open_multi_sheet_selective_dirty() {
4483 let mut wb = Workbook::new();
4484 wb.set_cell_value("Sheet1", "A1", "s1").unwrap();
4485 wb.new_sheet("Sheet2").unwrap();
4486 wb.set_cell_value("Sheet2", "A1", "s2").unwrap();
4487 wb.new_sheet("Sheet3").unwrap();
4488 wb.set_cell_value("Sheet3", "A1", "s3").unwrap();
4489 let buf = wb.save_to_buffer().unwrap();
4490
4491 let opts = crate::workbook::open_options::OpenOptions::new()
4492 .read_mode(crate::workbook::open_options::ReadMode::Lazy);
4493 let mut wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4494
4495 assert!(!wb2.is_sheet_dirty(0));
4497 assert!(!wb2.is_sheet_dirty(1));
4498 assert!(!wb2.is_sheet_dirty(2));
4499
4500 let _ = wb2.get_cell_value("Sheet1", "A1").unwrap();
4502 assert!(!wb2.is_sheet_dirty(0), "reading should not dirty a sheet");
4503
4504 wb2.set_cell_value("Sheet3", "B1", "modified").unwrap();
4506 assert!(!wb2.is_sheet_dirty(0));
4507 assert!(!wb2.is_sheet_dirty(1));
4508 assert!(wb2.is_sheet_dirty(2));
4509
4510 let saved = wb2.save_to_buffer().unwrap();
4512 let wb3 = Workbook::open_from_buffer(&saved).unwrap();
4513 assert_eq!(
4514 wb3.get_cell_value("Sheet1", "A1").unwrap(),
4515 CellValue::String("s1".to_string())
4516 );
4517 assert_eq!(
4518 wb3.get_cell_value("Sheet2", "A1").unwrap(),
4519 CellValue::String("s2".to_string())
4520 );
4521 assert_eq!(
4522 wb3.get_cell_value("Sheet3", "A1").unwrap(),
4523 CellValue::String("s3".to_string())
4524 );
4525 assert_eq!(
4526 wb3.get_cell_value("Sheet3", "B1").unwrap(),
4527 CellValue::String("modified".to_string())
4528 );
4529 }
4530
4531 #[test]
4532 fn test_sheets_filter_preserves_filtered_sheet_with_comments_on_save() {
4533 let mut wb = Workbook::new();
4534 wb.new_sheet("Sheet2").unwrap();
4535 wb.set_cell_value("Sheet1", "A1", CellValue::String("keep_me".to_string()))
4536 .unwrap();
4537 wb.set_cell_value("Sheet2", "A1", CellValue::String("s2".to_string()))
4538 .unwrap();
4539 wb.add_comment(
4540 "Sheet1",
4541 &crate::comment::CommentConfig {
4542 cell: "A1".to_string(),
4543 author: "Test".to_string(),
4544 text: "a comment".to_string(),
4545 },
4546 )
4547 .unwrap();
4548 let buf = wb.save_to_buffer().unwrap();
4549
4550 let opts = OpenOptions::new().sheets(vec!["Sheet2".to_string()]);
4551 let wb2 = Workbook::open_from_buffer_with_options(&buf, &opts).unwrap();
4552 assert_eq!(
4553 wb2.get_cell_value("Sheet1", "A1").unwrap(),
4554 CellValue::Empty
4555 );
4556
4557 let buf2 = wb2.save_to_buffer().unwrap();
4558 let opts_all = OpenOptions::new()
4559 .read_mode(ReadMode::Eager)
4560 .aux_parts(AuxParts::EagerLoad);
4561 let wb3 = Workbook::open_from_buffer_with_options(&buf2, &opts_all).unwrap();
4562 assert_eq!(
4563 wb3.get_cell_value("Sheet1", "A1").unwrap(),
4564 CellValue::String("keep_me".to_string()),
4565 );
4566 assert_eq!(
4567 wb3.get_cell_value("Sheet2", "A1").unwrap(),
4568 CellValue::String("s2".to_string()),
4569 );
4570 }
4571}