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 #[cfg(feature = "encryption")]
358 pub fn open_with_password<P: AsRef<Path>>(path: P, password: &str) -> Result<Self> {
359 let data = std::fs::read(path.as_ref())?;
360 let decrypted_zip = crate::crypt::decrypt_xlsx(&data, password)?;
361 let cursor = std::io::Cursor::new(decrypted_zip);
362 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
363 Self::from_archive(&mut archive)
364 }
365
366 #[cfg(feature = "encryption")]
369 pub fn save_with_password<P: AsRef<Path>>(&self, path: P, password: &str) -> Result<()> {
370 let mut zip_buf = Vec::new();
372 {
373 let cursor = std::io::Cursor::new(&mut zip_buf);
374 let mut zip = zip::ZipWriter::new(cursor);
375 let options =
376 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
377 self.write_zip_contents(&mut zip, options)?;
378 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
379 }
380
381 let cfb_data = crate::crypt::encrypt_xlsx(&zip_buf, password)?;
383 std::fs::write(path.as_ref(), &cfb_data)?;
384 Ok(())
385 }
386
387 fn write_zip_contents<W: std::io::Write + std::io::Seek>(
389 &self,
390 zip: &mut zip::ZipWriter<W>,
391 options: SimpleFileOptions,
392 ) -> Result<()> {
393 let mut content_types = self.content_types.clone();
394 let mut worksheet_rels = self.worksheet_rels.clone();
395
396 let mut vml_parts_to_write: Vec<(usize, String, Vec<u8>)> = Vec::new();
399 let mut legacy_drawing_rids: HashMap<usize, String> = HashMap::new();
401
402 let mut has_any_vml = false;
404
405 for sheet_idx in 0..self.worksheets.len() {
406 let has_comments = self
407 .sheet_comments
408 .get(sheet_idx)
409 .and_then(|c| c.as_ref())
410 .is_some();
411 if let Some(rels) = worksheet_rels.get_mut(&sheet_idx) {
412 rels.relationships
413 .retain(|r| r.rel_type != rel_types::COMMENTS);
414 rels.relationships
415 .retain(|r| r.rel_type != rel_types::VML_DRAWING);
416 }
417 if !has_comments {
418 continue;
419 }
420
421 let comment_path = format!("xl/comments{}.xml", sheet_idx + 1);
422 let part_name = format!("/{}", comment_path);
423 if !content_types
424 .overrides
425 .iter()
426 .any(|o| o.part_name == part_name && o.content_type == mime_types::COMMENTS)
427 {
428 content_types.overrides.push(ContentTypeOverride {
429 part_name,
430 content_type: mime_types::COMMENTS.to_string(),
431 });
432 }
433
434 let sheet_path = self.sheet_part_path(sheet_idx);
435 let target = relative_relationship_target(&sheet_path, &comment_path);
436 let rels = worksheet_rels
437 .entry(sheet_idx)
438 .or_insert_with(default_relationships);
439 let rid = crate::sheet::next_rid(&rels.relationships);
440 rels.relationships.push(Relationship {
441 id: rid,
442 rel_type: rel_types::COMMENTS.to_string(),
443 target,
444 target_mode: None,
445 });
446
447 let vml_path = format!("xl/drawings/vmlDrawing{}.vml", sheet_idx + 1);
449 let vml_bytes =
450 if let Some(bytes) = self.sheet_vml.get(sheet_idx).and_then(|v| v.as_ref()) {
451 bytes.clone()
452 } else {
453 let comments = self.sheet_comments[sheet_idx].as_ref().unwrap();
455 let cells: Vec<&str> = comments
456 .comment_list
457 .comments
458 .iter()
459 .map(|c| c.r#ref.as_str())
460 .collect();
461 crate::vml::build_vml_drawing(&cells).into_bytes()
462 };
463
464 let vml_part_name = format!("/{}", vml_path);
465 if !content_types
466 .overrides
467 .iter()
468 .any(|o| o.part_name == vml_part_name && o.content_type == mime_types::VML_DRAWING)
469 {
470 content_types.overrides.push(ContentTypeOverride {
471 part_name: vml_part_name,
472 content_type: mime_types::VML_DRAWING.to_string(),
473 });
474 }
475
476 let vml_target = relative_relationship_target(&sheet_path, &vml_path);
477 let vml_rid = crate::sheet::next_rid(&rels.relationships);
478 rels.relationships.push(Relationship {
479 id: vml_rid.clone(),
480 rel_type: rel_types::VML_DRAWING.to_string(),
481 target: vml_target,
482 target_mode: None,
483 });
484
485 legacy_drawing_rids.insert(sheet_idx, vml_rid);
486 vml_parts_to_write.push((sheet_idx, vml_path, vml_bytes));
487 has_any_vml = true;
488 }
489
490 if has_any_vml && !content_types.defaults.iter().any(|d| d.extension == "vml") {
492 content_types.defaults.push(ContentTypeDefault {
493 extension: "vml".to_string(),
494 content_type: mime_types::VML_DRAWING.to_string(),
495 });
496 }
497
498 write_xml_part(zip, "[Content_Types].xml", &content_types, options)?;
500
501 write_xml_part(zip, "_rels/.rels", &self.package_rels, options)?;
503
504 write_xml_part(zip, "xl/workbook.xml", &self.workbook_xml, options)?;
506
507 write_xml_part(
509 zip,
510 "xl/_rels/workbook.xml.rels",
511 &self.workbook_rels,
512 options,
513 )?;
514
515 for (i, (_name, ws)) in self.worksheets.iter().enumerate() {
517 let entry_name = self.sheet_part_path(i);
518 let sparklines = self.sheet_sparklines.get(i).cloned().unwrap_or_default();
519 let needs_legacy_drawing = legacy_drawing_rids.contains_key(&i);
520
521 if !needs_legacy_drawing && sparklines.is_empty() {
522 write_xml_part(zip, &entry_name, ws, options)?;
523 } else {
524 let mut ws_clone = ws.clone();
525 if let Some(rid) = legacy_drawing_rids.get(&i) {
526 ws_clone.legacy_drawing =
527 Some(sheetkit_xml::worksheet::LegacyDrawingRef { r_id: rid.clone() });
528 }
529 if sparklines.is_empty() {
530 write_xml_part(zip, &entry_name, &ws_clone, options)?;
531 } else {
532 let xml = serialize_worksheet_with_sparklines(&ws_clone, &sparklines)?;
533 zip.start_file(&entry_name, options)
534 .map_err(|e| Error::Zip(e.to_string()))?;
535 zip.write_all(xml.as_bytes())?;
536 }
537 }
538 }
539
540 write_xml_part(zip, "xl/styles.xml", &self.stylesheet, options)?;
542
543 let sst_xml = self.sst_runtime.to_sst();
545 write_xml_part(zip, "xl/sharedStrings.xml", &sst_xml, options)?;
546
547 for (i, comments) in self.sheet_comments.iter().enumerate() {
549 if let Some(ref c) = comments {
550 let entry_name = format!("xl/comments{}.xml", i + 1);
551 write_xml_part(zip, &entry_name, c, options)?;
552 }
553 }
554
555 for (_sheet_idx, vml_path, vml_bytes) in &vml_parts_to_write {
557 zip.start_file(vml_path, options)
558 .map_err(|e| Error::Zip(e.to_string()))?;
559 zip.write_all(vml_bytes)?;
560 }
561
562 for (path, drawing) in &self.drawings {
564 write_xml_part(zip, path, drawing, options)?;
565 }
566
567 for (path, chart) in &self.charts {
569 write_xml_part(zip, path, chart, options)?;
570 }
571 for (path, data) in &self.raw_charts {
572 if self.charts.iter().any(|(p, _)| p == path) {
573 continue;
574 }
575 zip.start_file(path, options)
576 .map_err(|e| Error::Zip(e.to_string()))?;
577 zip.write_all(data)?;
578 }
579
580 for (path, data) in &self.images {
582 zip.start_file(path, options)
583 .map_err(|e| Error::Zip(e.to_string()))?;
584 zip.write_all(data)?;
585 }
586
587 for (sheet_idx, rels) in &worksheet_rels {
589 let sheet_path = self.sheet_part_path(*sheet_idx);
590 let path = relationship_part_path(&sheet_path);
591 write_xml_part(zip, &path, rels, options)?;
592 }
593
594 for (drawing_idx, rels) in &self.drawing_rels {
596 if let Some((drawing_path, _)) = self.drawings.get(*drawing_idx) {
597 let path = relationship_part_path(drawing_path);
598 write_xml_part(zip, &path, rels, options)?;
599 }
600 }
601
602 for (path, pt) in &self.pivot_tables {
604 write_xml_part(zip, path, pt, options)?;
605 }
606
607 for (path, pcd) in &self.pivot_cache_defs {
609 write_xml_part(zip, path, pcd, options)?;
610 }
611
612 for (path, pcr) in &self.pivot_cache_records {
614 write_xml_part(zip, path, pcr, options)?;
615 }
616
617 {
619 let default_theme = crate::theme::default_theme_xml();
620 let theme_bytes = self.theme_xml.as_deref().unwrap_or(&default_theme);
621 zip.start_file("xl/theme/theme1.xml", options)
622 .map_err(|e| Error::Zip(e.to_string()))?;
623 zip.write_all(theme_bytes)?;
624 }
625
626 if let Some(ref props) = self.core_properties {
628 let xml_str = sheetkit_xml::doc_props::serialize_core_properties(props);
629 zip.start_file("docProps/core.xml", options)
630 .map_err(|e| Error::Zip(e.to_string()))?;
631 zip.write_all(xml_str.as_bytes())?;
632 }
633
634 if let Some(ref props) = self.app_properties {
636 write_xml_part(zip, "docProps/app.xml", props, options)?;
637 }
638
639 if let Some(ref props) = self.custom_properties {
641 let xml_str = sheetkit_xml::doc_props::serialize_custom_properties(props);
642 zip.start_file("docProps/custom.xml", options)
643 .map_err(|e| Error::Zip(e.to_string()))?;
644 zip.write_all(xml_str.as_bytes())?;
645 }
646
647 Ok(())
648 }
649}
650
651impl Default for Workbook {
652 fn default() -> Self {
653 Self::new()
654 }
655}
656
657pub(crate) fn serialize_xml<T: Serialize>(value: &T) -> Result<String> {
659 let body = quick_xml::se::to_string(value).map_err(|e| Error::XmlParse(e.to_string()))?;
660 Ok(format!("{XML_DECLARATION}\n{body}"))
661}
662
663pub(crate) fn read_xml_part<T: serde::de::DeserializeOwned, R: std::io::Read + std::io::Seek>(
665 archive: &mut zip::ZipArchive<R>,
666 name: &str,
667) -> Result<T> {
668 let mut entry = archive
669 .by_name(name)
670 .map_err(|e| Error::Zip(e.to_string()))?;
671 let mut content = String::new();
672 entry
673 .read_to_string(&mut content)
674 .map_err(|e| Error::Zip(e.to_string()))?;
675 quick_xml::de::from_str(&content).map_err(|e| Error::XmlDeserialize(e.to_string()))
676}
677
678pub(crate) fn read_string_part<R: std::io::Read + std::io::Seek>(
680 archive: &mut zip::ZipArchive<R>,
681 name: &str,
682) -> Result<String> {
683 let mut entry = archive
684 .by_name(name)
685 .map_err(|e| Error::Zip(e.to_string()))?;
686 let mut content = String::new();
687 entry
688 .read_to_string(&mut content)
689 .map_err(|e| Error::Zip(e.to_string()))?;
690 Ok(content)
691}
692
693pub(crate) fn read_bytes_part<R: std::io::Read + std::io::Seek>(
695 archive: &mut zip::ZipArchive<R>,
696 name: &str,
697) -> Result<Vec<u8>> {
698 let mut entry = archive
699 .by_name(name)
700 .map_err(|e| Error::Zip(e.to_string()))?;
701 let mut content = Vec::new();
702 entry
703 .read_to_end(&mut content)
704 .map_err(|e| Error::Zip(e.to_string()))?;
705 Ok(content)
706}
707
708pub(crate) fn serialize_worksheet_with_sparklines(
710 ws: &WorksheetXml,
711 sparklines: &[crate::sparkline::SparklineConfig],
712) -> Result<String> {
713 let body = quick_xml::se::to_string(ws).map_err(|e| Error::XmlParse(e.to_string()))?;
714
715 let closing = "</worksheet>";
716 let ext_xml = build_sparkline_ext_xml(sparklines);
717 if let Some(pos) = body.rfind(closing) {
718 let mut result =
719 String::with_capacity(XML_DECLARATION.len() + 1 + body.len() + ext_xml.len());
720 result.push_str(XML_DECLARATION);
721 result.push('\n');
722 result.push_str(&body[..pos]);
723 result.push_str(&ext_xml);
724 result.push_str(closing);
725 Ok(result)
726 } else {
727 Ok(format!("{XML_DECLARATION}\n{body}"))
728 }
729}
730
731pub(crate) fn build_sparkline_ext_xml(sparklines: &[crate::sparkline::SparklineConfig]) -> String {
733 use std::fmt::Write;
734 let mut xml = String::new();
735 let _ = write!(
736 xml,
737 "<extLst>\
738 <ext xmlns:x14=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main\" \
739 uri=\"{{05C60535-1F16-4fd2-B633-F4F36F0B64E0}}\">\
740 <x14:sparklineGroups \
741 xmlns:xm=\"http://schemas.microsoft.com/office/excel/2006/main\">"
742 );
743 for config in sparklines {
744 let group = crate::sparkline::config_to_xml_group(config);
745 let _ = write!(xml, "<x14:sparklineGroup");
746 if let Some(ref t) = group.sparkline_type {
747 let _ = write!(xml, " type=\"{t}\"");
748 }
749 if group.markers == Some(true) {
750 let _ = write!(xml, " markers=\"1\"");
751 }
752 if group.high == Some(true) {
753 let _ = write!(xml, " high=\"1\"");
754 }
755 if group.low == Some(true) {
756 let _ = write!(xml, " low=\"1\"");
757 }
758 if group.first == Some(true) {
759 let _ = write!(xml, " first=\"1\"");
760 }
761 if group.last == Some(true) {
762 let _ = write!(xml, " last=\"1\"");
763 }
764 if group.negative == Some(true) {
765 let _ = write!(xml, " negative=\"1\"");
766 }
767 if group.display_x_axis == Some(true) {
768 let _ = write!(xml, " displayXAxis=\"1\"");
769 }
770 if let Some(w) = group.line_weight {
771 let _ = write!(xml, " lineWeight=\"{w}\"");
772 }
773 let _ = write!(xml, "><x14:sparklines>");
774 for sp in &group.sparklines.items {
775 let _ = write!(
776 xml,
777 "<x14:sparkline><xm:f>{}</xm:f><xm:sqref>{}</xm:sqref></x14:sparkline>",
778 sp.formula, sp.sqref
779 );
780 }
781 let _ = write!(xml, "</x14:sparklines></x14:sparklineGroup>");
782 }
783 let _ = write!(xml, "</x14:sparklineGroups></ext></extLst>");
784 xml
785}
786
787pub(crate) fn parse_sparklines_from_xml(xml: &str) -> Vec<crate::sparkline::SparklineConfig> {
789 use crate::sparkline::{SparklineConfig, SparklineType};
790
791 let mut sparklines = Vec::new();
792
793 let mut search_from = 0;
795 while let Some(group_start) = xml[search_from..].find("<x14:sparklineGroup") {
796 let abs_start = search_from + group_start;
797 let group_end_tag = "</x14:sparklineGroup>";
798 let abs_end = match xml[abs_start..].find(group_end_tag) {
799 Some(pos) => abs_start + pos + group_end_tag.len(),
800 None => break,
801 };
802 let group_xml = &xml[abs_start..abs_end];
803
804 let sparkline_type = extract_xml_attr(group_xml, "type")
806 .and_then(|s| SparklineType::parse(&s))
807 .unwrap_or_default();
808 let markers = extract_xml_bool_attr(group_xml, "markers");
809 let high_point = extract_xml_bool_attr(group_xml, "high");
810 let low_point = extract_xml_bool_attr(group_xml, "low");
811 let first_point = extract_xml_bool_attr(group_xml, "first");
812 let last_point = extract_xml_bool_attr(group_xml, "last");
813 let negative_points = extract_xml_bool_attr(group_xml, "negative");
814 let show_axis = extract_xml_bool_attr(group_xml, "displayXAxis");
815 let line_weight =
816 extract_xml_attr(group_xml, "lineWeight").and_then(|s| s.parse::<f64>().ok());
817
818 let mut sp_from = 0;
820 while let Some(sp_start) = group_xml[sp_from..].find("<x14:sparkline>") {
821 let sp_abs = sp_from + sp_start;
822 let sp_end_tag = "</x14:sparkline>";
823 let sp_abs_end = match group_xml[sp_abs..].find(sp_end_tag) {
824 Some(pos) => sp_abs + pos + sp_end_tag.len(),
825 None => break,
826 };
827 let sp_xml = &group_xml[sp_abs..sp_abs_end];
828
829 let formula = extract_xml_element(sp_xml, "xm:f").unwrap_or_default();
830 let sqref = extract_xml_element(sp_xml, "xm:sqref").unwrap_or_default();
831
832 if !formula.is_empty() && !sqref.is_empty() {
833 sparklines.push(SparklineConfig {
834 data_range: formula,
835 location: sqref,
836 sparkline_type: sparkline_type.clone(),
837 markers,
838 high_point,
839 low_point,
840 first_point,
841 last_point,
842 negative_points,
843 show_axis,
844 line_weight,
845 style: None,
846 });
847 }
848 sp_from = sp_abs_end;
849 }
850 search_from = abs_end;
851 }
852 sparklines
853}
854
855pub(crate) fn extract_xml_attr(xml: &str, attr: &str) -> Option<String> {
857 let patterns = [format!(" {attr}=\""), format!(" {attr}='")];
859 for pat in &patterns {
860 if let Some(start) = xml.find(pat.as_str()) {
861 let val_start = start + pat.len();
862 let quote = pat.chars().last().unwrap();
863 if let Some(end) = xml[val_start..].find(quote) {
864 return Some(xml[val_start..val_start + end].to_string());
865 }
866 }
867 }
868 None
869}
870
871pub(crate) fn extract_xml_bool_attr(xml: &str, attr: &str) -> bool {
873 extract_xml_attr(xml, attr)
874 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
875 .unwrap_or(false)
876}
877
878pub(crate) fn extract_xml_element(xml: &str, tag: &str) -> Option<String> {
880 let open = format!("<{tag}>");
881 let close = format!("</{tag}>");
882 let start = xml.find(&open)?;
883 let content_start = start + open.len();
884 let end = xml[content_start..].find(&close)?;
885 Some(xml[content_start..content_start + end].to_string())
886}
887
888pub(crate) fn write_xml_part<T: Serialize, W: std::io::Write + std::io::Seek>(
890 zip: &mut zip::ZipWriter<W>,
891 name: &str,
892 value: &T,
893 options: SimpleFileOptions,
894) -> Result<()> {
895 let xml = serialize_xml(value)?;
896 zip.start_file(name, options)
897 .map_err(|e| Error::Zip(e.to_string()))?;
898 zip.write_all(xml.as_bytes())?;
899 Ok(())
900}
901
902#[cfg(test)]
903mod tests {
904 use super::*;
905 use tempfile::TempDir;
906
907 #[test]
908 fn test_new_workbook_has_sheet1() {
909 let wb = Workbook::new();
910 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
911 }
912
913 #[test]
914 fn test_new_workbook_save_creates_file() {
915 let dir = TempDir::new().unwrap();
916 let path = dir.path().join("test.xlsx");
917 let wb = Workbook::new();
918 wb.save(&path).unwrap();
919 assert!(path.exists());
920 }
921
922 #[test]
923 fn test_save_and_open_roundtrip() {
924 let dir = TempDir::new().unwrap();
925 let path = dir.path().join("roundtrip.xlsx");
926
927 let wb = Workbook::new();
928 wb.save(&path).unwrap();
929
930 let wb2 = Workbook::open(&path).unwrap();
931 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
932 }
933
934 #[test]
935 fn test_saved_file_is_valid_zip() {
936 let dir = TempDir::new().unwrap();
937 let path = dir.path().join("valid.xlsx");
938 let wb = Workbook::new();
939 wb.save(&path).unwrap();
940
941 let file = std::fs::File::open(&path).unwrap();
943 let mut archive = zip::ZipArchive::new(file).unwrap();
944
945 let expected_files = [
946 "[Content_Types].xml",
947 "_rels/.rels",
948 "xl/workbook.xml",
949 "xl/_rels/workbook.xml.rels",
950 "xl/worksheets/sheet1.xml",
951 "xl/styles.xml",
952 "xl/sharedStrings.xml",
953 ];
954
955 for name in &expected_files {
956 assert!(archive.by_name(name).is_ok(), "Missing ZIP entry: {}", name);
957 }
958 }
959
960 #[test]
961 fn test_open_nonexistent_file_returns_error() {
962 let result = Workbook::open("/nonexistent/path.xlsx");
963 assert!(result.is_err());
964 }
965
966 #[test]
967 fn test_saved_xml_has_declarations() {
968 let dir = TempDir::new().unwrap();
969 let path = dir.path().join("decl.xlsx");
970 let wb = Workbook::new();
971 wb.save(&path).unwrap();
972
973 let file = std::fs::File::open(&path).unwrap();
974 let mut archive = zip::ZipArchive::new(file).unwrap();
975
976 let mut content = String::new();
977 std::io::Read::read_to_string(
978 &mut archive.by_name("[Content_Types].xml").unwrap(),
979 &mut content,
980 )
981 .unwrap();
982 assert!(content.starts_with("<?xml"));
983 }
984
985 #[test]
986 fn test_default_trait() {
987 let wb = Workbook::default();
988 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
989 }
990
991 #[test]
992 fn test_serialize_xml_helper() {
993 let ct = ContentTypes::default();
994 let xml = serialize_xml(&ct).unwrap();
995 assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"));
996 assert!(xml.contains("<Types"));
997 }
998
999 #[cfg(feature = "encryption")]
1000 #[test]
1001 fn test_save_and_open_with_password_roundtrip() {
1002 let dir = TempDir::new().unwrap();
1003 let path = dir.path().join("encrypted.xlsx");
1004
1005 let mut wb = Workbook::new();
1007 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
1008 .unwrap();
1009 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
1010 .unwrap();
1011
1012 wb.save_with_password(&path, "test123").unwrap();
1014
1015 let data = std::fs::read(&path).unwrap();
1017 assert_eq!(
1018 &data[..8],
1019 &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]
1020 );
1021
1022 let result = Workbook::open(&path);
1024 assert!(matches!(result, Err(Error::FileEncrypted)));
1025
1026 let result = Workbook::open_with_password(&path, "wrong");
1028 assert!(matches!(result, Err(Error::IncorrectPassword)));
1029
1030 let wb2 = Workbook::open_with_password(&path, "test123").unwrap();
1032 assert_eq!(
1033 wb2.get_cell_value("Sheet1", "A1").unwrap(),
1034 CellValue::String("Hello".to_string())
1035 );
1036 assert_eq!(
1037 wb2.get_cell_value("Sheet1", "B2").unwrap(),
1038 CellValue::Number(42.0)
1039 );
1040 }
1041
1042 #[cfg(feature = "encryption")]
1043 #[test]
1044 fn test_open_encrypted_file_without_password_returns_file_encrypted() {
1045 let dir = TempDir::new().unwrap();
1046 let path = dir.path().join("encrypted2.xlsx");
1047
1048 let wb = Workbook::new();
1049 wb.save_with_password(&path, "secret").unwrap();
1050
1051 let result = Workbook::open(&path);
1052 assert!(matches!(result, Err(Error::FileEncrypted)))
1053 }
1054}