1use super::*;
2
3impl Workbook {
4 pub fn new() -> Self {
6 let sst_runtime = SharedStringTable::new();
7 let mut sheet_name_index = HashMap::new();
8 sheet_name_index.insert("Sheet1".to_string(), 0);
9 Self {
10 content_types: ContentTypes::default(),
11 package_rels: relationships::package_rels(),
12 workbook_xml: WorkbookXml::default(),
13 workbook_rels: relationships::workbook_rels(),
14 worksheets: vec![("Sheet1".to_string(), WorksheetXml::default())],
15 stylesheet: StyleSheet::default(),
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 sheet_name_index,
36 }
37 }
38
39 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
44 let data = std::fs::read(path.as_ref())?;
45
46 #[cfg(feature = "encryption")]
48 if data.len() >= 8 {
49 if let Ok(crate::crypt::ContainerFormat::Cfb) =
50 crate::crypt::detect_container_format(&data)
51 {
52 return Err(Error::FileEncrypted);
53 }
54 }
55
56 let cursor = std::io::Cursor::new(data);
57 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
58 Self::from_archive(&mut archive)
59 }
60
61 fn from_archive<R: std::io::Read + std::io::Seek>(
63 archive: &mut zip::ZipArchive<R>,
64 ) -> Result<Self> {
65 let content_types: ContentTypes = read_xml_part(archive, "[Content_Types].xml")?;
67
68 let package_rels: Relationships = read_xml_part(archive, "_rels/.rels")?;
70
71 let workbook_xml: WorkbookXml = read_xml_part(archive, "xl/workbook.xml")?;
73
74 let workbook_rels: Relationships = read_xml_part(archive, "xl/_rels/workbook.xml.rels")?;
76
77 let sheet_count = workbook_xml.sheets.sheets.len();
79 let mut worksheets = Vec::with_capacity(sheet_count);
80 let mut worksheet_paths = Vec::with_capacity(sheet_count);
81 for sheet_entry in &workbook_xml.sheets.sheets {
82 let rel = workbook_rels
84 .relationships
85 .iter()
86 .find(|r| r.id == sheet_entry.r_id && r.rel_type == rel_types::WORKSHEET);
87
88 let rel = rel.ok_or_else(|| {
89 Error::Internal(format!(
90 "missing worksheet relationship for sheet '{}'",
91 sheet_entry.name
92 ))
93 })?;
94
95 let sheet_path = resolve_relationship_target("xl/workbook.xml", &rel.target);
96 let ws: WorksheetXml = read_xml_part(archive, &sheet_path)?;
97 worksheets.push((sheet_entry.name.clone(), ws));
98 worksheet_paths.push(sheet_path);
99 }
100
101 let stylesheet: StyleSheet = read_xml_part(archive, "xl/styles.xml")?;
103
104 let shared_strings: Sst =
106 read_xml_part(archive, "xl/sharedStrings.xml").unwrap_or_default();
107
108 let sst_runtime = SharedStringTable::from_sst(shared_strings);
109
110 let (theme_xml, theme_colors) = match read_bytes_part(archive, "xl/theme/theme1.xml") {
112 Ok(bytes) => {
113 let colors = sheetkit_xml::theme::parse_theme_colors(&bytes);
114 (Some(bytes), colors)
115 }
116 Err(_) => (None, crate::theme::default_theme_colors()),
117 };
118
119 let mut worksheet_rels: HashMap<usize, Relationships> = HashMap::with_capacity(sheet_count);
121 for (i, sheet_path) in worksheet_paths.iter().enumerate() {
122 let rels_path = relationship_part_path(sheet_path);
123 if let Ok(rels) = read_xml_part::<Relationships, _>(archive, &rels_path) {
124 worksheet_rels.insert(i, rels);
125 }
126 }
127
128 let mut sheet_comments: Vec<Option<Comments>> = vec![None; worksheets.len()];
130 let mut sheet_vml: Vec<Option<Vec<u8>>> = vec![None; worksheets.len()];
131 let mut drawings: Vec<(String, WsDr)> = Vec::new();
132 let mut worksheet_drawings: HashMap<usize, usize> = HashMap::new();
133 let mut drawing_path_to_idx: HashMap<String, usize> = HashMap::new();
134
135 for (sheet_idx, sheet_path) in worksheet_paths.iter().enumerate() {
136 let Some(rels) = worksheet_rels.get(&sheet_idx) else {
137 continue;
138 };
139
140 if let Some(comment_rel) = rels
141 .relationships
142 .iter()
143 .find(|r| r.rel_type == rel_types::COMMENTS)
144 {
145 let comment_path = resolve_relationship_target(sheet_path, &comment_rel.target);
146 if let Ok(comments) = read_xml_part::<Comments, _>(archive, &comment_path) {
147 sheet_comments[sheet_idx] = Some(comments);
148 }
149 }
150
151 if let Some(vml_rel) = rels
152 .relationships
153 .iter()
154 .find(|r| r.rel_type == rel_types::VML_DRAWING)
155 {
156 let vml_path = resolve_relationship_target(sheet_path, &vml_rel.target);
157 if let Ok(bytes) = read_bytes_part(archive, &vml_path) {
158 sheet_vml[sheet_idx] = Some(bytes);
159 }
160 }
161
162 if let Some(drawing_rel) = rels
163 .relationships
164 .iter()
165 .find(|r| r.rel_type == rel_types::DRAWING)
166 {
167 let drawing_path = resolve_relationship_target(sheet_path, &drawing_rel.target);
168 let drawing_idx = if let Some(idx) = drawing_path_to_idx.get(&drawing_path) {
169 *idx
170 } else if let Ok(drawing) = read_xml_part::<WsDr, _>(archive, &drawing_path) {
171 let idx = drawings.len();
172 drawings.push((drawing_path.clone(), drawing));
173 drawing_path_to_idx.insert(drawing_path.clone(), idx);
174 idx
175 } else {
176 continue;
177 };
178 worksheet_drawings.insert(sheet_idx, drawing_idx);
179 }
180 }
181
182 for ovr in &content_types.overrides {
185 if ovr.content_type != mime_types::DRAWING {
186 continue;
187 }
188 let drawing_path = ovr.part_name.trim_start_matches('/').to_string();
189 if drawing_path_to_idx.contains_key(&drawing_path) {
190 continue;
191 }
192 if let Ok(drawing) = read_xml_part::<WsDr, _>(archive, &drawing_path) {
193 let idx = drawings.len();
194 drawings.push((drawing_path.clone(), drawing));
195 drawing_path_to_idx.insert(drawing_path, idx);
196 }
197 }
198
199 let mut drawing_rels: HashMap<usize, Relationships> = HashMap::new();
200 let mut charts: Vec<(String, ChartSpace)> = Vec::new();
201 let mut raw_charts: Vec<(String, Vec<u8>)> = Vec::new();
202 let mut images: Vec<(String, Vec<u8>)> = Vec::new();
203 let mut seen_chart_paths: HashSet<String> = HashSet::new();
204 let mut seen_image_paths: HashSet<String> = HashSet::new();
205
206 for (drawing_idx, (drawing_path, _)) in drawings.iter().enumerate() {
207 let drawing_rels_path = relationship_part_path(drawing_path);
208 let Ok(rels) = read_xml_part::<Relationships, _>(archive, &drawing_rels_path) else {
209 continue;
210 };
211
212 for rel in &rels.relationships {
213 if rel.rel_type == rel_types::CHART {
214 let chart_path = resolve_relationship_target(drawing_path, &rel.target);
215 if seen_chart_paths.insert(chart_path.clone()) {
216 match read_xml_part::<ChartSpace, _>(archive, &chart_path) {
217 Ok(chart) => charts.push((chart_path, chart)),
218 Err(_) => {
219 if let Ok(bytes) = read_bytes_part(archive, &chart_path) {
220 raw_charts.push((chart_path, bytes));
221 }
222 }
223 }
224 }
225 } else if rel.rel_type == rel_types::IMAGE {
226 let image_path = resolve_relationship_target(drawing_path, &rel.target);
227 if seen_image_paths.insert(image_path.clone()) {
228 if let Ok(bytes) = read_bytes_part(archive, &image_path) {
229 images.push((image_path, bytes));
230 }
231 }
232 }
233 }
234
235 drawing_rels.insert(drawing_idx, rels);
236 }
237
238 for ovr in &content_types.overrides {
241 if ovr.content_type != mime_types::CHART {
242 continue;
243 }
244 let chart_path = ovr.part_name.trim_start_matches('/').to_string();
245 if seen_chart_paths.insert(chart_path.clone()) {
246 match read_xml_part::<ChartSpace, _>(archive, &chart_path) {
247 Ok(chart) => charts.push((chart_path, chart)),
248 Err(_) => {
249 if let Ok(bytes) = read_bytes_part(archive, &chart_path) {
250 raw_charts.push((chart_path, bytes));
251 }
252 }
253 }
254 }
255 }
256
257 let core_properties = read_string_part(archive, "docProps/core.xml")
259 .ok()
260 .and_then(|xml_str| {
261 sheetkit_xml::doc_props::deserialize_core_properties(&xml_str).ok()
262 });
263
264 let app_properties: Option<sheetkit_xml::doc_props::ExtendedProperties> =
266 read_xml_part(archive, "docProps/app.xml").ok();
267
268 let custom_properties = read_string_part(archive, "docProps/custom.xml")
270 .ok()
271 .and_then(|xml_str| {
272 sheetkit_xml::doc_props::deserialize_custom_properties(&xml_str).ok()
273 });
274
275 let mut pivot_cache_defs = Vec::new();
277 let mut pivot_tables = Vec::new();
278 let mut pivot_cache_records = Vec::new();
279 for ovr in &content_types.overrides {
280 let path = ovr.part_name.trim_start_matches('/');
281 if ovr.content_type == mime_types::PIVOT_CACHE_DEFINITION {
282 if let Ok(pcd) = read_xml_part::<sheetkit_xml::pivot_cache::PivotCacheDefinition, _>(
283 archive, path,
284 ) {
285 pivot_cache_defs.push((path.to_string(), pcd));
286 }
287 } else if ovr.content_type == mime_types::PIVOT_TABLE {
288 if let Ok(pt) = read_xml_part::<sheetkit_xml::pivot_table::PivotTableDefinition, _>(
289 archive, path,
290 ) {
291 pivot_tables.push((path.to_string(), pt));
292 }
293 } else if ovr.content_type == mime_types::PIVOT_CACHE_RECORDS {
294 if let Ok(pcr) =
295 read_xml_part::<sheetkit_xml::pivot_cache::PivotCacheRecords, _>(archive, path)
296 {
297 pivot_cache_records.push((path.to_string(), pcr));
298 }
299 }
300 }
301
302 let mut sheet_sparklines: Vec<Vec<crate::sparkline::SparklineConfig>> =
304 vec![vec![]; worksheets.len()];
305 for (i, ws_path) in worksheet_paths.iter().enumerate() {
306 if let Ok(raw) = read_string_part(archive, ws_path) {
307 let parsed = parse_sparklines_from_xml(&raw);
308 if !parsed.is_empty() {
309 sheet_sparklines[i] = parsed;
310 }
311 }
312 }
313
314 let mut sheet_name_index = HashMap::with_capacity(worksheets.len());
316 for (i, (name, _)) in worksheets.iter().enumerate() {
317 sheet_name_index.insert(name.clone(), i);
318 }
319
320 for (_name, ws) in &mut worksheets {
323 ws.sheet_data.rows.sort_unstable_by_key(|r| r.r);
325 for row in &mut ws.sheet_data.rows {
326 for cell in &mut row.cells {
327 cell.col = fast_col_number(cell.r.as_str());
328 }
329 row.cells.sort_unstable_by_key(|c| c.col);
331 }
332 }
333
334 Ok(Self {
335 content_types,
336 package_rels,
337 workbook_xml,
338 workbook_rels,
339 worksheets,
340 stylesheet,
341 sst_runtime,
342 sheet_comments,
343 charts,
344 raw_charts,
345 drawings,
346 images,
347 worksheet_drawings,
348 worksheet_rels,
349 drawing_rels,
350 core_properties,
351 app_properties,
352 custom_properties,
353 pivot_tables,
354 pivot_cache_defs,
355 pivot_cache_records,
356 theme_xml,
357 theme_colors,
358 sheet_sparklines,
359 sheet_vml,
360 sheet_name_index,
361 })
362 }
363
364 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
366 let file = std::fs::File::create(path)?;
367 let mut zip = zip::ZipWriter::new(file);
368 let options = SimpleFileOptions::default()
369 .compression_method(CompressionMethod::Deflated)
370 .compression_level(Some(1));
371 self.write_zip_contents(&mut zip, options)?;
372 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
373 Ok(())
374 }
375
376 pub fn save_to_buffer(&self) -> Result<Vec<u8>> {
378 let estimated = self.worksheets.len() * 4000
380 + self.sst_runtime.len() * 60
381 + self.images.iter().map(|(_, d)| d.len()).sum::<usize>()
382 + 32_000;
383 let mut buf = Vec::with_capacity(estimated);
384 {
385 let cursor = std::io::Cursor::new(&mut buf);
386 let mut zip = zip::ZipWriter::new(cursor);
387 let options =
388 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
389 self.write_zip_contents(&mut zip, options)?;
390 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
391 }
392 Ok(buf)
393 }
394
395 pub fn open_from_buffer(data: &[u8]) -> Result<Self> {
397 #[cfg(feature = "encryption")]
399 if data.len() >= 8 {
400 if let Ok(crate::crypt::ContainerFormat::Cfb) =
401 crate::crypt::detect_container_format(data)
402 {
403 return Err(Error::FileEncrypted);
404 }
405 }
406
407 let cursor = std::io::Cursor::new(data);
408 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
409 Self::from_archive(&mut archive)
410 }
411
412 #[cfg(feature = "encryption")]
418 pub fn open_with_password<P: AsRef<Path>>(path: P, password: &str) -> Result<Self> {
419 let data = std::fs::read(path.as_ref())?;
420 let decrypted_zip = crate::crypt::decrypt_xlsx(&data, password)?;
421 let cursor = std::io::Cursor::new(decrypted_zip);
422 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::Zip(e.to_string()))?;
423 Self::from_archive(&mut archive)
424 }
425
426 #[cfg(feature = "encryption")]
429 pub fn save_with_password<P: AsRef<Path>>(&self, path: P, password: &str) -> Result<()> {
430 let mut zip_buf = Vec::new();
432 {
433 let cursor = std::io::Cursor::new(&mut zip_buf);
434 let mut zip = zip::ZipWriter::new(cursor);
435 let options =
436 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
437 self.write_zip_contents(&mut zip, options)?;
438 zip.finish().map_err(|e| Error::Zip(e.to_string()))?;
439 }
440
441 let cfb_data = crate::crypt::encrypt_xlsx(&zip_buf, password)?;
443 std::fs::write(path.as_ref(), &cfb_data)?;
444 Ok(())
445 }
446
447 fn write_zip_contents<W: std::io::Write + std::io::Seek>(
449 &self,
450 zip: &mut zip::ZipWriter<W>,
451 options: SimpleFileOptions,
452 ) -> Result<()> {
453 let mut content_types = self.content_types.clone();
454 let mut worksheet_rels = self.worksheet_rels.clone();
455
456 let comment_count = self.sheet_comments.iter().filter(|c| c.is_some()).count();
458 let mut vml_parts_to_write: Vec<(usize, String, Vec<u8>)> =
460 Vec::with_capacity(comment_count);
461 let mut legacy_drawing_rids: HashMap<usize, String> = HashMap::with_capacity(comment_count);
463
464 let mut has_any_vml = false;
466
467 for sheet_idx in 0..self.worksheets.len() {
468 let has_comments = self
469 .sheet_comments
470 .get(sheet_idx)
471 .and_then(|c| c.as_ref())
472 .is_some();
473 if let Some(rels) = worksheet_rels.get_mut(&sheet_idx) {
474 rels.relationships
475 .retain(|r| r.rel_type != rel_types::COMMENTS);
476 rels.relationships
477 .retain(|r| r.rel_type != rel_types::VML_DRAWING);
478 }
479 if !has_comments {
480 continue;
481 }
482
483 let comment_path = format!("xl/comments{}.xml", sheet_idx + 1);
484 let part_name = format!("/{}", comment_path);
485 if !content_types
486 .overrides
487 .iter()
488 .any(|o| o.part_name == part_name && o.content_type == mime_types::COMMENTS)
489 {
490 content_types.overrides.push(ContentTypeOverride {
491 part_name,
492 content_type: mime_types::COMMENTS.to_string(),
493 });
494 }
495
496 let sheet_path = self.sheet_part_path(sheet_idx);
497 let target = relative_relationship_target(&sheet_path, &comment_path);
498 let rels = worksheet_rels
499 .entry(sheet_idx)
500 .or_insert_with(default_relationships);
501 let rid = crate::sheet::next_rid(&rels.relationships);
502 rels.relationships.push(Relationship {
503 id: rid,
504 rel_type: rel_types::COMMENTS.to_string(),
505 target,
506 target_mode: None,
507 });
508
509 let vml_path = format!("xl/drawings/vmlDrawing{}.vml", sheet_idx + 1);
511 let vml_bytes =
512 if let Some(bytes) = self.sheet_vml.get(sheet_idx).and_then(|v| v.as_ref()) {
513 bytes.clone()
514 } else {
515 let comments = self.sheet_comments[sheet_idx].as_ref().unwrap();
517 let cells: Vec<&str> = comments
518 .comment_list
519 .comments
520 .iter()
521 .map(|c| c.r#ref.as_str())
522 .collect();
523 crate::vml::build_vml_drawing(&cells).into_bytes()
524 };
525
526 let vml_part_name = format!("/{}", vml_path);
527 if !content_types
528 .overrides
529 .iter()
530 .any(|o| o.part_name == vml_part_name && o.content_type == mime_types::VML_DRAWING)
531 {
532 content_types.overrides.push(ContentTypeOverride {
533 part_name: vml_part_name,
534 content_type: mime_types::VML_DRAWING.to_string(),
535 });
536 }
537
538 let vml_target = relative_relationship_target(&sheet_path, &vml_path);
539 let vml_rid = crate::sheet::next_rid(&rels.relationships);
540 rels.relationships.push(Relationship {
541 id: vml_rid.clone(),
542 rel_type: rel_types::VML_DRAWING.to_string(),
543 target: vml_target,
544 target_mode: None,
545 });
546
547 legacy_drawing_rids.insert(sheet_idx, vml_rid);
548 vml_parts_to_write.push((sheet_idx, vml_path, vml_bytes));
549 has_any_vml = true;
550 }
551
552 if has_any_vml && !content_types.defaults.iter().any(|d| d.extension == "vml") {
554 content_types.defaults.push(ContentTypeDefault {
555 extension: "vml".to_string(),
556 content_type: mime_types::VML_DRAWING.to_string(),
557 });
558 }
559
560 write_xml_part(zip, "[Content_Types].xml", &content_types, options)?;
562
563 write_xml_part(zip, "_rels/.rels", &self.package_rels, options)?;
565
566 write_xml_part(zip, "xl/workbook.xml", &self.workbook_xml, options)?;
568
569 write_xml_part(
571 zip,
572 "xl/_rels/workbook.xml.rels",
573 &self.workbook_rels,
574 options,
575 )?;
576
577 for (i, (_name, ws)) in self.worksheets.iter().enumerate() {
579 let entry_name = self.sheet_part_path(i);
580 let empty_sparklines: Vec<crate::sparkline::SparklineConfig> = vec![];
581 let sparklines = self.sheet_sparklines.get(i).unwrap_or(&empty_sparklines);
582 let legacy_rid = legacy_drawing_rids.get(&i).map(|s| s.as_str());
583
584 if legacy_rid.is_none() && sparklines.is_empty() {
585 write_xml_part(zip, &entry_name, ws, options)?;
586 } else {
587 let xml = serialize_worksheet_with_extras(ws, sparklines, legacy_rid)?;
590 zip.start_file(&entry_name, options)
591 .map_err(|e| Error::Zip(e.to_string()))?;
592 zip.write_all(xml.as_bytes())?;
593 }
594 }
595
596 write_xml_part(zip, "xl/styles.xml", &self.stylesheet, options)?;
598
599 let sst_xml = self.sst_runtime.to_sst();
601 write_xml_part(zip, "xl/sharedStrings.xml", &sst_xml, options)?;
602
603 for (i, comments) in self.sheet_comments.iter().enumerate() {
605 if let Some(ref c) = comments {
606 let entry_name = format!("xl/comments{}.xml", i + 1);
607 write_xml_part(zip, &entry_name, c, options)?;
608 }
609 }
610
611 for (_sheet_idx, vml_path, vml_bytes) in &vml_parts_to_write {
613 zip.start_file(vml_path, options)
614 .map_err(|e| Error::Zip(e.to_string()))?;
615 zip.write_all(vml_bytes)?;
616 }
617
618 for (path, drawing) in &self.drawings {
620 write_xml_part(zip, path, drawing, options)?;
621 }
622
623 for (path, chart) in &self.charts {
625 write_xml_part(zip, path, chart, options)?;
626 }
627 for (path, data) in &self.raw_charts {
628 if self.charts.iter().any(|(p, _)| p == path) {
629 continue;
630 }
631 zip.start_file(path, options)
632 .map_err(|e| Error::Zip(e.to_string()))?;
633 zip.write_all(data)?;
634 }
635
636 for (path, data) in &self.images {
638 zip.start_file(path, options)
639 .map_err(|e| Error::Zip(e.to_string()))?;
640 zip.write_all(data)?;
641 }
642
643 for (sheet_idx, rels) in &worksheet_rels {
645 let sheet_path = self.sheet_part_path(*sheet_idx);
646 let path = relationship_part_path(&sheet_path);
647 write_xml_part(zip, &path, rels, options)?;
648 }
649
650 for (drawing_idx, rels) in &self.drawing_rels {
652 if let Some((drawing_path, _)) = self.drawings.get(*drawing_idx) {
653 let path = relationship_part_path(drawing_path);
654 write_xml_part(zip, &path, rels, options)?;
655 }
656 }
657
658 for (path, pt) in &self.pivot_tables {
660 write_xml_part(zip, path, pt, options)?;
661 }
662
663 for (path, pcd) in &self.pivot_cache_defs {
665 write_xml_part(zip, path, pcd, options)?;
666 }
667
668 for (path, pcr) in &self.pivot_cache_records {
670 write_xml_part(zip, path, pcr, options)?;
671 }
672
673 {
675 let default_theme = crate::theme::default_theme_xml();
676 let theme_bytes = self.theme_xml.as_deref().unwrap_or(&default_theme);
677 zip.start_file("xl/theme/theme1.xml", options)
678 .map_err(|e| Error::Zip(e.to_string()))?;
679 zip.write_all(theme_bytes)?;
680 }
681
682 if let Some(ref props) = self.core_properties {
684 let xml_str = sheetkit_xml::doc_props::serialize_core_properties(props);
685 zip.start_file("docProps/core.xml", options)
686 .map_err(|e| Error::Zip(e.to_string()))?;
687 zip.write_all(xml_str.as_bytes())?;
688 }
689
690 if let Some(ref props) = self.app_properties {
692 write_xml_part(zip, "docProps/app.xml", props, options)?;
693 }
694
695 if let Some(ref props) = self.custom_properties {
697 let xml_str = sheetkit_xml::doc_props::serialize_custom_properties(props);
698 zip.start_file("docProps/custom.xml", options)
699 .map_err(|e| Error::Zip(e.to_string()))?;
700 zip.write_all(xml_str.as_bytes())?;
701 }
702
703 Ok(())
704 }
705}
706
707impl Default for Workbook {
708 fn default() -> Self {
709 Self::new()
710 }
711}
712
713pub(crate) fn serialize_xml<T: Serialize>(value: &T) -> Result<String> {
715 let body = quick_xml::se::to_string(value).map_err(|e| Error::XmlParse(e.to_string()))?;
716 let mut result = String::with_capacity(XML_DECLARATION.len() + 1 + body.len());
717 result.push_str(XML_DECLARATION);
718 result.push('\n');
719 result.push_str(&body);
720 Ok(result)
721}
722
723pub(crate) fn read_xml_part<T: serde::de::DeserializeOwned, R: std::io::Read + std::io::Seek>(
725 archive: &mut zip::ZipArchive<R>,
726 name: &str,
727) -> Result<T> {
728 let mut entry = archive
729 .by_name(name)
730 .map_err(|e| Error::Zip(e.to_string()))?;
731 let size_hint = entry.size() as usize;
732 let mut content = String::with_capacity(size_hint);
733 entry
734 .read_to_string(&mut content)
735 .map_err(|e| Error::Zip(e.to_string()))?;
736 quick_xml::de::from_str(&content).map_err(|e| Error::XmlDeserialize(e.to_string()))
737}
738
739pub(crate) fn read_string_part<R: std::io::Read + std::io::Seek>(
741 archive: &mut zip::ZipArchive<R>,
742 name: &str,
743) -> Result<String> {
744 let mut entry = archive
745 .by_name(name)
746 .map_err(|e| Error::Zip(e.to_string()))?;
747 let size_hint = entry.size() as usize;
748 let mut content = String::with_capacity(size_hint);
749 entry
750 .read_to_string(&mut content)
751 .map_err(|e| Error::Zip(e.to_string()))?;
752 Ok(content)
753}
754
755pub(crate) fn read_bytes_part<R: std::io::Read + std::io::Seek>(
757 archive: &mut zip::ZipArchive<R>,
758 name: &str,
759) -> Result<Vec<u8>> {
760 let mut entry = archive
761 .by_name(name)
762 .map_err(|e| Error::Zip(e.to_string()))?;
763 let size_hint = entry.size() as usize;
764 let mut content = Vec::with_capacity(size_hint);
765 entry
766 .read_to_end(&mut content)
767 .map_err(|e| Error::Zip(e.to_string()))?;
768 Ok(content)
769}
770
771pub(crate) fn serialize_worksheet_with_extras(
774 ws: &WorksheetXml,
775 sparklines: &[crate::sparkline::SparklineConfig],
776 legacy_drawing_rid: Option<&str>,
777) -> Result<String> {
778 let body = quick_xml::se::to_string(ws).map_err(|e| Error::XmlParse(e.to_string()))?;
779
780 let closing = "</worksheet>";
781 let ext_xml = if sparklines.is_empty() {
782 String::new()
783 } else {
784 build_sparkline_ext_xml(sparklines)
785 };
786 let legacy_xml = if let Some(rid) = legacy_drawing_rid {
787 format!("<legacyDrawing r:id=\"{rid}\"/>")
788 } else {
789 String::new()
790 };
791
792 if let Some(pos) = body.rfind(closing) {
793 let body_prefix = &body[..pos];
796 let stripped;
797 let prefix = if !legacy_xml.is_empty() {
798 if let Some(ld_start) = body_prefix.find("<legacyDrawing ") {
799 let ld_end = body_prefix[ld_start..]
801 .find("/>")
802 .map(|e| ld_start + e + 2)
803 .unwrap_or(ld_start);
804 stripped = format!("{}{}", &body_prefix[..ld_start], &body_prefix[ld_end..]);
805 stripped.as_str()
806 } else {
807 body_prefix
808 }
809 } else {
810 body_prefix
811 };
812
813 let extra_len = ext_xml.len() + legacy_xml.len();
814 let mut result = String::with_capacity(XML_DECLARATION.len() + 1 + body.len() + extra_len);
815 result.push_str(XML_DECLARATION);
816 result.push('\n');
817 result.push_str(prefix);
818 result.push_str(&legacy_xml);
819 result.push_str(&ext_xml);
820 result.push_str(closing);
821 Ok(result)
822 } else {
823 Ok(format!("{XML_DECLARATION}\n{body}"))
824 }
825}
826
827pub(crate) fn build_sparkline_ext_xml(sparklines: &[crate::sparkline::SparklineConfig]) -> String {
829 use std::fmt::Write;
830 let mut xml = String::new();
831 let _ = write!(
832 xml,
833 "<extLst>\
834 <ext xmlns:x14=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main\" \
835 uri=\"{{05C60535-1F16-4fd2-B633-F4F36F0B64E0}}\">\
836 <x14:sparklineGroups \
837 xmlns:xm=\"http://schemas.microsoft.com/office/excel/2006/main\">"
838 );
839 for config in sparklines {
840 let group = crate::sparkline::config_to_xml_group(config);
841 let _ = write!(xml, "<x14:sparklineGroup");
842 if let Some(ref t) = group.sparkline_type {
843 let _ = write!(xml, " type=\"{t}\"");
844 }
845 if group.markers == Some(true) {
846 let _ = write!(xml, " markers=\"1\"");
847 }
848 if group.high == Some(true) {
849 let _ = write!(xml, " high=\"1\"");
850 }
851 if group.low == Some(true) {
852 let _ = write!(xml, " low=\"1\"");
853 }
854 if group.first == Some(true) {
855 let _ = write!(xml, " first=\"1\"");
856 }
857 if group.last == Some(true) {
858 let _ = write!(xml, " last=\"1\"");
859 }
860 if group.negative == Some(true) {
861 let _ = write!(xml, " negative=\"1\"");
862 }
863 if group.display_x_axis == Some(true) {
864 let _ = write!(xml, " displayXAxis=\"1\"");
865 }
866 if let Some(w) = group.line_weight {
867 let _ = write!(xml, " lineWeight=\"{w}\"");
868 }
869 let _ = write!(xml, "><x14:sparklines>");
870 for sp in &group.sparklines.items {
871 let _ = write!(
872 xml,
873 "<x14:sparkline><xm:f>{}</xm:f><xm:sqref>{}</xm:sqref></x14:sparkline>",
874 sp.formula, sp.sqref
875 );
876 }
877 let _ = write!(xml, "</x14:sparklines></x14:sparklineGroup>");
878 }
879 let _ = write!(xml, "</x14:sparklineGroups></ext></extLst>");
880 xml
881}
882
883pub(crate) fn parse_sparklines_from_xml(xml: &str) -> Vec<crate::sparkline::SparklineConfig> {
885 use crate::sparkline::{SparklineConfig, SparklineType};
886
887 let mut sparklines = Vec::new();
888
889 let mut search_from = 0;
891 while let Some(group_start) = xml[search_from..].find("<x14:sparklineGroup") {
892 let abs_start = search_from + group_start;
893 let group_end_tag = "</x14:sparklineGroup>";
894 let abs_end = match xml[abs_start..].find(group_end_tag) {
895 Some(pos) => abs_start + pos + group_end_tag.len(),
896 None => break,
897 };
898 let group_xml = &xml[abs_start..abs_end];
899
900 let sparkline_type = extract_xml_attr(group_xml, "type")
902 .and_then(|s| SparklineType::parse(&s))
903 .unwrap_or_default();
904 let markers = extract_xml_bool_attr(group_xml, "markers");
905 let high_point = extract_xml_bool_attr(group_xml, "high");
906 let low_point = extract_xml_bool_attr(group_xml, "low");
907 let first_point = extract_xml_bool_attr(group_xml, "first");
908 let last_point = extract_xml_bool_attr(group_xml, "last");
909 let negative_points = extract_xml_bool_attr(group_xml, "negative");
910 let show_axis = extract_xml_bool_attr(group_xml, "displayXAxis");
911 let line_weight =
912 extract_xml_attr(group_xml, "lineWeight").and_then(|s| s.parse::<f64>().ok());
913
914 let mut sp_from = 0;
916 while let Some(sp_start) = group_xml[sp_from..].find("<x14:sparkline>") {
917 let sp_abs = sp_from + sp_start;
918 let sp_end_tag = "</x14:sparkline>";
919 let sp_abs_end = match group_xml[sp_abs..].find(sp_end_tag) {
920 Some(pos) => sp_abs + pos + sp_end_tag.len(),
921 None => break,
922 };
923 let sp_xml = &group_xml[sp_abs..sp_abs_end];
924
925 let formula = extract_xml_element(sp_xml, "xm:f").unwrap_or_default();
926 let sqref = extract_xml_element(sp_xml, "xm:sqref").unwrap_or_default();
927
928 if !formula.is_empty() && !sqref.is_empty() {
929 sparklines.push(SparklineConfig {
930 data_range: formula,
931 location: sqref,
932 sparkline_type: sparkline_type.clone(),
933 markers,
934 high_point,
935 low_point,
936 first_point,
937 last_point,
938 negative_points,
939 show_axis,
940 line_weight,
941 style: None,
942 });
943 }
944 sp_from = sp_abs_end;
945 }
946 search_from = abs_end;
947 }
948 sparklines
949}
950
951pub(crate) fn extract_xml_attr(xml: &str, attr: &str) -> Option<String> {
955 for quote in ['"', '\''] {
957 let haystack = xml.as_bytes();
959 let attr_bytes = attr.as_bytes();
960 let mut pos = 0;
961 while pos + 1 + attr_bytes.len() + 2 <= haystack.len() {
962 if haystack[pos] == b' '
963 && haystack[pos + 1..pos + 1 + attr_bytes.len()] == *attr_bytes
964 && haystack[pos + 1 + attr_bytes.len()] == b'='
965 && haystack[pos + 1 + attr_bytes.len() + 1] == quote as u8
966 {
967 let val_start = pos + 1 + attr_bytes.len() + 2;
968 if let Some(end) = xml[val_start..].find(quote) {
969 return Some(xml[val_start..val_start + end].to_string());
970 }
971 }
972 pos += 1;
973 }
974 }
975 None
976}
977
978pub(crate) fn extract_xml_bool_attr(xml: &str, attr: &str) -> bool {
980 extract_xml_attr(xml, attr)
981 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
982 .unwrap_or(false)
983}
984
985pub(crate) fn extract_xml_element(xml: &str, tag: &str) -> Option<String> {
987 let open = format!("<{tag}>");
988 let close = format!("</{tag}>");
989 let start = xml.find(&open)?;
990 let content_start = start + open.len();
991 let end = xml[content_start..].find(&close)?;
992 Some(xml[content_start..content_start + end].to_string())
993}
994
995pub(crate) fn write_xml_part<T: Serialize, W: std::io::Write + std::io::Seek>(
997 zip: &mut zip::ZipWriter<W>,
998 name: &str,
999 value: &T,
1000 options: SimpleFileOptions,
1001) -> Result<()> {
1002 let xml = serialize_xml(value)?;
1003 zip.start_file(name, options)
1004 .map_err(|e| Error::Zip(e.to_string()))?;
1005 zip.write_all(xml.as_bytes())?;
1006 Ok(())
1007}
1008
1009fn fast_col_number(cell_ref: &str) -> u32 {
1015 let mut col: u32 = 0;
1016 for b in cell_ref.bytes() {
1017 if b.is_ascii_alphabetic() {
1018 col = col * 26 + (b.to_ascii_uppercase() - b'A') as u32 + 1;
1019 } else {
1020 break;
1021 }
1022 }
1023 col
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028 use super::*;
1029 use tempfile::TempDir;
1030
1031 #[test]
1032 fn test_fast_col_number() {
1033 assert_eq!(fast_col_number("A1"), 1);
1034 assert_eq!(fast_col_number("B1"), 2);
1035 assert_eq!(fast_col_number("Z1"), 26);
1036 assert_eq!(fast_col_number("AA1"), 27);
1037 assert_eq!(fast_col_number("AZ1"), 52);
1038 assert_eq!(fast_col_number("BA1"), 53);
1039 assert_eq!(fast_col_number("XFD1"), 16384);
1040 }
1041
1042 #[test]
1043 fn test_extract_xml_attr() {
1044 let xml = r#"<tag type="column" markers="1" weight="2.5">"#;
1045 assert_eq!(extract_xml_attr(xml, "type"), Some("column".to_string()));
1046 assert_eq!(extract_xml_attr(xml, "markers"), Some("1".to_string()));
1047 assert_eq!(extract_xml_attr(xml, "weight"), Some("2.5".to_string()));
1048 assert_eq!(extract_xml_attr(xml, "missing"), None);
1049 let xml2 = "<tag name='hello'>";
1051 assert_eq!(extract_xml_attr(xml2, "name"), Some("hello".to_string()));
1052 }
1053
1054 #[test]
1055 fn test_extract_xml_bool_attr() {
1056 let xml = r#"<tag markers="1" hidden="0" visible="true">"#;
1057 assert!(extract_xml_bool_attr(xml, "markers"));
1058 assert!(!extract_xml_bool_attr(xml, "hidden"));
1059 assert!(extract_xml_bool_attr(xml, "visible"));
1060 assert!(!extract_xml_bool_attr(xml, "missing"));
1061 }
1062
1063 #[test]
1064 fn test_new_workbook_has_sheet1() {
1065 let wb = Workbook::new();
1066 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
1067 }
1068
1069 #[test]
1070 fn test_new_workbook_save_creates_file() {
1071 let dir = TempDir::new().unwrap();
1072 let path = dir.path().join("test.xlsx");
1073 let wb = Workbook::new();
1074 wb.save(&path).unwrap();
1075 assert!(path.exists());
1076 }
1077
1078 #[test]
1079 fn test_save_and_open_roundtrip() {
1080 let dir = TempDir::new().unwrap();
1081 let path = dir.path().join("roundtrip.xlsx");
1082
1083 let wb = Workbook::new();
1084 wb.save(&path).unwrap();
1085
1086 let wb2 = Workbook::open(&path).unwrap();
1087 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
1088 }
1089
1090 #[test]
1091 fn test_saved_file_is_valid_zip() {
1092 let dir = TempDir::new().unwrap();
1093 let path = dir.path().join("valid.xlsx");
1094 let wb = Workbook::new();
1095 wb.save(&path).unwrap();
1096
1097 let file = std::fs::File::open(&path).unwrap();
1099 let mut archive = zip::ZipArchive::new(file).unwrap();
1100
1101 let expected_files = [
1102 "[Content_Types].xml",
1103 "_rels/.rels",
1104 "xl/workbook.xml",
1105 "xl/_rels/workbook.xml.rels",
1106 "xl/worksheets/sheet1.xml",
1107 "xl/styles.xml",
1108 "xl/sharedStrings.xml",
1109 ];
1110
1111 for name in &expected_files {
1112 assert!(archive.by_name(name).is_ok(), "Missing ZIP entry: {}", name);
1113 }
1114 }
1115
1116 #[test]
1117 fn test_open_nonexistent_file_returns_error() {
1118 let result = Workbook::open("/nonexistent/path.xlsx");
1119 assert!(result.is_err());
1120 }
1121
1122 #[test]
1123 fn test_saved_xml_has_declarations() {
1124 let dir = TempDir::new().unwrap();
1125 let path = dir.path().join("decl.xlsx");
1126 let wb = Workbook::new();
1127 wb.save(&path).unwrap();
1128
1129 let file = std::fs::File::open(&path).unwrap();
1130 let mut archive = zip::ZipArchive::new(file).unwrap();
1131
1132 let mut content = String::new();
1133 std::io::Read::read_to_string(
1134 &mut archive.by_name("[Content_Types].xml").unwrap(),
1135 &mut content,
1136 )
1137 .unwrap();
1138 assert!(content.starts_with("<?xml"));
1139 }
1140
1141 #[test]
1142 fn test_default_trait() {
1143 let wb = Workbook::default();
1144 assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
1145 }
1146
1147 #[test]
1148 fn test_serialize_xml_helper() {
1149 let ct = ContentTypes::default();
1150 let xml = serialize_xml(&ct).unwrap();
1151 assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"));
1152 assert!(xml.contains("<Types"));
1153 }
1154
1155 #[test]
1156 fn test_save_to_buffer_and_open_from_buffer_roundtrip() {
1157 let mut wb = Workbook::new();
1158 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
1159 .unwrap();
1160 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
1161 .unwrap();
1162
1163 let buf = wb.save_to_buffer().unwrap();
1164 assert!(!buf.is_empty());
1165
1166 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
1167 assert_eq!(wb2.sheet_names(), vec!["Sheet1"]);
1168 assert_eq!(
1169 wb2.get_cell_value("Sheet1", "A1").unwrap(),
1170 CellValue::String("Hello".to_string())
1171 );
1172 assert_eq!(
1173 wb2.get_cell_value("Sheet1", "B2").unwrap(),
1174 CellValue::Number(42.0)
1175 );
1176 }
1177
1178 #[test]
1179 fn test_save_to_buffer_produces_valid_zip() {
1180 let wb = Workbook::new();
1181 let buf = wb.save_to_buffer().unwrap();
1182
1183 let cursor = std::io::Cursor::new(buf);
1184 let mut archive = zip::ZipArchive::new(cursor).unwrap();
1185
1186 let expected_files = [
1187 "[Content_Types].xml",
1188 "_rels/.rels",
1189 "xl/workbook.xml",
1190 "xl/_rels/workbook.xml.rels",
1191 "xl/worksheets/sheet1.xml",
1192 "xl/styles.xml",
1193 "xl/sharedStrings.xml",
1194 ];
1195
1196 for name in &expected_files {
1197 assert!(archive.by_name(name).is_ok(), "Missing ZIP entry: {}", name);
1198 }
1199 }
1200
1201 #[test]
1202 fn test_open_from_buffer_invalid_data() {
1203 let result = Workbook::open_from_buffer(b"not a zip file");
1204 assert!(result.is_err());
1205 }
1206
1207 #[cfg(feature = "encryption")]
1208 #[test]
1209 fn test_save_and_open_with_password_roundtrip() {
1210 let dir = TempDir::new().unwrap();
1211 let path = dir.path().join("encrypted.xlsx");
1212
1213 let mut wb = Workbook::new();
1215 wb.set_cell_value("Sheet1", "A1", CellValue::String("Hello".to_string()))
1216 .unwrap();
1217 wb.set_cell_value("Sheet1", "B2", CellValue::Number(42.0))
1218 .unwrap();
1219
1220 wb.save_with_password(&path, "test123").unwrap();
1222
1223 let data = std::fs::read(&path).unwrap();
1225 assert_eq!(
1226 &data[..8],
1227 &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]
1228 );
1229
1230 let result = Workbook::open(&path);
1232 assert!(matches!(result, Err(Error::FileEncrypted)));
1233
1234 let result = Workbook::open_with_password(&path, "wrong");
1236 assert!(matches!(result, Err(Error::IncorrectPassword)));
1237
1238 let wb2 = Workbook::open_with_password(&path, "test123").unwrap();
1240 assert_eq!(
1241 wb2.get_cell_value("Sheet1", "A1").unwrap(),
1242 CellValue::String("Hello".to_string())
1243 );
1244 assert_eq!(
1245 wb2.get_cell_value("Sheet1", "B2").unwrap(),
1246 CellValue::Number(42.0)
1247 );
1248 }
1249
1250 #[cfg(feature = "encryption")]
1251 #[test]
1252 fn test_open_encrypted_file_without_password_returns_file_encrypted() {
1253 let dir = TempDir::new().unwrap();
1254 let path = dir.path().join("encrypted2.xlsx");
1255
1256 let wb = Workbook::new();
1257 wb.save_with_password(&path, "secret").unwrap();
1258
1259 let result = Workbook::open(&path);
1260 assert!(matches!(result, Err(Error::FileEncrypted)))
1261 }
1262}