ravenrs_sheet/xlsx/passthrough/
rebase.rs1use super::read::{first_free_block, merge_vml};
2use super::write::{NameMap, attach, part_rels_xml, remap, write_parts};
3use super::*;
4
5pub struct SheetKept<'a> {
7 p: Option<&'a Preserved>,
8 names: NameMap,
9 ids: HashMap<String, String>,
10}
11
12impl<'a> SheetKept<'a> {
13 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 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 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 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 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 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 if !placed {
84 s.push_str(&crate::xlsx::controls::controls_element(controls));
85 }
86 s
87 }
88}
89
90fn 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
102fn local_name(name: &str) -> &str {
104 name.rsplit(':').next().unwrap_or(name)
105}
106
107fn 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
116fn 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
126pub 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#[derive(PartialEq)]
160struct Folder {
161 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
186fn rebase(target: &str, from: &Folder, to: &Folder) -> Option<String> {
189 if !is_relative_path(target) {
190 return None;
191 }
192 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
216fn 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
224fn 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
238fn 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}