Skip to main content

excelize_rs/
file.rs

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