Skip to main content

ravenrs_sheet/xlsx/passthrough/
rebase.rs

1use super::read::{first_free_block, merge_vml};
2use super::write::{NameMap, attach, part_rels_xml, remap, write_parts};
3use super::*;
4
5/// A sheet's kept content being written.
6pub struct SheetKept<'a> {
7    p: Option<&'a Preserved>,
8    names: NameMap,
9    ids: HashMap<String, String>,
10}
11
12impl<'a> SheetKept<'a> {
13    /// Writes the sheet's kept parts and adds their relationships to the sheet's.
14    pub fn write(sheet: &'a Sheet, rels: &mut Rels, out: &mut Out) -> SheetKept<'a> {
15        let Some(p) = sheet.preserved.as_deref() else { return SheetKept { p: None, names: NameMap::new(), ids: HashMap::new() } };
16        let names = write_parts(out, &p.parts);
17        let ids = attach(rels, &p.rels, &names);
18        SheetKept { p: Some(p), names, ids }
19    }
20
21    fn vml(&self) -> Option<&'a PreservedPart> {
22        self.p?.vml.as_ref()
23    }
24
25    fn write_vml_rels(&self, vml: &PreservedPart, name: &str, out: &mut Out) {
26        if !vml.rels.is_empty() {
27            out.part(&rels_part_name(name), None, part_rels_xml(&vml.rels, &self.names).into_bytes());
28        }
29    }
30
31    /// The number GridCraft's own VML shapes start after when they join the kept drawing
32    /// ([`SheetKept::notes_vml`] numbers them `base + 1`, `base + 2`…); `None` without one.
33    pub fn shape_base(&self) -> Option<u64> {
34        let vml = self.vml()?;
35        Some(first_free_block(&String::from_utf8_lossy(&vml.data)).saturating_mul(1024))
36    }
37
38    /// GridCraft's notes and form controls VML for part `name`, merged with the kept control and
39    /// object shapes.
40    pub fn notes_vml(&self, ours: String, name: &str, out: &mut Out) -> String {
41        let Some(vml) = self.vml() else { return ours };
42        self.write_vml_rels(vml, name, out);
43        merge_vml(&String::from_utf8_lossy(&vml.data), &ours)
44    }
45
46    /// The kept VML drawing of a sheet without notes; its new relationship id.
47    pub fn lone_vml(&self, rels: &mut Rels, out: &mut Out) -> Option<String> {
48        let vml = self.vml()?;
49        let name = out.kept.claim(&vml.name);
50        out.part(&name, Some(CT_VML), vml.data.to_vec());
51        self.write_vml_rels(vml, &name, out);
52        Some(rels.add("vmlDrawing", &format!("/{name}")))
53    }
54
55    /// The new relationship id for `<pageSetup r:id>` (printer settings).
56    pub fn page_setup_id(&self) -> Option<&str> {
57        let old = &self.p?.elements.iter().find(|e| e.slot == "pageSetup")?.xml;
58        self.ids.get(old).map(String::as_str)
59    }
60
61    /// The kept sheet elements, in schema order, with GridCraft's own form control entries
62    /// (`controls`, see `controls::write`) in the sheet's one `<controls>` element.
63    pub fn elements(&self, controls: &str) -> String {
64        let mut s = String::new();
65        let mut placed = controls.is_empty();
66        if let Some(p) = self.p {
67            for slot in SHEET_SLOTS {
68                for e in p.elements.iter().filter(|e| e.slot == *slot) {
69                    if let Some(mut x) = remap(&e.xml, &self.ids) {
70                        if *slot == "controls"
71                            && !placed
72                            && let Some(i) = controls_insert_at(&x)
73                        {
74                            x.insert_str(i, controls);
75                            placed = true;
76                        }
77                        s.push_str(&x);
78                    }
79                }
80            }
81        }
82        // `<controls>` comes last of the kept elements.
83        if !placed {
84            s.push_str(&crate::xlsx::controls::controls_element(controls));
85        }
86        s
87    }
88}
89
90/// Where GridCraft's control entries go in a kept `<controls>` fragment: just before the close
91/// tag of the `<controls>` element the reader reads (`controls::controls_of`), whatever its
92/// namespace prefix. In an `mc:AlternateContent` with separate `<controls>` elements in its
93/// Choice and Fallback branches that is the Choice's, the one an application that knows the
94/// x14 extensions reads; the Fallback's is left as the file had it. With no Choice, or a Choice
95/// without a `<controls>` element, it is the first `<controls>` element (a plain fragment, or a
96/// Fallback alone).
97fn controls_insert_at(x: &str) -> Option<usize> {
98    let from = open_tag(x, "Choice").unwrap_or(0);
99    close_tag_from(x, from, "controls")
100}
101
102/// The local name of the tag whose text starts at `name` (`x:controls` → `controls`).
103fn local_name(name: &str) -> &str {
104    name.rsplit(':').next().unwrap_or(name)
105}
106
107/// The byte offset of the first open tag of `local` in `x`, with any namespace prefix.
108fn open_tag(x: &str, local: &str) -> Option<usize> {
109    x.match_indices('<').find_map(|(i, _)| {
110        let rest = x.get(i + 1..)?;
111        let end = rest.find(|c: char| c.is_whitespace() || c == '>' || c == '/')?;
112        (local_name(rest.get(..end)?) == local).then_some(i)
113    })
114}
115
116/// The byte offset of the first close tag of `local` at or after `from` in `x`, with any
117/// namespace prefix (`</controls>`, `</x:controls>`).
118fn close_tag_from(x: &str, from: usize, local: &str) -> Option<usize> {
119    x.get(from..)?.match_indices("</").find_map(|(i, _)| {
120        let rest = x.get(from + i + 2..)?;
121        let name = rest.get(..rest.find('>')?)?.trim_end();
122        (local_name(name) == local).then_some(from + i)
123    })
124}
125
126// ---------------------------------------------------------------- linked workbooks
127
128/// `wb` as it is saved into the folder `to` (absolute): the relative paths of its linked
129/// workbooks, relative to the folder it was read from, are rewritten relative to `to`, as Excel
130/// does on Save As, or made absolute when no relative path exists (another drive). `None` when
131/// nothing changes.
132pub fn rebase_links(wb: &Workbook, to: &Path) -> Option<Workbook> {
133    let p = wb.preserved.as_deref()?;
134    let from = Folder::of(wb.source_dir.as_deref()?)?;
135    let to = Folder::of(to)?;
136    if from == to {
137        return None;
138    }
139    let mut parts = p.parts.clone();
140    let mut changed = false;
141    for r in parts.iter_mut().flat_map(|part| part.rels.iter_mut()) {
142        if r.external
143            && is_kind(r, &["externalLinkPath"])
144            && let Some(target) = rebase(&r.target, &from, &to)
145        {
146            r.target = target;
147            changed = true;
148        }
149    }
150    if !changed {
151        return None;
152    }
153    let mut out = wb.clone();
154    out.preserved = Some(Arc::new(Preserved { parts, ..p.clone() }));
155    Some(out)
156}
157
158/// An absolute folder, lexically normalized.
159#[derive(PartialEq)]
160struct Folder {
161    /// Drive or share and root separator (`/`, `C:\`, `\\server\share\`).
162    root: String,
163    names: Vec<String>,
164}
165
166impl Folder {
167    fn of(dir: &Path) -> Option<Folder> {
168        if !dir.is_absolute() {
169            return None;
170        }
171        let mut f = Folder { root: String::new(), names: vec![] };
172        for c in dir.components() {
173            match c {
174                Component::Prefix(_) | Component::RootDir => f.root.push_str(&c.as_os_str().to_string_lossy()),
175                Component::CurDir => {}
176                Component::ParentDir => {
177                    f.names.pop();
178                }
179                Component::Normal(n) => f.names.push(n.to_string_lossy().into_owned()),
180            }
181        }
182        Some(f)
183    }
184}
185
186/// A relative link target read from `from`, rewritten for `to`. `None` keeps it as it is: an
187/// absolute path or URL, or a path that climbs above the root.
188fn rebase(target: &str, from: &Folder, to: &Folder) -> Option<String> {
189    if !is_relative_path(target) {
190        return None;
191    }
192    // Each name as the file system knows it, and as it is written in a target. Names taken from
193    // the original target keep their spelling.
194    let mut path: Vec<(String, String)> = from.names.iter().map(|n| (n.clone(), encode_name(n))).collect();
195    for seg in target.split(['/', '\\']) {
196        match seg {
197            "" | "." => {}
198            ".." => {
199                path.pop()?;
200            }
201            _ => path.push((decode_name(seg), seg.to_string())),
202        }
203    }
204    let folders = path.len().checked_sub(1)?;
205    if !from.root.eq_ignore_ascii_case(&to.root) {
206        let sep = if from.root.contains('\\') { "\\" } else { "/" };
207        let names: Vec<&str> = path.iter().map(|(name, _)| name.as_str()).collect();
208        return Some(format!("file:///{}{}", from.root, names.join(sep)));
209    }
210    let common = to.names.iter().zip(&path).take(folders).take_while(|(a, (b, _))| *a == b).count();
211    let up = "../".repeat(to.names.len().saturating_sub(common));
212    let down: Vec<&str> = path.iter().skip(common).map(|(_, written)| written.as_str()).collect();
213    Some(format!("{up}{}", down.join("/")))
214}
215
216/// Not empty, no root, no scheme or drive (`file:`, `http:`, `C:`).
217fn is_relative_path(target: &str) -> bool {
218    let scheme = target.split_once(':').is_some_and(|(s, _)| {
219        s.starts_with(|c: char| c.is_ascii_alphabetic()) && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
220    });
221    !target.is_empty() && !target.starts_with(['/', '\\']) && !scheme
222}
223
224/// A folder name as a segment of a relationship target (a URI): ASCII characters a URI path
225/// can't hold are percent-encoded, other characters are written as they are.
226fn encode_name(name: &str) -> String {
227    let mut s = String::with_capacity(name.len());
228    for c in name.chars() {
229        if !c.is_ascii() || c.is_ascii_alphanumeric() || "-._~!$&'()+,;=@".contains(c) {
230            s.push(c);
231        } else {
232            let _ = write!(s, "%{:02X}", c as u32);
233        }
234    }
235    s
236}
237
238/// A target segment with its percent-escapes decoded; as written when they don't decode to UTF-8.
239fn decode_name(seg: &str) -> String {
240    let b = seg.as_bytes();
241    let mut out = Vec::with_capacity(b.len());
242    let mut i = 0;
243    while let Some(&c) = b.get(i) {
244        let hex = b.get(i + 1..i + 3).and_then(|h| std::str::from_utf8(h).ok()).and_then(|h| u8::from_str_radix(h, 16).ok());
245        match hex {
246            Some(byte) if c == b'%' => {
247                out.push(byte);
248                i += 3;
249            }
250            _ => {
251                out.push(c);
252                i += 1;
253            }
254        }
255    }
256    String::from_utf8(out).unwrap_or_else(|_| seg.to_string())
257}