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