mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-09 03:49:54 -06:00
191 lines
7.7 KiB
Rust
191 lines
7.7 KiB
Rust
//! 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 `<externalReferences>`, 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 `<picture>`, `<oleObjects>` and `<controls>` XML, the `r:id`
|
|
//! of `<pageSetup>`, the legacy VML shapes those controls need and the pictures of the headers and
|
|
//! footers, `&G`, with their `<legacyDrawingHF>`). The workbook also keeps its
|
|
//! `<fileVersion>` 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<usize> = 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 `<fileVersion>` (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 `<legacyDrawing>`, before `<tableParts>`).
|
|
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") }
|
|
}
|