Skip to main content

excelize_rs/
file.rs

1//! File lifecycle and lazy XML readers/writers.
2//!
3//! This module corresponds to `file.go` and the `File` struct/methods from
4//! `excelize.go` in the original Go implementation.
5
6use std::cell::RefCell;
7use std::collections::HashMap;
8use std::fmt;
9use std::fs;
10use std::io::{self, Cursor, Read, Seek, Write};
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, Mutex};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use dashmap::DashMap;
16use quick_xml::de::from_reader as xml_from_reader;
17use quick_xml::se::to_string as xml_to_string;
18use zip::write::SimpleFileOptions;
19use zip::{CompressionMethod, ZipArchive};
20
21use crate::calc::arg::FormulaArg;
22use crate::constants::{
23    CONTENT_TYPE_RELATIONSHIPS, CONTENT_TYPE_VBA, DEFAULT_XML_PATH_CONTENT_TYPES,
24    DEFAULT_XML_PATH_DOC_PROPS_APP, DEFAULT_XML_PATH_DOC_PROPS_CORE, DEFAULT_XML_PATH_METADATA,
25    DEFAULT_XML_PATH_RD_RICH_VALUE, DEFAULT_XML_PATH_RD_RICH_VALUE_REL,
26    DEFAULT_XML_PATH_RD_RICH_VALUE_REL_RELS, DEFAULT_XML_PATH_RD_RICH_VALUE_STRUCTURE,
27    DEFAULT_XML_PATH_RD_RICH_VALUE_WEB_IMAGE, DEFAULT_XML_PATH_RD_RICH_VALUE_WEB_IMAGE_RELS,
28    DEFAULT_XML_PATH_RELS, DEFAULT_XML_PATH_SHARED_STRINGS, DEFAULT_XML_PATH_SHEET,
29    DEFAULT_XML_PATH_STYLES, DEFAULT_XML_PATH_THEME, DEFAULT_XML_PATH_WORKBOOK,
30    DEFAULT_XML_PATH_WORKBOOK_RELS, MAX_FILE_PATH_LENGTH,
31    NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES, NAMESPACE_DRAWING_ML_MAIN,
32    NAMESPACE_EXTENDED_PROPERTIES, NAMESPACE_SPREADSHEET, NAMESPACE_SPREADSHEET_X14,
33    OLE_IDENTIFIER, SOURCE_RELATIONSHIP, SOURCE_RELATIONSHIP_CHART, SOURCE_RELATIONSHIP_COMMENTS,
34    SOURCE_RELATIONSHIP_CUSTOM_PROPERTIES, SOURCE_RELATIONSHIP_EXTEND_PROPERTIES,
35    SOURCE_RELATIONSHIP_IMAGE, SOURCE_RELATIONSHIP_OFFICE_DOCUMENT,
36    SOURCE_RELATIONSHIP_SHARED_STRINGS, SOURCE_RELATIONSHIP_VBA_PROJECT, STREAM_CHUNK_SIZE,
37    STRICT_NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES, STRICT_NAMESPACE_DRAWING_ML_MAIN,
38    STRICT_NAMESPACE_EXTENDED_PROPERTIES, STRICT_NAMESPACE_SPREADSHEET, STRICT_SOURCE_RELATIONSHIP,
39    STRICT_SOURCE_RELATIONSHIP_CHART, STRICT_SOURCE_RELATIONSHIP_COMMENTS,
40    STRICT_SOURCE_RELATIONSHIP_EXTEND_PROPERTIES, STRICT_SOURCE_RELATIONSHIP_IMAGE,
41    STRICT_SOURCE_RELATIONSHIP_OFFICE_DOCUMENT, UNZIP_SIZE_LIMIT, XML_HEADER,
42};
43use crate::crypt;
44use crate::errors::Result;
45use crate::errors::{
46    ErrDefinedNameDuplicate, ErrDefinedNameScope, ErrMaxFilePathLength, ErrOptionsUnzipSizeLimit,
47    ErrParameterInvalid, ErrSave, ErrUnprotectWorkbook, ErrUnprotectWorkbookPassword,
48    ErrWorkbookFileFormat, ErrWorkbookPassword,
49};
50use crate::lib_util::{count_utf16_string, in_str_slice};
51use crate::numfmt;
52use crate::options::{CULTURE_NAME_UNKNOWN, Options};
53use crate::templates::{
54    TEMPLATE_CONTENT_TYPES, TEMPLATE_DOC_PROPS_APP, TEMPLATE_DOC_PROPS_CORE, TEMPLATE_RELS,
55    TEMPLATE_SHEET, TEMPLATE_STYLES, TEMPLATE_THEME, TEMPLATE_WORKBOOK, TEMPLATE_WORKBOOK_RELS,
56};
57use crate::xml::calc_chain::{XlsxCalcChain, XlsxVolTypes};
58use crate::xml::content_types::{XlsxDefault, XlsxTypes};
59use crate::xml::drawing::XlsxWsDr;
60use crate::xml::styles::XlsxStyleSheet;
61use crate::xml::table::{XlsxSingleXmlCells, XlsxTable};
62use crate::xml::theme::{
63    DecodeTheme, XlsxBaseStyles, XlsxCtColor, XlsxFontCollection, XlsxSysClr, XlsxTheme,
64};
65use crate::xml::workbook::{
66    CalcPropsOptions, WorkbookPropsOptions, WorkbookProtectionOptions, XlsxCalcPr,
67    XlsxRelationship, XlsxRelationships, XlsxWorkbook, XlsxWorkbookPr, XlsxWorkbookProtection,
68};
69use crate::xml::worksheet::XlsxWorksheet;
70
71const SUPPORTED_CALC_MODE: &[&str] = &["manual", "auto", "autoNoTable"];
72const SUPPORTED_REF_MODE: &[&str] = &["A1", "R1C1"];
73const WORKBOOK_PROTECTION_SPIN_COUNT: i32 = 100_000;
74
75/// Combined writer trait used by the ZIP writer factory.
76pub trait WriteSeek: Write + Seek {}
77impl<T: Write + Seek> WriteSeek for T {}
78
79/// Trait for user-provided ZIP writers.
80///
81/// Mirrors the subset of `zip::ZipWriter` used when saving a workbook so that
82/// callers can inject custom archive implementations.
83pub trait ZipWriter {
84    /// Start a new file in the archive.
85    fn start_file(&mut self, name: &str, options: SimpleFileOptions) -> Result<()>;
86    /// Write bytes to the current archive entry.
87    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
88    /// Finish writing the archive.
89    fn finish(self: Box<Self>) -> Result<()>;
90}
91
92impl<W: Write + Seek> ZipWriter for zip::ZipWriter<W> {
93    fn start_file(&mut self, name: &str, options: SimpleFileOptions) -> Result<()> {
94        zip::ZipWriter::start_file(self, name, options)?;
95        Ok(())
96    }
97    fn write_all(&mut self, buf: &[u8]) -> Result<()> {
98        std::io::Write::write_all(self, buf)?;
99        Ok(())
100    }
101    fn finish(self: Box<Self>) -> Result<()> {
102        zip::ZipWriter::finish(*self)?;
103        Ok(())
104    }
105}
106
107/// Factory used to create a [`ZipWriter`] for a given output writer.
108pub type ZipWriterFactory =
109    Arc<dyn for<'a> Fn(&'a mut dyn WriteSeek) -> Box<dyn ZipWriter + 'a> + Send + Sync>;
110
111/// Charset transcoder: converts a named encoding to UTF-8 bytes.
112pub type CharsetTranscoderFn =
113    Arc<dyn Fn(&str, Box<dyn Read>) -> Result<Box<dyn Read>> + Send + Sync>;
114
115/// Backing state for a worksheet that is being written via [`StreamWriter`].
116///
117/// The temporary file is written directly to the ZIP package at save time
118/// without being loaded into memory.
119#[allow(dead_code)]
120pub(crate) struct StreamState {
121    pub tmp_path: PathBuf,
122    pub sheet_path: String,
123}
124
125/// Populated spreadsheet file.
126pub struct File {
127    /// Path used by `Save`.
128    pub path: Mutex<String>,
129    /// User-provided options.
130    pub options: Mutex<Options>,
131    /// Number of worksheets in the workbook.
132    pub sheet_count: Mutex<i32>,
133    /// In-memory package parts (path → raw bytes).
134    pub pkg: DashMap<String, Vec<u8>>,
135    /// Parsed worksheets (path → worksheet).
136    pub sheet: DashMap<String, XlsxWorksheet>,
137    /// Parsed relationship parts (path → relationships).
138    pub relationships: DashMap<String, XlsxRelationships>,
139    /// Temporary file mapping (path → filesystem path).
140    pub temp_files: DashMap<String, String>,
141    /// Map of worksheet names to XML paths.
142    pub sheet_map: Mutex<HashMap<String, String>>,
143    /// Captured root namespace attributes for each XML part.
144    pub xml_attr: DashMap<String, String>,
145    /// Marks worksheets that have already been validated.
146    pub checked: DashMap<String, bool>,
147    /// Streaming worksheet writers that have not yet been written to the ZIP.
148    pub(crate) streams: RefCell<HashMap<String, StreamState>>,
149    /// Entries that exceed 4GB and need a ZIP64 LFH patch.
150    pub zip64_entries: Mutex<Vec<String>>,
151    /// Lazily loaded [Content_Types].xml.
152    pub content_types: Mutex<Option<XlsxTypes>>,
153    /// Lazily loaded xl/styles.xml.
154    pub styles: Mutex<Option<XlsxStyleSheet>>,
155    /// Lazily loaded xl/workbook.xml.
156    pub workbook: Mutex<Option<XlsxWorkbook>>,
157    /// Lazily loaded xl/calcChain.xml.
158    pub calc_chain: Mutex<Option<XlsxCalcChain>>,
159    /// Lazily loaded xl/volatileDependencies.xml.
160    pub volatile_deps: Mutex<Option<XlsxVolTypes>>,
161    /// Cached calculated cell values (formatted).
162    pub calc_cache: Mutex<HashMap<String, String>>,
163    /// Cached calculated cell values (raw).
164    pub calc_raw_cache: Mutex<HashMap<String, String>>,
165    /// Cached formula argument values for dependent cell reuse.
166    pub formula_arg_cache: Mutex<HashMap<String, FormulaArg>>,
167    /// Lazily loaded theme part.
168    pub theme: Mutex<Option<DecodeTheme>>,
169    /// Lazily loaded shared string table.
170    pub shared_strings: Mutex<Option<crate::xml::shared_strings::XlsxSst>>,
171    /// Index map for shared strings.
172    pub shared_strings_map: Mutex<HashMap<String, i32>>,
173    /// Parsed worksheet drawings (path → drawing).
174    pub drawings: DashMap<String, XlsxWsDr>,
175    /// Parsed comments parts (path → comments).
176    pub comments: DashMap<String, crate::xml::comments::XlsxComments>,
177    /// Parsed VML drawings (path → VML drawing).
178    pub vml_drawing: DashMap<String, crate::xml::vml::VmlDrawing>,
179    /// Optional codepage transcoder for non-UTF-8 XML package parts.
180    pub charset_transcoder: Mutex<Option<CharsetTranscoderFn>>,
181    /// Optional ZIP writer factory used when saving the workbook.
182    pub zip_writer_factory: Mutex<Option<ZipWriterFactory>>,
183}
184
185impl fmt::Debug for File {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        f.debug_struct("File")
188            .field("path", &self.path)
189            .field("options", &self.options)
190            .field("sheet_count", &self.sheet_count)
191            .field("pkg", &self.pkg)
192            .field("sheet", &self.sheet)
193            .field("relationships", &self.relationships)
194            .field("temp_files", &self.temp_files)
195            .field("sheet_map", &self.sheet_map)
196            .field("xml_attr", &self.xml_attr)
197            .field("checked", &self.checked)
198            .field("streams", &"...")
199            .field("zip64_entries", &self.zip64_entries)
200            .field("content_types", &self.content_types)
201            .field("styles", &self.styles)
202            .field("workbook", &self.workbook)
203            .field("calc_chain", &self.calc_chain)
204            .field("volatile_deps", &self.volatile_deps)
205            .field("calc_cache", &"...")
206            .field("calc_raw_cache", &"...")
207            .field("formula_arg_cache", &"...")
208            .field("theme", &self.theme)
209            .field("shared_strings", &self.shared_strings)
210            .field("shared_strings_map", &self.shared_strings_map)
211            .field("drawings", &self.drawings)
212            .field("comments", &self.comments)
213            .field("vml_drawing", &self.vml_drawing)
214            .field("charset_transcoder", &"...")
215            .field("zip_writer_factory", &"...")
216            .finish()
217    }
218}
219
220impl File {
221    /// Object builder. Use `new_with_options` or `open_file` instead.
222    fn new_file() -> Self {
223        Self {
224            path: Mutex::new(String::new()),
225            options: Mutex::new(Options::default()),
226            sheet_count: Mutex::new(0),
227            pkg: DashMap::new(),
228            sheet: DashMap::new(),
229            relationships: DashMap::new(),
230            temp_files: DashMap::new(),
231            sheet_map: Mutex::new(HashMap::new()),
232            xml_attr: DashMap::new(),
233            checked: DashMap::new(),
234            streams: RefCell::new(HashMap::new()),
235            zip64_entries: Mutex::new(Vec::new()),
236            content_types: Mutex::new(None),
237            styles: Mutex::new(None),
238            workbook: Mutex::new(None),
239            calc_chain: Mutex::new(None),
240            volatile_deps: Mutex::new(None),
241            calc_cache: Mutex::new(HashMap::new()),
242            calc_raw_cache: Mutex::new(HashMap::new()),
243            formula_arg_cache: Mutex::new(HashMap::new()),
244            theme: Mutex::new(None),
245            shared_strings: Mutex::new(None),
246            shared_strings_map: Mutex::new(HashMap::new()),
247            drawings: DashMap::new(),
248            comments: DashMap::new(),
249            vml_drawing: DashMap::new(),
250            charset_transcoder: Mutex::new(None),
251            zip_writer_factory: Mutex::new(None),
252        }
253    }
254
255    /// Create a new blank workbook.
256    pub fn new() -> Self {
257        Self::new_with_options(Options::default())
258    }
259
260    /// Create a new blank workbook with options.
261    pub fn new_with_options(opts: Options) -> Self {
262        Self::try_new_with_options(opts).expect("failed to create new workbook")
263    }
264
265    fn try_new_with_options(opts: Options) -> Result<Self> {
266        let f = Self::new_file();
267        *f.options.lock().unwrap() = normalize_options(opts);
268
269        f.store_template(DEFAULT_XML_PATH_RELS, TEMPLATE_RELS);
270        f.store_template(DEFAULT_XML_PATH_DOC_PROPS_APP, TEMPLATE_DOC_PROPS_APP);
271        f.store_template(DEFAULT_XML_PATH_DOC_PROPS_CORE, TEMPLATE_DOC_PROPS_CORE);
272        f.store_template(DEFAULT_XML_PATH_WORKBOOK_RELS, TEMPLATE_WORKBOOK_RELS);
273        f.store_template(DEFAULT_XML_PATH_THEME, TEMPLATE_THEME);
274        f.store_template(DEFAULT_XML_PATH_SHEET, TEMPLATE_SHEET);
275        f.store_template(DEFAULT_XML_PATH_STYLES, TEMPLATE_STYLES);
276        f.store_template(DEFAULT_XML_PATH_WORKBOOK, TEMPLATE_WORKBOOK);
277        f.store_template(DEFAULT_XML_PATH_CONTENT_TYPES, TEMPLATE_CONTENT_TYPES);
278
279        *f.sheet_count.lock().unwrap() = 1;
280
281        // Prime lazy readers.
282        let _ = f.calc_chain_reader()?;
283        let _ = f.content_types_reader()?;
284        let _ = f.styles_reader()?;
285        let _ = f.workbook_reader()?;
286
287        f.relationships.insert(
288            DEFAULT_XML_PATH_WORKBOOK_RELS.to_string(),
289            f.rels_reader(DEFAULT_XML_PATH_WORKBOOK_RELS)?
290                .unwrap_or_default(),
291        );
292
293        f.sheet_map
294            .lock()
295            .unwrap()
296            .insert("Sheet1".to_string(), DEFAULT_XML_PATH_SHEET.to_string());
297
298        let ws = f.work_sheet_reader("Sheet1")?;
299        f.sheet.insert(DEFAULT_XML_PATH_SHEET.to_string(), ws);
300
301        let _ = f.theme_reader();
302        Ok(f)
303    }
304
305    /// Open a workbook from the filesystem.
306    pub fn open_file(path: &str, opts: Options) -> Result<Self> {
307        let file = fs::File::open(Path::new(path))?;
308        let size = file.metadata()?.len();
309        let f = Self::open_reader(file, size, opts)?;
310        *f.path.lock().unwrap() = path.to_string();
311        Ok(f)
312    }
313
314    /// Open a workbook from a readable/seekable source.
315    pub fn open_reader<R: Read + Seek>(mut reader: R, _size: u64, opts: Options) -> Result<Self> {
316        let mut header = [0u8; 8];
317        reader.read_exact(&mut header)?;
318        reader.seek(io::SeekFrom::Start(0))?;
319
320        let has_password = !opts.password.is_empty();
321        if header == OLE_IDENTIFIER {
322            if !has_password {
323                return Err(Box::new(ErrWorkbookFileFormat));
324            }
325            let mut encrypted = Vec::new();
326            reader.read_to_end(&mut encrypted)?;
327            let decrypted = crypt::decrypt(&encrypted, &opts)?;
328            return Self::open_reader_internal(Cursor::new(decrypted), opts, true);
329        }
330        if has_password {
331            return Err(Box::new(ErrWorkbookPassword));
332        }
333        Self::open_reader_internal(reader, opts, false)
334    }
335
336    fn open_reader_internal<R: Read + Seek>(
337        reader: R,
338        opts: Options,
339        has_password: bool,
340    ) -> Result<Self> {
341        let f = Self::new_file();
342        *f.options.lock().unwrap() = normalize_options(opts);
343        f.check_open_reader_options()?;
344
345        let zip_result = (|| -> Result<()> {
346            let mut archive = ZipArchive::new(reader)?;
347            let (files, sheet_count) = f.read_zip_archive(&mut archive)?;
348            drop(archive);
349
350            for (k, v) in files {
351                f.pkg.insert(k, v);
352            }
353            *f.sheet_count.lock().unwrap() = sheet_count;
354            Ok(())
355        })();
356
357        match zip_result {
358            Ok(()) => {
359                let _ = f.calc_chain_reader()?;
360                {
361                    let map = f.get_sheet_name_to_path_map()?;
362                    *f.sheet_map.lock().unwrap() = map;
363                }
364                let _ = f.styles_reader()?;
365                let _ = f.theme_reader();
366                Ok(f)
367            }
368            Err(_) if has_password => Err(Box::new(ErrWorkbookPassword)),
369            Err(e) => Err(e),
370        }
371    }
372
373    /// Save to the original path.
374    pub fn save(&self) -> Result<()> {
375        let path = self.path.lock().unwrap().clone();
376        if path.is_empty() {
377            return Err(Box::new(ErrSave));
378        }
379        self.write_to_path(&path)
380    }
381
382    /// Save the workbook to the given path.
383    pub fn save_as(&mut self, path: &str) -> Result<()> {
384        {
385            let mut p = self.path.lock().unwrap();
386            *p = path.to_string();
387        }
388        self.write_to_path(path)
389    }
390
391    fn write_to_path(&self, path: &str) -> Result<()> {
392        if count_utf16_string(path) > MAX_FILE_PATH_LENGTH {
393            return Err(Box::new(ErrMaxFilePathLength));
394        }
395        let ext = Path::new(path)
396            .extension()
397            .and_then(|e| e.to_str())
398            .unwrap_or("")
399            .to_lowercase();
400        if !crate::templates::SUPPORTED_CONTENT_TYPES.contains_key(&format!(".{ext}")) {
401            return Err(Box::new(ErrWorkbookFileFormat));
402        }
403        let file = fs::OpenOptions::new()
404            .write(true)
405            .truncate(true)
406            .create(true)
407            .open(Path::new(path))?;
408        self.write_to(file)
409    }
410
411    /// Write the workbook to any writer.
412    pub fn write_to<W: Write>(&self, mut writer: W) -> Result<()> {
413        let path = self.path.lock().unwrap().clone();
414        if !path.is_empty() {
415            let ext = Path::new(&path)
416                .extension()
417                .and_then(|e| e.to_str())
418                .unwrap_or("")
419                .to_lowercase();
420            if let Some(ct) = crate::templates::SUPPORTED_CONTENT_TYPES.get(&format!(".{ext}")) {
421                self.set_content_type_part_project_extensions(ct)?;
422            } else {
423                return Err(Box::new(ErrWorkbookFileFormat));
424            }
425        }
426        let buf = self.write_to_buffer()?;
427        writer.write_all(&buf)?;
428        Ok(())
429    }
430
431    /// Clean up temporary files.
432    pub fn close(&mut self) -> Result<()> {
433        let mut first_err: Option<io::Error> = None;
434        for entry in self.temp_files.iter() {
435            if let Err(e) = fs::remove_file(entry.value()) {
436                if first_err.is_none() {
437                    first_err = Some(e);
438                }
439            }
440        }
441        self.temp_files.clear();
442
443        for state in self.streams.borrow().values() {
444            if let Err(e) = fs::remove_file(&state.tmp_path) {
445                if first_err.is_none() {
446                    first_err = Some(e);
447                }
448            }
449        }
450        self.streams.borrow_mut().clear();
451
452        match first_err {
453            Some(e) => Err(Box::new(e)),
454            None => Ok(()),
455        }
456    }
457
458    /// Set a user-defined charset transcoder for non-UTF-8 XML package parts.
459    ///
460    /// Mirrors Go `File.CharsetTranscoder`.
461    pub fn charset_transcoder<F>(&self, transcoder: F) -> &Self
462    where
463        F: Fn(&str, Box<dyn Read>) -> Result<Box<dyn Read>> + Send + Sync + 'static,
464    {
465        *self.charset_transcoder.lock().unwrap() = Some(Arc::new(transcoder));
466        self
467    }
468
469    /// Set a user-defined ZIP writer factory for saving the workbook.
470    ///
471    /// Mirrors Go `File.SetZipWriter`. The factory receives a writer that also
472    /// implements `Seek` because the underlying `zip::ZipWriter` needs to seek
473    /// back to write the central directory.
474    pub fn set_zip_writer<F>(&self, factory: F) -> &Self
475    where
476        F: for<'a> Fn(&'a mut dyn WriteSeek) -> Box<dyn ZipWriter + 'a> + Send + Sync + 'static,
477    {
478        *self.zip_writer_factory.lock().unwrap() = Some(Arc::new(factory));
479        self
480    }
481
482    /// Apply the configured charset transcoder when the XML declaration names a
483    /// non-UTF-8 encoding.
484    pub(crate) fn apply_charset_transcoder(&self, data: &[u8]) -> Result<Vec<u8>> {
485        let encoding = detect_xml_encoding(data).unwrap_or("UTF-8");
486        if encoding.eq_ignore_ascii_case("UTF-8") || encoding.eq_ignore_ascii_case("UTF8") {
487            return Ok(data.to_vec());
488        }
489        if let Some(transcoder) = self.charset_transcoder.lock().unwrap().clone() {
490            let input: Box<dyn Read> = Box::new(Cursor::new(data.to_vec()));
491            let mut converted = transcoder(encoding, input)?;
492            let mut out = Vec::new();
493            converted.read_to_end(&mut out)?;
494            return Ok(out);
495        }
496        Ok(data.to_vec())
497    }
498
499    // ------------------------------------------------------------------
500    // Internal readers / helpers
501    // ------------------------------------------------------------------
502
503    fn store_template(&self, path: &str, template: &str) {
504        let bytes = format!("{XML_HEADER}{template}").into_bytes();
505        if let Some(attrs) = extract_root_namespace_attributes(&bytes) {
506            self.xml_attr.insert(path.to_string(), attrs);
507        }
508        self.pkg.insert(path.to_string(), bytes);
509    }
510
511    /// Read XML content as bytes from the in-memory package.
512    pub fn read_xml(&self, name: &str) -> Vec<u8> {
513        self.pkg
514            .get(name)
515            .map(|e| e.value().clone())
516            .unwrap_or_default()
517    }
518
519    /// Read file content as bytes, falling back to temporary files.
520    pub fn read_bytes(&self, name: &str) -> Vec<u8> {
521        let content = self.read_xml(name);
522        if !content.is_empty() {
523            return content;
524        }
525        match self.read_temp(name) {
526            Ok(mut file) => {
527                let mut content = Vec::new();
528                if file.read_to_end(&mut content).is_ok() {
529                    self.pkg.insert(name.to_string(), content.clone());
530                }
531                content
532            }
533            Err(_) => Vec::new(),
534        }
535    }
536
537    fn read_temp(&self, name: &str) -> io::Result<fs::File> {
538        let path = self
539            .temp_files
540            .get(name)
541            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no temp file"))?
542            .clone();
543        fs::File::open(path)
544    }
545
546    /// Update or add a package part, prefixing the standard XML header.
547    pub fn save_file_list(&self, name: &str, content: &[u8]) {
548        let mut out = Vec::with_capacity(XML_HEADER.len() + content.len());
549        out.extend_from_slice(XML_HEADER.as_bytes());
550        out.extend_from_slice(content);
551        self.pkg.insert(name.to_string(), out);
552    }
553
554    /// Extract a workbook from a `ZipArchive`.
555    fn read_zip_archive<R: Read + Seek>(
556        &self,
557        archive: &mut ZipArchive<R>,
558    ) -> Result<(HashMap<String, Vec<u8>>, i32)> {
559        let mut files = HashMap::new();
560        let mut worksheets = 0;
561        let mut unzip_size: i64 = 0;
562        let opts = self.options.lock().unwrap();
563
564        for i in 0..archive.len() {
565            let mut zip_file = archive.by_index(i)?;
566            let file_size = zip_file.size() as i64;
567            unzip_size += file_size;
568            if unzip_size > opts.unzip_size_limit {
569                return Err(Box::new(io::Error::new(
570                    io::ErrorKind::InvalidData,
571                    crate::errors::new_unzip_size_limit_error(opts.unzip_size_limit),
572                )));
573            }
574            let mut file_name = zip_file.name().replace('\\', "/");
575            let lower = file_name.to_lowercase();
576            if lower == "[content_types].xml" {
577                file_name = DEFAULT_XML_PATH_CONTENT_TYPES.to_string();
578            } else if lower == "xl/sharedstrings.xml" {
579                file_name = DEFAULT_XML_PATH_SHARED_STRINGS.to_string();
580            }
581
582            if lower == DEFAULT_XML_PATH_SHARED_STRINGS.to_lowercase()
583                && file_size > opts.unzip_xml_size_limit
584            {
585                if let Ok(tmp) = self.unzip_to_temp(&mut zip_file) {
586                    self.temp_files.insert(file_name, tmp);
587                }
588                continue;
589            }
590            if lower.starts_with("xl/worksheets/sheet") {
591                worksheets += 1;
592                if file_size > opts.unzip_xml_size_limit && !zip_file.is_dir() {
593                    if let Ok(tmp) = self.unzip_to_temp(&mut zip_file) {
594                        self.temp_files.insert(file_name, tmp);
595                    }
596                    continue;
597                }
598            }
599
600            let mut data = Vec::with_capacity(file_size.max(0) as usize);
601            zip_file.read_to_end(&mut data)?;
602            files.insert(file_name, data);
603        }
604        Ok((files, worksheets))
605    }
606
607    fn unzip_to_temp(&self, zip_file: &mut zip::read::ZipFile<'_>) -> io::Result<String> {
608        let tmp_dir = {
609            let opts = self.options.lock().unwrap();
610            if opts.tmp_dir.is_empty() {
611                std::env::temp_dir()
612            } else {
613                PathBuf::from(&opts.tmp_dir)
614            }
615        };
616        let now = SystemTime::now()
617            .duration_since(UNIX_EPOCH)
618            .unwrap_or_default();
619        let name = format!("excelize-{}-{}.xml", now.as_secs(), now.subsec_nanos());
620        let path = tmp_dir.join(name);
621        let mut file = fs::File::create(&path)?;
622        io::copy(zip_file, &mut file)?;
623        file.sync_all()?;
624        Ok(path.to_string_lossy().to_string())
625    }
626
627    /// Lazy reader for `[Content_Types].xml`.
628    pub fn content_types_reader(&self) -> Result<XlsxTypes> {
629        if self.content_types.lock().unwrap().is_none() {
630            let mut ct = XlsxTypes::default();
631            let data =
632                self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_CONTENT_TYPES))?;
633            let data = namespace_strict_to_transitional(&data);
634            if !data.is_empty() {
635                ct = xml_from_reader(data.as_slice())?;
636            }
637            *self.content_types.lock().unwrap() = Some(ct);
638        }
639        Ok(self.content_types.lock().unwrap().clone().unwrap())
640    }
641
642    /// Lazy reader for `xl/styles.xml`.
643    pub fn styles_reader(&self) -> Result<XlsxStyleSheet> {
644        if self.styles.lock().unwrap().is_none() {
645            let data = self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_STYLES))?;
646            let data = namespace_strict_to_transitional(&data);
647            let st = if data.is_empty() {
648                XlsxStyleSheet::default()
649            } else {
650                if let Some(attrs) = extract_root_namespace_attributes(&data) {
651                    self.xml_attr
652                        .insert(DEFAULT_XML_PATH_STYLES.to_string(), attrs);
653                }
654                xml_from_reader(data.as_slice()).unwrap_or_default()
655            };
656            *self.styles.lock().unwrap() = Some(st);
657        }
658        Ok(self.styles.lock().unwrap().clone().unwrap())
659    }
660
661    /// Lazy reader for `xl/workbook.xml`.
662    pub fn workbook_reader(&self) -> Result<XlsxWorkbook> {
663        if self.workbook.lock().unwrap().is_none() {
664            let wb_path = self.get_workbook_path();
665            let data = self.apply_charset_transcoder(&self.read_xml(&wb_path))?;
666            let data = namespace_strict_to_transitional(&data);
667            // Strip extension-list blocks before deserializing: quick-xml/serde
668            // cannot capture arbitrary nested XML in `<ext><$value/></ext>`.
669            let data = strip_xml_element(&data, "extLst");
670            let wb: XlsxWorkbook = if data.is_empty() {
671                XlsxWorkbook::default()
672            } else {
673                if let Some(attrs) = extract_root_namespace_attributes(&data) {
674                    self.xml_attr.insert(wb_path.clone(), attrs);
675                }
676                xml_from_reader(data.as_slice()).unwrap_or_default()
677            };
678            *self.workbook.lock().unwrap() = Some(wb);
679        }
680        Ok(self.workbook.lock().unwrap().clone().unwrap())
681    }
682
683    // Calculation chain helpers moved to `calc_chain.rs`.
684
685    /// Lazy reader for the theme part.
686    pub fn theme_reader(&self) -> Result<Option<DecodeTheme>> {
687        if self.theme.lock().unwrap().is_none() {
688            if self.pkg.contains_key(DEFAULT_XML_PATH_THEME)
689                || self.temp_files.contains_key(DEFAULT_XML_PATH_THEME)
690            {
691                let data = self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_THEME))?;
692                let data = namespace_strict_to_transitional(&data);
693                if !data.is_empty() {
694                    if let Some(attrs) = extract_root_namespace_attributes(&data) {
695                        self.xml_attr
696                            .insert(DEFAULT_XML_PATH_THEME.to_string(), attrs);
697                    }
698                    if let Ok(theme) = xml_from_reader::<_, DecodeTheme>(data.as_slice()) {
699                        *self.theme.lock().unwrap() = Some(theme);
700                    }
701                }
702            }
703        }
704        Ok(self.theme.lock().unwrap().clone())
705    }
706
707    /// Lazy reader for the shared string table.
708    pub fn shared_strings_reader(&self) -> Result<crate::xml::shared_strings::XlsxSst> {
709        if self.shared_strings.lock().unwrap().is_none() {
710            let mut sst = crate::xml::shared_strings::XlsxSst::default();
711            if self.pkg.contains_key(DEFAULT_XML_PATH_SHARED_STRINGS)
712                || self
713                    .temp_files
714                    .contains_key(DEFAULT_XML_PATH_SHARED_STRINGS)
715            {
716                let data = self
717                    .apply_charset_transcoder(&self.read_bytes(DEFAULT_XML_PATH_SHARED_STRINGS))?;
718                let data = namespace_strict_to_transitional(&data);
719                if !data.is_empty() {
720                    sst = xml_from_reader(data.as_slice()).unwrap_or_default();
721                }
722            }
723            let mut map = self.shared_strings_map.lock().unwrap();
724            map.clear();
725            for (i, si) in sst.si.iter().enumerate() {
726                let text = if let Some(t) = &si.t {
727                    t.val.clone()
728                } else {
729                    si.r.iter()
730                        .filter_map(|r| r.t.as_ref().map(|t| t.val.clone()))
731                        .collect()
732                };
733                map.insert(text, i as i32);
734            }
735            *self.shared_strings.lock().unwrap() = Some(sst);
736        }
737        Ok(self.shared_strings.lock().unwrap().clone().unwrap())
738    }
739
740    /// Lazy reader for relationship parts.
741    pub fn rels_reader(&self, path: &str) -> Result<Option<XlsxRelationships>> {
742        if let Some(rels) = self.relationships.get(path) {
743            return Ok(Some(rels.clone()));
744        }
745        if self.pkg.contains_key(path) || self.temp_files.contains_key(path) {
746            let data = self.apply_charset_transcoder(&self.read_xml(path))?;
747            let data = namespace_strict_to_transitional(&data);
748            let rels = if data.is_empty() {
749                XlsxRelationships::default()
750            } else {
751                if let Some(attrs) = extract_root_namespace_attributes(&data) {
752                    self.xml_attr.insert(path.to_string(), attrs);
753                }
754                xml_from_reader(data.as_slice()).unwrap_or_default()
755            };
756            self.relationships.insert(path.to_string(), rels.clone());
757            Ok(Some(rels))
758        } else {
759            Ok(None)
760        }
761    }
762
763    /// Lazy reader for `xl/metadata.xml`.
764    pub fn metadata_reader(&self) -> Result<crate::xml::metadata::XlsxMetadata> {
765        let data = namespace_strict_to_transitional(
766            &self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_METADATA))?,
767        );
768        if data.is_empty() {
769            return Ok(crate::xml::metadata::XlsxMetadata::default());
770        }
771        Ok(xml_from_reader(data.as_slice())?)
772    }
773
774    /// Lazy reader for `xl/richData/rdrichvalue.xml`.
775    pub fn rich_value_reader(&self) -> Result<crate::xml::metadata::XlsxRichValueData> {
776        let data = namespace_strict_to_transitional(
777            &self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_RD_RICH_VALUE))?,
778        );
779        if data.is_empty() {
780            return Ok(crate::xml::metadata::XlsxRichValueData::default());
781        }
782        Ok(xml_from_reader(data.as_slice())?)
783    }
784
785    /// Lazy reader for `xl/richData/richValueRel.xml`.
786    pub fn rich_value_rel_reader(&self) -> Result<crate::xml::metadata::XlsxRichValueRels> {
787        let data = namespace_strict_to_transitional(
788            &self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_RD_RICH_VALUE_REL))?,
789        );
790        if data.is_empty() {
791            return Ok(crate::xml::metadata::XlsxRichValueRels::default());
792        }
793        Ok(xml_from_reader(data.as_slice())?)
794    }
795
796    /// Lazy reader for `xl/richData/rdrichvaluestructure.xml`.
797    pub fn rich_value_structures_reader(
798        &self,
799    ) -> Result<crate::xml::metadata::XlsxRichValueStructures> {
800        let data =
801            namespace_strict_to_transitional(&self.apply_charset_transcoder(
802                &self.read_xml(DEFAULT_XML_PATH_RD_RICH_VALUE_STRUCTURE),
803            )?);
804        if data.is_empty() {
805            return Ok(crate::xml::metadata::XlsxRichValueStructures::default());
806        }
807        Ok(xml_from_reader(data.as_slice())?)
808    }
809
810    /// Lazy reader for `xl/richData/rdRichValueWebImage.xml`.
811    pub fn rich_value_web_image_reader(
812        &self,
813    ) -> Result<crate::xml::metadata::XlsxWebImagesSupportingRichData> {
814        let data =
815            namespace_strict_to_transitional(&self.apply_charset_transcoder(
816                &self.read_xml(DEFAULT_XML_PATH_RD_RICH_VALUE_WEB_IMAGE),
817            )?);
818        if data.is_empty() {
819            return Ok(crate::xml::metadata::XlsxWebImagesSupportingRichData::default());
820        }
821        Ok(xml_from_reader(data.as_slice())?)
822    }
823
824    /// Get a relationship from `xl/richData/_rels/richValueRel.xml.rels` by ID.
825    pub(crate) fn get_rich_data_rich_value_rel_relationship(
826        &self,
827        r_id: &str,
828    ) -> Option<XlsxRelationship> {
829        self.rels_reader(DEFAULT_XML_PATH_RD_RICH_VALUE_REL_RELS)
830            .ok()
831            .flatten()
832            .and_then(|rels| rels.relationships.into_iter().find(|rel| rel.id == r_id))
833    }
834
835    /// Get a relationship from `xl/richData/_rels/rdRichValueWebImage.xml.rels` by ID.
836    pub(crate) fn get_rich_value_web_image_relationship(
837        &self,
838        r_id: &str,
839    ) -> Option<XlsxRelationship> {
840        self.rels_reader(DEFAULT_XML_PATH_RD_RICH_VALUE_WEB_IMAGE_RELS)
841            .ok()
842            .flatten()
843            .and_then(|rels| rels.relationships.into_iter().find(|rel| rel.id == r_id))
844    }
845
846    /// Set an existing relationship entry, or add a new one if `r_id` is empty.
847    pub fn set_rels(
848        &self,
849        r_id: &str,
850        rel_path: &str,
851        rel_type: &str,
852        target: &str,
853        target_mode: &str,
854    ) -> i32 {
855        if r_id.is_empty() {
856            return self.add_rels(rel_path, rel_type, target, target_mode);
857        }
858        let mut rels = self
859            .rels_reader(rel_path)
860            .unwrap_or_default()
861            .unwrap_or_default();
862        let mut out_id = 0;
863        for rel in &mut rels.relationships {
864            if rel.id == r_id {
865                rel.r#type = rel_type.to_string();
866                rel.target = target.to_string();
867                rel.target_mode = Some(target_mode.to_string());
868                out_id = r_id.trim_start_matches("rId").parse().unwrap_or(0);
869                break;
870            }
871        }
872        self.relationships.insert(rel_path.to_string(), rels);
873        out_id
874    }
875
876    /// Add a relationship to a relationship part.
877    pub fn add_rels(&self, rel_path: &str, rel_type: &str, target: &str, target_mode: &str) -> i32 {
878        let uniq_part: HashMap<String, String> = [
879            (
880                SOURCE_RELATIONSHIP_CUSTOM_PROPERTIES.to_string(),
881                "/docProps/custom.xml".to_string(),
882            ),
883            (
884                SOURCE_RELATIONSHIP_SHARED_STRINGS.to_string(),
885                "/xl/sharedStrings.xml".to_string(),
886            ),
887        ]
888        .into_iter()
889        .collect();
890
891        let mut rels = self
892            .rels_reader(rel_path)
893            .unwrap_or_default()
894            .unwrap_or_default();
895        let mut r_id = 0;
896        for rel in &rels.relationships {
897            let id: i32 = rel.id.trim_start_matches("rId").parse().unwrap_or(0);
898            if id > r_id {
899                r_id = id;
900            }
901            if rel.r#type == rel_type {
902                if let Some(part_name) = uniq_part.get(&rel.r#type) {
903                    // This branch is handled by the caller updating the target.
904                    let _ = part_name;
905                }
906            }
907        }
908        r_id += 1;
909        let new_id = format!("rId{r_id}");
910        rels.relationships.push(XlsxRelationship {
911            id: new_id,
912            r#type: rel_type.to_string(),
913            target: target.to_string(),
914            target_mode: Some(target_mode.to_string()),
915        });
916        self.relationships.insert(rel_path.to_string(), rels);
917        r_id
918    }
919
920    /// Get the workbook XML path from the root relationships.
921    pub fn get_workbook_path(&self) -> String {
922        if let Ok(Some(rels)) = self.rels_reader(DEFAULT_XML_PATH_RELS) {
923            for rel in &rels.relationships {
924                if rel.r#type == crate::constants::SOURCE_RELATIONSHIP_OFFICE_DOCUMENT {
925                    return rel.target.trim_start_matches('/').to_string();
926                }
927            }
928        }
929        DEFAULT_XML_PATH_WORKBOOK.to_string()
930    }
931
932    /// Get the workbook relationships path.
933    pub fn get_workbook_rels_path(&self) -> String {
934        let wb = self.get_workbook_path();
935        let wb_dir = Path::new(&wb).parent().unwrap_or(Path::new("."));
936        let wb_base = Path::new(&wb).file_name().unwrap_or_default();
937        if wb_dir == Path::new(".") {
938            return format!("_rels/{}", wb_base.to_string_lossy()) + ".rels";
939        }
940        format!(
941            "{}/_rels/{}.rels",
942            wb_dir.to_string_lossy(),
943            wb_base.to_string_lossy()
944        )
945        .trim_start_matches('/')
946        .to_string()
947    }
948
949    /// Convert a relative relationship target to an absolute package path.
950    pub fn get_worksheet_path(&self, rel_target: &str) -> String {
951        let wb_path = self.get_workbook_path();
952        let wb_dir = Path::new(&wb_path).parent().unwrap_or(Path::new("."));
953        let mut combined = wb_dir.to_path_buf();
954        for c in rel_target.split('/') {
955            if c == ".." {
956                combined.pop();
957            } else if !c.is_empty() && c != "." {
958                combined.push(c);
959            }
960        }
961        let mut s = combined.to_string_lossy().replace('\\', "/");
962        s = s.trim_start_matches('/').to_string();
963        if rel_target.starts_with('/') {
964            s = Path::new(rel_target)
965                .to_string_lossy()
966                .replace('\\', "/")
967                .trim_start_matches('/')
968                .to_string();
969        }
970        s
971    }
972
973    /// Get the XML path for a worksheet by name.
974    pub fn get_sheet_xml_path(&self, sheet: &str) -> Option<String> {
975        let map = self.sheet_map.lock().unwrap();
976        for (name, path) in map.iter() {
977            if name.eq_ignore_ascii_case(sheet) {
978                return Some(path.clone());
979            }
980        }
981        None
982    }
983
984    /// Deserialize and return a worksheet by name.
985    pub fn work_sheet_reader(&self, sheet: &str) -> Result<XlsxWorksheet> {
986        crate::excelize::check_sheet_name(sheet)?;
987        let name = self.get_sheet_xml_path(sheet).ok_or_else(|| {
988            Box::new(crate::errors::ErrSheetNotExist {
989                sheet_name: sheet.to_string(),
990            }) as Box<dyn std::error::Error + Send + Sync>
991        })?;
992        if let Some(ws) = self.sheet.get(&name) {
993            return Ok(ws.clone());
994        }
995        let data = self.apply_charset_transcoder(&self.read_bytes(&name))?;
996        let data = namespace_strict_to_transitional(&data);
997        let mut data = data;
998        let ext_lst = extract_ext_lst(&data);
999        if ext_lst.is_some() {
1000            remove_ext_lst(&mut data);
1001        }
1002        let mut ws: XlsxWorksheet = xml_from_reader(data.as_slice())?;
1003        if let Some(xml) = ext_lst {
1004            ws.ext_lst = Some(crate::xml::common::parse_ext_lst_content(&xml)?);
1005        }
1006        if !self.checked.contains_key(&name) {
1007            // Worksheet validation is intentionally minimal in this phase.
1008            self.checked.insert(name.clone(), true);
1009        }
1010        self.sheet.insert(name.clone(), ws.clone());
1011        Ok(ws)
1012    }
1013
1014    // ------------------------------------------------------------------
1015    // Validation helpers
1016    // ------------------------------------------------------------------
1017
1018    pub(crate) fn check_open_reader_options(&self) -> Result<()> {
1019        let mut opts = self.options.lock().unwrap();
1020        if opts.unzip_size_limit == 0 {
1021            opts.unzip_size_limit = opts.unzip_xml_size_limit.max(UNZIP_SIZE_LIMIT);
1022        }
1023        if opts.unzip_xml_size_limit == 0 {
1024            opts.unzip_xml_size_limit = opts.unzip_size_limit.min(STREAM_CHUNK_SIZE);
1025        }
1026        if opts.unzip_xml_size_limit > opts.unzip_size_limit {
1027            return Err(Box::new(ErrOptionsUnzipSizeLimit));
1028        }
1029        let patterns = [
1030            opts.short_date_pattern.clone(),
1031            opts.long_date_pattern.clone(),
1032            opts.long_time_pattern.clone(),
1033        ];
1034        drop(opts);
1035        self.check_date_time_pattern(&patterns)
1036    }
1037
1038    fn check_date_time_pattern(&self, patterns: &[String]) -> Result<()> {
1039        for pattern in patterns {
1040            if !pattern.is_empty() && !numfmt::is_date_time_pattern(pattern) {
1041                return Err(Box::new(crate::errors::ErrUnsupportedNumberFormat));
1042            }
1043        }
1044        Ok(())
1045    }
1046
1047    // ------------------------------------------------------------------
1048    // Writers
1049    // ------------------------------------------------------------------
1050
1051    /// Serialize the workbook to an in-memory byte buffer.
1052    pub fn write_to_buffer(&self) -> Result<Vec<u8>> {
1053        let mut buf = Cursor::new(Vec::new());
1054        let factory = self.zip_writer_factory.lock().unwrap().clone();
1055        if let Some(factory) = factory {
1056            let mut zw = factory(&mut buf);
1057            self.write_to_zip(&mut *zw)?;
1058            zw.finish()?;
1059        } else {
1060            let mut zw = Box::new(zip::ZipWriter::new(&mut buf));
1061            self.write_to_zip(&mut *zw)?;
1062            zw.finish()?;
1063        }
1064        let mut inner = buf.into_inner();
1065        self.write_zip64_lfh(&mut inner)?;
1066        let opts = self.options.lock().unwrap().clone();
1067        if !opts.password.is_empty() {
1068            inner = crypt::encrypt(&inner, &opts)?;
1069        }
1070        Ok(inner)
1071    }
1072
1073    fn write_to_zip(&self, zw: &mut dyn ZipWriter) -> Result<()> {
1074        self.calc_chain_writer();
1075        self.comments_writer();
1076        self.shared_strings_registrar();
1077        self.content_types_writer();
1078        self.drawings_writer();
1079        self.volatile_deps_writer();
1080        self.vml_drawing_writer();
1081        self.workbook_writer();
1082        self.work_sheet_writer();
1083        self.rels_writer();
1084        let _ = self.shared_strings_loader();
1085        self.shared_strings_writer();
1086        self.style_sheet_writer();
1087        self.theme_writer();
1088
1089        // Write package parts in reverse alphabetical order (matches Go behavior).
1090        let mut files: Vec<String> = self.pkg.iter().map(|e| e.key().clone()).collect();
1091        files.sort_unstable_by(|a, b| b.cmp(a));
1092        for path in files {
1093            let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
1094            zw.start_file(&path, opts)?;
1095            let content = self.read_xml(&path);
1096            zw.write_all(&content)?;
1097            if content.len() as u64 > u32::MAX as u64 {
1098                self.zip64_entries.lock().unwrap().push(path.clone());
1099            }
1100        }
1101
1102        // Write any parts that only exist as temporary files.
1103        let mut temp_files: Vec<String> = self
1104            .temp_files
1105            .iter()
1106            .filter(|e| !self.pkg.contains_key(e.key()))
1107            .map(|e| e.key().clone())
1108            .collect();
1109        temp_files.sort_unstable_by(|a, b| b.cmp(a));
1110        for path in temp_files {
1111            let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
1112            zw.start_file(&path, opts)?;
1113            let content = self.read_bytes(&path);
1114            zw.write_all(&content)?;
1115            if content.len() as u64 > u32::MAX as u64 {
1116                self.zip64_entries.lock().unwrap().push(path.clone());
1117            }
1118        }
1119
1120        // Write worksheet parts produced by streaming writers directly from
1121        // their temporary files without loading them into memory.
1122        let mut streams: Vec<(String, PathBuf)> = self
1123            .streams
1124            .borrow_mut()
1125            .drain()
1126            .map(|(path, state)| (path, state.tmp_path))
1127            .collect();
1128        streams.sort_unstable_by(|a, b| b.0.cmp(&a.0));
1129        for (path, tmp_path) in streams {
1130            let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
1131            zw.start_file(&path, opts)?;
1132            let mut file = fs::File::open(&tmp_path)?;
1133            let size = file.metadata()?.len();
1134            let mut buf = [0u8; 8192];
1135            loop {
1136                let n = file.read(&mut buf)?;
1137                if n == 0 {
1138                    break;
1139                }
1140                zw.write_all(&buf[..n])?;
1141            }
1142            if size > u32::MAX as u64 {
1143                self.zip64_entries.lock().unwrap().push(path.clone());
1144            }
1145            let _ = fs::remove_file(&tmp_path);
1146        }
1147        Ok(())
1148    }
1149
1150    fn write_zip64_lfh(&self, buf: &mut [u8]) -> Result<()> {
1151        let entries = self.zip64_entries.lock().unwrap().clone();
1152        if entries.is_empty() {
1153            return Ok(());
1154        }
1155        let mut offset = 0usize;
1156        while offset < buf.len() {
1157            let window = &buf[offset..];
1158            let Some(idx) = find_subsequence(window, b"\x50\x4b\x03\x04") else {
1159                break;
1160            };
1161            let idx = idx + offset;
1162            if idx + 30 > buf.len() {
1163                break;
1164            }
1165            let filename_len = u16::from_le_bytes([buf[idx + 26], buf[idx + 27]]) as usize;
1166            if idx + 30 + filename_len > buf.len() {
1167                break;
1168            }
1169            let filename =
1170                std::str::from_utf8(&buf[idx + 30..idx + 30 + filename_len]).unwrap_or("");
1171            if in_str_slice(&entries, filename, true) != -1 {
1172                buf[idx + 4..idx + 6].copy_from_slice(&45u16.to_le_bytes());
1173            }
1174            offset = idx + 1;
1175        }
1176        Ok(())
1177    }
1178
1179    // Calculation chain writer moved to `calc_chain.rs`.
1180
1181    fn comments_writer(&self) {
1182        for entry in self.comments.iter() {
1183            let path = entry.key().clone();
1184            let comments = entry.value().clone();
1185            if let Ok(output) = quick_xml::se::to_string(&comments).map(|s| s.into_bytes()) {
1186                self.save_file_list(&path, &output);
1187            }
1188        }
1189    }
1190
1191    fn content_types_writer(&self) {
1192        if let Some(ct) = self.content_types.lock().unwrap().clone() {
1193            if let Ok(mut output) = xml_to_string(&ct).map(|s| s.into_bytes()) {
1194                self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_CONTENT_TYPES, &mut output);
1195                self.save_file_list(DEFAULT_XML_PATH_CONTENT_TYPES, &output);
1196            }
1197        }
1198    }
1199
1200    fn drawings_writer(&self) {
1201        for entry in self.drawings.iter() {
1202            let path = entry.key().clone();
1203            let drawing = entry.value().clone();
1204            if let Ok(mut output) = xml_to_string(&drawing).map(|s| s.into_bytes()) {
1205                self.replace_namespace_bytes_if_needed(&path, &mut output);
1206                self.save_file_list(&path, &output);
1207            }
1208        }
1209    }
1210
1211    // Volatile dependencies writer moved to `calc_chain.rs`.
1212
1213    fn vml_drawing_writer(&self) {
1214        for entry in self.vml_drawing.iter() {
1215            let path = entry.key().clone();
1216            let drawing = entry.value().clone();
1217            let output = drawing.to_xml();
1218            self.pkg.insert(path, output);
1219        }
1220    }
1221
1222    fn workbook_writer(&self) {
1223        if let Some(mut wb) = self.workbook.lock().unwrap().clone() {
1224            // If the workbook was read with the strict-namespace decoder, move
1225            // the decoded alternate content back to the serialized element so
1226            // the compatibility namespace is preserved on save.
1227            if let Some(decode) = wb.decode_alternate_content.take() {
1228                wb.alternate_content = Some(crate::xml::workbook::XlsxAlternateContent {
1229                    xmlns_mc: Some(
1230                        "http://schemas.openxmlformats.org/markup-compatibility/2006".to_string(),
1231                    ),
1232                    content: decode.content,
1233                });
1234            }
1235            if let Ok(mut output) = xml_to_string(&wb).map(|s| s.into_bytes()) {
1236                let path = self.get_workbook_path();
1237                self.replace_namespace_bytes_if_needed(&path, &mut output);
1238                self.save_file_list(&path, &output);
1239            }
1240        }
1241    }
1242
1243    fn work_sheet_writer(&self) {
1244        for entry in self.sheet.iter() {
1245            let path = entry.key().clone();
1246            let sheet = entry.value().clone();
1247            if let Ok(mut output) = xml_to_string(&sheet).map(|s| s.into_bytes()) {
1248                self.replace_namespace_bytes_if_needed(&path, &mut output);
1249                strip_empty_attributes(&mut output);
1250                if let Some(ext_lst) = &sheet.ext_lst {
1251                    let ext_xml = crate::xml::common::serialize_ext_lst(ext_lst);
1252                    if !ext_xml.is_empty() {
1253                        inject_ext_lst(&mut output, &ext_xml);
1254                    }
1255                }
1256                self.save_file_list(&path, &output);
1257            }
1258        }
1259    }
1260
1261    fn rels_writer(&self) {
1262        for entry in self.relationships.iter() {
1263            let path = entry.key().clone();
1264            let rels = entry.value().clone();
1265            if let Ok(mut output) = xml_to_string(&rels).map(|s| s.into_bytes()) {
1266                self.replace_namespace_bytes_if_needed(&path, &mut output);
1267                self.save_file_list(&path, &output);
1268            }
1269        }
1270    }
1271
1272    fn shared_strings_loader(&self) -> Result<()> {
1273        if let Some(entry) = self.temp_files.get(DEFAULT_XML_PATH_SHARED_STRINGS) {
1274            let temp_path = entry.value().clone();
1275            drop(entry);
1276            let data = self.read_bytes(DEFAULT_XML_PATH_SHARED_STRINGS);
1277            if !data.is_empty() {
1278                self.pkg
1279                    .insert(DEFAULT_XML_PATH_SHARED_STRINGS.to_string(), data);
1280            }
1281            self.temp_files.remove(DEFAULT_XML_PATH_SHARED_STRINGS);
1282            let _ = fs::remove_file(temp_path);
1283            *self.shared_strings.lock().unwrap() = None;
1284            self.shared_strings_map.lock().unwrap().clear();
1285        }
1286        Ok(())
1287    }
1288
1289    /// Ensure `[Content_Types].xml` and the workbook relationships reference
1290    /// the shared string table when it will be written to the package.
1291    fn shared_strings_registrar(&self) {
1292        let has_strings = self.shared_strings.lock().unwrap().is_some()
1293            || self.temp_files.contains_key(DEFAULT_XML_PATH_SHARED_STRINGS);
1294        if !has_strings {
1295            return;
1296        }
1297        if let Ok(mut ct) = self.content_types_reader() {
1298            let part_name = format!("/{DEFAULT_XML_PATH_SHARED_STRINGS}");
1299            let exists = ct.entries.iter().any(|e| {
1300                matches!(e, crate::xml::content_types::XlsxContentTypeEntry::Override(o) if o.part_name == part_name)
1301            });
1302            if !exists {
1303                ct.entries
1304                    .push(crate::xml::content_types::XlsxContentTypeEntry::Override(
1305                        crate::xml::content_types::XlsxOverride {
1306                            part_name,
1307                            content_type:
1308                                crate::constants::CONTENT_TYPE_SPREADSHEET_ML_SHARED_STRINGS
1309                                    .to_string(),
1310                        },
1311                    ));
1312                *self.content_types.lock().unwrap() = Some(ct);
1313            }
1314        }
1315        let rels_path = self.get_workbook_rels_path();
1316        let mut rels = self.rels_reader(&rels_path).unwrap_or_default().unwrap_or_default();
1317        crate::sheet::ensure_shared_strings_rel(&mut rels);
1318        self.relationships.insert(rels_path, rels);
1319    }
1320
1321    fn shared_strings_writer(&self) {
1322        if let Some(sst) = self.shared_strings.lock().unwrap().clone() {
1323            if let Ok(output) = xml_to_string(&sst).map(|s| s.into_bytes()) {
1324                self.save_file_list(DEFAULT_XML_PATH_SHARED_STRINGS, &output);
1325            }
1326        }
1327    }
1328
1329    fn style_sheet_writer(&self) {
1330        if let Some(st) = self.styles.lock().unwrap().clone() {
1331            if let Ok(mut output) = xml_to_string(&st).map(|s| s.into_bytes()) {
1332                self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_STYLES, &mut output);
1333                strip_empty_attributes(&mut output);
1334                self.save_file_list(DEFAULT_XML_PATH_STYLES, &output);
1335            }
1336        }
1337    }
1338
1339    fn theme_writer(&self) {
1340        if let Some(theme) = self.theme.lock().unwrap().clone() {
1341            let serialized = decode_theme_to_xlsx_theme(&theme);
1342            if let Ok(mut output) = xml_to_string(&serialized).map(|s| s.into_bytes()) {
1343                self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_THEME, &mut output);
1344                self.save_file_list(DEFAULT_XML_PATH_THEME, &output);
1345            }
1346        }
1347    }
1348
1349    // ------------------------------------------------------------------
1350    // Namespace helpers
1351    // ------------------------------------------------------------------
1352
1353    pub(crate) fn replace_namespace_bytes_if_needed(&self, path: &str, content: &mut Vec<u8>) {
1354        if let Some(attrs) = self.xml_attr.get(path) {
1355            let _ = replace_root_namespace_attributes(content, attrs.value());
1356        }
1357    }
1358
1359    /// Register a namespace attribute for the given XML part so that it is
1360    /// written back when the part is serialized.
1361    ///
1362    /// `ns` is the namespace URI; the prefix is looked up from the project's
1363    /// namespace dictionary. For known extension namespaces the `mc:Ignorable`
1364    /// attribute is also updated so Excel compatibility markup stays valid.
1365    pub fn add_name_spaces(&self, path: &str, ns: &str) {
1366        let prefix = match ns {
1367            crate::constants::SOURCE_RELATIONSHIP => "r",
1368            crate::constants::NAMESPACE_SPREADSHEET_X14 => "x14",
1369            crate::constants::NAMESPACE_SPREADSHEET_X15 => "x15",
1370            crate::constants::NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN => "xm",
1371            crate::constants::NAMESPACE_DRAWING_ML_MAIN => "a",
1372            crate::constants::NAMESPACE_DRAWING_ML_CHART => "c",
1373            crate::constants::NAMESPACE_DRAWING_ML_SPREADSHEET => "xdr",
1374            crate::constants::NAMESPACE_DRAWING_ML_A14 => "a14",
1375            crate::constants::NAMESPACE_DRAWING_2016_SVG => "asvg",
1376            // Default spreadsheet namespace is already present on worksheet/workbook roots.
1377            crate::constants::NAMESPACE_SPREADSHEET => return,
1378            _ => return,
1379        };
1380        if prefix.is_empty() {
1381            return;
1382        }
1383
1384        let attr_name = format!("xmlns:{prefix}");
1385        let mut attrs = self
1386            .xml_attr
1387            .get(path)
1388            .map(|a| a.clone())
1389            .unwrap_or_default();
1390        if !attrs.contains(&attr_name) {
1391            if !attrs.is_empty() {
1392                attrs.push(' ');
1393            }
1394            attrs.push_str(&format!("{attr_name}=\"{ns}\""));
1395        }
1396
1397        // Ensure the markup-compatibility namespace is present and mark the
1398        // extension namespace as ignorable when appropriate.
1399        if self.needs_ignorable_prefix(prefix) {
1400            let mc_ns = "http://schemas.openxmlformats.org/markup-compatibility/2006";
1401            if !attrs.contains("xmlns:mc") {
1402                if !attrs.is_empty() && !attrs.ends_with(' ') {
1403                    attrs.push(' ');
1404                }
1405                attrs.push_str(&format!("xmlns:mc=\"{mc_ns}\""));
1406            }
1407            self.set_ignorable_name_space(path, prefix, &mut attrs);
1408        }
1409
1410        self.xml_attr.insert(path.to_string(), attrs);
1411    }
1412
1413    fn needs_ignorable_prefix(&self, prefix: &str) -> bool {
1414        const IGNORABLE_NS: &[&str] = &[
1415            "c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14",
1416            "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9",
1417            "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x16", "x16r2", "mo", "mx", "mv", "o",
1418            "v",
1419        ];
1420        IGNORABLE_NS.contains(&prefix)
1421    }
1422
1423    fn set_ignorable_name_space(&self, _path: &str, prefix: &str, attrs: &mut String) {
1424        let marker = "mc:Ignorable=\"";
1425        if let Some(start) = attrs.find(marker) {
1426            let value_start = start + marker.len();
1427            if let Some(end) = attrs[value_start..].find('"') {
1428                let value = &attrs[value_start..value_start + end];
1429                let parts: Vec<&str> = value.split_whitespace().collect();
1430                if !parts.contains(&prefix) {
1431                    let new_value = format!("{} {}", value, prefix);
1432                    attrs.replace_range(value_start..value_start + end, &new_value);
1433                }
1434                return;
1435            }
1436        }
1437        if !attrs.is_empty() && !attrs.ends_with(' ') {
1438            attrs.push(' ');
1439        }
1440        attrs.push_str(&format!("mc:Ignorable=\"{prefix}\""));
1441    }
1442
1443    /// Register a namespace attribute for a worksheet XML part.
1444    pub(crate) fn add_sheet_name_space(&self, sheet: &str, ns: &str) {
1445        if let Some(path) = self.get_sheet_xml_path(sheet) {
1446            self.add_name_spaces(&path, ns);
1447        }
1448    }
1449
1450    /// Return the sheet ID (1-based) for a worksheet name.
1451    pub fn get_sheet_id(&self, sheet: &str) -> i32 {
1452        if let Ok(wb) = self.workbook_reader() {
1453            for s in &wb.sheets.sheet {
1454                if s.name.as_deref().unwrap_or("").eq_ignore_ascii_case(sheet) {
1455                    return s.sheet_id.unwrap_or(0) as i32;
1456                }
1457            }
1458        }
1459        -1
1460    }
1461
1462    /// Expand a 3D sheet range (e.g. `Sheet1:Sheet3`) into the ordered list of
1463    /// worksheet names between the two sheets inclusive.
1464    pub fn expand_3d_sheet_range(&self, sheet1: &str, sheet2: &str) -> Result<Vec<String>> {
1465        let mut idx1 = self.get_sheet_index(sheet1)?;
1466        let mut idx2 = self.get_sheet_index(sheet2)?;
1467        if idx1 > idx2 {
1468            std::mem::swap(&mut idx1, &mut idx2);
1469        }
1470        let list = self.get_sheet_list();
1471        Ok(list[(idx1 - 1) as usize..idx2 as usize].to_vec())
1472    }
1473
1474    /// Clear the in-memory calc cache so the chain is rewritten on save.
1475    pub fn clear_calc_cache(&self) {
1476        let _ = self.calc_chain.lock().unwrap().take();
1477        self.calc_cache.lock().unwrap().clear();
1478        self.calc_raw_cache.lock().unwrap().clear();
1479        self.formula_arg_cache.lock().unwrap().clear();
1480    }
1481
1482    /// Add a content type override for a numbered part.
1483    pub fn add_content_type_part(&self, id: i32, part: &str) -> Result<()> {
1484        match part {
1485            "comments" => self.set_content_type_part_vml_extensions()?,
1486            "drawings" => crate::sheet::set_content_type_part_image_extensions(self)?,
1487            _ => {}
1488        }
1489        let mut ct = self.content_types_reader()?;
1490        let content_type = match part {
1491            "table" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_TABLE,
1492            "pivotTable" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_PIVOT_TABLE,
1493            "pivotCache" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_PIVOT_CACHE_DEFINITION,
1494            "slicer" => crate::constants::CONTENT_TYPE_SLICER,
1495            "slicerCache" => crate::constants::CONTENT_TYPE_SLICER_CACHE,
1496            "drawings" => crate::constants::CONTENT_TYPE_DRAWING,
1497            "chart" => crate::constants::CONTENT_TYPE_DRAWING_ML,
1498            "chartsheet" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_CHARTSHEET,
1499            "comments" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_COMMENTS,
1500            _ => return Ok(()),
1501        };
1502        let part_name = match part {
1503            "table" => format!("/xl/tables/table{id}.xml"),
1504            "pivotTable" => format!("/xl/pivotTables/pivotTable{id}.xml"),
1505            "pivotCache" => format!("/xl/pivotCache/pivotCacheDefinition{id}.xml"),
1506            "slicer" => format!("/xl/slicers/slicer{id}.xml"),
1507            "slicerCache" => format!("/xl/slicerCaches/slicerCache{id}.xml"),
1508            "drawings" => format!("/xl/drawings/drawing{id}.xml"),
1509            "chart" => format!("/xl/charts/chart{id}.xml"),
1510            "chartsheet" => format!("/xl/chartsheets/sheet{id}.xml"),
1511            "comments" => format!("/xl/comments{id}.xml"),
1512            _ => return Ok(()),
1513        };
1514        for entry in &ct.entries {
1515            if let crate::xml::content_types::XlsxContentTypeEntry::Override(o) = entry {
1516                if o.part_name == part_name {
1517                    return Ok(());
1518                }
1519            }
1520        }
1521        ct.entries
1522            .push(crate::xml::content_types::XlsxContentTypeEntry::Override(
1523                crate::xml::content_types::XlsxOverride {
1524                    part_name,
1525                    content_type: content_type.to_string(),
1526                },
1527            ));
1528        *self.content_types.lock().unwrap() = Some(ct);
1529        self.set_content_type_part_rels_extensions()
1530    }
1531
1532    /// Ensure `[Content_Types].xml` contains the default relationship content type.
1533    pub(crate) fn set_content_type_part_rels_extensions(&self) -> Result<()> {
1534        let mut ct = self.content_types_reader()?;
1535        let exists = ct.entries.iter().any(|e| {
1536            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
1537                d.extension == "rels"
1538            } else {
1539                false
1540            }
1541        });
1542        if !exists {
1543            ct.entries
1544                .push(crate::xml::content_types::XlsxContentTypeEntry::Default(
1545                    crate::xml::content_types::XlsxDefault {
1546                        extension: "rels".to_string(),
1547                        content_type: crate::constants::CONTENT_TYPE_RELATIONSHIPS.to_string(),
1548                    },
1549                ));
1550        }
1551        *self.content_types.lock().unwrap() = Some(ct);
1552        Ok(())
1553    }
1554
1555    /// Ensure `[Content_Types].xml` contains the default VML content type.
1556    pub(crate) fn set_content_type_part_vml_extensions(&self) -> Result<()> {
1557        let mut ct = self.content_types_reader()?;
1558        let exists = ct.entries.iter().any(|e| {
1559            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
1560                d.extension == "vml"
1561            } else {
1562                false
1563            }
1564        });
1565        if !exists {
1566            ct.entries
1567                .push(crate::xml::content_types::XlsxContentTypeEntry::Default(
1568                    crate::xml::content_types::XlsxDefault {
1569                        extension: "vml".to_string(),
1570                        content_type: crate::constants::CONTENT_TYPE_VML.to_string(),
1571                    },
1572                ));
1573        }
1574        *self.content_types.lock().unwrap() = Some(ct);
1575        Ok(())
1576    }
1577
1578    /// Remove a content type override by content type and part name prefix.
1579    pub fn remove_content_types_part(&self, content_type: &str, part_name: &str) -> Result<()> {
1580        let mut ct = self.content_types_reader()?;
1581        ct.entries.retain(|e| {
1582            if let crate::xml::content_types::XlsxContentTypeEntry::Override(o) = e {
1583                !(o.content_type == content_type && o.part_name == part_name)
1584            } else {
1585                true
1586            }
1587        });
1588        *self.content_types.lock().unwrap() = Some(ct);
1589        Ok(())
1590    }
1591
1592    /// Return the relationship target for a worksheet relationship by rId.
1593    pub fn get_sheet_relationships_target_by_id(&self, sheet: &str, r_id: &str) -> String {
1594        let Some(sheet_xml_path) = self.get_sheet_xml_path(sheet) else {
1595            return String::new();
1596        };
1597        let sheet_rels = format!(
1598            "xl/worksheets/_rels/{}.rels",
1599            sheet_xml_path.trim_start_matches("xl/worksheets/")
1600        );
1601        if let Ok(Some(rels)) = self.rels_reader(&sheet_rels) {
1602            for rel in &rels.relationships {
1603                if rel.id == r_id {
1604                    return rel.target.clone();
1605                }
1606            }
1607        }
1608        String::new()
1609    }
1610
1611    /// Add a legacy drawing reference to a worksheet.
1612    pub fn add_sheet_legacy_drawing(&self, sheet: &str, r_id: i32) -> Result<()> {
1613        let mut ws = self.work_sheet_reader(sheet)?;
1614        ws.legacy_drawing = Some(crate::xml::worksheet::XlsxLegacyDrawing {
1615            rid: Some(format!("rId{r_id}")),
1616        });
1617        if let Some(path) = self.get_sheet_xml_path(sheet) {
1618            self.sheet.insert(path, ws);
1619        }
1620        Ok(())
1621    }
1622
1623    /// Add a legacy header/footer drawing reference to a worksheet.
1624    pub fn add_sheet_legacy_drawing_hf(&self, sheet: &str, r_id: i32) -> Result<()> {
1625        let mut ws = self.work_sheet_reader(sheet)?;
1626        ws.legacy_drawing_hf = Some(crate::xml::worksheet::XlsxLegacyDrawingHF {
1627            rid: Some(format!("rId{r_id}")),
1628        });
1629        if let Some(path) = self.get_sheet_xml_path(sheet) {
1630            self.sheet.insert(path, ws);
1631        }
1632        Ok(())
1633    }
1634
1635    /// Count existing comments parts in the package.
1636    pub fn count_comments(&self) -> i32 {
1637        let mut count = 0;
1638        for entry in self.pkg.iter() {
1639            if entry.key().contains("xl/comments") {
1640                count += 1;
1641            }
1642        }
1643        for entry in self.comments.iter() {
1644            if entry.key().contains("xl/comments") && !self.pkg.contains_key(entry.key()) {
1645                count += 1;
1646            }
1647        }
1648        count
1649    }
1650
1651    /// Count existing VML drawing parts in the package.
1652    pub fn count_vml_drawing(&self) -> i32 {
1653        let mut count = 0;
1654        for entry in self.pkg.iter() {
1655            if entry.key().contains("xl/drawings/vmlDrawing") {
1656                count += 1;
1657            }
1658        }
1659        for entry in self.vml_drawing.iter() {
1660            if entry.key().contains("xl/drawings/vmlDrawing") && !self.pkg.contains_key(entry.key())
1661            {
1662                count += 1;
1663            }
1664        }
1665        count
1666    }
1667
1668    /// Lazy reader for comments parts.
1669    pub fn comments_reader(
1670        &self,
1671        path: &str,
1672    ) -> Result<Option<crate::xml::comments::XlsxComments>> {
1673        if let Some(cmts) = self.comments.get(path) {
1674            return Ok(Some(cmts.clone()));
1675        }
1676        if self.pkg.contains_key(path) || self.temp_files.contains_key(path) {
1677            let data = self.apply_charset_transcoder(&self.read_bytes(path))?;
1678            let data = crate::file::namespace_strict_to_transitional(&data);
1679            let mut cmts: crate::xml::comments::XlsxComments =
1680                quick_xml::de::from_reader(data.as_slice()).unwrap_or_default();
1681            for cmt in &cmts.comment_list.comment {
1682                cmts.cells.push(cmt.r#ref.clone());
1683            }
1684            self.comments.insert(path.to_string(), cmts.clone());
1685            return Ok(Some(cmts));
1686        }
1687        Ok(None)
1688    }
1689
1690    /// Lazy reader for VML drawing parts.
1691    pub fn vml_drawing_reader(&self, path: &str) -> Result<Option<crate::xml::vml::VmlDrawing>> {
1692        if let Some(vml) = self.vml_drawing.get(path) {
1693            return Ok(Some(vml.clone()));
1694        }
1695        if self.pkg.contains_key(path) || self.temp_files.contains_key(path) {
1696            let data = self.apply_charset_transcoder(&self.read_bytes(path))?;
1697            let data = crate::file::namespace_strict_to_transitional(&data);
1698            let data = String::from_utf8_lossy(&data)
1699                .replace("<br>\r\n", "<br></br>\r\n")
1700                .into_bytes();
1701            let vml = crate::xml::vml::VmlDrawing::from_xml(&data).unwrap_or_default();
1702            self.vml_drawing.insert(path.to_string(), vml.clone());
1703            return Ok(Some(vml));
1704        }
1705        Ok(None)
1706    }
1707
1708    /// Delete a worksheet relationship by rId.
1709    pub fn delete_sheet_relationships(&self, sheet: &str, r_id: &str) {
1710        let Some(sheet_xml_path) = self.get_sheet_xml_path(sheet) else {
1711            return;
1712        };
1713        let sheet_rels = format!(
1714            "xl/worksheets/_rels/{}.rels",
1715            sheet_xml_path.trim_start_matches("xl/worksheets/")
1716        );
1717        if let Ok(Some(mut rels)) = self.rels_reader(&sheet_rels) {
1718            rels.relationships.retain(|r| r.id != r_id);
1719            self.relationships.insert(sheet_rels, rels);
1720        }
1721    }
1722
1723    /// Delete a workbook relationship by type and target, returning its rId.
1724    pub fn delete_workbook_rels(&self, rel_type: &str, target: &str) -> Result<String> {
1725        let rel_path = self.get_workbook_rels_path();
1726        let mut rels = self.rels_reader(&rel_path)?.unwrap_or_default();
1727        let mut r_id = String::new();
1728        rels.relationships.retain(|r| {
1729            if r.r#type == rel_type && (r.target == target || r.target == format!("/{target}")) {
1730                r_id = r.id.clone();
1731                false
1732            } else {
1733                true
1734            }
1735        });
1736        self.relationships.insert(rel_path, rels);
1737        Ok(r_id)
1738    }
1739
1740    /// Add a drawing relationship reference to a worksheet.
1741    pub fn add_sheet_drawing(&self, sheet: &str, r_id: i32) -> Result<()> {
1742        let mut ws = self.work_sheet_reader(sheet)?;
1743        ws.drawing = Some(crate::xml::worksheet::XlsxDrawing {
1744            rid: Some(format!("rId{r_id}")),
1745        });
1746        self.sheet
1747            .insert(self.get_sheet_xml_path(sheet).unwrap_or_default(), ws);
1748        Ok(())
1749    }
1750
1751    /// Count existing table parts in the package.
1752    pub fn count_tables(&self) -> i32 {
1753        let mut count = 0i32;
1754        for entry in self.pkg.iter() {
1755            let k = entry.key();
1756            if k.contains("xl/tables/tableSingleCells") {
1757                if let Ok(data) = self.apply_charset_transcoder(entry.value()) {
1758                    let data = namespace_strict_to_transitional(&data);
1759                    match xml_from_reader::<_, XlsxSingleXmlCells>(data.as_slice()) {
1760                        Ok(cells) => {
1761                            for cell in cells.single_xml_cell {
1762                                if count < cell.id as i32 {
1763                                    count = cell.id as i32;
1764                                }
1765                            }
1766                        }
1767                        Err(_) => count += 1,
1768                    }
1769                }
1770            }
1771            if k.contains("xl/tables/table") {
1772                if let Ok(data) = self.apply_charset_transcoder(entry.value()) {
1773                    let data = namespace_strict_to_transitional(&data);
1774                    match xml_from_reader::<_, XlsxTable>(data.as_slice()) {
1775                        Ok(t) => {
1776                            if count < t.id as i32 {
1777                                count = t.id as i32;
1778                            }
1779                        }
1780                        Err(_) => count += 1,
1781                    }
1782                }
1783            }
1784        }
1785        count
1786    }
1787
1788    /// Count existing drawing parts in the package.
1789    pub fn count_drawings(&self) -> i32 {
1790        let mut count = 0;
1791        for entry in self.pkg.iter() {
1792            if entry.key().contains("xl/drawings/drawing") {
1793                count += 1;
1794            }
1795        }
1796        for entry in self.drawings.iter() {
1797            if entry.key().contains("xl/drawings/drawing") && !self.pkg.contains_key(entry.key()) {
1798                count += 1;
1799            }
1800        }
1801        count
1802    }
1803
1804    /// Count existing chart parts in the package.
1805    pub fn count_charts(&self) -> i32 {
1806        let mut count = 0;
1807        for entry in self.pkg.iter() {
1808            if entry.key().contains("xl/charts/chart") {
1809                count += 1;
1810            }
1811        }
1812        count
1813    }
1814
1815    /// Count existing pivot table parts in the package.
1816    pub fn count_pivot_tables(&self) -> i32 {
1817        let mut count = 0;
1818        for entry in self.pkg.iter() {
1819            if entry.key().contains("xl/pivotTables/pivotTable") {
1820                count += 1;
1821            }
1822        }
1823        count
1824    }
1825
1826    /// Count existing pivot cache parts in the package.
1827    pub fn count_pivot_cache(&self) -> i32 {
1828        let mut count = 0;
1829        for entry in self.pkg.iter() {
1830            if entry.key().contains("xl/pivotCache/pivotCacheDefinition") {
1831                count += 1;
1832            }
1833        }
1834        count
1835    }
1836
1837    /// Count existing slicer parts in the package.
1838    pub fn count_slicers(&self) -> i32 {
1839        let mut count = 0;
1840        for entry in self.pkg.iter() {
1841            if entry.key().contains("xl/slicers/slicer") {
1842                count += 1;
1843            }
1844        }
1845        count
1846    }
1847
1848    /// Count existing slicer cache parts in the package.
1849    pub fn count_slicer_cache(&self) -> i32 {
1850        let mut count = 0;
1851        for entry in self.pkg.iter() {
1852            if entry.key().contains("xl/slicerCaches/slicerCache") {
1853                count += 1;
1854            }
1855        }
1856        count
1857    }
1858
1859    /// Return all defined names in the workbook.
1860    pub fn get_defined_names(&self) -> Result<Vec<crate::xml::workbook::DefinedName>> {
1861        let mut out = Vec::new();
1862        if let Ok(wb) = self.workbook_reader() {
1863            if let Some(dns) = wb.defined_names {
1864                for dn in dns.defined_name {
1865                    out.push(crate::xml::workbook::DefinedName {
1866                        name: dn.name.clone().unwrap_or_default(),
1867                        comment: dn.comment.clone().unwrap_or_default(),
1868                        refers_to: dn.data.clone(),
1869                        scope: dn
1870                            .local_sheet_id
1871                            .map(|id| {
1872                                if let Ok(wb2) = self.workbook_reader() {
1873                                    if let Some(s) = wb2.sheets.sheet.get(id as usize) {
1874                                        return s.name.clone().unwrap_or_default();
1875                                    }
1876                                }
1877                                String::new()
1878                            })
1879                            .unwrap_or_else(|| "Workbook".to_string()),
1880                    });
1881                }
1882            }
1883        }
1884        Ok(out)
1885    }
1886
1887    /// Add a defined name.
1888    pub fn set_defined_name(&self, dn: &crate::xml::workbook::DefinedName) -> Result<()> {
1889        let mut wb = self.workbook_reader()?;
1890        if wb.defined_names.is_none() {
1891            wb.defined_names = Some(crate::xml::workbook::XlsxDefinedNames::default());
1892        }
1893        let names = wb.defined_names.as_mut().unwrap();
1894        let local_sheet_id = if dn.scope.is_empty() || dn.scope == "Workbook" {
1895            None
1896        } else {
1897            Some((self.get_sheet_index(&dn.scope)? - 1) as i64)
1898        };
1899        for existing in &names.defined_name {
1900            if existing.name.as_deref().unwrap_or("") == dn.name
1901                && existing.local_sheet_id == local_sheet_id
1902            {
1903                return Err(Box::new(ErrDefinedNameDuplicate));
1904            }
1905        }
1906        names
1907            .defined_name
1908            .push(crate::xml::workbook::XlsxDefinedName {
1909                name: Some(dn.name.clone()),
1910                data: dn.refers_to.clone(),
1911                hidden: Some(false),
1912                local_sheet_id,
1913                ..Default::default()
1914            });
1915        *self.workbook.lock().unwrap() = Some(wb);
1916        Ok(())
1917    }
1918
1919    /// Delete a defined name.
1920    pub fn delete_defined_name(&self, dn: &crate::xml::workbook::DefinedName) -> Result<()> {
1921        let mut wb = self.workbook_reader()?;
1922        let Some(names) = wb.defined_names.as_mut() else {
1923            return Err(Box::new(ErrDefinedNameScope));
1924        };
1925        let local_sheet_id = if dn.scope.is_empty() || dn.scope == "Workbook" {
1926            None
1927        } else {
1928            Some((self.get_sheet_index(&dn.scope)? - 1) as i64)
1929        };
1930        let before = names.defined_name.len();
1931        names.defined_name.retain(|e| {
1932            !(e.name.as_deref().unwrap_or("") == dn.name && e.local_sheet_id == local_sheet_id)
1933        });
1934        if names.defined_name.len() == before {
1935            return Err(Box::new(ErrDefinedNameScope));
1936        }
1937        if names.defined_name.is_empty() {
1938            wb.defined_names = None;
1939        }
1940        *self.workbook.lock().unwrap() = Some(wb);
1941        Ok(())
1942    }
1943
1944    /// Return the reference a defined name resolves to.
1945    pub fn get_defined_name_ref_to(&self, name: &str, current_sheet: &str) -> String {
1946        let mut workbook_ref = String::new();
1947        let mut sheet_ref = String::new();
1948        if let Ok(names) = self.get_defined_names() {
1949            for dn in &names {
1950                if dn.name == name {
1951                    if dn.scope == "Workbook" {
1952                        workbook_ref = dn.refers_to.clone();
1953                    }
1954                    if dn.scope == current_sheet {
1955                        sheet_ref = dn.refers_to.clone();
1956                    }
1957                }
1958            }
1959        }
1960        if !sheet_ref.is_empty() {
1961            sheet_ref
1962        } else {
1963            workbook_ref
1964        }
1965    }
1966
1967    /// Alias for [`Self::get_defined_names`], matching the Go `GetDefinedName`
1968    /// API surface.
1969    pub fn get_defined_name(&self) -> Result<Vec<crate::xml::workbook::DefinedName>> {
1970        self.get_defined_names()
1971    }
1972}
1973
1974// ------------------------------------------------------------------
1975// Additional public File-level APIs ported from file.go / excelize.go
1976// ------------------------------------------------------------------
1977
1978impl File {
1979    /// Write the workbook to any writer. Alias for [`Self::write_to`].
1980    pub fn write<W: Write>(&self, writer: W) -> Result<()> {
1981        self.write_to(writer)
1982    }
1983
1984    /// Save to the original path, overriding the stored options.
1985    pub fn save_with_options(&mut self, opts: Options) -> Result<()> {
1986        {
1987            let mut o = self.options.lock().unwrap();
1988            *o = opts;
1989        }
1990        self.save()
1991    }
1992
1993    /// Save the workbook to the given path, overriding the stored options.
1994    pub fn save_as_with_options(&mut self, path: &str, opts: Options) -> Result<()> {
1995        {
1996            let mut o = self.options.lock().unwrap();
1997            *o = opts;
1998        }
1999        self.save_as(path)
2000    }
2001
2002    /// Clear cached linked values for formula cells so Excel recalculates them
2003    /// on open.
2004    ///
2005    /// Equivalent to Go `UpdateLinkedValue`.
2006    pub fn update_linked_value(&self) -> Result<()> {
2007        let mut wb = self.workbook_reader()?;
2008        wb.calc_pr = None;
2009        *self.workbook.lock().unwrap() = Some(wb);
2010        for name in self.get_sheet_list() {
2011            if let Ok(mut ws) = self.work_sheet_reader(&name) {
2012                let path = self.get_sheet_xml_path(&name).unwrap_or_default();
2013                let mut changed = false;
2014                for row in &mut ws.sheet_data.row {
2015                    for cell in &mut row.c {
2016                        if cell.f.is_some() && cell.v.is_some() {
2017                            cell.v = None;
2018                            cell.t = None;
2019                            changed = true;
2020                        }
2021                    }
2022                }
2023                if changed {
2024                    self.sheet.insert(path, ws);
2025                }
2026            }
2027        }
2028        Ok(())
2029    }
2030
2031    /// Extract spreadsheet package parts from a `ZipArchive`.
2032    ///
2033    /// Equivalent to Go `ReadZipReader`.
2034    pub fn read_zip_reader<R: Read + Seek>(
2035        &self,
2036        archive: &mut ZipArchive<R>,
2037    ) -> Result<(HashMap<String, Vec<u8>>, i32)> {
2038        self.read_zip_archive(archive)
2039    }
2040
2041    /// Set workbook properties.
2042    pub fn set_workbook_props(&self, opts: &WorkbookPropsOptions) -> Result<()> {
2043        let mut wb = self.workbook_reader()?;
2044        if wb.workbook_pr.is_none() {
2045            wb.workbook_pr = Some(XlsxWorkbookPr::default());
2046        }
2047        let pr = wb.workbook_pr.as_mut().unwrap();
2048        if let Some(v) = opts.date1904 {
2049            pr.date1904 = Some(v);
2050        }
2051        if let Some(v) = opts.filter_privacy {
2052            pr.filter_privacy = Some(v);
2053        }
2054        if let Some(ref v) = opts.code_name {
2055            pr.code_name = Some(v.clone());
2056        }
2057        *self.workbook.lock().unwrap() = Some(wb);
2058        Ok(())
2059    }
2060
2061    /// Get workbook properties.
2062    pub fn get_workbook_props(&self) -> Result<WorkbookPropsOptions> {
2063        let mut opts = WorkbookPropsOptions::default();
2064        let wb = self.workbook_reader()?;
2065        if let Some(pr) = &wb.workbook_pr {
2066            opts.date1904 = pr.date1904;
2067            opts.filter_privacy = pr.filter_privacy;
2068            opts.code_name = pr.code_name.clone();
2069        }
2070        Ok(opts)
2071    }
2072
2073    /// Set calculation properties.
2074    pub fn set_calc_props(&self, opts: &CalcPropsOptions) -> Result<()> {
2075        if let Some(ref mode) = opts.calc_mode {
2076            if in_str_slice(SUPPORTED_CALC_MODE, mode, true) == -1 {
2077                return Err(Box::new(ErrParameterInvalid));
2078            }
2079        }
2080        if let Some(ref mode) = opts.ref_mode {
2081            if in_str_slice(SUPPORTED_REF_MODE, mode, true) == -1 {
2082                return Err(Box::new(ErrParameterInvalid));
2083            }
2084        }
2085
2086        let mut wb = self.workbook_reader()?;
2087        if wb.calc_pr.is_none() {
2088            wb.calc_pr = Some(XlsxCalcPr::default());
2089        }
2090        let pr = wb.calc_pr.as_mut().unwrap();
2091        if let Some(v) = opts.calc_completed {
2092            pr.calc_completed = Some(v);
2093        }
2094        if let Some(v) = opts.calc_on_save {
2095            pr.calc_on_save = Some(v);
2096        }
2097        if let Some(v) = opts.force_full_calc {
2098            pr.force_full_calc = Some(v);
2099        }
2100        if let Some(v) = opts.full_calc_on_load {
2101            pr.full_calc_on_load = Some(v);
2102        }
2103        if let Some(v) = opts.full_precision {
2104            pr.full_precision = Some(v);
2105        }
2106        if let Some(v) = opts.iterate {
2107            pr.iterate = Some(v);
2108        }
2109        if let Some(v) = opts.iterate_delta {
2110            pr.iterate_delta = Some(v);
2111        }
2112        if let Some(ref v) = opts.calc_mode {
2113            pr.calc_mode = Some(v.clone());
2114        }
2115        if let Some(ref v) = opts.ref_mode {
2116            pr.ref_mode = Some(v.clone());
2117        }
2118        if let Some(v) = opts.calc_id {
2119            pr.calc_id = Some(v as i64);
2120        }
2121        if let Some(v) = opts.concurrent_manual_count {
2122            pr.concurrent_manual_count = Some(v as i64);
2123        }
2124        if let Some(v) = opts.iterate_count {
2125            pr.iterate_count = Some(v as i64);
2126        }
2127        pr.concurrent_calc = opts.concurrent_calc;
2128        *self.workbook.lock().unwrap() = Some(wb);
2129        Ok(())
2130    }
2131
2132    /// Get calculation properties.
2133    pub fn get_calc_props(&self) -> Result<CalcPropsOptions> {
2134        let mut opts = CalcPropsOptions::default();
2135        let wb = self.workbook_reader()?;
2136        if let Some(pr) = &wb.calc_pr {
2137            opts.calc_completed = pr.calc_completed;
2138            opts.calc_on_save = pr.calc_on_save;
2139            opts.force_full_calc = pr.force_full_calc;
2140            opts.full_calc_on_load = pr.full_calc_on_load;
2141            opts.full_precision = pr.full_precision;
2142            opts.iterate = pr.iterate;
2143            opts.iterate_delta = pr.iterate_delta;
2144            opts.calc_mode = pr.calc_mode.clone();
2145            opts.ref_mode = pr.ref_mode.clone();
2146            opts.calc_id = pr.calc_id.map(|v| v as u64);
2147            opts.concurrent_manual_count = pr.concurrent_manual_count.map(|v| v as u64);
2148            opts.iterate_count = pr.iterate_count.map(|v| v as u64);
2149            opts.concurrent_calc = pr.concurrent_calc;
2150        }
2151        Ok(opts)
2152    }
2153
2154    /// Protect the workbook with optional password and lock settings.
2155    pub fn protect_workbook(&self, opts: &WorkbookProtectionOptions) -> Result<()> {
2156        let mut wb = self.workbook_reader()?;
2157        let mut protection = XlsxWorkbookProtection {
2158            lock_structure: Some(opts.lock_structure),
2159            lock_windows: Some(opts.lock_windows),
2160            ..Default::default()
2161        };
2162        if !opts.password.is_empty() {
2163            let algorithm_name = if opts.algorithm_name.is_empty() {
2164                "SHA-512"
2165            } else {
2166                &opts.algorithm_name
2167            };
2168            let (hash_value, salt_value) = crypt::gen_iso_passwd_hash(
2169                &opts.password,
2170                algorithm_name,
2171                "",
2172                WORKBOOK_PROTECTION_SPIN_COUNT,
2173            )?;
2174            protection.workbook_algorithm_name = Some(algorithm_name.to_string());
2175            protection.workbook_hash_value = Some(hash_value);
2176            protection.workbook_salt_value = Some(salt_value);
2177            protection.workbook_spin_count = Some(WORKBOOK_PROTECTION_SPIN_COUNT as i64);
2178        }
2179        wb.workbook_protection = Some(protection);
2180        *self.workbook.lock().unwrap() = Some(wb);
2181        Ok(())
2182    }
2183
2184    /// Remove workbook protection, optionally verifying the password first.
2185    pub fn unprotect_workbook(&self, password: Option<&str>) -> Result<()> {
2186        let mut wb = self.workbook_reader()?;
2187        if let Some(pwd) = password {
2188            let protection = wb.workbook_protection.as_ref().ok_or_else(|| {
2189                Box::new(ErrUnprotectWorkbook) as Box<dyn std::error::Error + Send + Sync>
2190            })?;
2191            if let Some(ref algorithm_name) = protection.workbook_algorithm_name {
2192                let salt = protection.workbook_salt_value.as_deref().unwrap_or("");
2193                let spin_count = protection.workbook_spin_count.unwrap_or(0) as i32;
2194                let (hash_value, _) =
2195                    crypt::gen_iso_passwd_hash(pwd, algorithm_name, salt, spin_count)?;
2196                if protection.workbook_hash_value.as_deref().unwrap_or("") != hash_value {
2197                    return Err(Box::new(ErrUnprotectWorkbookPassword));
2198                }
2199            }
2200        }
2201        wb.workbook_protection = None;
2202        *self.workbook.lock().unwrap() = Some(wb);
2203        Ok(())
2204    }
2205
2206    /// Add a VBA project binary to the workbook.
2207    ///
2208    /// The data must start with the OLE compound-file identifier. A valid
2209    /// `vbaProject.bin` can be embedded by reading it from disk and passing
2210    /// the bytes to this method. The workbook should be saved with an `.xlsm`
2211    /// or `.xltm` extension.
2212    pub fn add_vba_project(&self, data: &[u8]) -> Result<()> {
2213        if data.len() < 8 || &data[..8] != OLE_IDENTIFIER {
2214            return Err(Box::new(crate::errors::ErrAddVBAProject));
2215        }
2216        let rel_path = self.get_workbook_rels_path();
2217        let mut rels = self.rels_reader(&rel_path)?.unwrap_or_default();
2218        let mut existing = false;
2219        let mut r_id = 0;
2220        for rel in &rels.relationships {
2221            if rel.target == "vbaProject.bin" && rel.r#type == SOURCE_RELATIONSHIP_VBA_PROJECT {
2222                existing = true;
2223            }
2224            let id: i32 = rel.id.trim_start_matches("rId").parse().unwrap_or(0);
2225            if id > r_id {
2226                r_id = id;
2227            }
2228        }
2229        if !existing {
2230            r_id += 1;
2231            rels.relationships.push(XlsxRelationship {
2232                id: format!("rId{r_id}"),
2233                r#type: SOURCE_RELATIONSHIP_VBA_PROJECT.to_string(),
2234                target: "vbaProject.bin".to_string(),
2235                target_mode: None,
2236            });
2237            self.relationships.insert(rel_path, rels);
2238        }
2239        self.pkg
2240            .insert("xl/vbaProject.bin".to_string(), data.to_vec());
2241        Ok(())
2242    }
2243
2244    /// Set the workbook content type and `.bin` default for macro workbooks.
2245    pub fn set_content_type_part_project_extensions(&self, content_type: &str) -> Result<()> {
2246        let mut ct = self.content_types_reader()?;
2247        let mut bin_ok = false;
2248        let mut entries = ct.entries.clone();
2249        for entry in &entries {
2250            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = entry {
2251                if d.extension == "bin" {
2252                    bin_ok = true;
2253                }
2254            }
2255        }
2256        for entry in &mut entries {
2257            if let crate::xml::content_types::XlsxContentTypeEntry::Override(o) = entry {
2258                if o.part_name == "/xl/workbook.xml" {
2259                    o.content_type = content_type.to_string();
2260                }
2261            }
2262        }
2263        if !bin_ok {
2264            entries.push(crate::xml::content_types::XlsxContentTypeEntry::Default(
2265                XlsxDefault {
2266                    extension: "bin".to_string(),
2267                    content_type: CONTENT_TYPE_VBA.to_string(),
2268                },
2269            ));
2270        }
2271        ct.entries = entries;
2272        *self.content_types.lock().unwrap() = Some(ct);
2273        Ok(())
2274    }
2275}
2276
2277// ------------------------------------------------------------------
2278// Free functions
2279// ------------------------------------------------------------------
2280
2281fn normalize_options(mut opts: Options) -> Options {
2282    if opts.unzip_size_limit == 0 {
2283        opts.unzip_size_limit = UNZIP_SIZE_LIMIT;
2284    }
2285    if opts.unzip_xml_size_limit == 0 {
2286        opts.unzip_xml_size_limit = STREAM_CHUNK_SIZE;
2287    }
2288    if opts.culture_info == 0 && opts.short_date_pattern.is_empty() {
2289        opts.culture_info = CULTURE_NAME_UNKNOWN;
2290    }
2291    opts
2292}
2293
2294/// Convert Strict Open XML namespaces to Transitional ones.
2295pub fn namespace_strict_to_transitional(content: &[u8]) -> Vec<u8> {
2296    if content.windows(13).all(|w| w != b"purl.oclc.org") {
2297        return content.to_vec();
2298    }
2299    let mut result = content.to_vec();
2300    let translations: &[(&[u8], &[u8])] = &[
2301        (
2302            STRICT_NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES.as_bytes(),
2303            NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES.as_bytes(),
2304        ),
2305        (
2306            STRICT_NAMESPACE_DRAWING_ML_MAIN.as_bytes(),
2307            NAMESPACE_DRAWING_ML_MAIN.as_bytes(),
2308        ),
2309        (
2310            STRICT_NAMESPACE_EXTENDED_PROPERTIES.as_bytes(),
2311            NAMESPACE_EXTENDED_PROPERTIES.as_bytes(),
2312        ),
2313        (
2314            STRICT_NAMESPACE_SPREADSHEET.as_bytes(),
2315            NAMESPACE_SPREADSHEET.as_bytes(),
2316        ),
2317        (
2318            STRICT_SOURCE_RELATIONSHIP.as_bytes(),
2319            SOURCE_RELATIONSHIP.as_bytes(),
2320        ),
2321        (
2322            STRICT_SOURCE_RELATIONSHIP_CHART.as_bytes(),
2323            SOURCE_RELATIONSHIP_CHART.as_bytes(),
2324        ),
2325        (
2326            STRICT_SOURCE_RELATIONSHIP_COMMENTS.as_bytes(),
2327            SOURCE_RELATIONSHIP_COMMENTS.as_bytes(),
2328        ),
2329        (
2330            STRICT_SOURCE_RELATIONSHIP_EXTEND_PROPERTIES.as_bytes(),
2331            SOURCE_RELATIONSHIP_EXTEND_PROPERTIES.as_bytes(),
2332        ),
2333        (
2334            STRICT_SOURCE_RELATIONSHIP_IMAGE.as_bytes(),
2335            SOURCE_RELATIONSHIP_IMAGE.as_bytes(),
2336        ),
2337        (
2338            STRICT_SOURCE_RELATIONSHIP_OFFICE_DOCUMENT.as_bytes(),
2339            SOURCE_RELATIONSHIP_OFFICE_DOCUMENT.as_bytes(),
2340        ),
2341    ];
2342    for (from, to) in translations {
2343        result = bytes_replace(&result, from, to);
2344    }
2345    result
2346}
2347
2348fn bytes_replace(content: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
2349    let mut result = Vec::with_capacity(content.len());
2350    let mut start = 0;
2351    while let Some(pos) = content[start..].windows(from.len()).position(|w| w == from) {
2352        let pos = start + pos;
2353        result.extend_from_slice(&content[start..pos]);
2354        result.extend_from_slice(to);
2355        start = pos + from.len();
2356    }
2357    result.extend_from_slice(&content[start..]);
2358    result
2359}
2360
2361/// Detect the encoding named in an XML declaration, returning `None` when no
2362/// declaration is present. Works on raw bytes so that non-UTF-8 documents can
2363/// still be inspected.
2364fn detect_xml_encoding(content: &[u8]) -> Option<&str> {
2365    let start = content.windows(5).position(|w| w == b"<?xml")?;
2366    let rest = &content[start..];
2367    let end = rest.windows(2).position(|w| w == b"?>")? + 2;
2368    let decl = &rest[..end];
2369    let key = b"encoding=";
2370    let pos = decl.windows(key.len()).position(|w| w == key)?;
2371    let rest = &decl[pos + key.len()..];
2372    let quote = *rest.first()?;
2373    let close = rest[1..].iter().position(|&b| b == quote)?;
2374    std::str::from_utf8(&rest[1..1 + close]).ok()
2375}
2376
2377/// Remove all occurrences of a single XML element (and its children) from a
2378/// UTF-8 document. Used as a deserialization workaround for elements that
2379/// contain arbitrary nested XML.
2380fn strip_xml_element(content: &[u8], name: &str) -> Vec<u8> {
2381    let s = String::from_utf8_lossy(content);
2382    let pattern = format!(
2383        r"(?s)<{}\b[^>]*>.*?</{}>",
2384        regex::escape(name),
2385        regex::escape(name)
2386    );
2387    if let Ok(re) = regex::Regex::new(&pattern) {
2388        return re.replace_all(&s, "").into_owned().into_bytes();
2389    }
2390    s.into_owned().into_bytes()
2391}
2392
2393/// Extract the raw attribute string from the root element of an XML document.
2394pub(crate) fn extract_root_namespace_attributes(content: &[u8]) -> Option<String> {
2395    let s = std::str::from_utf8(content).ok()?;
2396    // Skip optional XML declaration and whitespace.
2397    let mut pos = 0usize;
2398    while pos < s.len() {
2399        let c = s[pos..].chars().next()?;
2400        if c == '<' {
2401            if s[pos..].starts_with("<?") {
2402                if let Some(end) = s[pos..].find("?>") {
2403                    pos += end + 2;
2404                    continue;
2405                }
2406                return None;
2407            }
2408            break;
2409        }
2410        pos += c.len_utf8();
2411    }
2412    if pos >= s.len() {
2413        return None;
2414    }
2415    let start = pos;
2416    let close = s[start..].find('>')?;
2417    let tag = &s[start..start + close + 1];
2418    // Tag name ends at first whitespace.
2419    let name_end = tag
2420        .find(|c: char| c.is_whitespace())
2421        .unwrap_or(tag.len() - 1);
2422    let after_name = &tag[name_end..tag.len() - 1];
2423    let trimmed = after_name.trim();
2424    if trimmed.is_empty() {
2425        None
2426    } else {
2427        Some(trimmed.to_string())
2428    }
2429}
2430
2431/// Replace the attributes of the root element with the captured namespace string.
2432pub(crate) fn replace_root_namespace_attributes(content: &mut Vec<u8>, attrs: &str) -> Result<()> {
2433    let s = std::str::from_utf8(content)
2434        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
2435    let mut pos = 0usize;
2436    while pos < s.len() {
2437        let c = s[pos..]
2438            .chars()
2439            .next()
2440            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "empty"))?;
2441        if c == '<' {
2442            if s[pos..].starts_with("<?") {
2443                if let Some(end) = s[pos..].find("?>") {
2444                    pos += end + 2;
2445                    continue;
2446                }
2447                return Err(Box::new(io::Error::new(
2448                    io::ErrorKind::InvalidData,
2449                    "bad xml decl",
2450                )));
2451            }
2452            break;
2453        }
2454        pos += c.len_utf8();
2455    }
2456    let start = pos;
2457    let close = s[start..]
2458        .find('>')
2459        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "no root close"))?;
2460    let tag = &s[start..start + close + 1];
2461    let name_end = tag
2462        .find(|c: char| c.is_whitespace())
2463        .unwrap_or(tag.len() - 1);
2464    let tag_name = &tag[1..name_end];
2465    let end_tag = start + close + 1;
2466    let new_start = format!("<{tag_name} {attrs}>");
2467    let tail = content.split_off(end_tag);
2468    content.truncate(start);
2469    content.extend_from_slice(new_start.as_bytes());
2470    content.extend_from_slice(&tail);
2471    Ok(())
2472}
2473
2474fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
2475    haystack
2476        .windows(needle.len())
2477        .position(|window| window == needle)
2478}
2479
2480/// Remove attributes with empty values from serialized XML.
2481///
2482/// quick_xml emits `None` `Option` fields as `attr=""`, which cannot be
2483/// parsed back into boolean/integer fields. Stripping them lets round-trips
2484/// through `Option<T>` fields work until the XML types are updated to skip
2485/// `None` values explicitly.
2486pub(crate) fn strip_empty_attributes(content: &mut Vec<u8>) {
2487    static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
2488    let re = RE.get_or_init(|| regex::Regex::new(r#" ([a-zA-Z_][a-zA-Z0-9_:\-]*)="""#).unwrap());
2489    let s = String::from_utf8_lossy(content);
2490    let cleaned = re.replace_all(&s, "");
2491    *content = cleaned.into_owned().into_bytes();
2492}
2493
2494fn decode_theme_to_xlsx_theme(theme: &DecodeTheme) -> XlsxTheme {
2495    fn convert_color(c: &crate::xml::theme::DecodeCtColor) -> XlsxCtColor {
2496        XlsxCtColor {
2497            scrgb_clr: c.scrgb_clr.clone(),
2498            srgb_clr: c.srgb_clr.clone(),
2499            hsl_clr: c.hsl_clr.clone(),
2500            sys_clr: c.sys_clr.clone().map(|s| XlsxSysClr {
2501                val: s.val,
2502                last_clr: s.last_clr,
2503            }),
2504            scheme_clr: c.scheme_clr.clone(),
2505            prst_clr: c.prst_clr.clone(),
2506        }
2507    }
2508    fn convert_font(c: &crate::xml::theme::DecodeFontCollection) -> XlsxFontCollection {
2509        XlsxFontCollection {
2510            latin: c.latin.clone(),
2511            ea: c.ea.clone(),
2512            cs: c.cs.clone(),
2513            font: c.font.clone(),
2514            ext_lst: c.ext_lst.clone(),
2515        }
2516    }
2517    XlsxTheme {
2518        xmlns_a: None,
2519        xmlns_r: None,
2520        name: theme.name.clone(),
2521        theme_elements: XlsxBaseStyles {
2522            clr_scheme: crate::xml::theme::XlsxColorScheme {
2523                name: theme.theme_elements.clr_scheme.name.clone(),
2524                dk1: convert_color(&theme.theme_elements.clr_scheme.dk1),
2525                lt1: convert_color(&theme.theme_elements.clr_scheme.lt1),
2526                dk2: convert_color(&theme.theme_elements.clr_scheme.dk2),
2527                lt2: convert_color(&theme.theme_elements.clr_scheme.lt2),
2528                accent1: convert_color(&theme.theme_elements.clr_scheme.accent1),
2529                accent2: convert_color(&theme.theme_elements.clr_scheme.accent2),
2530                accent3: convert_color(&theme.theme_elements.clr_scheme.accent3),
2531                accent4: convert_color(&theme.theme_elements.clr_scheme.accent4),
2532                accent5: convert_color(&theme.theme_elements.clr_scheme.accent5),
2533                accent6: convert_color(&theme.theme_elements.clr_scheme.accent6),
2534                hlink: convert_color(&theme.theme_elements.clr_scheme.hlink),
2535                fol_hlink: convert_color(&theme.theme_elements.clr_scheme.fol_hlink),
2536                ext_lst: theme.theme_elements.clr_scheme.ext_lst.clone(),
2537            },
2538            font_scheme: crate::xml::theme::XlsxFontScheme {
2539                name: theme.theme_elements.font_scheme.name.clone(),
2540                major_font: convert_font(&theme.theme_elements.font_scheme.major_font),
2541                minor_font: convert_font(&theme.theme_elements.font_scheme.minor_font),
2542                ext_lst: theme.theme_elements.font_scheme.ext_lst.clone(),
2543            },
2544            fmt_scheme: crate::xml::theme::XlsxStyleMatrix {
2545                name: theme.theme_elements.fmt_scheme.name.clone(),
2546                fill_style_lst: theme.theme_elements.fmt_scheme.fill_style_lst.clone(),
2547                ln_style_lst: theme.theme_elements.fmt_scheme.ln_style_lst.clone(),
2548                effect_style_lst: theme.theme_elements.fmt_scheme.effect_style_lst.clone(),
2549                bg_fill_style_lst: theme.theme_elements.fmt_scheme.bg_fill_style_lst.clone(),
2550            },
2551            ext_lst: theme.theme_elements.ext_lst.clone(),
2552        },
2553        object_defaults: theme.object_defaults.clone(),
2554        extra_clr_scheme_lst: theme.extra_clr_scheme_lst.clone(),
2555        cust_clr_lst: theme.cust_clr_lst.clone(),
2556        ext_lst: theme.ext_lst.clone(),
2557    }
2558}
2559
2560// ------------------------------------------------------------------
2561// Trait-based helpers referenced by `excelize.rs`
2562// ------------------------------------------------------------------
2563
2564/// Helper to apply the workbook content-type for macro/template files.
2565pub fn set_content_type_part_project_extensions(file: &File, content_type: &str) -> Result<()> {
2566    file.set_content_type_part_project_extensions(content_type)
2567}
2568
2569/// Add a VBA project binary to the workbook.
2570pub fn add_vba_project(file: &File, data: &[u8]) -> Result<()> {
2571    file.add_vba_project(data)
2572}
2573
2574// ------------------------------------------------------------------
2575// Extension list helpers
2576// ------------------------------------------------------------------
2577
2578/// Extract the inner XML of the `<extLst>` element, if present.
2579fn extract_ext_lst(data: &[u8]) -> Option<String> {
2580    let s = String::from_utf8_lossy(data);
2581    let start_key = "<extLst";
2582    let start = s.find(start_key)?;
2583    let close_bracket = s[start..].find('>')? + start + 1;
2584    let end_key = "</extLst>";
2585    let end = s.find(end_key)? + end_key.len();
2586    Some(s[close_bracket..end - end_key.len()].to_string())
2587}
2588
2589/// Remove the `<extLst>` element from raw worksheet XML so that serde can
2590/// deserialize the remainder.
2591fn remove_ext_lst(data: &mut Vec<u8>) {
2592    let s = String::from_utf8_lossy(data);
2593    let Some(start) = s.find("<extLst") else {
2594        return;
2595    };
2596    let Some(end) = s.find("</extLst>") else {
2597        return;
2598    };
2599    let end = end + "</extLst>".len();
2600    let mut result = s[..start].as_bytes().to_vec();
2601    result.extend_from_slice(s[end..].as_bytes());
2602    *data = result;
2603}
2604
2605/// Inject the serialized `<extLst>` inner XML before the closing
2606/// `</worksheet>` tag.
2607fn inject_ext_lst(output: &mut Vec<u8>, ext_xml: &str) {
2608    let s = String::from_utf8_lossy(output);
2609    let Some(pos) = s.rfind("</worksheet>") else {
2610        return;
2611    };
2612    let mut result = s[..pos].as_bytes().to_vec();
2613    result.extend_from_slice(b"<extLst>");
2614    result.extend_from_slice(ext_xml.as_bytes());
2615    result.extend_from_slice(b"</extLst>");
2616    result.extend_from_slice(s[pos..].as_bytes());
2617    *output = result;
2618}
2619
2620impl Drop for File {
2621    fn drop(&mut self) {
2622        // Best-effort cleanup of temporary files that may still be around if
2623        // the user did not call `close()` or if an error path left them behind.
2624        for entry in self.temp_files.iter() {
2625            let _ = fs::remove_file(entry.value());
2626        }
2627        self.temp_files.clear();
2628        for state in self.streams.borrow().values() {
2629            let _ = fs::remove_file(&state.tmp_path);
2630        }
2631        self.streams.borrow_mut().clear();
2632    }
2633}
2634
2635#[cfg(test)]
2636mod tests {
2637    use super::*;
2638
2639    #[test]
2640    fn new_file_round_trip() {
2641        let mut f = File::new_with_options(Options::default());
2642        let tmp = std::env::temp_dir().join("excelize_rust_test.xlsx");
2643        f.save_as(tmp.to_str().unwrap()).unwrap();
2644
2645        let file = fs::File::open(&tmp).unwrap();
2646        let archive = ZipArchive::new(file).unwrap();
2647        let names: Vec<String> = archive.file_names().map(|s| s.to_string()).collect();
2648        assert!(names.contains(&"[Content_Types].xml".to_string()));
2649        assert!(names.contains(&"xl/workbook.xml".to_string()));
2650        assert!(names.contains(&"xl/worksheets/sheet1.xml".to_string()));
2651        assert!(names.contains(&"xl/styles.xml".to_string()));
2652        assert!(names.contains(&"xl/theme/theme1.xml".to_string()));
2653        drop(archive);
2654        let _ = fs::remove_file(&tmp);
2655    }
2656
2657    #[test]
2658    fn save_and_reopen() {
2659        let mut f = File::new_with_options(Options::default());
2660        let tmp = std::env::temp_dir().join("excelize_rust_reopen_test.xlsx");
2661        f.save_as(tmp.to_str().unwrap()).unwrap();
2662
2663        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
2664        assert_eq!(*f2.sheet_count.lock().unwrap(), 1);
2665        assert_eq!(f2.get_sheet_list(), vec!["Sheet1"]);
2666        let _ = fs::remove_file(&tmp);
2667    }
2668
2669    #[test]
2670    fn open_existing_xlsx() {
2671        // Open one of the fixtures shipped with the repository.
2672        let path = "test/Book1.xlsx";
2673        let f = File::open_file(path, Options::default()).unwrap();
2674        assert!(*f.sheet_count.lock().unwrap() >= 1);
2675        let list = f.get_sheet_list();
2676        assert!(!list.is_empty());
2677    }
2678
2679    #[test]
2680    fn workbook_props_round_trip() {
2681        let f = File::new();
2682        let mut opts = WorkbookPropsOptions::default();
2683        opts.date1904 = Some(true);
2684        opts.filter_privacy = Some(false);
2685        opts.code_name = Some("Workbook1".to_string());
2686        f.set_workbook_props(&opts).unwrap();
2687
2688        let got = f.get_workbook_props().unwrap();
2689        assert_eq!(got.date1904, Some(true));
2690        assert_eq!(got.filter_privacy, Some(false));
2691        assert_eq!(got.code_name, Some("Workbook1".to_string()));
2692    }
2693
2694    #[test]
2695    fn calc_props_round_trip() {
2696        let f = File::new();
2697        let mut opts = CalcPropsOptions::default();
2698        opts.calc_mode = Some("manual".to_string());
2699        opts.ref_mode = Some("R1C1".to_string());
2700        opts.full_calc_on_load = Some(true);
2701        opts.calc_id = Some(152511);
2702        opts.iterate_count = Some(100);
2703        f.set_calc_props(&opts).unwrap();
2704
2705        let got = f.get_calc_props().unwrap();
2706        assert_eq!(got.calc_mode, Some("manual".to_string()));
2707        assert_eq!(got.ref_mode, Some("R1C1".to_string()));
2708        assert_eq!(got.full_calc_on_load, Some(true));
2709        assert_eq!(got.calc_id, Some(152511));
2710        assert_eq!(got.iterate_count, Some(100));
2711    }
2712
2713    #[test]
2714    fn calc_props_rejects_invalid_mode() {
2715        let f = File::new();
2716        let mut opts = CalcPropsOptions::default();
2717        opts.calc_mode = Some("invalid".to_string());
2718        assert!(f.set_calc_props(&opts).is_err());
2719
2720        opts.calc_mode = None;
2721        opts.ref_mode = Some("B3".to_string());
2722        assert!(f.set_calc_props(&opts).is_err());
2723    }
2724
2725    #[test]
2726    fn protect_workbook_round_trip() {
2727        let f = File::new();
2728        let opts = WorkbookProtectionOptions {
2729            password: "password".to_string(),
2730            lock_structure: true,
2731            lock_windows: false,
2732            ..Default::default()
2733        };
2734        f.protect_workbook(&opts).unwrap();
2735        let wb = f.workbook_reader().unwrap();
2736        let protection = wb.workbook_protection.as_ref().unwrap();
2737        assert_eq!(protection.lock_structure, Some(true));
2738        assert!(protection.workbook_hash_value.is_some());
2739
2740        assert!(f.unprotect_workbook(Some("wrong")).is_err());
2741        f.unprotect_workbook(Some("password")).unwrap();
2742        assert!(f.workbook_reader().unwrap().workbook_protection.is_none());
2743    }
2744
2745    #[test]
2746    fn add_vba_project() {
2747        let f = File::new();
2748        let mut sheet_opts = crate::sheet::SheetPropsOptions::default();
2749        sheet_opts.code_name = Some("Sheet1".to_string());
2750        f.set_sheet_props("Sheet1", &sheet_opts).unwrap();
2751
2752        let bad = fs::read("test/Book1.xlsx").unwrap();
2753        assert!(f.add_vba_project(&bad).is_err());
2754
2755        let data = fs::read("test/vbaProject.bin").unwrap();
2756        f.add_vba_project(&data).unwrap();
2757        // Adding the same VBA project again should be idempotent.
2758        f.add_vba_project(&data).unwrap();
2759
2760        let rels = f
2761            .rels_reader("xl/_rels/workbook.xml.rels")
2762            .unwrap()
2763            .unwrap();
2764        let vba_rel = rels
2765            .relationships
2766            .iter()
2767            .find(|r| r.target == "vbaProject.bin" && r.r#type == SOURCE_RELATIONSHIP_VBA_PROJECT);
2768        assert!(vba_rel.is_some());
2769
2770        let tmp = std::env::temp_dir().join("excelize_rust_vba.xlsm");
2771        let mut f2 = File::new();
2772        f2.set_sheet_props("Sheet1", &sheet_opts).unwrap();
2773        f2.add_vba_project(&data).unwrap();
2774        f2.save_as(tmp.to_str().unwrap()).unwrap();
2775
2776        let f3 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
2777        assert!(f3.pkg.contains_key("xl/vbaProject.bin"));
2778        let _ = fs::remove_file(&tmp);
2779    }
2780
2781    #[test]
2782    fn charset_transcoder_is_used_for_non_utf8_encoding() {
2783        use crate::constants::DEFAULT_XML_PATH_CALC_CHAIN;
2784        use std::io::Read;
2785        use std::sync::{Arc, Mutex};
2786
2787        let f = File::new();
2788        let xml = br#"<?xml version="1.0" encoding="X-TEST" standalone="yes"?>
2789<calcChain xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><c r="A1" i="1"/></calcChain>"#;
2790        f.pkg
2791            .insert(DEFAULT_XML_PATH_CALC_CHAIN.to_string(), xml.to_vec());
2792        *f.calc_chain.lock().unwrap() = None;
2793
2794        let called = Arc::new(Mutex::new(String::new()));
2795        let called_clone = called.clone();
2796        f.charset_transcoder(move |charset, mut input| {
2797            *called_clone.lock().unwrap() = charset.to_string();
2798            let mut buf = Vec::new();
2799            input.read_to_end(&mut buf).unwrap();
2800            Ok(Box::new(Cursor::new(buf)) as Box<dyn Read>)
2801        });
2802
2803        let cc = f.calc_chain_reader().unwrap();
2804        assert_eq!(cc.c.len(), 1);
2805        assert_eq!(cc.c[0].r, "A1");
2806        assert_eq!(called.lock().unwrap().as_str(), "X-TEST");
2807    }
2808
2809    #[test]
2810    fn charset_transcoder_handles_invalid_utf8_bytes() {
2811        use crate::constants::DEFAULT_XML_PATH_CALC_CHAIN;
2812        use std::io::Read;
2813        use std::sync::{Arc, Mutex};
2814
2815        let f = File::new();
2816        let mut xml = br#"<?xml version="1.0" encoding="X-BAD" standalone="yes"?>
2817<calcChain xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><c r="A1" i="1"/></calcChain>"#
2818            .to_vec();
2819        xml.push(0xFF); // invalid UTF-8 trailing byte
2820        f.pkg.insert(DEFAULT_XML_PATH_CALC_CHAIN.to_string(), xml);
2821        *f.calc_chain.lock().unwrap() = None;
2822
2823        let called = Arc::new(Mutex::new(false));
2824        let called_clone = called.clone();
2825        f.charset_transcoder(move |_charset, mut input| {
2826            *called_clone.lock().unwrap() = true;
2827            let mut buf = Vec::new();
2828            input.read_to_end(&mut buf).unwrap();
2829            buf.pop(); // strip the invalid trailing byte
2830            Ok(Box::new(Cursor::new(buf)) as Box<dyn Read>)
2831        });
2832
2833        let cc = f.calc_chain_reader().unwrap();
2834        assert_eq!(cc.c.len(), 1);
2835        assert_eq!(cc.c[0].r, "A1");
2836        assert!(*called.lock().unwrap());
2837    }
2838
2839    #[test]
2840    fn set_zip_writer_uses_custom_factory() {
2841        use std::sync::{Arc, Mutex};
2842
2843        struct MockZipWriter {
2844            calls: Arc<Mutex<Vec<String>>>,
2845        }
2846
2847        impl ZipWriter for MockZipWriter {
2848            fn start_file(&mut self, name: &str, _options: SimpleFileOptions) -> Result<()> {
2849                self.calls.lock().unwrap().push(format!("start:{name}"));
2850                Ok(())
2851            }
2852            fn write_all(&mut self, _buf: &[u8]) -> Result<()> {
2853                self.calls.lock().unwrap().push("write".to_string());
2854                Ok(())
2855            }
2856            fn finish(self: Box<Self>) -> Result<()> {
2857                self.calls.lock().unwrap().push("finish".to_string());
2858                Ok(())
2859            }
2860        }
2861
2862        let f = File::new();
2863        let calls = Arc::new(Mutex::new(Vec::new()));
2864        let calls_clone = calls.clone();
2865        f.set_zip_writer(move |_writer| {
2866            Box::new(MockZipWriter {
2867                calls: calls_clone.clone(),
2868            })
2869        });
2870
2871        let _ = f.write_to_buffer().unwrap();
2872        let calls = calls.lock().unwrap();
2873        assert!(calls.iter().any(|c| c.starts_with("start:")));
2874        assert!(calls.contains(&"write".to_string()));
2875        assert!(calls.contains(&"finish".to_string()));
2876    }
2877
2878    #[test]
2879    fn get_defined_name_alias() {
2880        let f = File::new();
2881        let names = f.get_defined_name().unwrap();
2882        assert!(names.is_empty());
2883        assert_eq!(f.get_defined_names().unwrap(), names);
2884    }
2885
2886    #[test]
2887    fn update_linked_value_clears_formula_cache() {
2888        let f = File::new_with_options(Options::default());
2889        f.set_cell_int("Sheet1", "A1", 1).unwrap();
2890        f.set_cell_int("Sheet1", "A2", 2).unwrap();
2891        f.set_cell_formula("Sheet1", "A3", "A1+A2").unwrap();
2892        f.calc_cell_value("Sheet1", "A3").unwrap();
2893        f.update_linked_value().unwrap();
2894        assert!(f.workbook_reader().unwrap().calc_pr.is_none());
2895    }
2896
2897    #[test]
2898    fn read_zip_reader_extracts_parts() {
2899        let mut f = File::new_with_options(Options::default());
2900        let tmp = std::env::temp_dir().join("excelize_rust_read_zip_reader.xlsx");
2901        f.save_as(tmp.to_str().unwrap()).unwrap();
2902
2903        let file = fs::File::open(&tmp).unwrap();
2904        let mut archive = ZipArchive::new(file).unwrap();
2905        let f2 = File::new_with_options(Options::default());
2906        let (parts, count) = f2.read_zip_reader(&mut archive).unwrap();
2907        assert!(parts.contains_key(DEFAULT_XML_PATH_WORKBOOK));
2908        assert_eq!(count, 1);
2909        let _ = fs::remove_file(&tmp);
2910    }
2911
2912    #[test]
2913    fn set_content_type_part_rels_extensions_adds_default() {
2914        let f = File::new();
2915        f.set_content_type_part_rels_extensions().unwrap();
2916        let ct = f.content_types_reader().unwrap();
2917        assert!(ct.entries.iter().any(|e| {
2918            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
2919                d.extension == "rels" && d.content_type == CONTENT_TYPE_RELATIONSHIPS
2920            } else {
2921                false
2922            }
2923        }));
2924    }
2925
2926    #[test]
2927    fn add_content_type_part_adds_rels_default() {
2928        let f = File::new();
2929        f.add_content_type_part(1, "table").unwrap();
2930        let ct = f.content_types_reader().unwrap();
2931        assert!(ct.entries.iter().any(|e| {
2932            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
2933                d.extension == "rels"
2934            } else {
2935                false
2936            }
2937        }));
2938    }
2939
2940    #[test]
2941    fn set_content_type_part_image_extensions_uses_defaults() {
2942        let f = File::new();
2943        crate::sheet::set_content_type_part_image_extensions(&f).unwrap();
2944        let ct = f.content_types_reader().unwrap();
2945        assert!(ct.entries.iter().any(|e| {
2946            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
2947                d.extension == "png" && d.content_type == "image/png"
2948            } else {
2949                false
2950            }
2951        }));
2952        assert!(ct.entries.iter().any(|e| {
2953            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
2954                d.extension == "jpeg" && d.content_type == "image/jpeg"
2955            } else {
2956                false
2957            }
2958        }));
2959        // Should not create Overrides with fake part names.
2960        assert!(!ct.entries.iter().any(|e| {
2961            if let crate::xml::content_types::XlsxContentTypeEntry::Override(o) = e {
2962                o.part_name.contains("image1")
2963            } else {
2964                false
2965            }
2966        }));
2967    }
2968
2969    #[test]
2970    fn add_name_spaces_registers_namespace() {
2971        let f = File::new();
2972        let path = f.get_sheet_xml_path("Sheet1").unwrap();
2973        f.add_name_spaces(&path, SOURCE_RELATIONSHIP);
2974        let attrs = f.xml_attr.get(&path).map(|a| a.clone()).unwrap_or_default();
2975        assert!(attrs.contains("xmlns:r=\""));
2976        assert!(attrs.contains(SOURCE_RELATIONSHIP));
2977    }
2978
2979    #[test]
2980    fn add_name_spaces_marks_extension_namespace_ignorable() {
2981        let f = File::new();
2982        let path = f.get_sheet_xml_path("Sheet1").unwrap();
2983        f.add_name_spaces(&path, NAMESPACE_SPREADSHEET_X14);
2984        let attrs = f.xml_attr.get(&path).map(|a| a.clone()).unwrap_or_default();
2985        assert!(attrs.contains("xmlns:x14=\""));
2986        assert!(attrs.contains("xmlns:mc=\""));
2987        assert!(attrs.contains("mc:Ignorable=\"x14\""));
2988    }
2989
2990    #[test]
2991    fn add_sheet_name_space_resolves_path() {
2992        let f = File::new();
2993        f.add_sheet_name_space("Sheet1", SOURCE_RELATIONSHIP);
2994        let path = f.get_sheet_xml_path("Sheet1").unwrap();
2995        let attrs = f.xml_attr.get(&path).map(|a| a.clone()).unwrap_or_default();
2996        assert!(attrs.contains("xmlns:r=\""));
2997    }
2998
2999    #[test]
3000    fn workbook_writer_preserves_alternate_content() {
3001        let f = File::new();
3002        let mut wb = f.workbook_reader().unwrap();
3003        wb.decode_alternate_content = Some(crate::xml::common::XlsxInnerXml {
3004            content: "<mc:Choice Requires=\"a14\" xmlns:a14=\"http://schemas.microsoft.com/office/drawing/2010/main\"><foo/></mc:Choice>".to_string(),
3005        });
3006        *f.workbook.lock().unwrap() = Some(wb);
3007
3008        f.workbook_writer();
3009
3010        let path = f.get_workbook_path();
3011        let bytes = f.read_xml(&path);
3012        let output = String::from_utf8_lossy(&bytes);
3013        assert!(output.contains("mc:AlternateContent"));
3014        assert!(output.contains("mc:Choice"));
3015    }
3016}