1use std::collections::HashMap;
4use std::io::{Read, Seek, Write};
5use std::path::Path;
6
7use zip::ZipWriter;
8use zip::read::ZipArchive;
9use zip::write::SimpleFileOptions;
10
11use crate::content_types::ContentTypes;
12use crate::error::{OpcError, Result};
13use crate::relationship::{Relationships, rel_types};
14
15#[derive(Debug, Clone)]
17pub struct PackagePart {
18 pub name: String,
20 pub data: Vec<u8>,
22}
23
24#[derive(Debug, Clone)]
26pub struct OpcPackage {
27 pub content_types: ContentTypes,
29 pub package_rels: Relationships,
31 pub part_rels: HashMap<String, Relationships>,
35 pub parts: HashMap<String, Vec<u8>>,
37}
38
39impl OpcPackage {
40 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
42 let file = std::fs::File::open(path)?;
43 Self::from_reader(file)
44 }
45
46 pub fn from_reader<R: Read + Seek>(reader: R) -> Result<Self> {
48 let mut archive = ZipArchive::new(reader)?;
49 let mut raw_parts: HashMap<String, Vec<u8>> = HashMap::new();
50
51 for i in 0..archive.len() {
53 let mut entry = archive.by_index(i)?;
54 if entry.is_dir() {
55 continue;
56 }
57 let name = normalize_part_name(entry.name());
58 let name = name.strip_prefix('/').unwrap_or(&name).to_string();
59 let mut data = Vec::new();
60 entry.read_to_end(&mut data)?;
61 raw_parts.insert(name, data);
62 }
63
64 let ct_xml = raw_parts
66 .get("[Content_Types].xml")
67 .ok_or_else(|| OpcError::PartNotFound("[Content_Types].xml".into()))?;
68 let content_types = ContentTypes::from_xml(ct_xml)?;
69
70 let package_rels = if let Some(rels_xml) = raw_parts.get("_rels/.rels") {
72 Relationships::from_xml(rels_xml)?
73 } else {
74 Relationships::new()
75 };
76
77 let mut part_rels = HashMap::new();
79 let rels_entries: Vec<String> = raw_parts
80 .keys()
81 .filter(|k| k.ends_with(".rels") && *k != "_rels/.rels")
82 .cloned()
83 .collect();
84
85 for rels_path in rels_entries {
86 if let Some(xml_data) = raw_parts.get(&rels_path) {
87 let rels = Relationships::from_xml(xml_data)?;
88 let part_name = rels_path_to_part_name(&rels_path);
91 part_rels.insert(part_name, rels);
92 }
93 }
94
95 let mut parts = HashMap::new();
97 for (name, data) in &raw_parts {
98 if name == "[Content_Types].xml" || name == "_rels/.rels" || name.ends_with(".rels") {
99 continue;
100 }
101 let normalized = if name.starts_with('/') {
102 name.clone()
103 } else {
104 format!("/{name}")
105 };
106 parts.insert(normalized, data.clone());
107 }
108
109 Ok(OpcPackage {
110 content_types,
111 package_rels,
112 part_rels,
113 parts,
114 })
115 }
116
117 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
119 let file = std::fs::File::create(path)?;
120 self.write_to(file)
121 }
122
123 pub fn write_to<W: Write + Seek>(&self, writer: W) -> Result<()> {
125 let mut zip = ZipWriter::new(writer);
126 let options =
127 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
128
129 let ct_xml = self.content_types.to_xml()?;
131 zip.start_file("[Content_Types].xml", options)?;
132 zip.write_all(&ct_xml)?;
133
134 let pkg_rels_xml = self.package_rels.to_xml()?;
136 zip.start_file("_rels/.rels", options)?;
137 zip.write_all(&pkg_rels_xml)?;
138
139 let mut rels_names: Vec<&String> = self.part_rels.keys().collect();
143 rels_names.sort_unstable();
144 for part_name in rels_names {
145 let rels = &self.part_rels[part_name];
146 let rels_path = part_name_to_rels_path(part_name);
147 let rels_xml = rels.to_xml()?;
148 zip.start_file(&rels_path, options)?;
149 zip.write_all(&rels_xml)?;
150 }
151
152 let mut part_names: Vec<&String> = self.parts.keys().collect();
154 part_names.sort_unstable();
155 for name in part_names {
156 let zip_name = name.strip_prefix('/').unwrap_or(name);
158 zip.start_file(zip_name, options)?;
159 zip.write_all(&self.parts[name])?;
160 }
161
162 zip.finish()?;
163 Ok(())
164 }
165
166 pub fn get_part(&self, part_name: &str) -> Option<&[u8]> {
168 self.parts.get(part_name).map(|v| v.as_slice())
169 }
170
171 pub fn set_part(&mut self, part_name: &str, data: Vec<u8>) {
173 self.parts.insert(part_name.to_string(), data);
174 }
175
176 pub fn get_part_rels(&self, part_name: &str) -> Option<&Relationships> {
178 self.part_rels.get(part_name)
179 }
180
181 pub fn get_or_create_part_rels(&mut self, part_name: &str) -> &mut Relationships {
183 self.part_rels.entry(part_name.to_string()).or_default()
184 }
185
186 pub fn resolve_rel_target(source_part: &str, rel_target: &str) -> String {
193 let joined = if rel_target.starts_with('/') {
194 rel_target.to_string()
195 } else {
196 let dir = match source_part.rfind('/') {
198 Some(pos) => &source_part[..=pos],
199 None => "/",
200 };
201 format!("{dir}{rel_target}")
202 };
203 normalize_part_name(&joined)
204 }
205
206 pub fn main_document_part(&self) -> Option<String> {
208 self.package_rels
209 .get_by_type(rel_types::DOCUMENT)
210 .map(|rel| {
211 if rel.target.starts_with('/') {
212 rel.target.clone()
213 } else {
214 format!("/{}", rel.target)
215 }
216 })
217 }
218
219 pub fn new() -> Self {
221 OpcPackage {
222 content_types: ContentTypes::minimal(),
223 package_rels: Relationships::new(),
224 part_rels: HashMap::new(),
225 parts: HashMap::new(),
226 }
227 }
228
229 pub fn with_main_part(part_name: &str, content_type: &str) -> Self {
231 let mut package = Self::new();
232 let part_name = part_name.strip_prefix('/').unwrap_or(part_name);
233 package.package_rels.add(rel_types::DOCUMENT, part_name);
234 package
235 .content_types
236 .add_override(&format!("/{part_name}"), content_type);
237 package
238 }
239}
240
241impl Default for OpcPackage {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247#[cfg(test)]
248fn docx_package() -> OpcPackage {
249 let mut package = OpcPackage::with_main_part(
250 "word/document.xml",
251 "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
252 );
253 package.content_types.add_override(
254 "/word/styles.xml",
255 "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
256 );
257 package
258}
259
260fn normalize_part_name(path: &str) -> String {
265 if !path.contains("./") && !path.ends_with("/.") && !path.ends_with("/..") {
266 return path.to_string();
267 }
268
269 let mut segments: Vec<&str> = Vec::new();
270 for segment in path.split('/') {
271 match segment {
272 "" | "." => {}
273 ".." => {
274 segments.pop();
275 }
276 other => segments.push(other),
277 }
278 }
279
280 let mut out = String::with_capacity(path.len());
281 for segment in segments {
282 out.push('/');
283 out.push_str(segment);
284 }
285 if out.is_empty() { "/".to_string() } else { out }
286}
287
288fn rels_path_to_part_name(rels_path: &str) -> String {
291 let without_suffix = rels_path.strip_suffix(".rels").unwrap_or(rels_path);
295 let path = match without_suffix.rfind("_rels/") {
296 Some(pos) => format!(
297 "{}{}",
298 &without_suffix[..pos],
299 &without_suffix[pos + "_rels/".len()..]
300 ),
301 None => without_suffix.to_string(),
302 };
303 if path.starts_with('/') {
304 path
305 } else {
306 format!("/{path}")
307 }
308}
309
310fn part_name_to_rels_path(part_name: &str) -> String {
313 let name = part_name.strip_prefix('/').unwrap_or(part_name);
314 if let Some(pos) = name.rfind('/') {
315 let dir = &name[..pos];
316 let file = &name[pos + 1..];
317 format!("{dir}/_rels/{file}.rels")
318 } else {
319 format!("_rels/{name}.rels")
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 fn package_zip(entries: &[(&str, &[u8])]) -> std::io::Cursor<Vec<u8>> {
328 let mut buffer = std::io::Cursor::new(Vec::new());
329 {
330 let mut zip = ZipWriter::new(&mut buffer);
331 let options = SimpleFileOptions::default();
332 for (name, data) in entries {
333 zip.start_file(name, options).unwrap();
334 zip.write_all(data).unwrap();
335 }
336 zip.finish().unwrap();
337 }
338 buffer.set_position(0);
339 buffer
340 }
341
342 const MINIMAL_CONTENT_TYPES: &[u8] =
343 br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
344<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
345 <Default Extension="xml" ContentType="application/xml"/>
346</Types>"#;
347
348 fn independently_built_pptx() -> std::io::Cursor<Vec<u8>> {
349 const CONTENT_TYPES: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
350<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
351 <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
352 <Default Extension="xml" ContentType="application/xml"/>
353 <Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
354 <Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
355 <Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
356 <Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
357 <Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
358</Types>"#;
359 const PACKAGE_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
360<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
361 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
362</Relationships>"#;
363 const PRESENTATION_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
364<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
365 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>
366 <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
367</Relationships>"#;
368 const SLIDE_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
369<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
370 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
371</Relationships>"#;
372 const SLIDE_LAYOUT_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
373<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
374 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>
375</Relationships>"#;
376 const SLIDE_MASTER_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
377<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
378 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
379 <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>
380</Relationships>"#;
381 const PRESENTATION: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
382<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
383 <p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
384 <p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst>
385 <p:sldSz cx="12192000" cy="6858000"/>
386 <p:notesSz cx="6858000" cy="9144000"/>
387</p:presentation>"#;
388 const SLIDE: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
389<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
390 <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
391 <p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
392</p:sld>"#;
393 const SLIDE_LAYOUT: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
394<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
395 <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
396 <p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
397</p:sldLayout>"#;
398 const SLIDE_MASTER: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
399<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
400 <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
401 <p:clrMap accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" bg1="lt1" bg2="lt2" folHlink="folHlink" hlink="hlink" tx1="dk1" tx2="dk2"/>
402 <p:sldLayoutIdLst><p:sldLayoutId id="2147483648" r:id="rId1"/></p:sldLayoutIdLst>
403 <p:txStyles><p:titleStyle/><p:bodyStyle/><p:otherStyle/></p:txStyles>
404</p:sldMaster>"#;
405 const THEME: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
406<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Minimal">
407 <a:themeElements>
408 <a:clrScheme name="Minimal">
409 <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
410 <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
411 <a:dk2><a:srgbClr val="1F497D"/></a:dk2>
412 <a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
413 <a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
414 <a:accent2><a:srgbClr val="C0504D"/></a:accent2>
415 <a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
416 <a:accent4><a:srgbClr val="8064A2"/></a:accent4>
417 <a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
418 <a:accent6><a:srgbClr val="F79646"/></a:accent6>
419 <a:hlink><a:srgbClr val="0000FF"/></a:hlink>
420 <a:folHlink><a:srgbClr val="800080"/></a:folHlink>
421 </a:clrScheme>
422 <a:fontScheme name="Minimal">
423 <a:majorFont><a:latin typeface="Arial"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
424 <a:minorFont><a:latin typeface="Arial"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
425 </a:fontScheme>
426 <a:fmtScheme name="Minimal">
427 <a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst>
428 <a:lnStyleLst><a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="12700"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst>
429 <a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>
430 <a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst>
431 </a:fmtScheme>
432 </a:themeElements>
433</a:theme>"#;
434
435 package_zip(&[
436 ("[Content_Types].xml", CONTENT_TYPES),
437 ("_rels/.rels", PACKAGE_RELS),
438 ("ppt/presentation.xml", PRESENTATION),
439 ("ppt/_rels/presentation.xml.rels", PRESENTATION_RELS),
440 ("ppt/slides/slide1.xml", SLIDE),
441 ("ppt/slides/_rels/slide1.xml.rels", SLIDE_RELS),
442 ("ppt/slideLayouts/slideLayout1.xml", SLIDE_LAYOUT),
443 (
444 "ppt/slideLayouts/_rels/slideLayout1.xml.rels",
445 SLIDE_LAYOUT_RELS,
446 ),
447 ("ppt/slideMasters/slideMaster1.xml", SLIDE_MASTER),
448 (
449 "ppt/slideMasters/_rels/slideMaster1.xml.rels",
450 SLIDE_MASTER_RELS,
451 ),
452 ("ppt/theme/theme1.xml", THEME),
453 ])
454 }
455
456 fn pptx_package() -> OpcPackage {
457 let mut package =
458 OpcPackage::with_main_part("ppt/presentation.xml", crate::content_types::PRESENTATION);
459 package
460 .content_types
461 .add_override("/ppt/slides/slide1.xml", crate::content_types::SLIDE);
462 package.content_types.add_override(
463 "/ppt/slideLayouts/slideLayout1.xml",
464 crate::content_types::SLIDE_LAYOUT,
465 );
466 package.set_part("/ppt/presentation.xml", b"<p:presentation/>".to_vec());
467 package.set_part("/ppt/slides/slide1.xml", b"<p:sld/>".to_vec());
468 package.set_part(
469 "/ppt/slideLayouts/slideLayout1.xml",
470 b"<p:sldLayout/>".to_vec(),
471 );
472 package
473 .get_or_create_part_rels("/ppt/presentation.xml")
474 .add(rel_types::SLIDE, "slides/slide1.xml");
475 package
476 .get_or_create_part_rels("/ppt/slides/slide1.xml")
477 .add(rel_types::SLIDE_LAYOUT, "../slideLayouts/slideLayout1.xml");
478 package
479 }
480
481 #[test]
482 fn with_main_part_resolves_and_round_trips() {
483 let mut package = OpcPackage::with_main_part(
484 "ppt/presentation.xml",
485 "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml",
486 );
487 package.set_part("/ppt/presentation.xml", b"<p:presentation/>".to_vec());
488
489 assert_eq!(
490 package.main_document_part().as_deref(),
491 Some("/ppt/presentation.xml")
492 );
493 assert_eq!(
494 package
495 .content_types
496 .content_type_for("/ppt/presentation.xml"),
497 Some(
498 "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
499 )
500 );
501
502 let mut buffer = std::io::Cursor::new(Vec::new());
503 package.write_to(&mut buffer).unwrap();
504 buffer.set_position(0);
505 let round_tripped = OpcPackage::from_reader(buffer).unwrap();
506
507 assert_eq!(
508 round_tripped.main_document_part().as_deref(),
509 Some("/ppt/presentation.xml")
510 );
511 assert_eq!(
512 round_tripped.get_part("/ppt/presentation.xml"),
513 Some(b"<p:presentation/>".as_slice())
514 );
515 }
516
517 #[test]
518 fn pptx_package_resolves_main_slide_and_layout_parts() {
519 let package = pptx_package();
520 let mut buffer = std::io::Cursor::new(Vec::new());
521 package.write_to(&mut buffer).unwrap();
522 buffer.set_position(0);
523 let reopened = OpcPackage::from_reader(buffer).unwrap();
524
525 let presentation_part = reopened.main_document_part().unwrap();
526 assert_eq!(presentation_part, "/ppt/presentation.xml");
527
528 let slide_target = &reopened
529 .get_part_rels(&presentation_part)
530 .unwrap()
531 .get_by_type(rel_types::SLIDE)
532 .unwrap()
533 .target;
534 let slide_part = OpcPackage::resolve_rel_target(&presentation_part, slide_target);
535 assert_eq!(slide_part, "/ppt/slides/slide1.xml");
536
537 let layout_target = &reopened
538 .get_part_rels(&slide_part)
539 .unwrap()
540 .get_by_type(rel_types::SLIDE_LAYOUT)
541 .unwrap()
542 .target;
543 let layout_part = OpcPackage::resolve_rel_target(&slide_part, layout_target);
544 assert_eq!(layout_part, "/ppt/slideLayouts/slideLayout1.xml");
545
546 assert!(reopened.parts.contains_key(&presentation_part));
547 assert!(reopened.parts.contains_key(&slide_part));
548 assert!(reopened.parts.contains_key(&layout_part));
549 }
550
551 #[test]
552 fn independently_built_pptx_opens_and_resolves_relationships() {
553 let package = OpcPackage::from_reader(independently_built_pptx()).unwrap();
554
555 let presentation_part = package.main_document_part().unwrap();
556 assert_eq!(presentation_part, "/ppt/presentation.xml");
557
558 let presentation_master_target = &package
559 .get_part_rels(&presentation_part)
560 .unwrap()
561 .get_by_type(rel_types::SLIDE_MASTER)
562 .unwrap()
563 .target;
564 let presentation_master_part =
565 OpcPackage::resolve_rel_target(&presentation_part, presentation_master_target);
566 assert_eq!(
567 presentation_master_part,
568 "/ppt/slideMasters/slideMaster1.xml"
569 );
570
571 let slide_target = &package
572 .get_part_rels(&presentation_part)
573 .unwrap()
574 .get_by_type(rel_types::SLIDE)
575 .unwrap()
576 .target;
577 let slide_part = OpcPackage::resolve_rel_target(&presentation_part, slide_target);
578 assert_eq!(slide_part, "/ppt/slides/slide1.xml");
579
580 let layout_target = &package
581 .get_part_rels(&slide_part)
582 .unwrap()
583 .get_by_type(rel_types::SLIDE_LAYOUT)
584 .unwrap()
585 .target;
586 let layout_part = OpcPackage::resolve_rel_target(&slide_part, layout_target);
587 assert_eq!(layout_part, "/ppt/slideLayouts/slideLayout1.xml");
588
589 let master_target = &package
590 .get_part_rels(&layout_part)
591 .unwrap()
592 .get_by_type(rel_types::SLIDE_MASTER)
593 .unwrap()
594 .target;
595 let master_part = OpcPackage::resolve_rel_target(&layout_part, master_target);
596 assert_eq!(master_part, "/ppt/slideMasters/slideMaster1.xml");
597 assert_eq!(master_part, presentation_master_part);
598
599 let master_layout_target = &package
600 .get_part_rels(&master_part)
601 .unwrap()
602 .get_by_type(rel_types::SLIDE_LAYOUT)
603 .unwrap()
604 .target;
605 let master_layout_part = OpcPackage::resolve_rel_target(&master_part, master_layout_target);
606 assert_eq!(master_layout_part, layout_part);
607
608 let theme_target = &package
609 .get_part_rels(&master_part)
610 .unwrap()
611 .get_by_type(rel_types::THEME)
612 .unwrap()
613 .target;
614 let theme_part = OpcPackage::resolve_rel_target(&master_part, theme_target);
615 assert_eq!(theme_part, "/ppt/theme/theme1.xml");
616
617 let master_xml = std::str::from_utf8(package.parts.get(&master_part).unwrap()).unwrap();
618 let layout_id = master_xml
619 .split_once("<p:sldLayoutId id=\"")
620 .unwrap()
621 .1
622 .split_once('"')
623 .unwrap()
624 .0
625 .parse::<u64>()
626 .unwrap();
627 assert!(layout_id >= 2_147_483_648);
628
629 assert!(package.parts.contains_key(&presentation_part));
630 assert!(package.parts.contains_key(&slide_part));
631 assert!(package.parts.contains_key(&layout_part));
632 assert!(package.parts.contains_key(&master_part));
633 assert!(package.parts.contains_key(&theme_part));
634 }
635
636 #[test]
637 fn presentation_layout_target_resolves_one_directory_up() {
638 assert_eq!(
639 OpcPackage::resolve_rel_target(
640 "/ppt/slides/slide1.xml",
641 "../slideLayouts/slideLayout1.xml"
642 ),
643 "/ppt/slideLayouts/slideLayout1.xml"
644 );
645 }
646
647 #[test]
648 fn rels_path_conversion() {
649 assert_eq!(
650 rels_path_to_part_name("word/_rels/document.xml.rels"),
651 "/word/document.xml"
652 );
653 assert_eq!(
654 part_name_to_rels_path("/word/document.xml"),
655 "word/_rels/document.xml.rels"
656 );
657 }
658
659 #[test]
660 fn resolve_relative_target() {
661 assert_eq!(
662 OpcPackage::resolve_rel_target("/word/document.xml", "styles.xml"),
663 "/word/styles.xml"
664 );
665 assert_eq!(
666 OpcPackage::resolve_rel_target("/word/document.xml", "/word/styles.xml"),
667 "/word/styles.xml"
668 );
669 }
670
671 #[test]
672 fn resolve_target_collapses_parent_segments() {
673 assert_eq!(
675 OpcPackage::resolve_rel_target("/word/charts/chart1.xml", "../media/image1.png"),
676 "/word/media/image1.png"
677 );
678 assert_eq!(
679 OpcPackage::resolve_rel_target("/word/document.xml", "./styles.xml"),
680 "/word/styles.xml"
681 );
682 assert_eq!(
683 OpcPackage::resolve_rel_target("/word/document.xml", "../../../etc/passwd"),
684 "/etc/passwd"
685 );
686 }
687
688 #[test]
689 fn zip_entry_that_escapes_root_is_clamped_to_root() {
690 let archive = package_zip(&[
691 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
692 ("../../etc/passwd", b"not a real password file"),
693 ]);
694
695 let package = OpcPackage::from_reader(archive).unwrap();
696
697 assert_eq!(
698 package.get_part("/etc/passwd"),
699 Some(b"not a real password file".as_slice())
700 );
701 assert!(package.parts.keys().all(|name| !name.contains("..")));
702 }
703
704 #[test]
705 fn absolute_zip_entry_is_normalized_to_package_root() {
706 let archive = package_zip(&[
707 ("/[Content_Types].xml", MINIMAL_CONTENT_TYPES),
708 ("/absolute/path.xml", b"<absolute/>"),
709 ]);
710
711 let package = OpcPackage::from_reader(archive).unwrap();
712
713 assert_eq!(
714 package.get_part("/absolute/path.xml"),
715 Some(b"<absolute/>".as_slice())
716 );
717 assert_eq!(
718 package
719 .parts
720 .keys()
721 .filter(|name| name.as_str() == "/absolute/path.xml")
722 .count(),
723 1
724 );
725 }
726
727 #[test]
728 fn rels_path_suffix_is_stripped_once() {
729 assert_eq!(
730 rels_path_to_part_name("word/_rels/document.xml.rels"),
731 "/word/document.xml"
732 );
733 assert_eq!(
735 rels_path_to_part_name("word/_rels/odd.rels.rels"),
736 "/word/odd.rels"
737 );
738 }
739
740 #[test]
741 fn saved_packages_are_byte_identical() {
742 let mut pkg = docx_package();
743 for i in 0..40 {
744 pkg.set_part(&format!("/word/media/image{i}.png"), vec![i as u8]);
745 }
746 pkg.get_or_create_part_rels("/word/document.xml")
747 .add(rel_types::STYLES, "styles.xml");
748
749 let write = || {
750 let mut buf = std::io::Cursor::new(Vec::new());
751 pkg.write_to(&mut buf).unwrap();
752 buf.into_inner()
753 };
754 assert_eq!(write(), write());
755 }
756
757 #[test]
758 fn new_docx_package() {
759 let pkg = docx_package();
760 assert!(pkg.main_document_part().is_some());
761 assert_eq!(pkg.main_document_part().unwrap(), "/word/document.xml");
762 }
763
764 #[test]
765 fn round_trip_package() {
766 let mut pkg = docx_package();
767 pkg.set_part("/word/document.xml", b"<document/>".to_vec());
768
769 let mut buf = std::io::Cursor::new(Vec::new());
771 pkg.write_to(&mut buf).unwrap();
772
773 buf.set_position(0);
775 let pkg2 = OpcPackage::from_reader(buf).unwrap();
776 assert_eq!(
777 pkg2.get_part("/word/document.xml"),
778 Some(b"<document/>".as_slice())
779 );
780 assert!(pkg2.main_document_part().is_some());
781 }
782}