//! Original synthetic custom XML stores, written from the public OOXML package contract. use std::collections::BTreeMap; use std::io::{Cursor, Read, Write}; use wordcraft_doc::{Path, StoryRef}; const REL_NS: &str = "http://schemas.openxmlformats.org/package/2006/relationships"; const RT: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/"; const DATA: &str = "Atlas StudiosCASE-17"; const PROPS: &str = r#""#; fn rels(rows: &[(&str, &str, &str, bool)]) -> String { let mut out = format!(r#""#); for (id, kind, target, external) in rows { let mode = if *external { r#" TargetMode="External""# } else { "" }; out.push_str(&format!(r#""#)); } out + "" } fn fixture() -> BTreeMap> { let doc = r#"Independent edit.Atlas Studios"#; let types = r#""#; [ ("[Content_Types].xml", types.to_string()), ("_rels/.rels", rels(&[("office", "officeDocument", "word/document.xml", false)])), ("word/document.xml", doc.to_string()), ("word/_rels/document.xml.rels", rels(&[("data", "customXml", "../customXml/item1.xml", false)])), ("customXml/item1.xml", DATA.to_string()), ("customXml/itemProps1.xml", PROPS.to_string()), ("customXml/_rels/item1.xml.rels", rels(&[("properties", "customXmlProps", "itemProps1.xml", false)])), ] .into_iter() .map(|(p, s)| (p.into(), s.into_bytes())) .collect() } fn zip(parts: &BTreeMap>) -> Vec { let mut z = zip::ZipWriter::new(Cursor::new(Vec::new())); for (name, bytes) in parts { z.start_file(name, zip::write::SimpleFileOptions::default()).unwrap(); z.write_all(bytes).unwrap(); } z.finish().unwrap().into_inner() } fn unzip(bytes: &[u8]) -> BTreeMap> { let mut z = zip::ZipArchive::new(Cursor::new(bytes)).unwrap(); let mut parts = BTreeMap::new(); for i in 0..z.len() { let mut f = z.by_index(i).unwrap(); let name = f.name().to_string(); let mut bytes = Vec::new(); f.read_to_end(&mut bytes).unwrap(); assert!(parts.insert(name, bytes).is_none(), "duplicate ZIP entry"); } parts } fn text(parts: &BTreeMap>, name: &str) -> String { String::from_utf8(parts[name].clone()).unwrap() } fn edited_cycles(input: &BTreeMap>, check: impl Fn(&BTreeMap>)) { let mut doc = wordcraft_docx::read(&zip(input)).unwrap(); for _ in 0..2 { doc.para_mut(StoryRef::Body, &Path::top(0)).unwrap().insert_text(0, "Unrelated edit. ", &Default::default()).unwrap(); let bytes = wordcraft_docx::write(&doc).unwrap(); check(&unzip(&bytes)); doc = wordcraft_docx::read(&bytes).unwrap(); } } #[test] fn bound_store_bytes_properties_and_relationships_survive_two_edited_saves() { edited_cycles(&fixture(), |p| { let document = text(p, "word/document.xml"); assert!(document.contains("Independent store".to_vec()); p.insert("customXml/_rels/other.xml.rels".into(), rels(&[("shared", "customXmlProps", "itemProps1.xml", false)]).into_bytes()); // A cycle must not cause repeated parts, and an external schema stays external. p.insert( "customXml/_rels/itemProps1.xml.rels".into(), rels(&[("cycle", "customXml", "item1.xml", false), ("schema", "schema", "https://example.invalid/schema", true)]).into_bytes(), ); edited_cycles(&p, |out| { assert_eq!(out.get("customXml/item1.xml").map(Vec::as_slice), Some(DATA.as_bytes())); assert_eq!(out.get("customXml/other.xml"), p.get("customXml/other.xml")); assert!(text(out, "_rels/.rels").contains(r#"Target="customXml/item1.xml""#)); assert!(text(out, "word/_rels/document.xml.rels").contains(r#"Target="../customXml/other.xml""#)); assert!(text(out, "customXml/_rels/itemProps1.xml.rels").contains(r#"TargetMode="External""#)); assert!(text(out, "customXml/_rels/other.xml.rels").contains(r#"Target="itemProps1.xml""#)); }); } #[test] fn absent_and_external_stores_are_not_fetched_or_written() { let mut p = fixture(); p.insert( "word/_rels/document.xml.rels".into(), rels(&[ ("missing", "customXml", "../customXml/missing.xml", false), ("external", "customXml", "https://example.invalid/store.xml", true), ("valid", "customXml", "../customXml/item1.xml", false), ]) .into_bytes(), ); p.insert("customXml/_rels/item1.xml.rels".into(), rels(&[("missing", "customXmlProps", "missing.xml", false)]).into_bytes()); edited_cycles(&p, |out| { assert_eq!(out.get("customXml/item1.xml").map(Vec::as_slice), Some(DATA.as_bytes())); let roots = text(out, "word/_rels/document.xml.rels"); assert!(!roots.contains("missing.xml")); assert!(!roots.contains("example.invalid")); assert!(!out.contains_key("customXml/_rels/item1.xml.rels")); }); } #[test] fn custom_stores_do_not_shadow_reserved_bibliography_names() { let mut doc = wordcraft_docx::read(&zip(&fixture())).unwrap(); doc.sources.push(wordcraft_doc::Source { tag: "OriginalSource".into(), title: "Original synthetic reference".into(), ..Default::default() }); let out = unzip(&wordcraft_docx::write(&doc).unwrap()); assert_eq!(out.get("customXml/item1_2.xml").map(Vec::as_slice), Some(DATA.as_bytes())); assert_eq!(out.get("customXml/itemProps1_2.xml").map(Vec::as_slice), Some(PROPS.as_bytes())); assert!(text(&out, "word/_rels/document.xml.rels").contains(r#"Target="../customXml/item1_2.xml""#)); assert!(text(&out, "customXml/_rels/item1_2.xml.rels").contains(r#"Target="itemProps1_2.xml""#)); } #[test] fn opaque_non_utf8_data_and_strict_relationship_types_are_preserved() { let mut p = fixture(); let data: Vec = "Atlas Studios".encode_utf16().flat_map(u16::to_le_bytes).collect(); let mut bytes = vec![0xff, 0xfe]; bytes.extend(data); p.insert("customXml/item1.xml".into(), bytes.clone()); let strict = rels(&[("strict", "customXml", "../customXml/item1.xml", false)]).replace(RT, "http://purl.oclc.org/ooxml/officeDocument/relationships/"); p.insert("word/_rels/document.xml.rels".into(), strict.into_bytes()); edited_cycles(&p, |out| { assert_eq!(out.get("customXml/item1.xml"), Some(&bytes)); assert!(text(out, "word/_rels/document.xml.rels").contains("http://purl.oclc.org/ooxml/officeDocument/relationships/customXml")); }); } #[test] fn reserved_part_targets_are_renamed_instead_of_shadowing_or_looping() { let mut p = fixture(); p.insert("word/_rels/document.xml.rels".into(), rels(&[("hostile", "customXml", "../docProps/foreign.xml", false)]).into_bytes()); p.insert("docProps/foreign.xml".into(), DATA.as_bytes().to_vec()); edited_cycles(&p, |out| { assert!(out.values().any(|b| b == DATA.as_bytes())); assert!(!out.contains_key("docProps/foreign.xml")); assert!(text(out, "word/_rels/document.xml.rels").contains(r#"Target="embeddings/part.xml""#)); assert!(text(out, "docProps/core.xml").contains("coreProperties")); }); }