Skip to main content

excelize_rs/
file.rs

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