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 mut 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            // quick-xml/serde surfaces the namespaced `r:id` attribute under
679            // its local name; move it back so sheets serialize with the
680            // required `r:id` attribute.
681            for sheet in &mut wb.sheets.sheet {
682                if sheet.id.is_none() {
683                    sheet.id = sheet.plain_id.take();
684                }
685            }
686            *self.workbook.lock().unwrap() = Some(wb);
687        }
688        Ok(self.workbook.lock().unwrap().clone().unwrap())
689    }
690
691    // Calculation chain helpers moved to `calc_chain.rs`.
692
693    /// Lazy reader for the theme part.
694    pub fn theme_reader(&self) -> Result<Option<DecodeTheme>> {
695        if self.theme.lock().unwrap().is_none() {
696            if self.pkg.contains_key(DEFAULT_XML_PATH_THEME)
697                || self.temp_files.contains_key(DEFAULT_XML_PATH_THEME)
698            {
699                let data = self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_THEME))?;
700                let data = namespace_strict_to_transitional(&data);
701                if !data.is_empty() {
702                    if let Some(attrs) = extract_root_namespace_attributes(&data) {
703                        self.xml_attr
704                            .insert(DEFAULT_XML_PATH_THEME.to_string(), attrs);
705                    }
706                    if let Ok(theme) = xml_from_reader::<_, DecodeTheme>(data.as_slice()) {
707                        *self.theme.lock().unwrap() = Some(theme);
708                    }
709                }
710            }
711        }
712        Ok(self.theme.lock().unwrap().clone())
713    }
714
715    /// Lazy reader for the shared string table.
716    pub fn shared_strings_reader(&self) -> Result<crate::xml::shared_strings::XlsxSst> {
717        if self.shared_strings.lock().unwrap().is_none() {
718            let mut sst = crate::xml::shared_strings::XlsxSst::default();
719            if self.pkg.contains_key(DEFAULT_XML_PATH_SHARED_STRINGS)
720                || self
721                    .temp_files
722                    .contains_key(DEFAULT_XML_PATH_SHARED_STRINGS)
723            {
724                let data = self
725                    .apply_charset_transcoder(&self.read_bytes(DEFAULT_XML_PATH_SHARED_STRINGS))?;
726                let data = namespace_strict_to_transitional(&data);
727                if !data.is_empty() {
728                    sst = xml_from_reader(data.as_slice()).unwrap_or_default();
729                }
730            }
731            let mut map = self.shared_strings_map.lock().unwrap();
732            map.clear();
733            for (i, si) in sst.si.iter().enumerate() {
734                let text = if let Some(t) = &si.t {
735                    t.val.clone()
736                } else {
737                    si.r.iter()
738                        .filter_map(|r| r.t.as_ref().map(|t| t.val.clone()))
739                        .collect()
740                };
741                map.insert(text, i as i32);
742            }
743            *self.shared_strings.lock().unwrap() = Some(sst);
744        }
745        Ok(self.shared_strings.lock().unwrap().clone().unwrap())
746    }
747
748    /// Lazy reader for relationship parts.
749    pub fn rels_reader(&self, path: &str) -> Result<Option<XlsxRelationships>> {
750        if let Some(rels) = self.relationships.get(path) {
751            return Ok(Some(rels.clone()));
752        }
753        if self.pkg.contains_key(path) || self.temp_files.contains_key(path) {
754            let data = self.apply_charset_transcoder(&self.read_xml(path))?;
755            let data = namespace_strict_to_transitional(&data);
756            let rels = if data.is_empty() {
757                XlsxRelationships::default()
758            } else {
759                if let Some(attrs) = extract_root_namespace_attributes(&data) {
760                    self.xml_attr.insert(path.to_string(), attrs);
761                }
762                xml_from_reader(data.as_slice()).unwrap_or_default()
763            };
764            self.relationships.insert(path.to_string(), rels.clone());
765            Ok(Some(rels))
766        } else {
767            Ok(None)
768        }
769    }
770
771    /// Lazy reader for `xl/metadata.xml`.
772    pub fn metadata_reader(&self) -> Result<crate::xml::metadata::XlsxMetadata> {
773        let data = namespace_strict_to_transitional(
774            &self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_METADATA))?,
775        );
776        if data.is_empty() {
777            return Ok(crate::xml::metadata::XlsxMetadata::default());
778        }
779        // Strip blocks with arbitrary nested XML that quick-xml/serde cannot
780        // capture in `$value` fields; the value metadata used by in-cell
781        // pictures is preserved.
782        let data = strip_xml_element(&data, "metadataTypes");
783        let data = strip_xml_element(&data, "extLst");
784        Ok(xml_from_reader(data.as_slice())?)
785    }
786
787    /// Lazy reader for `xl/richData/rdrichvalue.xml`.
788    pub fn rich_value_reader(&self) -> Result<crate::xml::metadata::XlsxRichValueData> {
789        let data = namespace_strict_to_transitional(
790            &self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_RD_RICH_VALUE))?,
791        );
792        if data.is_empty() {
793            return Ok(crate::xml::metadata::XlsxRichValueData::default());
794        }
795        Ok(xml_from_reader(data.as_slice())?)
796    }
797
798    /// Lazy reader for `xl/richData/richValueRel.xml`.
799    pub fn rich_value_rel_reader(&self) -> Result<crate::xml::metadata::XlsxRichValueRels> {
800        let data = namespace_strict_to_transitional(
801            &self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_RD_RICH_VALUE_REL))?,
802        );
803        if data.is_empty() {
804            return Ok(crate::xml::metadata::XlsxRichValueRels::default());
805        }
806        Ok(xml_from_reader(data.as_slice())?)
807    }
808
809    /// Lazy reader for `xl/richData/rdrichvaluestructure.xml`.
810    pub fn rich_value_structures_reader(
811        &self,
812    ) -> Result<crate::xml::metadata::XlsxRichValueStructures> {
813        let data =
814            namespace_strict_to_transitional(&self.apply_charset_transcoder(
815                &self.read_xml(DEFAULT_XML_PATH_RD_RICH_VALUE_STRUCTURE),
816            )?);
817        if data.is_empty() {
818            return Ok(crate::xml::metadata::XlsxRichValueStructures::default());
819        }
820        Ok(xml_from_reader(data.as_slice())?)
821    }
822
823    /// Lazy reader for `xl/richData/rdRichValueWebImage.xml`.
824    pub fn rich_value_web_image_reader(
825        &self,
826    ) -> Result<crate::xml::metadata::XlsxWebImagesSupportingRichData> {
827        let data =
828            namespace_strict_to_transitional(&self.apply_charset_transcoder(
829                &self.read_xml(DEFAULT_XML_PATH_RD_RICH_VALUE_WEB_IMAGE),
830            )?);
831        if data.is_empty() {
832            return Ok(crate::xml::metadata::XlsxWebImagesSupportingRichData::default());
833        }
834        Ok(xml_from_reader(data.as_slice())?)
835    }
836
837    /// Get a relationship from `xl/richData/_rels/richValueRel.xml.rels` by ID.
838    pub(crate) fn get_rich_data_rich_value_rel_relationship(
839        &self,
840        r_id: &str,
841    ) -> Option<XlsxRelationship> {
842        self.rels_reader(DEFAULT_XML_PATH_RD_RICH_VALUE_REL_RELS)
843            .ok()
844            .flatten()
845            .and_then(|rels| rels.relationships.into_iter().find(|rel| rel.id == r_id))
846    }
847
848    /// Get a relationship from `xl/richData/_rels/rdRichValueWebImage.xml.rels` by ID.
849    pub(crate) fn get_rich_value_web_image_relationship(
850        &self,
851        r_id: &str,
852    ) -> Option<XlsxRelationship> {
853        self.rels_reader(DEFAULT_XML_PATH_RD_RICH_VALUE_WEB_IMAGE_RELS)
854            .ok()
855            .flatten()
856            .and_then(|rels| rels.relationships.into_iter().find(|rel| rel.id == r_id))
857    }
858
859    /// Set an existing relationship entry, or add a new one if `r_id` is empty.
860    pub fn set_rels(
861        &self,
862        r_id: &str,
863        rel_path: &str,
864        rel_type: &str,
865        target: &str,
866        target_mode: &str,
867    ) -> i32 {
868        if r_id.is_empty() {
869            return self.add_rels(rel_path, rel_type, target, target_mode);
870        }
871        let mut rels = self
872            .rels_reader(rel_path)
873            .unwrap_or_default()
874            .unwrap_or_default();
875        let mut out_id = 0;
876        for rel in &mut rels.relationships {
877            if rel.id == r_id {
878                rel.r#type = rel_type.to_string();
879                rel.target = target.to_string();
880                rel.target_mode = Some(target_mode.to_string());
881                out_id = r_id.trim_start_matches("rId").parse().unwrap_or(0);
882                break;
883            }
884        }
885        self.relationships.insert(rel_path.to_string(), rels);
886        out_id
887    }
888
889    /// Add a relationship to a relationship part.
890    pub fn add_rels(&self, rel_path: &str, rel_type: &str, target: &str, target_mode: &str) -> i32 {
891        let uniq_part: HashMap<String, String> = [
892            (
893                SOURCE_RELATIONSHIP_CUSTOM_PROPERTIES.to_string(),
894                "/docProps/custom.xml".to_string(),
895            ),
896            (
897                SOURCE_RELATIONSHIP_SHARED_STRINGS.to_string(),
898                "/xl/sharedStrings.xml".to_string(),
899            ),
900        ]
901        .into_iter()
902        .collect();
903
904        let mut rels = self
905            .rels_reader(rel_path)
906            .unwrap_or_default()
907            .unwrap_or_default();
908        let mut r_id = 0;
909        for rel in &rels.relationships {
910            let id: i32 = rel.id.trim_start_matches("rId").parse().unwrap_or(0);
911            if id > r_id {
912                r_id = id;
913            }
914            if rel.r#type == rel_type {
915                if let Some(part_name) = uniq_part.get(&rel.r#type) {
916                    // This branch is handled by the caller updating the target.
917                    let _ = part_name;
918                }
919            }
920        }
921        r_id += 1;
922        let new_id = format!("rId{r_id}");
923        rels.relationships.push(XlsxRelationship {
924            id: new_id,
925            r#type: rel_type.to_string(),
926            target: target.to_string(),
927            target_mode: if target_mode.is_empty() {
928                None
929            } else {
930                Some(target_mode.to_string())
931            },
932        });
933        self.relationships.insert(rel_path.to_string(), rels);
934        r_id
935    }
936
937    /// Get the workbook XML path from the root relationships.
938    pub fn get_workbook_path(&self) -> String {
939        if let Ok(Some(rels)) = self.rels_reader(DEFAULT_XML_PATH_RELS) {
940            for rel in &rels.relationships {
941                if rel.r#type == crate::constants::SOURCE_RELATIONSHIP_OFFICE_DOCUMENT {
942                    return rel.target.trim_start_matches('/').to_string();
943                }
944            }
945        }
946        DEFAULT_XML_PATH_WORKBOOK.to_string()
947    }
948
949    /// Get the workbook relationships path.
950    pub fn get_workbook_rels_path(&self) -> String {
951        let wb = self.get_workbook_path();
952        let wb_dir = Path::new(&wb).parent().unwrap_or(Path::new("."));
953        let wb_base = Path::new(&wb).file_name().unwrap_or_default();
954        if wb_dir == Path::new(".") {
955            return format!("_rels/{}", wb_base.to_string_lossy()) + ".rels";
956        }
957        format!(
958            "{}/_rels/{}.rels",
959            wb_dir.to_string_lossy(),
960            wb_base.to_string_lossy()
961        )
962        .trim_start_matches('/')
963        .to_string()
964    }
965
966    /// Convert a relative relationship target to an absolute package path.
967    pub fn get_worksheet_path(&self, rel_target: &str) -> String {
968        let wb_path = self.get_workbook_path();
969        let wb_dir = Path::new(&wb_path).parent().unwrap_or(Path::new("."));
970        let mut combined = wb_dir.to_path_buf();
971        for c in rel_target.split('/') {
972            if c == ".." {
973                combined.pop();
974            } else if !c.is_empty() && c != "." {
975                combined.push(c);
976            }
977        }
978        let mut s = combined.to_string_lossy().replace('\\', "/");
979        s = s.trim_start_matches('/').to_string();
980        if rel_target.starts_with('/') {
981            s = Path::new(rel_target)
982                .to_string_lossy()
983                .replace('\\', "/")
984                .trim_start_matches('/')
985                .to_string();
986        }
987        s
988    }
989
990    /// Get the XML path for a worksheet by name.
991    pub fn get_sheet_xml_path(&self, sheet: &str) -> Option<String> {
992        let map = self.sheet_map.lock().unwrap();
993        for (name, path) in map.iter() {
994            if name.eq_ignore_ascii_case(sheet) {
995                return Some(path.clone());
996            }
997        }
998        None
999    }
1000
1001    /// Deserialize and return a worksheet by name.
1002    pub fn work_sheet_reader(&self, sheet: &str) -> Result<XlsxWorksheet> {
1003        crate::excelize::check_sheet_name(sheet)?;
1004        let name = self.get_sheet_xml_path(sheet).ok_or_else(|| {
1005            Box::new(crate::errors::ErrSheetNotExist {
1006                sheet_name: sheet.to_string(),
1007            }) as Box<dyn std::error::Error + Send + Sync>
1008        })?;
1009        if let Some(ws) = self.sheet.get(&name) {
1010            return Ok(ws.clone());
1011        }
1012        let data = self.apply_charset_transcoder(&self.read_bytes(&name))?;
1013        let data = namespace_strict_to_transitional(&data);
1014        let mut data = data;
1015        let ext_lst = extract_ext_lst(&data);
1016        if ext_lst.is_some() {
1017            remove_ext_lst(&mut data);
1018        }
1019        let mut ws: XlsxWorksheet = xml_from_reader(data.as_slice())?;
1020        if let Some(xml) = ext_lst {
1021            ws.ext_lst = Some(crate::xml::common::parse_ext_lst_content(&xml)?);
1022        }
1023        if !self.checked.contains_key(&name) {
1024            // Worksheet validation is intentionally minimal in this phase.
1025            self.checked.insert(name.clone(), true);
1026        }
1027        self.sheet.insert(name.clone(), ws.clone());
1028        Ok(ws)
1029    }
1030
1031    // ------------------------------------------------------------------
1032    // Validation helpers
1033    // ------------------------------------------------------------------
1034
1035    pub(crate) fn check_open_reader_options(&self) -> Result<()> {
1036        let mut opts = self.options.lock().unwrap();
1037        if opts.unzip_size_limit == 0 {
1038            opts.unzip_size_limit = opts.unzip_xml_size_limit.max(UNZIP_SIZE_LIMIT);
1039        }
1040        if opts.unzip_xml_size_limit == 0 {
1041            opts.unzip_xml_size_limit = opts.unzip_size_limit.min(STREAM_CHUNK_SIZE);
1042        }
1043        if opts.unzip_xml_size_limit > opts.unzip_size_limit {
1044            return Err(Box::new(ErrOptionsUnzipSizeLimit));
1045        }
1046        let patterns = [
1047            opts.short_date_pattern.clone(),
1048            opts.long_date_pattern.clone(),
1049            opts.long_time_pattern.clone(),
1050        ];
1051        drop(opts);
1052        self.check_date_time_pattern(&patterns)
1053    }
1054
1055    fn check_date_time_pattern(&self, patterns: &[String]) -> Result<()> {
1056        for pattern in patterns {
1057            if !pattern.is_empty() && !numfmt::is_date_time_pattern(pattern) {
1058                return Err(Box::new(crate::errors::ErrUnsupportedNumberFormat));
1059            }
1060        }
1061        Ok(())
1062    }
1063
1064    // ------------------------------------------------------------------
1065    // Writers
1066    // ------------------------------------------------------------------
1067
1068    /// Serialize the workbook to an in-memory byte buffer.
1069    pub fn write_to_buffer(&self) -> Result<Vec<u8>> {
1070        let mut buf = Cursor::new(Vec::new());
1071        let factory = self.zip_writer_factory.lock().unwrap().clone();
1072        if let Some(factory) = factory {
1073            let mut zw = factory(&mut buf);
1074            self.write_to_zip(&mut *zw)?;
1075            zw.finish()?;
1076        } else {
1077            let mut zw = Box::new(zip::ZipWriter::new(&mut buf));
1078            self.write_to_zip(&mut *zw)?;
1079            zw.finish()?;
1080        }
1081        let mut inner = buf.into_inner();
1082        self.write_zip64_lfh(&mut inner)?;
1083        let opts = self.options.lock().unwrap().clone();
1084        if !opts.password.is_empty() {
1085            inner = crypt::encrypt(&inner, &opts)?;
1086        }
1087        Ok(inner)
1088    }
1089
1090    fn write_to_zip(&self, zw: &mut dyn ZipWriter) -> Result<()> {
1091        self.calc_chain_writer();
1092        self.comments_writer();
1093        self.shared_strings_registrar();
1094        self.content_types_writer();
1095        self.drawings_writer();
1096        self.volatile_deps_writer();
1097        self.vml_drawing_writer();
1098        self.workbook_writer();
1099        self.work_sheet_writer();
1100        self.rels_writer();
1101        let _ = self.shared_strings_loader();
1102        self.shared_strings_writer();
1103        self.style_sheet_writer();
1104        self.theme_writer();
1105
1106        // Write package parts in reverse alphabetical order (matches Go behavior).
1107        let mut files: Vec<String> = self.pkg.iter().map(|e| e.key().clone()).collect();
1108        files.sort_unstable_by(|a, b| b.cmp(a));
1109        for path in files {
1110            let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
1111            zw.start_file(&path, opts)?;
1112            let content = self.read_xml(&path);
1113            zw.write_all(&content)?;
1114            if content.len() as u64 > u32::MAX as u64 {
1115                self.zip64_entries.lock().unwrap().push(path.clone());
1116            }
1117        }
1118
1119        // Write any parts that only exist as temporary files.
1120        let mut temp_files: Vec<String> = self
1121            .temp_files
1122            .iter()
1123            .filter(|e| !self.pkg.contains_key(e.key()))
1124            .map(|e| e.key().clone())
1125            .collect();
1126        temp_files.sort_unstable_by(|a, b| b.cmp(a));
1127        for path in temp_files {
1128            let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
1129            zw.start_file(&path, opts)?;
1130            let content = self.read_bytes(&path);
1131            zw.write_all(&content)?;
1132            if content.len() as u64 > u32::MAX as u64 {
1133                self.zip64_entries.lock().unwrap().push(path.clone());
1134            }
1135        }
1136
1137        // Write worksheet parts produced by streaming writers directly from
1138        // their temporary files without loading them into memory.
1139        let mut streams: Vec<(String, PathBuf)> = self
1140            .streams
1141            .borrow_mut()
1142            .drain()
1143            .map(|(path, state)| (path, state.tmp_path))
1144            .collect();
1145        streams.sort_unstable_by(|a, b| b.0.cmp(&a.0));
1146        for (path, tmp_path) in streams {
1147            let opts = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
1148            zw.start_file(&path, opts)?;
1149            let mut file = fs::File::open(&tmp_path)?;
1150            let size = file.metadata()?.len();
1151            let mut buf = [0u8; 8192];
1152            loop {
1153                let n = file.read(&mut buf)?;
1154                if n == 0 {
1155                    break;
1156                }
1157                zw.write_all(&buf[..n])?;
1158            }
1159            if size > u32::MAX as u64 {
1160                self.zip64_entries.lock().unwrap().push(path.clone());
1161            }
1162            let _ = fs::remove_file(&tmp_path);
1163        }
1164        Ok(())
1165    }
1166
1167    fn write_zip64_lfh(&self, buf: &mut [u8]) -> Result<()> {
1168        let entries = self.zip64_entries.lock().unwrap().clone();
1169        if entries.is_empty() {
1170            return Ok(());
1171        }
1172        let mut offset = 0usize;
1173        while offset < buf.len() {
1174            let window = &buf[offset..];
1175            let Some(idx) = find_subsequence(window, b"\x50\x4b\x03\x04") else {
1176                break;
1177            };
1178            let idx = idx + offset;
1179            if idx + 30 > buf.len() {
1180                break;
1181            }
1182            let filename_len = u16::from_le_bytes([buf[idx + 26], buf[idx + 27]]) as usize;
1183            if idx + 30 + filename_len > buf.len() {
1184                break;
1185            }
1186            let filename =
1187                std::str::from_utf8(&buf[idx + 30..idx + 30 + filename_len]).unwrap_or("");
1188            if in_str_slice(&entries, filename, true) != -1 {
1189                buf[idx + 4..idx + 6].copy_from_slice(&45u16.to_le_bytes());
1190            }
1191            offset = idx + 1;
1192        }
1193        Ok(())
1194    }
1195
1196    // Calculation chain writer moved to `calc_chain.rs`.
1197
1198    fn comments_writer(&self) {
1199        for entry in self.comments.iter() {
1200            let path = entry.key().clone();
1201            let comments = entry.value().clone();
1202            if let Ok(output) = quick_xml::se::to_string(&comments).map(|s| s.into_bytes()) {
1203                self.save_file_list(&path, &output);
1204            }
1205        }
1206    }
1207
1208    fn content_types_writer(&self) {
1209        if let Some(ct) = self.content_types.lock().unwrap().clone() {
1210            if let Ok(mut output) = xml_to_string(&ct).map(|s| s.into_bytes()) {
1211                self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_CONTENT_TYPES, &mut output);
1212                self.save_file_list(DEFAULT_XML_PATH_CONTENT_TYPES, &output);
1213            }
1214        }
1215    }
1216
1217    fn drawings_writer(&self) {
1218        for entry in self.drawings.iter() {
1219            let path = entry.key().clone();
1220            let drawing = entry.value().clone();
1221            if let Ok(mut output) = xml_to_string(&drawing).map(|s| s.into_bytes()) {
1222                self.replace_namespace_bytes_if_needed(&path, &mut output);
1223                self.save_file_list(&path, &output);
1224            }
1225        }
1226    }
1227
1228    // Volatile dependencies writer moved to `calc_chain.rs`.
1229
1230    fn vml_drawing_writer(&self) {
1231        for entry in self.vml_drawing.iter() {
1232            let path = entry.key().clone();
1233            let drawing = entry.value().clone();
1234            let output = drawing.to_xml();
1235            self.pkg.insert(path, output);
1236        }
1237    }
1238
1239    fn workbook_writer(&self) {
1240        if let Some(mut wb) = self.workbook.lock().unwrap().clone() {
1241            // If the workbook was read with the strict-namespace decoder, move
1242            // the decoded alternate content back to the serialized element so
1243            // the compatibility namespace is preserved on save.
1244            if let Some(decode) = wb.decode_alternate_content.take() {
1245                wb.alternate_content = Some(crate::xml::workbook::XlsxAlternateContent {
1246                    xmlns_mc: Some(
1247                        "http://schemas.openxmlformats.org/markup-compatibility/2006".to_string(),
1248                    ),
1249                    content: decode.content,
1250                });
1251            }
1252            if let Ok(mut output) = xml_to_string(&wb).map(|s| s.into_bytes()) {
1253                let path = self.get_workbook_path();
1254                self.replace_namespace_bytes_if_needed(&path, &mut output);
1255                self.save_file_list(&path, &output);
1256            }
1257        }
1258    }
1259
1260    fn work_sheet_writer(&self) {
1261        for entry in self.sheet.iter() {
1262            let path = entry.key().clone();
1263            let sheet = entry.value().clone();
1264            if let Ok(mut output) = xml_to_string(&sheet).map(|s| s.into_bytes()) {
1265                self.replace_namespace_bytes_if_needed(&path, &mut output);
1266                strip_empty_attributes(&mut output);
1267                if let Some(ext_lst) = &sheet.ext_lst {
1268                    let ext_xml = crate::xml::common::serialize_ext_lst(ext_lst);
1269                    if !ext_xml.is_empty() {
1270                        inject_ext_lst(&mut output, &ext_xml);
1271                    }
1272                }
1273                self.save_file_list(&path, &output);
1274            }
1275        }
1276    }
1277
1278    fn rels_writer(&self) {
1279        for entry in self.relationships.iter() {
1280            let path = entry.key().clone();
1281            let rels = entry.value().clone();
1282            if let Ok(mut output) = xml_to_string(&rels).map(|s| s.into_bytes()) {
1283                self.replace_namespace_bytes_if_needed(&path, &mut output);
1284                if !output.windows(6).any(|w| w == b"xmlns=".as_slice()) {
1285                    let _ = replace_root_namespace_attributes(
1286                        &mut output,
1287                        "xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"",
1288                    );
1289                }
1290                strip_empty_attributes(&mut output);
1291                self.save_file_list(&path, &output);
1292            }
1293        }
1294    }
1295
1296    fn shared_strings_loader(&self) -> Result<()> {
1297        if let Some(entry) = self.temp_files.get(DEFAULT_XML_PATH_SHARED_STRINGS) {
1298            let temp_path = entry.value().clone();
1299            drop(entry);
1300            let data = self.read_bytes(DEFAULT_XML_PATH_SHARED_STRINGS);
1301            if !data.is_empty() {
1302                self.pkg
1303                    .insert(DEFAULT_XML_PATH_SHARED_STRINGS.to_string(), data);
1304            }
1305            self.temp_files.remove(DEFAULT_XML_PATH_SHARED_STRINGS);
1306            let _ = fs::remove_file(temp_path);
1307            *self.shared_strings.lock().unwrap() = None;
1308            self.shared_strings_map.lock().unwrap().clear();
1309        }
1310        Ok(())
1311    }
1312
1313    /// Ensure `[Content_Types].xml` and the workbook relationships reference
1314    /// the shared string table when it will be written to the package.
1315    fn shared_strings_registrar(&self) {
1316        let has_strings = self.shared_strings.lock().unwrap().is_some()
1317            || self.temp_files.contains_key(DEFAULT_XML_PATH_SHARED_STRINGS);
1318        if !has_strings {
1319            return;
1320        }
1321        if let Ok(mut ct) = self.content_types_reader() {
1322            let part_name = format!("/{DEFAULT_XML_PATH_SHARED_STRINGS}");
1323            let exists = ct.entries.iter().any(|e| {
1324                matches!(e, crate::xml::content_types::XlsxContentTypeEntry::Override(o) if o.part_name == part_name)
1325            });
1326            if !exists {
1327                ct.entries
1328                    .push(crate::xml::content_types::XlsxContentTypeEntry::Override(
1329                        crate::xml::content_types::XlsxOverride {
1330                            part_name,
1331                            content_type:
1332                                crate::constants::CONTENT_TYPE_SPREADSHEET_ML_SHARED_STRINGS
1333                                    .to_string(),
1334                        },
1335                    ));
1336                *self.content_types.lock().unwrap() = Some(ct);
1337            }
1338        }
1339        let rels_path = self.get_workbook_rels_path();
1340        let mut rels = self.rels_reader(&rels_path).unwrap_or_default().unwrap_or_default();
1341        crate::sheet::ensure_shared_strings_rel(&mut rels);
1342        self.relationships.insert(rels_path, rels);
1343    }
1344
1345    fn shared_strings_writer(&self) {
1346        if let Some(sst) = self.shared_strings.lock().unwrap().clone() {
1347            if let Ok(output) = xml_to_string(&sst).map(|s| s.into_bytes()) {
1348                self.save_file_list(DEFAULT_XML_PATH_SHARED_STRINGS, &output);
1349            }
1350        }
1351    }
1352
1353    fn style_sheet_writer(&self) {
1354        if let Some(st) = self.styles.lock().unwrap().clone() {
1355            if let Ok(mut output) = xml_to_string(&st).map(|s| s.into_bytes()) {
1356                self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_STYLES, &mut output);
1357                strip_empty_attributes(&mut output);
1358                self.save_file_list(DEFAULT_XML_PATH_STYLES, &output);
1359            }
1360        }
1361    }
1362
1363    fn theme_writer(&self) {
1364        if let Some(theme) = self.theme.lock().unwrap().clone() {
1365            let serialized = decode_theme_to_xlsx_theme(&theme);
1366            if let Ok(mut output) = xml_to_string(&serialized).map(|s| s.into_bytes()) {
1367                self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_THEME, &mut output);
1368                self.save_file_list(DEFAULT_XML_PATH_THEME, &output);
1369            }
1370        }
1371    }
1372
1373    // ------------------------------------------------------------------
1374    // Namespace helpers
1375    // ------------------------------------------------------------------
1376
1377    pub(crate) fn replace_namespace_bytes_if_needed(&self, path: &str, content: &mut Vec<u8>) {
1378        if let Some(attrs) = self.xml_attr.get(path) {
1379            let _ = replace_root_namespace_attributes(content, attrs.value());
1380        }
1381    }
1382
1383    /// Register a namespace attribute for the given XML part so that it is
1384    /// written back when the part is serialized.
1385    ///
1386    /// `ns` is the namespace URI; the prefix is looked up from the project's
1387    /// namespace dictionary. For known extension namespaces the `mc:Ignorable`
1388    /// attribute is also updated so Excel compatibility markup stays valid.
1389    pub fn add_name_spaces(&self, path: &str, ns: &str) {
1390        let prefix = match ns {
1391            crate::constants::SOURCE_RELATIONSHIP => "r",
1392            crate::constants::NAMESPACE_SPREADSHEET_X14 => "x14",
1393            crate::constants::NAMESPACE_SPREADSHEET_X15 => "x15",
1394            crate::constants::NAMESPACE_SPREADSHEET_EXCEL_2006_MAIN => "xm",
1395            crate::constants::NAMESPACE_DRAWING_ML_MAIN => "a",
1396            crate::constants::NAMESPACE_DRAWING_ML_CHART => "c",
1397            crate::constants::NAMESPACE_DRAWING_ML_SPREADSHEET => "xdr",
1398            crate::constants::NAMESPACE_DRAWING_ML_A14 => "a14",
1399            crate::constants::NAMESPACE_DRAWING_2016_SVG => "asvg",
1400            // Default spreadsheet namespace is already present on worksheet/workbook roots.
1401            crate::constants::NAMESPACE_SPREADSHEET => return,
1402            _ => return,
1403        };
1404        if prefix.is_empty() {
1405            return;
1406        }
1407
1408        let attr_name = format!("xmlns:{prefix}");
1409        let mut attrs = self
1410            .xml_attr
1411            .get(path)
1412            .map(|a| a.clone())
1413            .unwrap_or_default();
1414        if !attrs.contains(&attr_name) {
1415            if !attrs.is_empty() {
1416                attrs.push(' ');
1417            }
1418            attrs.push_str(&format!("{attr_name}=\"{ns}\""));
1419        }
1420
1421        // Ensure the markup-compatibility namespace is present and mark the
1422        // extension namespace as ignorable when appropriate.
1423        if self.needs_ignorable_prefix(prefix) {
1424            let mc_ns = "http://schemas.openxmlformats.org/markup-compatibility/2006";
1425            if !attrs.contains("xmlns:mc") {
1426                if !attrs.is_empty() && !attrs.ends_with(' ') {
1427                    attrs.push(' ');
1428                }
1429                attrs.push_str(&format!("xmlns:mc=\"{mc_ns}\""));
1430            }
1431            self.set_ignorable_name_space(path, prefix, &mut attrs);
1432        }
1433
1434        self.xml_attr.insert(path.to_string(), attrs);
1435    }
1436
1437    fn needs_ignorable_prefix(&self, prefix: &str) -> bool {
1438        const IGNORABLE_NS: &[&str] = &[
1439            "c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14",
1440            "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9",
1441            "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x16", "x16r2", "mo", "mx", "mv", "o",
1442            "v",
1443        ];
1444        IGNORABLE_NS.contains(&prefix)
1445    }
1446
1447    fn set_ignorable_name_space(&self, _path: &str, prefix: &str, attrs: &mut String) {
1448        let marker = "mc:Ignorable=\"";
1449        if let Some(start) = attrs.find(marker) {
1450            let value_start = start + marker.len();
1451            if let Some(end) = attrs[value_start..].find('"') {
1452                let value = &attrs[value_start..value_start + end];
1453                let parts: Vec<&str> = value.split_whitespace().collect();
1454                if !parts.contains(&prefix) {
1455                    let new_value = format!("{} {}", value, prefix);
1456                    attrs.replace_range(value_start..value_start + end, &new_value);
1457                }
1458                return;
1459            }
1460        }
1461        if !attrs.is_empty() && !attrs.ends_with(' ') {
1462            attrs.push(' ');
1463        }
1464        attrs.push_str(&format!("mc:Ignorable=\"{prefix}\""));
1465    }
1466
1467    /// Register a namespace attribute for a worksheet XML part.
1468    pub(crate) fn add_sheet_name_space(&self, sheet: &str, ns: &str) {
1469        if let Some(path) = self.get_sheet_xml_path(sheet) {
1470            self.add_name_spaces(&path, ns);
1471        }
1472    }
1473
1474    /// Return the sheet ID (1-based) for a worksheet name.
1475    pub fn get_sheet_id(&self, sheet: &str) -> i32 {
1476        if let Ok(wb) = self.workbook_reader() {
1477            for s in &wb.sheets.sheet {
1478                if s.name.as_deref().unwrap_or("").eq_ignore_ascii_case(sheet) {
1479                    return s.sheet_id.unwrap_or(0) as i32;
1480                }
1481            }
1482        }
1483        -1
1484    }
1485
1486    /// Expand a 3D sheet range (e.g. `Sheet1:Sheet3`) into the ordered list of
1487    /// worksheet names between the two sheets inclusive.
1488    pub fn expand_3d_sheet_range(&self, sheet1: &str, sheet2: &str) -> Result<Vec<String>> {
1489        let mut idx1 = self.get_sheet_index(sheet1)?;
1490        let mut idx2 = self.get_sheet_index(sheet2)?;
1491        if idx1 > idx2 {
1492            std::mem::swap(&mut idx1, &mut idx2);
1493        }
1494        let list = self.get_sheet_list();
1495        Ok(list[(idx1 - 1) as usize..idx2 as usize].to_vec())
1496    }
1497
1498    /// Clear the in-memory calc cache so the chain is rewritten on save.
1499    pub fn clear_calc_cache(&self) {
1500        let _ = self.calc_chain.lock().unwrap().take();
1501        self.calc_cache.lock().unwrap().clear();
1502        self.calc_raw_cache.lock().unwrap().clear();
1503        self.formula_arg_cache.lock().unwrap().clear();
1504    }
1505
1506    /// Add a content type override for a numbered part.
1507    pub fn add_content_type_part(&self, id: i32, part: &str) -> Result<()> {
1508        match part {
1509            "comments" => self.set_content_type_part_vml_extensions()?,
1510            "drawings" => crate::sheet::set_content_type_part_image_extensions(self)?,
1511            _ => {}
1512        }
1513        let mut ct = self.content_types_reader()?;
1514        let content_type = match part {
1515            "table" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_TABLE,
1516            "pivotTable" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_PIVOT_TABLE,
1517            "pivotCache" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_PIVOT_CACHE_DEFINITION,
1518            "slicer" => crate::constants::CONTENT_TYPE_SLICER,
1519            "slicerCache" => crate::constants::CONTENT_TYPE_SLICER_CACHE,
1520            "drawings" => crate::constants::CONTENT_TYPE_DRAWING,
1521            "chart" => crate::constants::CONTENT_TYPE_DRAWING_ML,
1522            "chartsheet" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_CHARTSHEET,
1523            "comments" => crate::constants::CONTENT_TYPE_SPREADSHEET_ML_COMMENTS,
1524            _ => return Ok(()),
1525        };
1526        let part_name = match part {
1527            "table" => format!("/xl/tables/table{id}.xml"),
1528            "pivotTable" => format!("/xl/pivotTables/pivotTable{id}.xml"),
1529            "pivotCache" => format!("/xl/pivotCache/pivotCacheDefinition{id}.xml"),
1530            "slicer" => format!("/xl/slicers/slicer{id}.xml"),
1531            "slicerCache" => format!("/xl/slicerCaches/slicerCache{id}.xml"),
1532            "drawings" => format!("/xl/drawings/drawing{id}.xml"),
1533            "chart" => format!("/xl/charts/chart{id}.xml"),
1534            "chartsheet" => format!("/xl/chartsheets/sheet{id}.xml"),
1535            "comments" => format!("/xl/comments{id}.xml"),
1536            _ => return Ok(()),
1537        };
1538        for entry in &ct.entries {
1539            if let crate::xml::content_types::XlsxContentTypeEntry::Override(o) = entry {
1540                if o.part_name == part_name {
1541                    return Ok(());
1542                }
1543            }
1544        }
1545        ct.entries
1546            .push(crate::xml::content_types::XlsxContentTypeEntry::Override(
1547                crate::xml::content_types::XlsxOverride {
1548                    part_name,
1549                    content_type: content_type.to_string(),
1550                },
1551            ));
1552        *self.content_types.lock().unwrap() = Some(ct);
1553        self.set_content_type_part_rels_extensions()
1554    }
1555
1556    /// Ensure `[Content_Types].xml` contains the default relationship content type.
1557    pub(crate) fn set_content_type_part_rels_extensions(&self) -> Result<()> {
1558        let mut ct = self.content_types_reader()?;
1559        let exists = ct.entries.iter().any(|e| {
1560            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
1561                d.extension == "rels"
1562            } else {
1563                false
1564            }
1565        });
1566        if !exists {
1567            ct.entries
1568                .push(crate::xml::content_types::XlsxContentTypeEntry::Default(
1569                    crate::xml::content_types::XlsxDefault {
1570                        extension: "rels".to_string(),
1571                        content_type: crate::constants::CONTENT_TYPE_RELATIONSHIPS.to_string(),
1572                    },
1573                ));
1574        }
1575        *self.content_types.lock().unwrap() = Some(ct);
1576        Ok(())
1577    }
1578
1579    /// Ensure `[Content_Types].xml` contains the default VML content type.
1580    pub(crate) fn set_content_type_part_vml_extensions(&self) -> Result<()> {
1581        let mut ct = self.content_types_reader()?;
1582        let exists = ct.entries.iter().any(|e| {
1583            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
1584                d.extension == "vml"
1585            } else {
1586                false
1587            }
1588        });
1589        if !exists {
1590            ct.entries
1591                .push(crate::xml::content_types::XlsxContentTypeEntry::Default(
1592                    crate::xml::content_types::XlsxDefault {
1593                        extension: "vml".to_string(),
1594                        content_type: crate::constants::CONTENT_TYPE_VML.to_string(),
1595                    },
1596                ));
1597        }
1598        *self.content_types.lock().unwrap() = Some(ct);
1599        Ok(())
1600    }
1601
1602    /// Remove a content type override by content type and part name prefix.
1603    pub fn remove_content_types_part(&self, content_type: &str, part_name: &str) -> Result<()> {
1604        let mut ct = self.content_types_reader()?;
1605        ct.entries.retain(|e| {
1606            if let crate::xml::content_types::XlsxContentTypeEntry::Override(o) = e {
1607                !(o.content_type == content_type && o.part_name == part_name)
1608            } else {
1609                true
1610            }
1611        });
1612        *self.content_types.lock().unwrap() = Some(ct);
1613        Ok(())
1614    }
1615
1616    /// Return the relationship target for a worksheet relationship by rId.
1617    pub fn get_sheet_relationships_target_by_id(&self, sheet: &str, r_id: &str) -> String {
1618        let Some(sheet_xml_path) = self.get_sheet_xml_path(sheet) else {
1619            return String::new();
1620        };
1621        let sheet_rels = format!(
1622            "xl/worksheets/_rels/{}.rels",
1623            sheet_xml_path.trim_start_matches("xl/worksheets/")
1624        );
1625        if let Ok(Some(rels)) = self.rels_reader(&sheet_rels) {
1626            for rel in &rels.relationships {
1627                if rel.id == r_id {
1628                    return rel.target.clone();
1629                }
1630            }
1631        }
1632        String::new()
1633    }
1634
1635    /// Add a legacy drawing reference to a worksheet.
1636    pub fn add_sheet_legacy_drawing(&self, sheet: &str, r_id: i32) -> Result<()> {
1637        let mut ws = self.work_sheet_reader(sheet)?;
1638        ws.legacy_drawing = Some(crate::xml::worksheet::XlsxLegacyDrawing {
1639            rid: Some(format!("rId{r_id}")),
1640        });
1641        if let Some(path) = self.get_sheet_xml_path(sheet) {
1642            self.sheet.insert(path, ws);
1643        }
1644        Ok(())
1645    }
1646
1647    /// Add a legacy header/footer drawing reference to a worksheet.
1648    pub fn add_sheet_legacy_drawing_hf(&self, sheet: &str, r_id: i32) -> Result<()> {
1649        let mut ws = self.work_sheet_reader(sheet)?;
1650        ws.legacy_drawing_hf = Some(crate::xml::worksheet::XlsxLegacyDrawingHF {
1651            rid: Some(format!("rId{r_id}")),
1652        });
1653        if let Some(path) = self.get_sheet_xml_path(sheet) {
1654            self.sheet.insert(path, ws);
1655        }
1656        Ok(())
1657    }
1658
1659    /// Count existing comments parts in the package.
1660    pub fn count_comments(&self) -> i32 {
1661        let mut count = 0;
1662        for entry in self.pkg.iter() {
1663            if entry.key().contains("xl/comments") {
1664                count += 1;
1665            }
1666        }
1667        for entry in self.comments.iter() {
1668            if entry.key().contains("xl/comments") && !self.pkg.contains_key(entry.key()) {
1669                count += 1;
1670            }
1671        }
1672        count
1673    }
1674
1675    /// Count existing VML drawing parts in the package.
1676    pub fn count_vml_drawing(&self) -> i32 {
1677        let mut count = 0;
1678        for entry in self.pkg.iter() {
1679            if entry.key().contains("xl/drawings/vmlDrawing") {
1680                count += 1;
1681            }
1682        }
1683        for entry in self.vml_drawing.iter() {
1684            if entry.key().contains("xl/drawings/vmlDrawing") && !self.pkg.contains_key(entry.key())
1685            {
1686                count += 1;
1687            }
1688        }
1689        count
1690    }
1691
1692    /// Lazy reader for comments parts.
1693    pub fn comments_reader(
1694        &self,
1695        path: &str,
1696    ) -> Result<Option<crate::xml::comments::XlsxComments>> {
1697        if let Some(cmts) = self.comments.get(path) {
1698            return Ok(Some(cmts.clone()));
1699        }
1700        if self.pkg.contains_key(path) || self.temp_files.contains_key(path) {
1701            let data = self.apply_charset_transcoder(&self.read_bytes(path))?;
1702            let data = crate::file::namespace_strict_to_transitional(&data);
1703            let mut cmts: crate::xml::comments::XlsxComments =
1704                quick_xml::de::from_reader(data.as_slice()).unwrap_or_default();
1705            for cmt in &cmts.comment_list.comment {
1706                cmts.cells.push(cmt.r#ref.clone());
1707            }
1708            self.comments.insert(path.to_string(), cmts.clone());
1709            return Ok(Some(cmts));
1710        }
1711        Ok(None)
1712    }
1713
1714    /// Lazy reader for VML drawing parts.
1715    pub fn vml_drawing_reader(&self, path: &str) -> Result<Option<crate::xml::vml::VmlDrawing>> {
1716        if let Some(vml) = self.vml_drawing.get(path) {
1717            return Ok(Some(vml.clone()));
1718        }
1719        if self.pkg.contains_key(path) || self.temp_files.contains_key(path) {
1720            let data = self.apply_charset_transcoder(&self.read_bytes(path))?;
1721            let data = crate::file::namespace_strict_to_transitional(&data);
1722            let data = String::from_utf8_lossy(&data)
1723                .replace("<br>\r\n", "<br></br>\r\n")
1724                .into_bytes();
1725            let vml = crate::xml::vml::VmlDrawing::from_xml(&data).unwrap_or_default();
1726            self.vml_drawing.insert(path.to_string(), vml.clone());
1727            return Ok(Some(vml));
1728        }
1729        Ok(None)
1730    }
1731
1732    /// Delete a worksheet relationship by rId.
1733    pub fn delete_sheet_relationships(&self, sheet: &str, r_id: &str) {
1734        let Some(sheet_xml_path) = self.get_sheet_xml_path(sheet) else {
1735            return;
1736        };
1737        let sheet_rels = format!(
1738            "xl/worksheets/_rels/{}.rels",
1739            sheet_xml_path.trim_start_matches("xl/worksheets/")
1740        );
1741        if let Ok(Some(mut rels)) = self.rels_reader(&sheet_rels) {
1742            rels.relationships.retain(|r| r.id != r_id);
1743            self.relationships.insert(sheet_rels, rels);
1744        }
1745    }
1746
1747    /// Delete a workbook relationship by type and target, returning its rId.
1748    pub fn delete_workbook_rels(&self, rel_type: &str, target: &str) -> Result<String> {
1749        let rel_path = self.get_workbook_rels_path();
1750        let mut rels = self.rels_reader(&rel_path)?.unwrap_or_default();
1751        let mut r_id = String::new();
1752        rels.relationships.retain(|r| {
1753            if r.r#type == rel_type && (r.target == target || r.target == format!("/{target}")) {
1754                r_id = r.id.clone();
1755                false
1756            } else {
1757                true
1758            }
1759        });
1760        self.relationships.insert(rel_path, rels);
1761        Ok(r_id)
1762    }
1763
1764    /// Add a drawing relationship reference to a worksheet.
1765    pub fn add_sheet_drawing(&self, sheet: &str, r_id: i32) -> Result<()> {
1766        let mut ws = self.work_sheet_reader(sheet)?;
1767        ws.drawing = Some(crate::xml::worksheet::XlsxDrawing {
1768            rid: Some(format!("rId{r_id}")),
1769        });
1770        self.sheet
1771            .insert(self.get_sheet_xml_path(sheet).unwrap_or_default(), ws);
1772        Ok(())
1773    }
1774
1775    /// Count existing table parts in the package.
1776    pub fn count_tables(&self) -> i32 {
1777        let mut count = 0i32;
1778        for entry in self.pkg.iter() {
1779            let k = entry.key();
1780            if k.contains("xl/tables/tableSingleCells") {
1781                if let Ok(data) = self.apply_charset_transcoder(entry.value()) {
1782                    let data = namespace_strict_to_transitional(&data);
1783                    match xml_from_reader::<_, XlsxSingleXmlCells>(data.as_slice()) {
1784                        Ok(cells) => {
1785                            for cell in cells.single_xml_cell {
1786                                if count < cell.id as i32 {
1787                                    count = cell.id as i32;
1788                                }
1789                            }
1790                        }
1791                        Err(_) => count += 1,
1792                    }
1793                }
1794            }
1795            if k.contains("xl/tables/table") {
1796                if let Ok(data) = self.apply_charset_transcoder(entry.value()) {
1797                    let data = namespace_strict_to_transitional(&data);
1798                    match xml_from_reader::<_, XlsxTable>(data.as_slice()) {
1799                        Ok(t) => {
1800                            if count < t.id as i32 {
1801                                count = t.id as i32;
1802                            }
1803                        }
1804                        Err(_) => count += 1,
1805                    }
1806                }
1807            }
1808        }
1809        count
1810    }
1811
1812    /// Count existing drawing parts in the package.
1813    pub fn count_drawings(&self) -> i32 {
1814        let mut count = 0;
1815        for entry in self.pkg.iter() {
1816            if entry.key().contains("xl/drawings/drawing") {
1817                count += 1;
1818            }
1819        }
1820        for entry in self.drawings.iter() {
1821            if entry.key().contains("xl/drawings/drawing") && !self.pkg.contains_key(entry.key()) {
1822                count += 1;
1823            }
1824        }
1825        count
1826    }
1827
1828    /// Count existing chart parts in the package.
1829    pub fn count_charts(&self) -> i32 {
1830        let mut count = 0;
1831        for entry in self.pkg.iter() {
1832            if entry.key().contains("xl/charts/chart") {
1833                count += 1;
1834            }
1835        }
1836        count
1837    }
1838
1839    /// Count existing pivot table parts in the package.
1840    pub fn count_pivot_tables(&self) -> i32 {
1841        let mut count = 0;
1842        for entry in self.pkg.iter() {
1843            if entry.key().contains("xl/pivotTables/pivotTable") {
1844                count += 1;
1845            }
1846        }
1847        count
1848    }
1849
1850    /// Count existing pivot cache parts in the package.
1851    pub fn count_pivot_cache(&self) -> i32 {
1852        let mut count = 0;
1853        for entry in self.pkg.iter() {
1854            if entry.key().contains("xl/pivotCache/pivotCacheDefinition") {
1855                count += 1;
1856            }
1857        }
1858        count
1859    }
1860
1861    /// Count existing slicer parts in the package.
1862    pub fn count_slicers(&self) -> i32 {
1863        let mut count = 0;
1864        for entry in self.pkg.iter() {
1865            if entry.key().contains("xl/slicers/slicer") {
1866                count += 1;
1867            }
1868        }
1869        count
1870    }
1871
1872    /// Count existing slicer cache parts in the package.
1873    pub fn count_slicer_cache(&self) -> i32 {
1874        let mut count = 0;
1875        for entry in self.pkg.iter() {
1876            if entry.key().contains("xl/slicerCaches/slicerCache") {
1877                count += 1;
1878            }
1879        }
1880        count
1881    }
1882
1883    /// Return all defined names in the workbook.
1884    pub fn get_defined_names(&self) -> Result<Vec<crate::xml::workbook::DefinedName>> {
1885        let mut out = Vec::new();
1886        if let Ok(wb) = self.workbook_reader() {
1887            if let Some(dns) = wb.defined_names {
1888                for dn in dns.defined_name {
1889                    out.push(crate::xml::workbook::DefinedName {
1890                        name: dn.name.clone().unwrap_or_default(),
1891                        comment: dn.comment.clone().unwrap_or_default(),
1892                        refers_to: dn.data.clone(),
1893                        scope: dn
1894                            .local_sheet_id
1895                            .map(|id| {
1896                                if let Ok(wb2) = self.workbook_reader() {
1897                                    if let Some(s) = wb2.sheets.sheet.get(id as usize) {
1898                                        return s.name.clone().unwrap_or_default();
1899                                    }
1900                                }
1901                                String::new()
1902                            })
1903                            .unwrap_or_else(|| "Workbook".to_string()),
1904                    });
1905                }
1906            }
1907        }
1908        Ok(out)
1909    }
1910
1911    /// Add a defined name.
1912    pub fn set_defined_name(&self, dn: &crate::xml::workbook::DefinedName) -> Result<()> {
1913        let mut wb = self.workbook_reader()?;
1914        if wb.defined_names.is_none() {
1915            wb.defined_names = Some(crate::xml::workbook::XlsxDefinedNames::default());
1916        }
1917        let names = wb.defined_names.as_mut().unwrap();
1918        let local_sheet_id = if dn.scope.is_empty() || dn.scope == "Workbook" {
1919            None
1920        } else {
1921            Some((self.get_sheet_index(&dn.scope)? - 1) as i64)
1922        };
1923        for existing in &names.defined_name {
1924            if existing.name.as_deref().unwrap_or("") == dn.name
1925                && existing.local_sheet_id == local_sheet_id
1926            {
1927                return Err(Box::new(ErrDefinedNameDuplicate));
1928            }
1929        }
1930        names
1931            .defined_name
1932            .push(crate::xml::workbook::XlsxDefinedName {
1933                name: Some(dn.name.clone()),
1934                data: dn.refers_to.clone(),
1935                hidden: Some(false),
1936                local_sheet_id,
1937                ..Default::default()
1938            });
1939        *self.workbook.lock().unwrap() = Some(wb);
1940        Ok(())
1941    }
1942
1943    /// Delete a defined name.
1944    pub fn delete_defined_name(&self, dn: &crate::xml::workbook::DefinedName) -> Result<()> {
1945        let mut wb = self.workbook_reader()?;
1946        let Some(names) = wb.defined_names.as_mut() else {
1947            return Err(Box::new(ErrDefinedNameScope));
1948        };
1949        let local_sheet_id = if dn.scope.is_empty() || dn.scope == "Workbook" {
1950            None
1951        } else {
1952            Some((self.get_sheet_index(&dn.scope)? - 1) as i64)
1953        };
1954        let before = names.defined_name.len();
1955        names.defined_name.retain(|e| {
1956            !(e.name.as_deref().unwrap_or("") == dn.name && e.local_sheet_id == local_sheet_id)
1957        });
1958        if names.defined_name.len() == before {
1959            return Err(Box::new(ErrDefinedNameScope));
1960        }
1961        if names.defined_name.is_empty() {
1962            wb.defined_names = None;
1963        }
1964        *self.workbook.lock().unwrap() = Some(wb);
1965        Ok(())
1966    }
1967
1968    /// Return the reference a defined name resolves to.
1969    pub fn get_defined_name_ref_to(&self, name: &str, current_sheet: &str) -> String {
1970        let mut workbook_ref = String::new();
1971        let mut sheet_ref = String::new();
1972        if let Ok(names) = self.get_defined_names() {
1973            for dn in &names {
1974                if dn.name == name {
1975                    if dn.scope == "Workbook" {
1976                        workbook_ref = dn.refers_to.clone();
1977                    }
1978                    if dn.scope == current_sheet {
1979                        sheet_ref = dn.refers_to.clone();
1980                    }
1981                }
1982            }
1983        }
1984        if !sheet_ref.is_empty() {
1985            sheet_ref
1986        } else {
1987            workbook_ref
1988        }
1989    }
1990
1991    /// Alias for [`Self::get_defined_names`], matching the Go `GetDefinedName`
1992    /// API surface.
1993    pub fn get_defined_name(&self) -> Result<Vec<crate::xml::workbook::DefinedName>> {
1994        self.get_defined_names()
1995    }
1996}
1997
1998// ------------------------------------------------------------------
1999// Additional public File-level APIs ported from file.go / excelize.go
2000// ------------------------------------------------------------------
2001
2002impl File {
2003    /// Write the workbook to any writer. Alias for [`Self::write_to`].
2004    pub fn write<W: Write>(&self, writer: W) -> Result<()> {
2005        self.write_to(writer)
2006    }
2007
2008    /// Save to the original path, overriding the stored options.
2009    pub fn save_with_options(&mut self, opts: Options) -> Result<()> {
2010        {
2011            let mut o = self.options.lock().unwrap();
2012            *o = opts;
2013        }
2014        self.save()
2015    }
2016
2017    /// Save the workbook to the given path, overriding the stored options.
2018    pub fn save_as_with_options(&mut self, path: &str, opts: Options) -> Result<()> {
2019        {
2020            let mut o = self.options.lock().unwrap();
2021            *o = opts;
2022        }
2023        self.save_as(path)
2024    }
2025
2026    /// Clear cached linked values for formula cells so Excel recalculates them
2027    /// on open.
2028    ///
2029    /// Equivalent to Go `UpdateLinkedValue`.
2030    pub fn update_linked_value(&self) -> Result<()> {
2031        let mut wb = self.workbook_reader()?;
2032        wb.calc_pr = None;
2033        *self.workbook.lock().unwrap() = Some(wb);
2034        for name in self.get_sheet_list() {
2035            if let Ok(mut ws) = self.work_sheet_reader(&name) {
2036                let path = self.get_sheet_xml_path(&name).unwrap_or_default();
2037                let mut changed = false;
2038                for row in &mut ws.sheet_data.row {
2039                    for cell in &mut row.c {
2040                        if cell.f.is_some() && cell.v.is_some() {
2041                            cell.v = None;
2042                            cell.t = None;
2043                            changed = true;
2044                        }
2045                    }
2046                }
2047                if changed {
2048                    self.sheet.insert(path, ws);
2049                }
2050            }
2051        }
2052        Ok(())
2053    }
2054
2055    /// Extract spreadsheet package parts from a `ZipArchive`.
2056    ///
2057    /// Equivalent to Go `ReadZipReader`.
2058    pub fn read_zip_reader<R: Read + Seek>(
2059        &self,
2060        archive: &mut ZipArchive<R>,
2061    ) -> Result<(HashMap<String, Vec<u8>>, i32)> {
2062        self.read_zip_archive(archive)
2063    }
2064
2065    /// Set workbook properties.
2066    pub fn set_workbook_props(&self, opts: &WorkbookPropsOptions) -> Result<()> {
2067        let mut wb = self.workbook_reader()?;
2068        if wb.workbook_pr.is_none() {
2069            wb.workbook_pr = Some(XlsxWorkbookPr::default());
2070        }
2071        let pr = wb.workbook_pr.as_mut().unwrap();
2072        if let Some(v) = opts.date1904 {
2073            pr.date1904 = Some(v);
2074        }
2075        if let Some(v) = opts.filter_privacy {
2076            pr.filter_privacy = Some(v);
2077        }
2078        if let Some(ref v) = opts.code_name {
2079            pr.code_name = Some(v.clone());
2080        }
2081        *self.workbook.lock().unwrap() = Some(wb);
2082        Ok(())
2083    }
2084
2085    /// Get workbook properties.
2086    pub fn get_workbook_props(&self) -> Result<WorkbookPropsOptions> {
2087        let mut opts = WorkbookPropsOptions::default();
2088        let wb = self.workbook_reader()?;
2089        if let Some(pr) = &wb.workbook_pr {
2090            opts.date1904 = pr.date1904;
2091            opts.filter_privacy = pr.filter_privacy;
2092            opts.code_name = pr.code_name.clone();
2093        }
2094        Ok(opts)
2095    }
2096
2097    /// Set calculation properties.
2098    pub fn set_calc_props(&self, opts: &CalcPropsOptions) -> Result<()> {
2099        if let Some(ref mode) = opts.calc_mode {
2100            if in_str_slice(SUPPORTED_CALC_MODE, mode, true) == -1 {
2101                return Err(Box::new(ErrParameterInvalid));
2102            }
2103        }
2104        if let Some(ref mode) = opts.ref_mode {
2105            if in_str_slice(SUPPORTED_REF_MODE, mode, true) == -1 {
2106                return Err(Box::new(ErrParameterInvalid));
2107            }
2108        }
2109
2110        let mut wb = self.workbook_reader()?;
2111        if wb.calc_pr.is_none() {
2112            wb.calc_pr = Some(XlsxCalcPr::default());
2113        }
2114        let pr = wb.calc_pr.as_mut().unwrap();
2115        if let Some(v) = opts.calc_completed {
2116            pr.calc_completed = Some(v);
2117        }
2118        if let Some(v) = opts.calc_on_save {
2119            pr.calc_on_save = Some(v);
2120        }
2121        if let Some(v) = opts.force_full_calc {
2122            pr.force_full_calc = Some(v);
2123        }
2124        if let Some(v) = opts.full_calc_on_load {
2125            pr.full_calc_on_load = Some(v);
2126        }
2127        if let Some(v) = opts.full_precision {
2128            pr.full_precision = Some(v);
2129        }
2130        if let Some(v) = opts.iterate {
2131            pr.iterate = Some(v);
2132        }
2133        if let Some(v) = opts.iterate_delta {
2134            pr.iterate_delta = Some(v);
2135        }
2136        if let Some(ref v) = opts.calc_mode {
2137            pr.calc_mode = Some(v.clone());
2138        }
2139        if let Some(ref v) = opts.ref_mode {
2140            pr.ref_mode = Some(v.clone());
2141        }
2142        if let Some(v) = opts.calc_id {
2143            pr.calc_id = Some(v as i64);
2144        }
2145        if let Some(v) = opts.concurrent_manual_count {
2146            pr.concurrent_manual_count = Some(v as i64);
2147        }
2148        if let Some(v) = opts.iterate_count {
2149            pr.iterate_count = Some(v as i64);
2150        }
2151        pr.concurrent_calc = opts.concurrent_calc;
2152        *self.workbook.lock().unwrap() = Some(wb);
2153        Ok(())
2154    }
2155
2156    /// Get calculation properties.
2157    pub fn get_calc_props(&self) -> Result<CalcPropsOptions> {
2158        let mut opts = CalcPropsOptions::default();
2159        let wb = self.workbook_reader()?;
2160        if let Some(pr) = &wb.calc_pr {
2161            opts.calc_completed = pr.calc_completed;
2162            opts.calc_on_save = pr.calc_on_save;
2163            opts.force_full_calc = pr.force_full_calc;
2164            opts.full_calc_on_load = pr.full_calc_on_load;
2165            opts.full_precision = pr.full_precision;
2166            opts.iterate = pr.iterate;
2167            opts.iterate_delta = pr.iterate_delta;
2168            opts.calc_mode = pr.calc_mode.clone();
2169            opts.ref_mode = pr.ref_mode.clone();
2170            opts.calc_id = pr.calc_id.map(|v| v as u64);
2171            opts.concurrent_manual_count = pr.concurrent_manual_count.map(|v| v as u64);
2172            opts.iterate_count = pr.iterate_count.map(|v| v as u64);
2173            opts.concurrent_calc = pr.concurrent_calc;
2174        }
2175        Ok(opts)
2176    }
2177
2178    /// Protect the workbook with optional password and lock settings.
2179    pub fn protect_workbook(&self, opts: &WorkbookProtectionOptions) -> Result<()> {
2180        let mut wb = self.workbook_reader()?;
2181        let mut protection = XlsxWorkbookProtection {
2182            lock_structure: Some(opts.lock_structure),
2183            lock_windows: Some(opts.lock_windows),
2184            ..Default::default()
2185        };
2186        if !opts.password.is_empty() {
2187            let algorithm_name = if opts.algorithm_name.is_empty() {
2188                "SHA-512"
2189            } else {
2190                &opts.algorithm_name
2191            };
2192            let (hash_value, salt_value) = crypt::gen_iso_passwd_hash(
2193                &opts.password,
2194                algorithm_name,
2195                "",
2196                WORKBOOK_PROTECTION_SPIN_COUNT,
2197            )?;
2198            protection.workbook_algorithm_name = Some(algorithm_name.to_string());
2199            protection.workbook_hash_value = Some(hash_value);
2200            protection.workbook_salt_value = Some(salt_value);
2201            protection.workbook_spin_count = Some(WORKBOOK_PROTECTION_SPIN_COUNT as i64);
2202        }
2203        wb.workbook_protection = Some(protection);
2204        *self.workbook.lock().unwrap() = Some(wb);
2205        Ok(())
2206    }
2207
2208    /// Remove workbook protection, optionally verifying the password first.
2209    pub fn unprotect_workbook(&self, password: Option<&str>) -> Result<()> {
2210        let mut wb = self.workbook_reader()?;
2211        if let Some(pwd) = password {
2212            let protection = wb.workbook_protection.as_ref().ok_or_else(|| {
2213                Box::new(ErrUnprotectWorkbook) as Box<dyn std::error::Error + Send + Sync>
2214            })?;
2215            if let Some(ref algorithm_name) = protection.workbook_algorithm_name {
2216                let salt = protection.workbook_salt_value.as_deref().unwrap_or("");
2217                let spin_count = protection.workbook_spin_count.unwrap_or(0) as i32;
2218                let (hash_value, _) =
2219                    crypt::gen_iso_passwd_hash(pwd, algorithm_name, salt, spin_count)?;
2220                if protection.workbook_hash_value.as_deref().unwrap_or("") != hash_value {
2221                    return Err(Box::new(ErrUnprotectWorkbookPassword));
2222                }
2223            }
2224        }
2225        wb.workbook_protection = None;
2226        *self.workbook.lock().unwrap() = Some(wb);
2227        Ok(())
2228    }
2229
2230    /// Add a VBA project binary to the workbook.
2231    ///
2232    /// The data must start with the OLE compound-file identifier. A valid
2233    /// `vbaProject.bin` can be embedded by reading it from disk and passing
2234    /// the bytes to this method. The workbook should be saved with an `.xlsm`
2235    /// or `.xltm` extension.
2236    pub fn add_vba_project(&self, data: &[u8]) -> Result<()> {
2237        if data.len() < 8 || &data[..8] != OLE_IDENTIFIER {
2238            return Err(Box::new(crate::errors::ErrAddVBAProject));
2239        }
2240        let rel_path = self.get_workbook_rels_path();
2241        let mut rels = self.rels_reader(&rel_path)?.unwrap_or_default();
2242        let mut existing = false;
2243        let mut r_id = 0;
2244        for rel in &rels.relationships {
2245            if rel.target == "vbaProject.bin" && rel.r#type == SOURCE_RELATIONSHIP_VBA_PROJECT {
2246                existing = true;
2247            }
2248            let id: i32 = rel.id.trim_start_matches("rId").parse().unwrap_or(0);
2249            if id > r_id {
2250                r_id = id;
2251            }
2252        }
2253        if !existing {
2254            r_id += 1;
2255            rels.relationships.push(XlsxRelationship {
2256                id: format!("rId{r_id}"),
2257                r#type: SOURCE_RELATIONSHIP_VBA_PROJECT.to_string(),
2258                target: "vbaProject.bin".to_string(),
2259                target_mode: None,
2260            });
2261            self.relationships.insert(rel_path, rels);
2262        }
2263        self.pkg
2264            .insert("xl/vbaProject.bin".to_string(), data.to_vec());
2265        Ok(())
2266    }
2267
2268    /// Set the workbook content type and `.bin` default for macro workbooks.
2269    pub fn set_content_type_part_project_extensions(&self, content_type: &str) -> Result<()> {
2270        let mut ct = self.content_types_reader()?;
2271        let mut bin_ok = false;
2272        let mut entries = ct.entries.clone();
2273        for entry in &entries {
2274            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = entry {
2275                if d.extension == "bin" {
2276                    bin_ok = true;
2277                }
2278            }
2279        }
2280        for entry in &mut entries {
2281            if let crate::xml::content_types::XlsxContentTypeEntry::Override(o) = entry {
2282                if o.part_name == "/xl/workbook.xml" {
2283                    o.content_type = content_type.to_string();
2284                }
2285            }
2286        }
2287        if !bin_ok {
2288            entries.push(crate::xml::content_types::XlsxContentTypeEntry::Default(
2289                XlsxDefault {
2290                    extension: "bin".to_string(),
2291                    content_type: CONTENT_TYPE_VBA.to_string(),
2292                },
2293            ));
2294        }
2295        ct.entries = entries;
2296        *self.content_types.lock().unwrap() = Some(ct);
2297        Ok(())
2298    }
2299}
2300
2301// ------------------------------------------------------------------
2302// Free functions
2303// ------------------------------------------------------------------
2304
2305fn normalize_options(mut opts: Options) -> Options {
2306    if opts.unzip_size_limit == 0 {
2307        opts.unzip_size_limit = UNZIP_SIZE_LIMIT;
2308    }
2309    if opts.unzip_xml_size_limit == 0 {
2310        opts.unzip_xml_size_limit = STREAM_CHUNK_SIZE;
2311    }
2312    if opts.culture_info == 0 && opts.short_date_pattern.is_empty() {
2313        opts.culture_info = CULTURE_NAME_UNKNOWN;
2314    }
2315    opts
2316}
2317
2318/// Convert Strict Open XML namespaces to Transitional ones.
2319pub fn namespace_strict_to_transitional(content: &[u8]) -> Vec<u8> {
2320    if content.windows(13).all(|w| w != b"purl.oclc.org") {
2321        return content.to_vec();
2322    }
2323    let mut result = content.to_vec();
2324    let translations: &[(&[u8], &[u8])] = &[
2325        (
2326            STRICT_NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES.as_bytes(),
2327            NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES.as_bytes(),
2328        ),
2329        (
2330            STRICT_NAMESPACE_DRAWING_ML_MAIN.as_bytes(),
2331            NAMESPACE_DRAWING_ML_MAIN.as_bytes(),
2332        ),
2333        (
2334            STRICT_NAMESPACE_EXTENDED_PROPERTIES.as_bytes(),
2335            NAMESPACE_EXTENDED_PROPERTIES.as_bytes(),
2336        ),
2337        (
2338            STRICT_NAMESPACE_SPREADSHEET.as_bytes(),
2339            NAMESPACE_SPREADSHEET.as_bytes(),
2340        ),
2341        (
2342            STRICT_SOURCE_RELATIONSHIP.as_bytes(),
2343            SOURCE_RELATIONSHIP.as_bytes(),
2344        ),
2345        (
2346            STRICT_SOURCE_RELATIONSHIP_CHART.as_bytes(),
2347            SOURCE_RELATIONSHIP_CHART.as_bytes(),
2348        ),
2349        (
2350            STRICT_SOURCE_RELATIONSHIP_COMMENTS.as_bytes(),
2351            SOURCE_RELATIONSHIP_COMMENTS.as_bytes(),
2352        ),
2353        (
2354            STRICT_SOURCE_RELATIONSHIP_EXTEND_PROPERTIES.as_bytes(),
2355            SOURCE_RELATIONSHIP_EXTEND_PROPERTIES.as_bytes(),
2356        ),
2357        (
2358            STRICT_SOURCE_RELATIONSHIP_IMAGE.as_bytes(),
2359            SOURCE_RELATIONSHIP_IMAGE.as_bytes(),
2360        ),
2361        (
2362            STRICT_SOURCE_RELATIONSHIP_OFFICE_DOCUMENT.as_bytes(),
2363            SOURCE_RELATIONSHIP_OFFICE_DOCUMENT.as_bytes(),
2364        ),
2365    ];
2366    for (from, to) in translations {
2367        result = bytes_replace(&result, from, to);
2368    }
2369    result
2370}
2371
2372fn bytes_replace(content: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
2373    let mut result = Vec::with_capacity(content.len());
2374    let mut start = 0;
2375    while let Some(pos) = content[start..].windows(from.len()).position(|w| w == from) {
2376        let pos = start + pos;
2377        result.extend_from_slice(&content[start..pos]);
2378        result.extend_from_slice(to);
2379        start = pos + from.len();
2380    }
2381    result.extend_from_slice(&content[start..]);
2382    result
2383}
2384
2385/// Detect the encoding named in an XML declaration, returning `None` when no
2386/// declaration is present. Works on raw bytes so that non-UTF-8 documents can
2387/// still be inspected.
2388fn detect_xml_encoding(content: &[u8]) -> Option<&str> {
2389    let start = content.windows(5).position(|w| w == b"<?xml")?;
2390    let rest = &content[start..];
2391    let end = rest.windows(2).position(|w| w == b"?>")? + 2;
2392    let decl = &rest[..end];
2393    let key = b"encoding=";
2394    let pos = decl.windows(key.len()).position(|w| w == key)?;
2395    let rest = &decl[pos + key.len()..];
2396    let quote = *rest.first()?;
2397    let close = rest[1..].iter().position(|&b| b == quote)?;
2398    std::str::from_utf8(&rest[1..1 + close]).ok()
2399}
2400
2401/// Remove all occurrences of a single XML element (and its children) from a
2402/// UTF-8 document. Used as a deserialization workaround for elements that
2403/// contain arbitrary nested XML.
2404fn strip_xml_element(content: &[u8], name: &str) -> Vec<u8> {
2405    let s = String::from_utf8_lossy(content);
2406    let pattern = format!(
2407        r"(?s)<{}\b[^>]*>.*?</{}>",
2408        regex::escape(name),
2409        regex::escape(name)
2410    );
2411    if let Ok(re) = regex::Regex::new(&pattern) {
2412        return re.replace_all(&s, "").into_owned().into_bytes();
2413    }
2414    s.into_owned().into_bytes()
2415}
2416
2417/// Extract the raw attribute string from the root element of an XML document.
2418pub(crate) fn extract_root_namespace_attributes(content: &[u8]) -> Option<String> {
2419    let s = std::str::from_utf8(content).ok()?;
2420    // Skip optional XML declaration and whitespace.
2421    let mut pos = 0usize;
2422    while pos < s.len() {
2423        let c = s[pos..].chars().next()?;
2424        if c == '<' {
2425            if s[pos..].starts_with("<?") {
2426                if let Some(end) = s[pos..].find("?>") {
2427                    pos += end + 2;
2428                    continue;
2429                }
2430                return None;
2431            }
2432            break;
2433        }
2434        pos += c.len_utf8();
2435    }
2436    if pos >= s.len() {
2437        return None;
2438    }
2439    let start = pos;
2440    let close = s[start..].find('>')?;
2441    let tag = &s[start..start + close + 1];
2442    // Tag name ends at first whitespace.
2443    let name_end = tag
2444        .find(|c: char| c.is_whitespace())
2445        .unwrap_or(tag.len() - 1);
2446    let after_name = &tag[name_end..tag.len() - 1];
2447    let trimmed = after_name.trim();
2448    if trimmed.is_empty() {
2449        None
2450    } else {
2451        Some(trimmed.to_string())
2452    }
2453}
2454
2455/// Replace the attributes of the root element with the captured namespace string.
2456pub(crate) fn replace_root_namespace_attributes(content: &mut Vec<u8>, attrs: &str) -> Result<()> {
2457    let s = std::str::from_utf8(content)
2458        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
2459    let mut pos = 0usize;
2460    while pos < s.len() {
2461        let c = s[pos..]
2462            .chars()
2463            .next()
2464            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "empty"))?;
2465        if c == '<' {
2466            if s[pos..].starts_with("<?") {
2467                if let Some(end) = s[pos..].find("?>") {
2468                    pos += end + 2;
2469                    continue;
2470                }
2471                return Err(Box::new(io::Error::new(
2472                    io::ErrorKind::InvalidData,
2473                    "bad xml decl",
2474                )));
2475            }
2476            break;
2477        }
2478        pos += c.len_utf8();
2479    }
2480    let start = pos;
2481    let close = s[start..]
2482        .find('>')
2483        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "no root close"))?;
2484    let tag = &s[start..start + close + 1];
2485    let name_end = tag
2486        .find(|c: char| c.is_whitespace())
2487        .unwrap_or(tag.len() - 1);
2488    let tag_name = &tag[1..name_end];
2489    let end_tag = start + close + 1;
2490    let new_start = format!("<{tag_name} {attrs}>");
2491    let tail = content.split_off(end_tag);
2492    content.truncate(start);
2493    content.extend_from_slice(new_start.as_bytes());
2494    content.extend_from_slice(&tail);
2495    Ok(())
2496}
2497
2498fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
2499    haystack
2500        .windows(needle.len())
2501        .position(|window| window == needle)
2502}
2503
2504/// Remove attributes with empty values from serialized XML.
2505///
2506/// quick_xml emits `None` `Option` fields as `attr=""`, which cannot be
2507/// parsed back into boolean/integer fields. Stripping them lets round-trips
2508/// through `Option<T>` fields work until the XML types are updated to skip
2509/// `None` values explicitly.
2510pub(crate) fn strip_empty_attributes(content: &mut Vec<u8>) {
2511    static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
2512    let re = RE.get_or_init(|| regex::Regex::new(r#" ([a-zA-Z_][a-zA-Z0-9_:\-]*)="""#).unwrap());
2513    let s = String::from_utf8_lossy(content);
2514    let cleaned = re.replace_all(&s, "");
2515    *content = cleaned.into_owned().into_bytes();
2516}
2517
2518fn decode_theme_to_xlsx_theme(theme: &DecodeTheme) -> XlsxTheme {
2519    fn convert_color(c: &crate::xml::theme::DecodeCtColor) -> XlsxCtColor {
2520        XlsxCtColor {
2521            scrgb_clr: c.scrgb_clr.clone(),
2522            srgb_clr: c.srgb_clr.clone(),
2523            hsl_clr: c.hsl_clr.clone(),
2524            sys_clr: c.sys_clr.clone().map(|s| XlsxSysClr {
2525                val: s.val,
2526                last_clr: s.last_clr,
2527            }),
2528            scheme_clr: c.scheme_clr.clone(),
2529            prst_clr: c.prst_clr.clone(),
2530        }
2531    }
2532    fn convert_font(c: &crate::xml::theme::DecodeFontCollection) -> XlsxFontCollection {
2533        XlsxFontCollection {
2534            latin: c.latin.clone(),
2535            ea: c.ea.clone(),
2536            cs: c.cs.clone(),
2537            font: c.font.clone(),
2538            ext_lst: c.ext_lst.clone(),
2539        }
2540    }
2541    XlsxTheme {
2542        xmlns_a: None,
2543        xmlns_r: None,
2544        name: theme.name.clone(),
2545        theme_elements: XlsxBaseStyles {
2546            clr_scheme: crate::xml::theme::XlsxColorScheme {
2547                name: theme.theme_elements.clr_scheme.name.clone(),
2548                dk1: convert_color(&theme.theme_elements.clr_scheme.dk1),
2549                lt1: convert_color(&theme.theme_elements.clr_scheme.lt1),
2550                dk2: convert_color(&theme.theme_elements.clr_scheme.dk2),
2551                lt2: convert_color(&theme.theme_elements.clr_scheme.lt2),
2552                accent1: convert_color(&theme.theme_elements.clr_scheme.accent1),
2553                accent2: convert_color(&theme.theme_elements.clr_scheme.accent2),
2554                accent3: convert_color(&theme.theme_elements.clr_scheme.accent3),
2555                accent4: convert_color(&theme.theme_elements.clr_scheme.accent4),
2556                accent5: convert_color(&theme.theme_elements.clr_scheme.accent5),
2557                accent6: convert_color(&theme.theme_elements.clr_scheme.accent6),
2558                hlink: convert_color(&theme.theme_elements.clr_scheme.hlink),
2559                fol_hlink: convert_color(&theme.theme_elements.clr_scheme.fol_hlink),
2560                ext_lst: theme.theme_elements.clr_scheme.ext_lst.clone(),
2561            },
2562            font_scheme: crate::xml::theme::XlsxFontScheme {
2563                name: theme.theme_elements.font_scheme.name.clone(),
2564                major_font: convert_font(&theme.theme_elements.font_scheme.major_font),
2565                minor_font: convert_font(&theme.theme_elements.font_scheme.minor_font),
2566                ext_lst: theme.theme_elements.font_scheme.ext_lst.clone(),
2567            },
2568            fmt_scheme: crate::xml::theme::XlsxStyleMatrix {
2569                name: theme.theme_elements.fmt_scheme.name.clone(),
2570                fill_style_lst: theme.theme_elements.fmt_scheme.fill_style_lst.clone(),
2571                ln_style_lst: theme.theme_elements.fmt_scheme.ln_style_lst.clone(),
2572                effect_style_lst: theme.theme_elements.fmt_scheme.effect_style_lst.clone(),
2573                bg_fill_style_lst: theme.theme_elements.fmt_scheme.bg_fill_style_lst.clone(),
2574            },
2575            ext_lst: theme.theme_elements.ext_lst.clone(),
2576        },
2577        object_defaults: theme.object_defaults.clone(),
2578        extra_clr_scheme_lst: theme.extra_clr_scheme_lst.clone(),
2579        cust_clr_lst: theme.cust_clr_lst.clone(),
2580        ext_lst: theme.ext_lst.clone(),
2581    }
2582}
2583
2584// ------------------------------------------------------------------
2585// Trait-based helpers referenced by `excelize.rs`
2586// ------------------------------------------------------------------
2587
2588/// Helper to apply the workbook content-type for macro/template files.
2589pub fn set_content_type_part_project_extensions(file: &File, content_type: &str) -> Result<()> {
2590    file.set_content_type_part_project_extensions(content_type)
2591}
2592
2593/// Add a VBA project binary to the workbook.
2594pub fn add_vba_project(file: &File, data: &[u8]) -> Result<()> {
2595    file.add_vba_project(data)
2596}
2597
2598// ------------------------------------------------------------------
2599// Extension list helpers
2600// ------------------------------------------------------------------
2601
2602/// Extract the inner XML of the `<extLst>` element, if present.
2603fn extract_ext_lst(data: &[u8]) -> Option<String> {
2604    let s = String::from_utf8_lossy(data);
2605    let start_key = "<extLst";
2606    let start = s.find(start_key)?;
2607    let close_bracket = s[start..].find('>')? + start + 1;
2608    let end_key = "</extLst>";
2609    let end = s.find(end_key)? + end_key.len();
2610    Some(s[close_bracket..end - end_key.len()].to_string())
2611}
2612
2613/// Remove the `<extLst>` element from raw worksheet XML so that serde can
2614/// deserialize the remainder.
2615fn remove_ext_lst(data: &mut Vec<u8>) {
2616    let s = String::from_utf8_lossy(data);
2617    let Some(start) = s.find("<extLst") else {
2618        return;
2619    };
2620    let Some(end) = s.find("</extLst>") else {
2621        return;
2622    };
2623    let end = end + "</extLst>".len();
2624    let mut result = s[..start].as_bytes().to_vec();
2625    result.extend_from_slice(s[end..].as_bytes());
2626    *data = result;
2627}
2628
2629/// Inject the serialized `<extLst>` inner XML before the closing
2630/// `</worksheet>` tag.
2631fn inject_ext_lst(output: &mut Vec<u8>, ext_xml: &str) {
2632    let s = String::from_utf8_lossy(output);
2633    let Some(pos) = s.rfind("</worksheet>") else {
2634        return;
2635    };
2636    let mut result = s[..pos].as_bytes().to_vec();
2637    result.extend_from_slice(b"<extLst>");
2638    result.extend_from_slice(ext_xml.as_bytes());
2639    result.extend_from_slice(b"</extLst>");
2640    result.extend_from_slice(s[pos..].as_bytes());
2641    *output = result;
2642}
2643
2644impl Drop for File {
2645    fn drop(&mut self) {
2646        // Best-effort cleanup of temporary files that may still be around if
2647        // the user did not call `close()` or if an error path left them behind.
2648        for entry in self.temp_files.iter() {
2649            let _ = fs::remove_file(entry.value());
2650        }
2651        self.temp_files.clear();
2652        for state in self.streams.borrow().values() {
2653            let _ = fs::remove_file(&state.tmp_path);
2654        }
2655        self.streams.borrow_mut().clear();
2656    }
2657}
2658
2659#[cfg(test)]
2660mod tests {
2661    use super::*;
2662
2663    #[test]
2664    fn new_file_round_trip() {
2665        let mut f = File::new_with_options(Options::default());
2666        let tmp = std::env::temp_dir().join("excelize_rust_test.xlsx");
2667        f.save_as(tmp.to_str().unwrap()).unwrap();
2668
2669        let file = fs::File::open(&tmp).unwrap();
2670        let archive = ZipArchive::new(file).unwrap();
2671        let names: Vec<String> = archive.file_names().map(|s| s.to_string()).collect();
2672        assert!(names.contains(&"[Content_Types].xml".to_string()));
2673        assert!(names.contains(&"xl/workbook.xml".to_string()));
2674        assert!(names.contains(&"xl/worksheets/sheet1.xml".to_string()));
2675        assert!(names.contains(&"xl/styles.xml".to_string()));
2676        assert!(names.contains(&"xl/theme/theme1.xml".to_string()));
2677        drop(archive);
2678        let _ = fs::remove_file(&tmp);
2679    }
2680
2681    #[test]
2682    fn save_and_reopen() {
2683        let mut f = File::new_with_options(Options::default());
2684        let tmp = std::env::temp_dir().join("excelize_rust_reopen_test.xlsx");
2685        f.save_as(tmp.to_str().unwrap()).unwrap();
2686
2687        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
2688        assert_eq!(*f2.sheet_count.lock().unwrap(), 1);
2689        assert_eq!(f2.get_sheet_list(), vec!["Sheet1"]);
2690        let _ = fs::remove_file(&tmp);
2691    }
2692
2693    #[test]
2694    fn open_existing_xlsx() {
2695        // Open one of the fixtures shipped with the repository.
2696        let path = "test/Book1.xlsx";
2697        let f = File::open_file(path, Options::default()).unwrap();
2698        assert!(*f.sheet_count.lock().unwrap() >= 1);
2699        let list = f.get_sheet_list();
2700        assert!(!list.is_empty());
2701    }
2702
2703    #[test]
2704    fn workbook_props_round_trip() {
2705        let f = File::new();
2706        let mut opts = WorkbookPropsOptions::default();
2707        opts.date1904 = Some(true);
2708        opts.filter_privacy = Some(false);
2709        opts.code_name = Some("Workbook1".to_string());
2710        f.set_workbook_props(&opts).unwrap();
2711
2712        let got = f.get_workbook_props().unwrap();
2713        assert_eq!(got.date1904, Some(true));
2714        assert_eq!(got.filter_privacy, Some(false));
2715        assert_eq!(got.code_name, Some("Workbook1".to_string()));
2716    }
2717
2718    #[test]
2719    fn calc_props_round_trip() {
2720        let f = File::new();
2721        let mut opts = CalcPropsOptions::default();
2722        opts.calc_mode = Some("manual".to_string());
2723        opts.ref_mode = Some("R1C1".to_string());
2724        opts.full_calc_on_load = Some(true);
2725        opts.calc_id = Some(152511);
2726        opts.iterate_count = Some(100);
2727        f.set_calc_props(&opts).unwrap();
2728
2729        let got = f.get_calc_props().unwrap();
2730        assert_eq!(got.calc_mode, Some("manual".to_string()));
2731        assert_eq!(got.ref_mode, Some("R1C1".to_string()));
2732        assert_eq!(got.full_calc_on_load, Some(true));
2733        assert_eq!(got.calc_id, Some(152511));
2734        assert_eq!(got.iterate_count, Some(100));
2735    }
2736
2737    #[test]
2738    fn calc_props_rejects_invalid_mode() {
2739        let f = File::new();
2740        let mut opts = CalcPropsOptions::default();
2741        opts.calc_mode = Some("invalid".to_string());
2742        assert!(f.set_calc_props(&opts).is_err());
2743
2744        opts.calc_mode = None;
2745        opts.ref_mode = Some("B3".to_string());
2746        assert!(f.set_calc_props(&opts).is_err());
2747    }
2748
2749    #[test]
2750    fn protect_workbook_round_trip() {
2751        let f = File::new();
2752        let opts = WorkbookProtectionOptions {
2753            password: "password".to_string(),
2754            lock_structure: true,
2755            lock_windows: false,
2756            ..Default::default()
2757        };
2758        f.protect_workbook(&opts).unwrap();
2759        let wb = f.workbook_reader().unwrap();
2760        let protection = wb.workbook_protection.as_ref().unwrap();
2761        assert_eq!(protection.lock_structure, Some(true));
2762        assert!(protection.workbook_hash_value.is_some());
2763
2764        assert!(f.unprotect_workbook(Some("wrong")).is_err());
2765        f.unprotect_workbook(Some("password")).unwrap();
2766        assert!(f.workbook_reader().unwrap().workbook_protection.is_none());
2767    }
2768
2769    #[test]
2770    fn add_vba_project() {
2771        let f = File::new();
2772        let mut sheet_opts = crate::sheet::SheetPropsOptions::default();
2773        sheet_opts.code_name = Some("Sheet1".to_string());
2774        f.set_sheet_props("Sheet1", &sheet_opts).unwrap();
2775
2776        let bad = fs::read("test/Book1.xlsx").unwrap();
2777        assert!(f.add_vba_project(&bad).is_err());
2778
2779        let data = fs::read("test/vbaProject.bin").unwrap();
2780        f.add_vba_project(&data).unwrap();
2781        // Adding the same VBA project again should be idempotent.
2782        f.add_vba_project(&data).unwrap();
2783
2784        let rels = f
2785            .rels_reader("xl/_rels/workbook.xml.rels")
2786            .unwrap()
2787            .unwrap();
2788        let vba_rel = rels
2789            .relationships
2790            .iter()
2791            .find(|r| r.target == "vbaProject.bin" && r.r#type == SOURCE_RELATIONSHIP_VBA_PROJECT);
2792        assert!(vba_rel.is_some());
2793
2794        let tmp = std::env::temp_dir().join("excelize_rust_vba.xlsm");
2795        let mut f2 = File::new();
2796        f2.set_sheet_props("Sheet1", &sheet_opts).unwrap();
2797        f2.add_vba_project(&data).unwrap();
2798        f2.save_as(tmp.to_str().unwrap()).unwrap();
2799
2800        let f3 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
2801        assert!(f3.pkg.contains_key("xl/vbaProject.bin"));
2802        let _ = fs::remove_file(&tmp);
2803    }
2804
2805    #[test]
2806    fn charset_transcoder_is_used_for_non_utf8_encoding() {
2807        use crate::constants::DEFAULT_XML_PATH_CALC_CHAIN;
2808        use std::io::Read;
2809        use std::sync::{Arc, Mutex};
2810
2811        let f = File::new();
2812        let xml = br#"<?xml version="1.0" encoding="X-TEST" standalone="yes"?>
2813<calcChain xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><c r="A1" i="1"/></calcChain>"#;
2814        f.pkg
2815            .insert(DEFAULT_XML_PATH_CALC_CHAIN.to_string(), xml.to_vec());
2816        *f.calc_chain.lock().unwrap() = None;
2817
2818        let called = Arc::new(Mutex::new(String::new()));
2819        let called_clone = called.clone();
2820        f.charset_transcoder(move |charset, mut input| {
2821            *called_clone.lock().unwrap() = charset.to_string();
2822            let mut buf = Vec::new();
2823            input.read_to_end(&mut buf).unwrap();
2824            Ok(Box::new(Cursor::new(buf)) as Box<dyn Read>)
2825        });
2826
2827        let cc = f.calc_chain_reader().unwrap();
2828        assert_eq!(cc.c.len(), 1);
2829        assert_eq!(cc.c[0].r, "A1");
2830        assert_eq!(called.lock().unwrap().as_str(), "X-TEST");
2831    }
2832
2833    #[test]
2834    fn charset_transcoder_handles_invalid_utf8_bytes() {
2835        use crate::constants::DEFAULT_XML_PATH_CALC_CHAIN;
2836        use std::io::Read;
2837        use std::sync::{Arc, Mutex};
2838
2839        let f = File::new();
2840        let mut xml = br#"<?xml version="1.0" encoding="X-BAD" standalone="yes"?>
2841<calcChain xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><c r="A1" i="1"/></calcChain>"#
2842            .to_vec();
2843        xml.push(0xFF); // invalid UTF-8 trailing byte
2844        f.pkg.insert(DEFAULT_XML_PATH_CALC_CHAIN.to_string(), xml);
2845        *f.calc_chain.lock().unwrap() = None;
2846
2847        let called = Arc::new(Mutex::new(false));
2848        let called_clone = called.clone();
2849        f.charset_transcoder(move |_charset, mut input| {
2850            *called_clone.lock().unwrap() = true;
2851            let mut buf = Vec::new();
2852            input.read_to_end(&mut buf).unwrap();
2853            buf.pop(); // strip the invalid trailing byte
2854            Ok(Box::new(Cursor::new(buf)) as Box<dyn Read>)
2855        });
2856
2857        let cc = f.calc_chain_reader().unwrap();
2858        assert_eq!(cc.c.len(), 1);
2859        assert_eq!(cc.c[0].r, "A1");
2860        assert!(*called.lock().unwrap());
2861    }
2862
2863    #[test]
2864    fn set_zip_writer_uses_custom_factory() {
2865        use std::sync::{Arc, Mutex};
2866
2867        struct MockZipWriter {
2868            calls: Arc<Mutex<Vec<String>>>,
2869        }
2870
2871        impl ZipWriter for MockZipWriter {
2872            fn start_file(&mut self, name: &str, _options: SimpleFileOptions) -> Result<()> {
2873                self.calls.lock().unwrap().push(format!("start:{name}"));
2874                Ok(())
2875            }
2876            fn write_all(&mut self, _buf: &[u8]) -> Result<()> {
2877                self.calls.lock().unwrap().push("write".to_string());
2878                Ok(())
2879            }
2880            fn finish(self: Box<Self>) -> Result<()> {
2881                self.calls.lock().unwrap().push("finish".to_string());
2882                Ok(())
2883            }
2884        }
2885
2886        let f = File::new();
2887        let calls = Arc::new(Mutex::new(Vec::new()));
2888        let calls_clone = calls.clone();
2889        f.set_zip_writer(move |_writer| {
2890            Box::new(MockZipWriter {
2891                calls: calls_clone.clone(),
2892            })
2893        });
2894
2895        let _ = f.write_to_buffer().unwrap();
2896        let calls = calls.lock().unwrap();
2897        assert!(calls.iter().any(|c| c.starts_with("start:")));
2898        assert!(calls.contains(&"write".to_string()));
2899        assert!(calls.contains(&"finish".to_string()));
2900    }
2901
2902    #[test]
2903    fn get_defined_name_alias() {
2904        let f = File::new();
2905        let names = f.get_defined_name().unwrap();
2906        assert!(names.is_empty());
2907        assert_eq!(f.get_defined_names().unwrap(), names);
2908    }
2909
2910    #[test]
2911    fn update_linked_value_clears_formula_cache() {
2912        let f = File::new_with_options(Options::default());
2913        f.set_cell_int("Sheet1", "A1", 1).unwrap();
2914        f.set_cell_int("Sheet1", "A2", 2).unwrap();
2915        f.set_cell_formula("Sheet1", "A3", "A1+A2").unwrap();
2916        f.calc_cell_value("Sheet1", "A3").unwrap();
2917        f.update_linked_value().unwrap();
2918        assert!(f.workbook_reader().unwrap().calc_pr.is_none());
2919    }
2920
2921    #[test]
2922    fn read_zip_reader_extracts_parts() {
2923        let mut f = File::new_with_options(Options::default());
2924        let tmp = std::env::temp_dir().join("excelize_rust_read_zip_reader.xlsx");
2925        f.save_as(tmp.to_str().unwrap()).unwrap();
2926
2927        let file = fs::File::open(&tmp).unwrap();
2928        let mut archive = ZipArchive::new(file).unwrap();
2929        let f2 = File::new_with_options(Options::default());
2930        let (parts, count) = f2.read_zip_reader(&mut archive).unwrap();
2931        assert!(parts.contains_key(DEFAULT_XML_PATH_WORKBOOK));
2932        assert_eq!(count, 1);
2933        let _ = fs::remove_file(&tmp);
2934    }
2935
2936    #[test]
2937    fn set_content_type_part_rels_extensions_adds_default() {
2938        let f = File::new();
2939        f.set_content_type_part_rels_extensions().unwrap();
2940        let ct = f.content_types_reader().unwrap();
2941        assert!(ct.entries.iter().any(|e| {
2942            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
2943                d.extension == "rels" && d.content_type == CONTENT_TYPE_RELATIONSHIPS
2944            } else {
2945                false
2946            }
2947        }));
2948    }
2949
2950    #[test]
2951    fn add_content_type_part_adds_rels_default() {
2952        let f = File::new();
2953        f.add_content_type_part(1, "table").unwrap();
2954        let ct = f.content_types_reader().unwrap();
2955        assert!(ct.entries.iter().any(|e| {
2956            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
2957                d.extension == "rels"
2958            } else {
2959                false
2960            }
2961        }));
2962    }
2963
2964    #[test]
2965    fn set_content_type_part_image_extensions_uses_defaults() {
2966        let f = File::new();
2967        crate::sheet::set_content_type_part_image_extensions(&f).unwrap();
2968        let ct = f.content_types_reader().unwrap();
2969        assert!(ct.entries.iter().any(|e| {
2970            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
2971                d.extension == "png" && d.content_type == "image/png"
2972            } else {
2973                false
2974            }
2975        }));
2976        assert!(ct.entries.iter().any(|e| {
2977            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
2978                d.extension == "jpeg" && d.content_type == "image/jpeg"
2979            } else {
2980                false
2981            }
2982        }));
2983        // Should not create Overrides with fake part names.
2984        assert!(!ct.entries.iter().any(|e| {
2985            if let crate::xml::content_types::XlsxContentTypeEntry::Override(o) = e {
2986                o.part_name.contains("image1")
2987            } else {
2988                false
2989            }
2990        }));
2991    }
2992
2993    #[test]
2994    fn add_name_spaces_registers_namespace() {
2995        let f = File::new();
2996        let path = f.get_sheet_xml_path("Sheet1").unwrap();
2997        f.add_name_spaces(&path, SOURCE_RELATIONSHIP);
2998        let attrs = f.xml_attr.get(&path).map(|a| a.clone()).unwrap_or_default();
2999        assert!(attrs.contains("xmlns:r=\""));
3000        assert!(attrs.contains(SOURCE_RELATIONSHIP));
3001    }
3002
3003    #[test]
3004    fn add_name_spaces_marks_extension_namespace_ignorable() {
3005        let f = File::new();
3006        let path = f.get_sheet_xml_path("Sheet1").unwrap();
3007        f.add_name_spaces(&path, NAMESPACE_SPREADSHEET_X14);
3008        let attrs = f.xml_attr.get(&path).map(|a| a.clone()).unwrap_or_default();
3009        assert!(attrs.contains("xmlns:x14=\""));
3010        assert!(attrs.contains("xmlns:mc=\""));
3011        assert!(attrs.contains("mc:Ignorable=\"x14\""));
3012    }
3013
3014    #[test]
3015    fn add_sheet_name_space_resolves_path() {
3016        let f = File::new();
3017        f.add_sheet_name_space("Sheet1", SOURCE_RELATIONSHIP);
3018        let path = f.get_sheet_xml_path("Sheet1").unwrap();
3019        let attrs = f.xml_attr.get(&path).map(|a| a.clone()).unwrap_or_default();
3020        assert!(attrs.contains("xmlns:r=\""));
3021    }
3022
3023    #[test]
3024    fn workbook_writer_preserves_alternate_content() {
3025        let f = File::new();
3026        let mut wb = f.workbook_reader().unwrap();
3027        wb.decode_alternate_content = Some(crate::xml::common::XlsxInnerXml {
3028            content: "<mc:Choice Requires=\"a14\" xmlns:a14=\"http://schemas.microsoft.com/office/drawing/2010/main\"><foo/></mc:Choice>".to_string(),
3029        });
3030        *f.workbook.lock().unwrap() = Some(wb);
3031
3032        f.workbook_writer();
3033
3034        let path = f.get_workbook_path();
3035        let bytes = f.read_xml(&path);
3036        let output = String::from_utf8_lossy(&bytes);
3037        assert!(output.contains("mc:AlternateContent"));
3038        assert!(output.contains("mc:Choice"));
3039    }
3040}