From 601823d98ca124f2cf974ab2ab479535ea484be2 Mon Sep 17 00:00:00 2001 From: Eduardo Garcia Date: Sun, 18 Oct 2026 12:22:07 -0300 Subject: [PATCH] Declare the a: and r: namespaces on the modern comments part (#974) p188:cmLst declared only xmlns:p188 but its comment bodies use a:bodyPr, a:p, a:r and a:t, so the part was not namespace-well-formed and PowerPoint refused the saved file. A regression test parses every written XML part with a namespace-aware reader. Co-Authored-By: Claude Sonnet 5.5 --- crates/pptx/src/write/mod.rs | 2 +- crates/pptx/tests/comments.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/crates/pptx/src/write/mod.rs b/crates/pptx/src/write/mod.rs index bea8dc64..f96f82c5 100644 --- a/crates/pptx/src/write/mod.rs +++ b/crates/pptx/src/write/mod.rs @@ -876,7 +876,7 @@ fn author_slot(c: &Comment, authors: &mut Vec<(String, String, u32)>) -> usize { /// the flattened legacy part cannot express (issue #70). fn modern_comments_xml(list: &[Comment], part: &str, authors: &mut Vec<(String, String, u32)>) -> Vec { let mut w = W::new(); - w.open("p188:cmLst", A::new().a("xmlns:p188", NS_P188)); + w.open("p188:cmLst", A::new().a("xmlns:p188", NS_P188).a("xmlns:a", NS_A).a("xmlns:r", NS_R)); for (ti, c) in list.iter().take(10_000).enumerate() { modern_comment(&mut w, c, part, ti, authors, 0); } diff --git a/crates/pptx/tests/comments.rs b/crates/pptx/tests/comments.rs index c3befe09..1c4a7875 100644 --- a/crates/pptx/tests/comments.rs +++ b/crates/pptx/tests/comments.rs @@ -206,3 +206,37 @@ fn modern_only_package_imports() { assert!(b[0].resolved); assert_eq!((b[0].x, b[0].y), (90.0, 45.0), "modern positions are EMU"); } + +/// Every prefix an exported part uses is bound: PowerPoint refuses a package whose part is not +/// namespace-well-formed (issue #974, the modern comments part used `a:` without declaring it). +#[test] +fn every_written_xml_part_binds_its_prefixes() { + use quick_xml::events::Event; + use quick_xml::name::ResolveResult; + + let bytes = deckcraft_pptx::export(&deck()).expect("export"); + let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).expect("zip"); + let mut modern = 0; + for i in 0..z.len() { + let mut f = z.by_index(i).expect("entry"); + let name = f.name().to_string(); + if !(name.ends_with(".xml") || name.ends_with(".rels")) { + continue; + } + let mut data = vec![]; + f.read_to_end(&mut data).expect("read"); + modern += usize::from(name.contains("modernComment")); + let mut r = quick_xml::NsReader::from_reader(data.as_slice()); + loop { + match r.read_resolved_event() { + Ok((ns, Event::Start(e) | Event::Empty(e))) => { + assert!(!matches!(ns, ResolveResult::Unknown(_)), "{name}: unbound prefix on <{}>", String::from_utf8_lossy(e.name().as_ref())); + } + Ok((_, Event::Eof)) => break, + Ok(_) => {} + Err(e) => panic!("{name}: not well-formed: {e}"), + } + } + } + assert!(modern > 0, "the deck should write a modern comments part"); +}