1use super::*;
2
3impl Workbook {
4 pub fn new() -> Self {
6 let shared_strings = Sst::default();
7 let sst_runtime = SharedStringTable::from_sst(&shared_strings);
8 Self {
9 content_types: ContentTypes::default(),
10 package_rels: relationships::package_rels(),
11 workbook_xml: WorkbookXml::default(),
12 workbook_rels: relationships::workbook_rels(),
13 worksheets: vec![("Sheet1".to_string(), WorksheetXml::default())],
14 stylesheet: StyleSheet::default(),
15 shared_strings,
16 sst_runtime,
17 sheet_comments: vec![None],
18 charts: vec![],
19 raw_charts: vec![],
20 drawings: vec![],
21 images: vec![],
22 worksheet_drawings: HashMap::new(),
23 worksheet_rels: HashMap::new(),
24 drawing_rels: HashMap::new(),
25 core_properties: None,
26 app_properties: None,
27 custom_properties: None,
28 pivot_tables: vec![],
29 pivot_cache_defs: vec![],
30 pivot_cache_records: vec![],
31 theme_xml: None,
32 theme_colors: crate::theme::default_theme_colors(),
33 sheet_sparklines: vec![vec![]],
34 sheet_vml: vec![None],
35 }
36 }
37
38 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
43 let data = std::fs::read(path.as_ref())?;
44
45 #[cfg(feature = "encryption")]
47 if data.len() >= 8 {
48 if let Ok(crate::crypt::ContainerFormat::Cfb) =
49 crate::crypt::detect_container_format(&data)
50 {
51 return Err(Error::FileEncrypted);
52 }
53 }
54
55 let cursor = std::io::Cursor::new(data);
56 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
57 Self::from_archive(&mut archive)
58 }
59
60 fn from_archive<R: std::io::Read + std::io::Seek>(
62 archive: &mut zip::ZipArchive<R>,
63 ) -> Result<Self> {
64 let content_types: ContentTypes = read_xml_part(archive, "[Content_Types].xml")?;
66
67 let package_rels: Relationships = read_xml_part(archive, "_rels/.rels")?;
69
70 let workbook_xml: WorkbookXml = read_xml_part(archive, "xl/workbook.xml")?;
72
73 let workbook_rels: Relationships = read_xml_part(archive, "xl/_rels/workbook.xml.rels")?;
75
76 let mut worksheets = Vec::new();
78 let mut worksheet_paths = Vec::new();
79 for sheet_entry in &workbook_xml.sheets.sheets {
80 let rel = workbook_rels
82 .relationships
83 .iter()
84 .find(|r| r.id == sheet_entry.r_id && r.rel_type == rel_types::WORKSHEET);
85
86 let rel = rel.ok_or_else(|| {
87 Error::Internal(format!(
88 "missing worksheet relationship for sheet '{}'",
89 sheet_entry.name
90 ))
91 })?;
92
93 let sheet_path = resolve_relationship_target("xl/workbook.xml", &rel.target);
94 let ws: WorksheetXml = read_xml_part(archive, &sheet_path)?;
95 worksheets.push((sheet_entry.name.clone(), ws));
96 worksheet_paths.push(sheet_path);
97 }
98
99 let stylesheet: StyleSheet = read_xml_part(archive, "xl/styles.xml")?;
101
102 let shared_strings: Sst =
104 read_xml_part(archive, "xl/sharedStrings.xml").unwrap_or_default();
105
106 let sst_runtime = SharedStringTable::from_sst(&shared_strings);
107
108 let (theme_xml, theme_colors) = match read_bytes_part(archive, "xl/theme/theme1.xml") {
110 Ok(bytes) => {
111 let colors = sheetkit_xml::theme::parse_theme_colors(&bytes);
112 (Some(bytes), colors)
113 }
114 Err(_) => (None, crate::theme::default_theme_colors()),
115 };
116
117 let mut worksheet_rels: HashMap<usize, Relationships> = HashMap::new();
119 for (i, sheet_path) in worksheet_paths.iter().enumerate() {
120 let rels_path = relationship_part_path(sheet_path);
121 if let Ok(rels) = read_xml_part::<Relationships, _>(archive, &rels_path) {
122 worksheet_rels.insert(i, rels);
123 }
124 }
125
126 let mut sheet_comments: Vec<Option<Comments>> = vec![None; worksheets.len()];
128 let mut sheet_vml: Vec<Option<Vec<u8>>> = vec![None; worksheets.len()];
129 let mut drawings: Vec<(String, WsDr)> = Vec::new();
130 let mut worksheet_drawings: HashMap<usize, usize> = HashMap::new();
131 let mut drawing_path_to_idx: HashMap<String, usize> = HashMap::new();
132
133 for (sheet_idx, sheet_path) in worksheet_paths.iter().enumerate() {
134 let Some(rels) = worksheet_rels.get(&sheet_idx) else {
135 continue;
136 };
137
138 if let Some(comment_rel) = rels
139 .relationships
140 .iter()
141 .find(|r| r.rel_type == rel_types::COMMENTS)
142 {
143 let comment_path = resolve_relationship_target(sheet_path, &comment_rel.target);
144 if let Ok(comments) = read_xml_part::<Comments, _>(archive, &comment_path) {
145 sheet_comments[sheet_idx] = Some(comments);
146 }
147 }
148
149 if let Some(vml_rel) = rels
150 .relationships
151 .iter()
152 .find(|r| r.rel_type == rel_types::VML_DRAWING)
153 {
154 let vml_path = resolve_relationship_target(sheet_path, &vml_rel.target);
155 if let Ok(bytes) = read_bytes_part(archive, &vml_path) {
156 sheet_vml[sheet_idx] = Some(bytes);
157 }
158 }
159
160 if let Some(drawing_rel) = rels
161 .relationships
162 .iter()
163 .find(|r| r.rel_type == rel_types::DRAWING)
164 {
165 let drawing_path = resolve_relationship_target(sheet_path, &drawing_rel.target);
166 let drawing_idx = if let Some(idx) = drawing_path_to_idx.get(&drawing_path) {
167 *idx
168 } else if let Ok(drawing) = read_xml_part::<WsDr, _>(archive, &drawing_path) {
169 let idx = drawings.len();
170 drawings.push((drawing_path.clone(), drawing));
171 drawing_path_to_idx.insert(drawing_path.clone(), idx);
172 idx
173 } else {
174 continue;
175 };
176 worksheet_drawings.insert(sheet_idx, drawing_idx);
177 }
178 }
179
180 for ovr in &content_types.overrides {
183 if ovr.content_type != mime_types::DRAWING {
184 continue;
185 }
186 let drawing_path = ovr.part_name.trim_start_matches('/').to_string();
187 if drawing_path_to_idx.contains_key(&drawing_path) {
188 continue;
189 }
190 if let Ok(drawing) = read_xml_part::<WsDr, _>(archive, &drawing_path) {
191 let idx = drawings.len();
192 drawings.push((drawing_path.clone(), drawing));
193 drawing_path_to_idx.insert(drawing_path, idx);
194 }
195 }
196
197 let mut drawing_rels: HashMap<usize, Relationships> = HashMap::new();
198 let mut charts: Vec<(String, ChartSpace)> = Vec::new();
199 let mut raw_charts: Vec<(String, Vec<u8>)> = Vec::new();
200 let mut images: Vec<(String, Vec<u8>)> = Vec::new();
201 let mut seen_chart_paths: HashSet<String> = HashSet::new();
202 let mut seen_image_paths: HashSet<String> = HashSet::new();
203
204 for (drawing_idx, (drawing_path, _)) in drawings.iter().enumerate() {
205 let drawing_rels_path = relationship_part_path(drawing_path);
206 let Ok(rels) = read_xml_part::<Relationships, _>(archive, &drawing_rels_path) else {
207 continue;
208 };
209
210 for rel in &rels.relationships {
211 if rel.rel_type == rel_types::CHART {
212 let chart_path = resolve_relationship_target(drawing_path, &rel.target);
213 if seen_chart_paths.insert(chart_path.clone()) {
214 match read_xml_part::<ChartSpace, _>(archive, &chart_path) {
215 Ok(chart) => charts.push((chart_path, chart)),
216 Err(_) => {
217 if let Ok(bytes) = read_bytes_part(archive, &chart_path) {
218 raw_charts.push((chart_path, bytes));
219 }
220 }
221 }
222 }
223 } else if rel.rel_type == rel_types::IMAGE {
224 let image_path = resolve_relationship_target(drawing_path, &rel.target);
225 if seen_image_paths.insert(image_path.clone()) {
226 if let Ok(bytes) = read_bytes_part(archive, &image_path) {
227 images.push((image_path, bytes));
228 }
229 }
230 }
231 }
232
233 drawing_rels.insert(drawing_idx, rels);
234 }
235
236 for ovr in &content_types.overrides {
239 if ovr.content_type != mime_types::CHART {
240 continue;
241 }
242 let chart_path = ovr.part_name.trim_start_matches('/').to_string();
243 if seen_chart_paths.insert(chart_path.clone()) {
244 match read_xml_part::<ChartSpace, _>(archive, &chart_path) {
245 Ok(chart) => charts.push((chart_path, chart)),
246 Err(_) => {
247 if let Ok(bytes) = read_bytes_part(archive, &chart_path) {
248 raw_charts.push((chart_path, bytes));
249 }
250 }
251 }
252 }
253 }
254
255 let core_properties = read_string_part(archive, "docProps/core.xml")
257 .ok()
258 .and_then(|xml_str| {
259 sheetkit_xml::doc_props::deserialize_core_properties(&xml_str).ok()
260 });
261
262 let app_properties: Option<sheetkit_xml::doc_props::ExtendedProperties> =
264 read_xml_part(archive, "docProps/app.xml").ok();
265
266 let custom_properties = read_string_part(archive, "docProps/custom.xml")
268 .ok()
269 .and_then(|xml_str| {
270 sheetkit_xml::doc_props::deserialize_custom_properties(&xml_str).ok()
271 });
272
273 let mut pivot_cache_defs = Vec::new();
275 let mut pivot_tables = Vec::new();
276 let mut pivot_cache_records = Vec::new();
277 for ovr in &content_types.overrides {
278 let path = ovr.part_name.trim_start_matches('/');
279 if ovr.content_type == mime_types::PIVOT_CACHE_DEFINITION {
280 if let Ok(pcd) = read_xml_part::<sheetkit_xml::pivot_cache::PivotCacheDefinition, _>(
281 archive, path,
282 ) {
283 pivot_cache_defs.push((path.to_string(), pcd));
284 }
285 } else if ovr.content_type == mime_types::PIVOT_TABLE {
286 if let Ok(pt) = read_xml_part::<sheetkit_xml::pivot_table::PivotTableDefinition, _>(
287 archive, path,
288 ) {
289 pivot_tables.push((path.to_string(), pt));
290 }
291 } else if ovr.content_type == mime_types::PIVOT_CACHE_RECORDS {
292 if let Ok(pcr) =
293 read_xml_part::<sheetkit_xml::pivot_cache::PivotCacheRecords, _>(archive, path)
294 {
295 pivot_cache_records.push((path.to_string(), pcr));
296 }
297 }
298 }
299
300 let mut sheet_sparklines: Vec<Vec<crate::sparkline::SparklineConfig>> =
302 vec![vec![]; worksheets.len()];
303 for (i, ws_path) in worksheet_paths.iter().enumerate() {
304 if let Ok(raw) = read_string_part(archive, ws_path) {
305 let parsed = parse_sparklines_from_xml(&raw);
306 if !parsed.is_empty() {
307 sheet_sparklines[i] = parsed;
308 }
309 }
310 }
311
312 Ok(Self {
313 content_types,
314 package_rels,
315 workbook_xml,
316 workbook_rels,
317 worksheets,
318 stylesheet,
319 shared_strings,
320 sst_runtime,
321 sheet_comments,
322 charts,
323 raw_charts,
324 drawings,
325 images,
326 worksheet_drawings,
327 worksheet_rels,
328 drawing_rels,
329 core_properties,
330 app_properties,
331 custom_properties,
332 pivot_tables,
333 pivot_cache_defs,
334 pivot_cache_records,
335 theme_xml,
336 theme_colors,
337 sheet_sparklines,
338 sheet_vml,
339 })
340 }
341
342 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
344 let file = std::fs::File::create(path)?;
345 let mut zip = zip::ZipWriter::new(file);
346 let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
347 self.write_zip_contents(&mut zip, options)?;
348 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
349 Ok(())
350 }
351
352 pub fn save_to_buffer(&self) -> Result<Vec<u8>> {
354 let mut buf = Vec::new();
355 {
356 let cursor = std::io::Cursor::new(&mut buf);
357 let mut zip = zip::ZipWriter::new(cursor);
358 let options =
359 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
360 self.write_zip_contents(&mut zip, options)?;
361 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
362 }
363 Ok(buf)
364 }
365
366 pub fn open_from_buffer(data: &[u8]) -> Result<Self> {
368 #[cfg(feature = "encryption")]
370 if data.len() >= 8 {
371 if let Ok(crate::crypt::ContainerFormat::Cfb) =
372 crate::crypt::detect_container_format(data)
373 {
374 return Err(Error::FileEncrypted);
375 }
376 }
377
378 let cursor = std::io::Cursor::new(data);
379 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
380 Self::from_archive(&mut archive)
381 }
382
383 #[cfg(feature = "encryption")]
389 pub fn open_with_password<P: AsRef<Path>>(path: P, password: &str) -> Result<Self> {
390 let data = std::fs::read(path.as_ref())?;
391 let decrypted_zip = crate::crypt::decrypt_xlsx(&data, password)?;
392 let cursor = std::io::Cursor::new(decrypted_zip);
393 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
394 Self::from_archive(&mut archive)
395 }
396
397 #[cfg(feature = "encryption")]
400 pub fn save_with_password<P: AsRef<Path>>(&self, path: P, password: &str) -> Result<()> {
401 let mut zip_buf = Vec::new();
403 {
404 let cursor = std::io::Cursor::new(&mut zip_buf);
405 let mut zip = zip::ZipWriter::new(cursor);
406 let options =
407 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
408 self.write_zip_contents(&mut zip, options)?;
409 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
410 }
411
412 let cfb_data = crate::crypt::encrypt_xlsx(&zip_buf, password)?;
414 std::fs::write(path.as_ref(), &cfb_data)?;
415 Ok(())
416 }
417
418 fn write_zip_contents<W: std::io::Write + std::io::Seek>(
420 &self,
421 zip: &mut zip::ZipWriter<W>,
422 options: SimpleFileOptions,
423 ) -> Result<()> {
424 let mut content_types = self.content_types.clone();
425 let mut worksheet_rels = self.worksheet_rels.clone();
426
427 let mut vml_parts_to_write: Vec<(usize, String, Vec<u8>)> = Vec::new();
430 let mut legacy_drawing_rids: HashMap<usize, String> = HashMap::new();
432
433 let mut has_any_vml = false;
435
436 for sheet_idx in 0..self.worksheets.len() {
437 let has_comments = self
438 .sheet_comments
439 .get(sheet_idx)
440 .and_then(|c| c.as_ref())
441 .is_some();
442 if let Some(rels) = worksheet_rels.get_mut(&sheet_idx) {
443 rels.relationships
444 .retain(|r| r.rel_type != rel_types::COMMENTS);
445 rels.relationships
446 .retain(|r| r.rel_type != rel_types::VML_DRAWING);
447 }
448 if !has_comments {
449 continue;
450 }
451
452 let comment_path = format!("xl/comments{}.xml", sheet_idx + 1);
453 let part_name = format!("/{}", comment_path);
454 if !content_types
455 .overrides
456 .iter()
457 .any(|o| o.part_name == part_name && o.content_type == mime_types::COMMENTS)
458 {
459 content_types.overrides.push(ContentTypeOverride {
460 part_name,
461 content_type: mime_types::COMMENTS.to_string(),
462 });
463 }
464
465 let sheet_path = self.sheet_part_path(sheet_idx);
466 let target = relative_relationship_target(&sheet_path, &comment_path);
467 let rels = worksheet_rels
468 .entry(sheet_idx)
469 .or_insert_with(default_relationships);
470 let rid = crate::sheet::next_rid(&rels.relationships);
471 rels.relationships.push(Relationship {
472 id: rid,
473 rel_type: rel_types::COMMENTS.to_string(),
474 target,
475 target_mode: None,
476 });
477
478 let vml_path = format!("xl/drawings/vmlDrawing{}.vml", sheet_idx + 1);
480 let vml_bytes =
481 if let Some(bytes) = self.sheet_vml.get(sheet_idx).and_then(|v| v.as_ref()) {
482 bytes.clone()
483 } else {
484 let comments = self.sheet_comments[sheet_idx].as_ref().unwrap();
486 let cells: Vec<&str> = comments
487 .comment_list
488 .comments
489 .iter()
490 .map(|c| c.r#ref.as_str())
491 .collect();
492 crate::vml::build_vml_drawing(&cells).into_bytes()
493 };
494
495 let vml_part_name = format!("/{}", vml_path);
496 if !content_types
497 .overrides
498 .iter()
499 .any(|o| o.part_name == vml_part_name && o.content_type == mime_types::VML_DRAWING)
500 {
501 content_types.overrides.push(ContentTypeOverride {
502 part_name: vml_part_name,
503 content_type: mime_types::VML_DRAWING.to_string(),
504 });
505 }
506
507 let vml_target = relative_relationship_target(&sheet_path, &vml_path);
508 let vml_rid = crate::sheet::next_rid(&rels.relationships);
509 rels.relationships.push(Relationship {
510 id: vml_rid.clone(),
511 rel_type: rel_types::VML_DRAWING.to_string(),
512 target: vml_target,
513 target_mode: None,
514 });
515
516 legacy_drawing_rids.insert(sheet_idx, vml_rid);
517 vml_parts_to_write.push((sheet_idx, vml_path, vml_bytes));
518 has_any_vml = true;
519 }
520
521 if has_any_vml && !content_types.defaults.iter().any(|d| d.extension == "vml") {
523 content_types.defaults.push(ContentTypeDefault {
524 extension: "vml".to_string(),
525 content_type: mime_types::VML_DRAWING.to_string(),
526 });
527 }
528
529 write_xml_part(zip, "[Content_Types].xml", &content_types, options)?;
531
532 write_xml_part(zip, "_rels/.rels", &self.package_rels, options)?;
534
535 write_xml_part(zip, "xl/workbook.xml", &self.workbook_xml, options)?;
537
538 write_xml_part(
540 zip,
541 "xl/_rels/workbook.xml.rels",
542 &self.workbook_rels,
543 options,
544 )?;
545
546 for (i, (_name, ws)) in self.worksheets.iter().enumerate() {
548 let entry_name = self.sheet_part_path(i);
549 let sparklines = self.sheet_sparklines.get(i).cloned().unwrap_or_default();
550 let needs_legacy_drawing = legacy_drawing_rids.contains_key(&i);
551
552 if !needs_legacy_drawing && sparklines.is_empty() {
553 write_xml_part(zip, &entry_name, ws, options)?;
554 } else {
555 let mut ws_clone = ws.clone();
556 if let Some(rid) = legacy_drawing_rids.get(&i) {
557 ws_clone.legacy_drawing =
558 Some(sheetkit_xml::worksheet::LegacyDrawingRef { r_id: rid.clone() });
559 }
560 if sparklines.is_empty() {
561 write_xml_part(zip, &entry_name, &ws_clone, options)?;
562 } else {
563 let xml = serialize_worksheet_with_sparklines(&ws_clone, &sparklines)?;
564 zip.start_file(&entry_name, options)
565 .map_err(|e| Error::Zip(e.to_string()))?;
566 zip.write_all(xml.as_bytes())?;
567 }
568 }
569 }
570
571 write_xml_part(zip, "xl/styles.xml", &self.stylesheet, options)?;
573
574 let sst_xml = self.sst_runtime.to_sst();
576 write_xml_part(zip, "xl/sharedStrings.xml", &sst_xml, options)?;
577
578 for (i, comments) in self.sheet_comments.iter().enumerate() {
580 if let Some(ref c) = comments {
581 let entry_name = format!("xl/comments{}.xml", i + 1);
582 write_xml_part(zip, &entry_name, c, options)?;
583 }
584 }
585
586 for (_sheet_idx, vml_path, vml_bytes) in &vml_parts_to_write {
588 zip.start_file(vml_path, options)
589 .map_err(|e| Error::Zip(e.to_string()))?;
590 zip.write_all(vml_bytes)?;
591 }
592
593 for (path, drawing) in &self.drawings {
595 write_xml_part(zip, path, drawing, options)?;
596 }
597
598 for (path, chart) in &self.charts {
600 write_xml_part(zip, path, chart, options)?;
601 }
602 for (path, data) in &self.raw_charts {
603 if self.charts.iter().any(|(p, _)| p == path) {
604 continue;
605 }
606 zip.start_file(path, options)
607 .map_err(|e| Error::Zip(e.to_string()))?;
608 zip.write_all(data)?;
609 }
610
611 for (path, data) in &self.images {
613 zip.start_file(path, options)
614 .map_err(|e| Error::Zip(e.to_string()))?;
615 zip.write_all(data)?;
616 }
617
618 for (sheet_idx, rels) in &worksheet_rels {
620 let sheet_path = self.sheet_part_path(*sheet_idx);
621 let path = relationship_part_path(&sheet_path);
622 write_xml_part(zip, &path, rels, options)?;
623 }
624
625 for (drawing_idx, rels) in &self.drawing_rels {
627 if let Some((drawing_path, _)) = self.drawings.get(*drawing_idx) {
628 let path = relationship_part_path(drawing_path);
629 write_xml_part(zip, &path, rels, options)?;
630 }
631 }
632
633 for (path, pt) in &self.pivot_tables {
635 write_xml_part(zip, path, pt, options)?;
636 }
637
638 for (path, pcd) in &self.pivot_cache_defs {
640 write_xml_part(zip, path, pcd, options)?;
641 }
642
643 for (path, pcr) in &self.pivot_cache_records {
645 write_xml_part(zip, path, pcr, options)?;
646 }
647
648 {
650 let default_theme = crate::theme::default_theme_xml();
651 let theme_bytes = self.theme_xml.as_deref().unwrap_or(&default_theme);
652 zip.start_file("xl/theme/theme1.xml", options)
653 .map_err(|e| Error::Zip(e.to_string()))?;
654 zip.write_all(theme_bytes)?;
655 }
656
657 if let Some(ref props) = self.core_properties {
659 let xml_str = sheetkit_xml::doc_props::serialize_core_properties(props);
660 zip.start_file("docProps/core.xml", options)
661 .map_err(|e| Error::Zip(e.to_string()))?;
662 zip.write_all(xml_str.as_bytes())?;
663 }
664
665 if let Some(ref props) = self.app_properties {
667 write_xml_part(zip, "docProps/app.xml", props, options)?;
668 }
669
670 if let Some(ref props) = self.custom_properties {
672 let xml_str = sheetkit_xml::doc_props::serialize_custom_properties(props);
673 zip.start_file("docProps/custom.xml", options)
674 .map_err(|e| Error::Zip(e.to_string()))?;
675 zip.write_all(xml_str.as_bytes())?;
676 }
677
678 Ok(())
679 }
680}
681
682impl Default for Workbook {
683 fn default() -> Self {
684 Self::new()
685 }
686}
687
688pub(crate) fn serialize_xml<T: Serialize>(value: &T) -> Result<String> {
690 let body = quick_xml::se::to_string(value).map_err(|e| Error::XmlParse(e.to_string()))?;
691 Ok(format!("{XML_DECLARATION}\n{body}"))
692}
693
694pub(crate) fn read_xml_part<T: serde::de::DeserializeOwned, R: std::io::Read + std::io::Seek>(
696 archive: &mut zip::ZipArchive<R>,
697 name: &str,
698) -> Result<T> {
699 let mut entry = archive
700 .by_name(name)
701 .map_err(|e| Error::Zip(e.to_string()))?;
702 let mut content = String::new();
703 entry
704 .read_to_string(&mut content)
705 .map_err(|e| Error::Zip(e.to_string()))?;
706 quick_xml::de::from_str(&content).map_err(|e| Error::XmlDeserialize(e.to_string()))
707}
708
709pub(crate) fn read_string_part<R: std::io::Read + std::io::Seek>(
711 archive: &mut zip::ZipArchive<R>,
712 name: &str,
713) -> Result<String> {
714 let mut entry = archive
715 .by_name(name)
716 .map_err(|e| Error::Zip(e.to_string()))?;
717 let mut content = String::new();
718 entry
719 .read_to_string(&mut content)
720 .map_err(|e| Error::Zip(e.to_string()))?;
721 Ok(content)
722}
723
724pub(crate) fn read_bytes_part<R: std::io::Read + std::io::Seek>(
726 archive: &mut zip::ZipArchive<R>,
727 name: &str,
728) -> Result<Vec<u8>> {
729 let mut entry = archive
730 .by_name(name)
731 .map_err(|e| Error::Zip(e.to_string()))?;
732 let mut content = Vec::new();
733 entry
734 .read_to_end(&mut content)
735 .map_err(|e| Error::Zip(e.to_string()))?;
736 Ok(content)
737}
738
739pub(crate) fn serialize_worksheet_with_sparklines(
741 ws: &WorksheetXml,
742 sparklines: &[crate::sparkline::SparklineConfig],
743) -> Result<String> {
744 let body = quick_xml::se::to_string(ws).map_err(|e| Error::XmlParse(e.to_string()))?;
745
746 let closing = "</worksheet>";
747 let ext_xml = build_sparkline_ext_xml(sparklines);
748 if let Some(pos) = body.rfind(closing) {
749 let mut result =
750 String::with_capacity(XML_DECLARATION.len() + 1 + body.len() + ext_xml.len());
751 result.push_str(XML_DECLARATION);
752 result.push('\n');
753 result.push_str(&body[..pos]);
754 result.push_str(&ext_xml);
755 result.push_str(closing);
756 Ok(result)
757 } else {
758 Ok(format!("{XML_DECLARATION}\n{body}"))
759 }
760}
761
762pub(crate) fn build_sparkline_ext_xml(sparklines: &[crate::sparkline::SparklineConfig]) -> String {
764 use std::fmt::Write;
765 let mut xml = String::new();
766 let _ = write!(
767 xml,
768 "<extLst>\
769 <ext xmlns:x14=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main\" \
770 uri=\"{{05C60535-1F16-4fd2-B633-F4F36F0B64E0}}\">\
771 <x14:sparklineGroups \
772 xmlns:xm=\"http://schemas.microsoft.com/office/excel/2006/main\">"
773 );
774 for config in sparklines {
775 let group = crate::sparkline::config_to_xml_group(config);
776 let _ = write!(xml, "<x14:sparklineGroup");
777 if let Some(ref t) = group.sparkline_type {
778 let _ = write!(xml, " type=\"{t}\"");
779 }
780 if group.markers == Some(true) {
781 let _ = write!(xml, " markers=\"1\"");
782 }
783 if group.high == Some(true) {
784 let _ = write!(xml, " high=\"1\"");
785 }
786 if group.low == Some(true) {
787 let _ = write!(xml, " low=\"1\"");
788 }
789 if group.first == Some(true) {
790 let _ = write!(xml, " first=\"1\"");
791 }
792 if group.last == Some(true) {
793 let _ = write!(xml, " last=\"1\"");
794 }
795 if group.negative == Some(true) {
796 let _ = write!(xml, " negative=\"1\"");
797 }
798 if group.display_x_axis == Some(true) {
799 let _ = write!(xml, " displayXAxis=\"1\"");
800 }
801 if let Some(w) = group.line_weight {
802 let _ = write!(xml, " lineWeight=\"{w}\"");
803 }
804 let _ = write!(xml, "><x14:sparklines>");
805 for sp in &group.sparklines.items {
806 let _ = write!(
807 xml,
808 "<x14:sparkline><xm:f>{}</xm:f><xm:sqref>{}</xm:sqref></x14:sparkline>",
809 sp.formula, sp.sqref
810 );
811 }
812 let _ = write!(xml, "</x14:sparklines></x14:sparklineGroup>");
813 }
814 let _ = write!(xml, "</x14:sparklineGroups></ext></extLst>");
815 xml
816}
817
818pub(crate) fn parse_sparklines_from_xml(xml: &str) -> Vec<crate::sparkline::SparklineConfig> {
820 use crate::sparkline::{SparklineConfig, SparklineType};
821
822 let mut sparklines = Vec::new();
823
824 let mut search_from = 0;
826 while let Some(group_start) = xml[search_from..].find("<x14:sparklineGroup") {
827 let abs_start = search_from + group_start;
828 let group_end_tag = "</x14:sparklineGroup>";
829 let abs_end = match xml[abs_start..].find(group_end_tag) {
830 Some(pos) => abs_start + pos + group_end_tag.len(),
831 None => break,
832 };
833 let group_xml = &xml[abs_start..abs_end];
834
835 let sparkline_type = extract_xml_attr(group_xml, "type")
837 .and_then(|s| SparklineType::parse(&s))
838 .unwrap_or_default();
839 let markers = extract_xml_bool_attr(group_xml, "markers");
840 let high_point = extract_xml_bool_attr(group_xml, "high");
841 let low_point = extract_xml_bool_attr(group_xml, "low");
842 let first_point = extract_xml_bool_attr(group_xml, "first");
843 let last_point = extract_xml_bool_attr(group_xml, "last");
844 let negative_points = extract_xml_bool_attr(group_xml, "negative");
845 let show_axis = extract_xml_bool_attr(group_xml, "displayXAxis");
846 let line_weight =
847 extract_xml_attr(group_xml, "lineWeight").and_then(|s| s.parse::<f64>().ok());
848
849 let mut sp_from = 0;
851 while let Some(sp_start) = group_xml[sp_from..].find("<x14:sparkline>") {
852 let sp_abs = sp_from + sp_start;
853 let sp_end_tag = "</x14:sparkline>";
854 let sp_abs_end = match group_xml[sp_abs..].find(sp_end_tag) {
855 Some(pos) => sp_abs + pos + sp_end_tag.len(),
856 None => break,
857 };
858 let sp_xml = &group_xml[sp_abs..sp_abs_end];
859
860 let formula = extract_xml_element(sp_xml, "xm:f").unwrap_or_default();
861 let sqref = extract_xml_element(sp_xml, "xm:sqref").unwrap_or_default();
862
863 if !formula.is_empty() && !sqref.is_empty() {
864 sparklines.push(SparklineConfig {
865 data_range: formula,
866 location: sqref,
867 sparkline_type: sparkline_type.clone(),
868 markers,
869 high_point,
870 low_point,
871 first_point,
872 last_point,
873 negative_points,
874 show_axis,
875 line_weight,
876 style: None,
877 });
878 }
879 sp_from = sp_abs_end;
880 }
881 search_from = abs_end;
882 }
883 sparklines
884}
885
886pub(crate) fn extract_xml_attr(xml: &str, attr: &str) -> Option<String> {
888 let patterns = [format!(" {attr}=\""), format!(" {attr}='")];
890 for pat in &patterns {
891 if let Some(start) = xml.find(pat.as_str()) {
892 let val_start = start + pat.len();
893 let quote = pat.chars().last().unwrap();
894 if let Some(end) = xml[val_start..].find(quote) {
895 return Some(xml[val_start..val_start + end].to_string());
896 }
897 }
898 }
899 None
900}
901
902pub(crate) fn extract_xml_bool_attr(xml: &str, attr: &str) -> bool {
904 extract_xml_attr(xml, attr)
905 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
906 .unwrap_or(false)
907}
908
909pub(crate) fn extract_xml_element(xml: &str, tag: &str) -> Option<String> {
911 let open = format!("<{tag}>");
912 let close = format!("</{tag}>");
913 let start = xml.find(&open)?;
914 let content_start = start + open.len();
915 let end = xml[content_start..].find(&close)?;
916 Some(xml[content_start..content_start + end].to_string())
917}
918
919pub(crate) fn write_xml_part<T: Serialize, W: std::io::Write + std::io::Seek>(
921 zip: &mut zip::ZipWriter<W>,
922 name: &str,
923 value: &T,
924 options: SimpleFileOptions,
925) -> Result<()> {
926 let xml = serialize_xml(value)?;
927 zip.start_file(name, options)
928 .map_err(|e| Error::Zip(e.to_string()))?;
929 zip.write_all(xml.as_bytes())?;
930 Ok(())
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936 use tempfile::TempDir;
937
938 #[test]
939 fn test_new_workbook_has_sheet1() {
940 let wb = Workbook::new();
941 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
942 }
943
944 #[test]
945 fn test_new_workbook_save_creates_file() {
946 let dir = TempDir::new().unwrap();
947 let path = dir.path().join("test.xlsx");
948 let wb = Workbook::new();
949 wb.save(&path).unwrap();
950 assert!(path.exists());
951 }
952
953 #[test]
954 fn test_save_and_open_roundtrip() {
955 let dir = TempDir::new().unwrap();
956 let path = dir.path().join("roundtrip.xlsx");
957
958 let wb = Workbook::new();
959 wb.save(&path).unwrap();
960
961 let wb2 = Workbook::open(&path).unwrap();
962 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
963 }
964
965 #[test]
966 fn test_saved_file_is_valid_zip() {
967 let dir = TempDir::new().unwrap();
968 let path = dir.path().join("valid.xlsx");
969 let wb = Workbook::new();
970 wb.save(&path).unwrap();
971
972 let file = std::fs::File::open(&path).unwrap();
974 let mut archive = zip::ZipArchive::new(file).unwrap();
975
976 let expected_files = [
977 "[Content_Types].xml",
978 "_rels/.rels",
979 "xl/workbook.xml",
980 "xl/_rels/workbook.xml.rels",
981 "xl/worksheets/sheet1.xml",
982 "xl/styles.xml",
983 "xl/sharedStrings.xml",
984 ];
985
986 for name in &expected_files {
987 assert!(archive.by_name(name).is_ok(), "Missing ZIP entry: {}", name);
988 }
989 }
990
991 #[test]
992 fn test_open_nonexistent_file_returns_error() {
993 let result = Workbook::open("/nonexistent/path.xlsx");
994 assert!(result.is_err());
995 }
996
997 #[test]
998 fn test_saved_xml_has_declarations() {
999 let dir = TempDir::new().unwrap();
1000 let path = dir.path().join("decl.xlsx");
1001 let wb = Workbook::new();
1002 wb.save(&path).unwrap();
1003
1004 let file = std::fs::File::open(&path).unwrap();
1005 let mut archive = zip::ZipArchive::new(file).unwrap();
1006
1007 let mut content = String::new();
1008 std::io::Read::read_to_string(
1009 &mut archive.by_name("[Content_Types].xml").unwrap(),
1010 &mut content,
1011 )
1012 .unwrap();
1013 assert!(content.starts_with("<?xml"));
1014 }
1015
1016 #[test]
1017 fn test_default_trait() {
1018 let wb = Workbook::default();
1019 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
1020 }
1021
1022 #[test]
1023 fn test_serialize_xml_helper() {
1024 let ct = ContentTypes::default();
1025 let xml = serialize_xml(&ct).unwrap();
1026 assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"));
1027 assert!(xml.contains("<Types"));
1028 }
1029
1030 #[test]
1031 fn test_save_to_buffer_and_open_from_buffer_roundtrip() {
1032 let mut wb = Workbook::new();
1033 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
1034 .unwrap();
1035 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
1036 .unwrap();
1037
1038 let buf = wb.save_to_buffer().unwrap();
1039 assert!(!buf.is_empty());
1040
1041 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
1042 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
1043 assert_eq!(
1044 wb2.get_cell_value("Sheet1", "A1").unwrap(),
1045 CellValue::String("Hello".to_string())
1046 );
1047 assert_eq!(
1048 wb2.get_cell_value("Sheet1", "B2").unwrap(),
1049 CellValue::Number(42.0)
1050 );
1051 }
1052
1053 #[test]
1054 fn test_save_to_buffer_produces_valid_zip() {
1055 let wb = Workbook::new();
1056 let buf = wb.save_to_buffer().unwrap();
1057
1058 let cursor = std::io::Cursor::new(buf);
1059 let mut archive = zip::ZipArchive::new(cursor).unwrap();
1060
1061 let expected_files = [
1062 "[Content_Types].xml",
1063 "_rels/.rels",
1064 "xl/workbook.xml",
1065 "xl/_rels/workbook.xml.rels",
1066 "xl/worksheets/sheet1.xml",
1067 "xl/styles.xml",
1068 "xl/sharedStrings.xml",
1069 ];
1070
1071 for name in &expected_files {
1072 assert!(archive.by_name(name).is_ok(), "Missing ZIP entry: {}", name);
1073 }
1074 }
1075
1076 #[test]
1077 fn test_open_from_buffer_invalid_data() {
1078 let result = Workbook::open_from_buffer(b"not a zip file");
1079 assert!(result.is_err());
1080 }
1081
1082 #[cfg(feature = "encryption")]
1083 #[test]
1084 fn test_save_and_open_with_password_roundtrip() {
1085 let dir = TempDir::new().unwrap();
1086 let path = dir.path().join("encrypted.xlsx");
1087
1088 let mut wb = Workbook::new();
1090 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
1091 .unwrap();
1092 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
1093 .unwrap();
1094
1095 wb.save_with_password(&path, "test123").unwrap();
1097
1098 let data = std::fs::read(&path).unwrap();
1100 assert_eq!(
1101 &data[..8],
1102 &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]
1103 );
1104
1105 let result = Workbook::open(&path);
1107 assert!(matches!(result, Err(Error::FileEncrypted)));
1108
1109 let result = Workbook::open_with_password(&path, "wrong");
1111 assert!(matches!(result, Err(Error::IncorrectPassword)));
1112
1113 let wb2 = Workbook::open_with_password(&path, "test123").unwrap();
1115 assert_eq!(
1116 wb2.get_cell_value("Sheet1", "A1").unwrap(),
1117 CellValue::String("Hello".to_string())
1118 );
1119 assert_eq!(
1120 wb2.get_cell_value("Sheet1", "B2").unwrap(),
1121 CellValue::Number(42.0)
1122 );
1123 }
1124
1125 #[cfg(feature = "encryption")]
1126 #[test]
1127 fn test_open_encrypted_file_without_password_returns_file_encrypted() {
1128 let dir = TempDir::new().unwrap();
1129 let path = dir.path().join("encrypted2.xlsx");
1130
1131 let wb = Workbook::new();
1132 wb.save_with_password(&path, "secret").unwrap();
1133
1134 let result = Workbook::open(&path);
1135 assert!(matches!(result, Err(Error::FileEncrypted)))
1136 }
1137}