//! Pass-through of package content GridCraft does not model, so that open → edit → save keeps it. //! //! On read, the raw bytes of unmodelled parts are kept with their content type and their //! relationship graph, per owner: the package root (custom document properties, ribbon //! customisation, add-in task panes), the workbook (custom XML, links to external workbooks with //! their ``, data connections, XML maps) and each sheet (query tables, cells //! mapped to XML elements, form controls, ActiveX controls, OLE objects, the sheet background and //! printer settings, with the sheet's ``, `` and `` XML, the `r:id` //! of ``, the legacy VML shapes those controls need and the pictures of the headers and //! footers, `&G`, with their ``). The workbook also keeps its //! `` and the calcFeatures extension, which Excel needs to trust the saved values. //! //! On write, kept parts get fresh part names where needed and new relationship ids; the `r:id`s in //! kept XML are rewritten to match. Kept content is conservative: //! - Anything whose relationship graph reaches a part GridCraft reads and rewrites (worksheets, //! drawings, charts, tables, PivotTables, comments, slicers…) is not kept: it would point at //! content that no longer exists in the same form. //! - External relationship targets (the path of a linked workbook) are kept as text and never //! opened, followed or refreshed. A relative path to a linked workbook is rewritten when the //! workbook is saved into another folder ([`rebase_links`]), so it still names the same file. //! - Sheet content lives on the sheet: deleting the sheet drops it, Copy Sheet doesn't copy it. //! Cell references inside kept parts (a control's linked cell, a query table's range, a cell //! mapped to an XML element) are kept as they were, not adjusted to inserted or deleted rows. //! - Strict Open XML files are not passed through (their namespaces differ from what we write). //! - Sizes are bounded: per part and per workbook. use std::borrow::Cow; use std::collections::{HashMap, HashSet, VecDeque}; use std::fmt::Write as _; use std::path::{Component, Path}; use std::sync::Arc; use gridcraft_model::{Preserved, PreservedPart, PreservedRel, PreservedSheet, PreservedXml, Sheet, Visibility, Workbook}; use quick_xml::events::attributes::Attribute; use quick_xml::events::{BytesStart, Event}; use quick_xml::name::QName; use quick_xml::{Reader, Writer}; use crate::IoError; use crate::package::{self, normalize}; use crate::read::Ctx; use crate::write::{NS_REL, Out, Rels, XML_DECL}; use crate::xml::{self, esc_attr}; mod read; mod rebase; mod write; pub use read::KeptReader; pub use rebase::{SheetKept, rebase_links}; #[cfg(test)] pub(crate) use write::sheet_names_in; pub use write::{KeptOut, SheetIds, WorkbookKept, workbook_elements, write_package_rels, write_workbook}; /// Most bytes kept for one workbook. pub const MAX_KEPT_TOTAL: usize = 256 * 1024 * 1024; /// Largest single kept part. pub const MAX_KEPT_PART: u64 = 64 * 1024 * 1024; /// Most parts kept for one workbook. const MAX_KEPT_PARTS: usize = 4096; /// Most relationships read from one rels part. const MAX_RELS: usize = 10_000; /// Largest relationships, content-types or VML part read here. const MAX_META: u64 = 16 * 1024 * 1024; /// Longest chain of relationships followed from an owner. const MAX_CHAIN: usize = 16; /// Largest kept XML fragment of a sheet. const MAX_FRAGMENT: usize = 4 * 1024 * 1024; /// Deepest element nesting accepted in a kept fragment. const MAX_FRAGMENT_DEPTH: usize = 256; #[cfg(test)] thread_local! { /// A smaller kept-bytes allowance for tests on this thread. pub(crate) static KEPT_TOTAL_FOR_TEST: std::cell::Cell = const { std::cell::Cell::new(MAX_KEPT_TOTAL) }; } fn kept_total() -> usize { #[cfg(test)] { KEPT_TOTAL_FOR_TEST.with(std::cell::Cell::get) } #[cfg(not(test))] { MAX_KEPT_TOTAL } } const NS_REL_STRICT: &str = "http://purl.oclc.org/ooxml/officeDocument/relationships"; const CT_VML: &str = "application/vnd.openxmlformats-officedocument.vmlDrawing"; /// Relationship kinds (last segment of the type) kept from the package root. const PACKAGE_ROOTS: &[&str] = &["custom-properties", "extensibility", "webextensiontaskpanes"]; /// Relationship kinds kept from the workbook without a workbook element. const WORKBOOK_ROOTS: &[&str] = &["customXml", "connections", "xmlMaps"]; /// Relationship kinds kept from a sheet without a sheet element. const SHEET_ROOTS: &[&str] = &["queryTable", "tableSingleCells"]; /// Sheet relationship kinds that mean the sheet XML may hold elements worth keeping. const SHEET_ELEMENT_KINDS: &[&str] = &["oleObject", "ctrlProp", "control", "image", "package", "printerSettings"]; /// Attributes of `` (CT_FileVersion). const FILE_VERSION_ATTRS: &[&str] = &["appName", "lastEdited", "lowestEdited", "rupBuild", "codeName"]; /// The workbook extension listing the calculation features the saved values were computed with. const CALC_FEATURES_URI: &str = "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}"; const NS_CALC_FEATURES: &str = "http://schemas.microsoft.com/office/spreadsheetml/2018/calcfeatures"; /// Kept sheet elements, in schema order (all after ``, before ``). const SHEET_SLOTS: &[&str] = &["legacyDrawingHF", "picture", "oleObjects", "controls"]; /// Relationship kinds GridCraft reads and rewrites, or that depend on content it rewrites: /// kept content whose graph reaches one of them is dropped. const REWRITTEN: &[&str] = &[ "officedocument", "core-properties", "extended-properties", "worksheet", "chartsheet", "dialogsheet", "xlmacrosheet", "drawing", "chart", "chartex", "chartstyle", "chartcolorstyle", "chartusershapes", "table", "pivottable", "pivotcachedefinition", "pivotcacherecords", "comments", "vmldrawing", "threadedcomment", "person", "sharedstrings", "styles", "theme", "sheetmetadata", "calcchain", "vbaproject", "rdrichvalue", "rdrichvaluestructure", "rdrichvaluetypes", "rdrichvaluewebimage", "rdarray", "rdsupportingpropertybag", "rdsupportingpropertybagstructure", "richvaluerel", "slicer", "slicercache", "timeline", "timelinecache", "powerpivotdata", ]; /// Relationship kinds a chart sheet reaches that are otherwise rewritten: its drawing, chart and /// their style, colour and shape parts are kept byte for byte (the chart sheet is not modelled). const CHART_SHEET_GRAPH: &[&str] = &["chartsheet", "drawing", "chart", "chartex", "chartstyle", "chartcolorstyle", "chartusershapes"]; /// Part-name prefixes (lower case) GridCraft's writer never produces: kept parts there keep their /// original names. Anything else is renamed so it can't collide with written parts. const OWN_DIRS: &[&str] = &[ "customxml/", "docprops/custom.xml", "customui/", "xl/externallinks/", "xl/connections.xml", "xl/xmlmaps.xml", "xl/querytables/", "xl/ctrlprops/", "xl/embeddings/", "xl/activex/", "xl/webextensions/", "xl/printersettings/", ]; fn kind(rel_type: &str) -> &str { rel_type.rsplit('/').next().unwrap_or("") } fn is_kind(r: &PreservedRel, kinds: &[&str]) -> bool { let k = kind(&r.rel_type); kinds.iter().any(|x| x.eq_ignore_ascii_case(k)) } fn rewritten(r: &PreservedRel) -> bool { !r.external && REWRITTEN.contains(&kind(&r.rel_type).to_ascii_lowercase().as_str()) } fn rels_part_name(part: &str) -> String { let (dir, file) = package::split_dir(part); if dir.is_empty() { format!("_rels/{file}.rels") } else { format!("{dir}/_rels/{file}.rels") } }