1use quick_xml::de::from_reader as xml_from_reader;
6use quick_xml::se::to_string as xml_to_string;
7
8use crate::constants::{DEFAULT_XML_PATH_CALC_CHAIN, DEFAULT_XML_PATH_VOLATILE_DEPS};
9use crate::errors::Result;
10use crate::file::{File, namespace_strict_to_transitional};
11use crate::xml::calc_chain::{XlsxCalcChain, XlsxCalcChainC, XlsxVolTypes};
12use crate::xml::content_types::XlsxContentTypeEntry;
13
14impl File {
15 pub fn calc_chain_reader(&self) -> Result<XlsxCalcChain> {
17 if self.calc_chain.lock().unwrap().is_none() {
18 let data =
19 self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_CALC_CHAIN))?;
20 let data = namespace_strict_to_transitional(&data);
21 let cc = if data.is_empty() {
22 XlsxCalcChain::default()
23 } else {
24 xml_from_reader(data.as_slice())?
25 };
26 *self.calc_chain.lock().unwrap() = Some(cc);
27 }
28 Ok(self.calc_chain.lock().unwrap().clone().unwrap())
29 }
30
31 pub fn calc_chain_writer(&self) {
33 if let Some(cc) = self.calc_chain.lock().unwrap().clone() {
34 if !cc.c.is_empty() {
35 if let Ok(mut output) = xml_to_string(&cc).map(|s| s.into_bytes()) {
36 self.replace_namespace_bytes_if_needed(
37 DEFAULT_XML_PATH_CALC_CHAIN,
38 &mut output,
39 );
40 self.save_file_list(DEFAULT_XML_PATH_CALC_CHAIN, &output);
41 }
42 }
43 }
44 }
45
46 pub fn delete_calc_chain(&self, index: i32, cell: &str) -> Result<()> {
48 let mut calc = self.calc_chain_reader()?;
49 calc.c = filter_calc_chain(calc.c, |c| {
50 (c.i != index || c.r != cell)
51 && (c.i != index || !cell.is_empty())
52 && (c.i != 0 || c.r != cell)
53 });
54 if calc.c.is_empty() {
55 *self.calc_chain.lock().unwrap() = None;
56 self.pkg.remove(DEFAULT_XML_PATH_CALC_CHAIN);
57 let mut ct = self.content_types_reader()?;
58 ct.entries.retain(|e| {
59 if let XlsxContentTypeEntry::Override(o) = e {
60 o.part_name != "/xl/calcChain.xml"
61 } else {
62 true
63 }
64 });
65 *self.content_types.lock().unwrap() = Some(ct);
66 } else {
67 *self.calc_chain.lock().unwrap() = Some(calc);
68 }
69 Ok(())
70 }
71
72 pub fn volatile_deps_reader(&self) -> Result<Option<XlsxVolTypes>> {
74 if self.volatile_deps.lock().unwrap().is_none() {
75 if !self.pkg.contains_key(DEFAULT_XML_PATH_VOLATILE_DEPS)
76 && !self.temp_files.contains_key(DEFAULT_XML_PATH_VOLATILE_DEPS)
77 {
78 return Ok(None);
79 }
80 let data =
81 self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_VOLATILE_DEPS))?;
82 let data = namespace_strict_to_transitional(&data);
83 let vt = if data.is_empty() {
84 XlsxVolTypes::default()
85 } else {
86 xml_from_reader(data.as_slice())?
87 };
88 *self.volatile_deps.lock().unwrap() = Some(vt);
89 }
90 Ok(self.volatile_deps.lock().unwrap().clone())
91 }
92
93 pub fn volatile_deps_writer(&self) {
95 if let Some(vt) = self.volatile_deps.lock().unwrap().clone() {
96 if let Ok(mut output) = xml_to_string(&vt).map(|s| s.into_bytes()) {
97 self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_VOLATILE_DEPS, &mut output);
98 self.save_file_list(DEFAULT_XML_PATH_VOLATILE_DEPS, &output);
99 }
100 }
101 }
102}
103
104fn filter_calc_chain(
107 collection: Vec<XlsxCalcChainC>,
108 predicate: impl Fn(&XlsxCalcChainC) -> bool,
109) -> Vec<XlsxCalcChainC> {
110 collection.into_iter().filter(predicate).collect()
111}
112
113pub fn delete_vol_topic_ref(vt: &mut XlsxVolTypes, i1: usize, i2: usize, i3: usize, i4: usize) {
115 if i1 < vt.vol_type.len()
116 && i2 < vt.vol_type[i1].main.len()
117 && i3 < vt.vol_type[i1].main[i2].tp.len()
118 && i4 < vt.vol_type[i1].main[i2].tp[i3].tr.len()
119 {
120 vt.vol_type[i1].main[i2].tp[i3].tr.remove(i4);
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::constants::DEFAULT_XML_PATH_CONTENT_TYPES;
128 use crate::file::File;
129 use crate::options::Options;
130 use crate::xml::calc_chain::{XlsxVolMain, XlsxVolTopic, XlsxVolTopicRef, XlsxVolType};
131 use crate::xml::content_types::{XlsxContentTypeEntry, XlsxOverride};
132
133 const MACINTOSH_CYRILLIC_CHARSET: &[u8] =
135 &[0x8F, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2, 0x20, 0xEC, 0xE8, 0xF0];
136
137 #[test]
138 fn calc_chain_reader_empty_new_file() {
139 let f = File::new_with_options(Options::default());
140 let cc = f.calc_chain_reader().unwrap();
141 assert!(cc.c.is_empty());
142 }
143
144 #[test]
145 fn calc_chain_reader_unsupported_charset() {
146 let f = File::new_with_options(Options::default());
147 *f.calc_chain.lock().unwrap() = None;
148 f.pkg.insert(
149 DEFAULT_XML_PATH_CALC_CHAIN.to_string(),
150 MACINTOSH_CYRILLIC_CHARSET.to_vec(),
151 );
152 assert!(f.calc_chain_reader().is_err());
153 }
154
155 #[test]
156 fn calc_chain_reader_round_trip_from_pkg() {
157 let f = File::new_with_options(Options::default());
158 let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
159<calcChain xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
160 <c r="A1" i="1"/>
161 <c r="B2" i="2" l="1" s="1" t="1" a="1"/>
162</calcChain>"#;
163 f.pkg
164 .insert(DEFAULT_XML_PATH_CALC_CHAIN.to_string(), xml.to_vec());
165 *f.calc_chain.lock().unwrap() = None;
166
167 let cc = f.calc_chain_reader().unwrap();
168 assert_eq!(cc.c.len(), 2);
169 assert_eq!(cc.c[0].r, "A1");
170 assert_eq!(cc.c[0].i, 1);
171 assert_eq!(cc.c[1].r, "B2");
172 assert_eq!(cc.c[1].i, 2);
173 assert!(cc.c[1].l);
174 assert!(cc.c[1].s);
175 assert!(cc.c[1].t);
176 assert!(cc.c[1].a);
177 }
178
179 #[test]
180 fn calc_chain_writer_saves_entries() {
181 let f = File::new_with_options(Options::default());
182 let cc = XlsxCalcChain {
183 c: vec![XlsxCalcChainC {
184 r: "B2".to_string(),
185 i: 1,
186 l: true,
187 s: true,
188 t: true,
189 a: true,
190 }],
191 };
192 *f.calc_chain.lock().unwrap() = Some(cc);
193 f.calc_chain_writer();
194
195 let raw = f.read_xml(DEFAULT_XML_PATH_CALC_CHAIN);
196 let xml = String::from_utf8(raw).unwrap();
197 assert!(xml.contains("<calcChain"));
198 assert!(xml.contains("B2"));
199 }
200
201 #[test]
202 fn calc_chain_writer_skips_empty_chain() {
203 let f = File::new_with_options(Options::default());
204 *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain::default());
205 f.calc_chain_writer();
206 assert!(!f.pkg.contains_key(DEFAULT_XML_PATH_CALC_CHAIN));
207 }
208
209 #[test]
210 fn delete_calc_chain_filters_entries_by_index_and_cell() {
211 let f = File::new_with_options(Options::default());
212 *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain {
213 c: vec![
214 XlsxCalcChainC {
215 r: "A1".to_string(),
216 i: 1,
217 ..Default::default()
218 },
219 XlsxCalcChainC {
220 r: "A2".to_string(),
221 i: 1,
222 ..Default::default()
223 },
224 XlsxCalcChainC {
225 r: "A1".to_string(),
226 i: 2,
227 ..Default::default()
228 },
229 XlsxCalcChainC {
230 r: "A3".to_string(),
231 i: 0,
232 ..Default::default()
233 },
234 ],
235 });
236 f.delete_calc_chain(1, "A1").unwrap();
237
238 let cc = f.calc_chain_reader().unwrap();
239 assert_eq!(cc.c.len(), 3);
240 assert!(cc.c.iter().any(|c| c.r == "A2" && c.i == 1));
241 assert!(cc.c.iter().any(|c| c.r == "A1" && c.i == 2));
242 assert!(cc.c.iter().any(|c| c.r == "A3" && c.i == 0));
243 }
244
245 #[test]
246 fn delete_calc_chain_filters_all_entries_for_index_when_cell_empty() {
247 let f = File::new_with_options(Options::default());
248 *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain {
249 c: vec![
250 XlsxCalcChainC {
251 r: "A1".to_string(),
252 i: 1,
253 ..Default::default()
254 },
255 XlsxCalcChainC {
256 r: "A2".to_string(),
257 i: 1,
258 ..Default::default()
259 },
260 XlsxCalcChainC {
261 r: "A1".to_string(),
262 i: 2,
263 ..Default::default()
264 },
265 ],
266 });
267 f.delete_calc_chain(1, "").unwrap();
268
269 let cc = f.calc_chain_reader().unwrap();
270 assert_eq!(cc.c.len(), 1);
271 assert_eq!(cc.c[0].r, "A1");
272 assert_eq!(cc.c[0].i, 2);
273 }
274
275 #[test]
276 fn delete_calc_chain_filters_entries_with_index_zero() {
277 let f = File::new_with_options(Options::default());
278 *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain {
279 c: vec![
280 XlsxCalcChainC {
281 r: "A1".to_string(),
282 i: 1,
283 ..Default::default()
284 },
285 XlsxCalcChainC {
286 r: "A1".to_string(),
287 i: 0,
288 ..Default::default()
289 },
290 XlsxCalcChainC {
291 r: "A2".to_string(),
292 i: 0,
293 ..Default::default()
294 },
295 ],
296 });
297 f.delete_calc_chain(0, "A1").unwrap();
298
299 let cc = f.calc_chain_reader().unwrap();
300 assert_eq!(cc.c.len(), 2);
301 assert!(cc.c.iter().any(|c| c.r == "A1" && c.i == 1));
302 assert!(cc.c.iter().any(|c| c.r == "A2" && c.i == 0));
303 }
304
305 #[test]
306 fn delete_calc_chain_removes_empty_chain() {
307 let f = File::new_with_options(Options::default());
308 *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain { c: Vec::new() });
309 f.pkg.insert(
310 DEFAULT_XML_PATH_CALC_CHAIN.to_string(),
311 b"<calcChain/>".to_vec(),
312 );
313 let mut ct = f.content_types_reader().unwrap();
314 ct.entries
315 .push(XlsxContentTypeEntry::Override(XlsxOverride {
316 part_name: "/xl/calcChain.xml".to_string(),
317 content_type:
318 "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"
319 .to_string(),
320 }));
321 *f.content_types.lock().unwrap() = Some(ct);
322
323 f.delete_calc_chain(1, "A1").unwrap();
324
325 assert!(f.calc_chain.lock().unwrap().is_none());
326 assert!(!f.pkg.contains_key(DEFAULT_XML_PATH_CALC_CHAIN));
327 let ct = f.content_types_reader().unwrap();
328 assert!(!ct.entries.iter().any(
329 |e| matches!(e, XlsxContentTypeEntry::Override(o) if o.part_name == "/xl/calcChain.xml")
330 ));
331 }
332
333 #[test]
334 fn delete_calc_chain_unsupported_charset() {
335 let f = File::new_with_options(Options::default());
336 *f.calc_chain.lock().unwrap() = None;
337 f.pkg.insert(
338 DEFAULT_XML_PATH_CALC_CHAIN.to_string(),
339 MACINTOSH_CYRILLIC_CHARSET.to_vec(),
340 );
341 assert!(f.delete_calc_chain(1, "A1").is_err());
342 }
343
344 #[test]
345 fn delete_calc_chain_unsupported_charset_content_types() {
346 let f = File::new_with_options(Options::default());
347 *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain { c: Vec::new() });
348 *f.content_types.lock().unwrap() = None;
349 f.pkg.insert(
350 DEFAULT_XML_PATH_CONTENT_TYPES.to_string(),
351 MACINTOSH_CYRILLIC_CHARSET.to_vec(),
352 );
353 assert!(f.delete_calc_chain(1, "A1").is_err());
354 }
355
356 #[test]
357 fn volatile_deps_reader_missing_file() {
358 let f = File::new_with_options(Options::default());
359 assert_eq!(f.volatile_deps_reader().unwrap(), None);
360 }
361
362 #[test]
363 fn volatile_deps_reader_unsupported_charset() {
364 let f = File::new_with_options(Options::default());
365 f.pkg.insert(
366 DEFAULT_XML_PATH_VOLATILE_DEPS.to_string(),
367 MACINTOSH_CYRILLIC_CHARSET.to_vec(),
368 );
369 assert!(f.volatile_deps_reader().is_err());
370 }
371
372 #[test]
373 fn volatile_deps_writer_round_trip() {
374 let f = File::new_with_options(Options::default());
375 let vt = XlsxVolTypes {
376 vol_type: vec![XlsxVolType {
377 r#type: "realTimeData".to_string(),
378 main: vec![XlsxVolMain {
379 first: "A1".to_string(),
380 tp: vec![XlsxVolTopic {
381 t: None,
382 v: String::new(),
383 stp: Vec::new(),
384 tr: vec![XlsxVolTopicRef {
385 r: "A1".to_string(),
386 s: 1,
387 }],
388 }],
389 }],
390 }],
391 ext_lst: None,
392 };
393 *f.volatile_deps.lock().unwrap() = Some(vt);
394 f.volatile_deps_writer();
395
396 let raw = f.read_xml(DEFAULT_XML_PATH_VOLATILE_DEPS);
397 let xml = String::from_utf8(raw).unwrap();
398 assert!(xml.contains("<volTypes"));
399 assert!(xml.contains("realTimeData"));
400 }
401
402 #[test]
403 fn delete_vol_topic_ref_removes_index() {
404 let mut vt = XlsxVolTypes {
405 vol_type: vec![XlsxVolType {
406 r#type: "realTimeData".to_string(),
407 main: vec![XlsxVolMain {
408 first: "A1".to_string(),
409 tp: vec![XlsxVolTopic {
410 t: None,
411 v: String::new(),
412 stp: Vec::new(),
413 tr: vec![
414 XlsxVolTopicRef {
415 r: "A1".to_string(),
416 s: 1,
417 },
418 XlsxVolTopicRef {
419 r: "A2".to_string(),
420 s: 2,
421 },
422 XlsxVolTopicRef {
423 r: "A3".to_string(),
424 s: 3,
425 },
426 ],
427 }],
428 }],
429 }],
430 ext_lst: None,
431 };
432 delete_vol_topic_ref(&mut vt, 0, 0, 0, 1);
433 assert_eq!(vt.vol_type[0].main[0].tp[0].tr.len(), 2);
434 assert_eq!(vt.vol_type[0].main[0].tp[0].tr[0].r, "A1");
435 assert_eq!(vt.vol_type[0].main[0].tp[0].tr[1].r, "A3");
436 }
437
438 #[test]
439 fn delete_vol_topic_ref_out_of_bounds_is_no_op() {
440 let mut vt = XlsxVolTypes::default();
441 delete_vol_topic_ref(&mut vt, 0, 0, 0, 0);
442 assert!(vt.vol_type.is_empty());
443 }
444}