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