use std::collections::HashSet;
use quick_xml::encoding::Decoder;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
use quick_xml::name::ResolveResult;
use quick_xml::reader::NsReader;
use quick_xml::{Writer, XmlVersion};
use crate::error::{OpcError, Result};
const RELATIONSHIPS_NAMESPACE: &[u8] =
b"http://schemas.openxmlformats.org/package/2006/relationships";
pub mod rel_types {
pub const CORE_PROPERTIES: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
pub const THUMBNAIL: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail";
pub const DIGITAL_SIGNATURE_ORIGIN: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin";
pub const DIGITAL_SIGNATURE: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/signature";
pub const DOCUMENT: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
pub const EXTENDED_PROPERTIES: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
pub const CUSTOM_PROPERTIES: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
pub const STYLES: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
pub const NUMBERING: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
pub const HEADER: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
pub const FOOTER: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
pub const IMAGE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
pub const AUDIO: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio";
pub const VIDEO: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/video";
pub const POWERPOINT_MEDIA: &str =
"http://schemas.microsoft.com/office/2007/relationships/media";
pub const SETTINGS: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
pub const WEB_SETTINGS: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings";
pub const FONT_TABLE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable";
pub const FONT: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/font";
pub const THEME: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
pub const HYPERLINK: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
pub const FOOTNOTES: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
pub const ENDNOTES: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
pub const COMMENTS: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
pub const GLOSSARY_DOCUMENT: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument";
pub const CHART: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
pub const DIAGRAM_DATA: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData";
pub const DIAGRAM_LAYOUT: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout";
pub const DIAGRAM_QUICK_STYLE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle";
pub const DIAGRAM_COLORS: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors";
pub const DIAGRAM_DRAWING: &str =
"http://schemas.microsoft.com/office/2007/relationships/diagramDrawing";
pub const PACKAGE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/package";
pub const OLE_OBJECT: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject";
pub const CONTROL: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/control";
pub const STRICT_OLE_OBJECT: &str =
"http://purl.oclc.org/ooxml/officeDocument/relationships/oleObject";
pub const STRICT_CONTROL: &str =
"http://purl.oclc.org/ooxml/officeDocument/relationships/control";
pub const ACTIVEX_CONTROL_BINARY: &str =
"http://schemas.microsoft.com/office/2006/relationships/activeXControlBinary";
pub const VBA_PROJECT: &str =
"http://schemas.microsoft.com/office/2006/relationships/vbaProject";
pub const VBA_PROJECT_SIGNATURE: &str =
"http://schemas.microsoft.com/office/2006/relationships/vbaProjectSignature";
pub const VBA_PROJECT_SIGNATURE_AGILE: &str =
"http://schemas.microsoft.com/office/2006/relationships/vbaProjectSignatureAgile";
pub const WORKSHEET: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";
pub const SHARED_STRINGS: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
pub const SLIDE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
pub const SLIDE_LAYOUT: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
pub const SLIDE_MASTER: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
pub const NOTES_SLIDE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
pub const NOTES_MASTER: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster";
pub const PRES_PROPS: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps";
pub const VIEW_PROPS: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps";
pub const TABLE_STYLES: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles";
pub const HANDOUT_MASTER: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster";
pub const POWERPOINT_COMMENTS: &str =
"http://schemas.microsoft.com/office/2018/10/relationships/comments";
pub const POWERPOINT_AUTHORS: &str =
"http://schemas.microsoft.com/office/2018/10/relationships/authors";
}
#[derive(Debug, Clone, PartialEq)]
pub struct Relationship {
pub id: String,
pub rel_type: String,
pub target: String,
pub target_mode: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Relationships {
pub items: Vec<Relationship>,
next_id: u32,
source: Option<(Vec<u8>, Vec<Relationship>)>,
}
impl Relationships {
pub fn new() -> Self {
Self {
items: Vec::new(),
next_id: 1,
source: None,
}
}
pub fn from_xml(xml: &[u8]) -> Result<Self> {
let mut reader = NsReader::from_reader(xml);
reader.config_mut().trim_text(true);
let mut items = Vec::new();
let mut ids = HashSet::new();
let mut max_id: u32 = 0;
let mut buf = Vec::new();
loop {
match reader.read_resolved_event_into(&mut buf) {
Ok((namespace, Event::Empty(ref e)))
if matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == RELATIONSHIPS_NAMESPACE)
&& e.local_name().as_ref() == b"Relationship" =>
{
let relationship = parse_relationship(e, reader.decoder())?;
if !ids.insert(relationship.id.clone()) {
return Err(OpcError::InvalidRelationship);
}
update_max_relationship_id(&relationship.id, &mut max_id);
items.push(relationship);
}
Ok((namespace, Event::Start(ref e)))
if matches!(namespace, ResolveResult::Bound(value) if value.as_ref() == RELATIONSHIPS_NAMESPACE)
&& e.local_name().as_ref() == b"Relationship" =>
{
let relationship = parse_relationship(e, reader.decoder())?;
if !ids.insert(relationship.id.clone()) {
return Err(OpcError::InvalidRelationship);
}
update_max_relationship_id(&relationship.id, &mut max_id);
items.push(relationship);
reader.read_to_end_into(e.name(), &mut Vec::new())?;
}
Ok((_, Event::Eof)) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(Relationships {
source: Some((xml.to_vec(), items.clone())),
items,
next_id: next_relationship_number(max_id),
})
}
pub fn to_xml(&self) -> Result<Vec<u8>> {
self.validate_ids()?;
if let Some((xml, items)) = &self.source
&& items == &self.items
{
return Ok(xml.clone());
}
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
writer.write_event(Event::Decl(BytesDecl::new(
"1.0",
Some("UTF-8"),
Some("yes"),
)))?;
let mut rels_start = BytesStart::new("Relationships");
rels_start.push_attribute((
"xmlns",
"http://schemas.openxmlformats.org/package/2006/relationships",
));
writer.write_event(Event::Start(rels_start))?;
for rel in &self.items {
let mut elem = BytesStart::new("Relationship");
elem.push_attribute(("Id", rel.id.as_str()));
elem.push_attribute(("Type", rel.rel_type.as_str()));
elem.push_attribute(("Target", rel.target.as_str()));
if let Some(ref mode) = rel.target_mode {
elem.push_attribute(("TargetMode", mode.as_str()));
}
writer.write_event(Event::Empty(elem))?;
}
writer.write_event(Event::End(BytesEnd::new("Relationships")))?;
Ok(writer.into_inner())
}
pub fn get_by_id(&self, id: &str) -> Option<&Relationship> {
self.items.iter().find(|r| r.id == id)
}
pub fn get_by_type(&self, rel_type: &str) -> Option<&Relationship> {
self.items.iter().find(|r| r.rel_type == rel_type)
}
pub fn get_all_by_type(&self, rel_type: &str) -> Vec<&Relationship> {
self.items
.iter()
.filter(|r| r.rel_type == rel_type)
.collect()
}
pub fn add(&mut self, rel_type: &str, target: &str) -> String {
let id = self.allocate_id();
self.items.push(Relationship {
id: id.clone(),
rel_type: rel_type.to_string(),
target: target.to_string(),
target_mode: None,
});
id
}
pub fn add_external(&mut self, rel_type: &str, target: &str) -> String {
let id = self.allocate_id();
self.items.push(Relationship {
id: id.clone(),
rel_type: rel_type.to_string(),
target: target.to_string(),
target_mode: Some("External".to_string()),
});
id
}
pub fn add_with_id(&mut self, id: &str, rel_type: &str, target: &str) {
self.items.retain(|r| r.id != id);
self.items.push(Relationship {
id: id.to_string(),
rel_type: rel_type.to_string(),
target: target.to_string(),
target_mode: None,
});
if let Some(num) = id.strip_prefix("rId").and_then(|s| s.parse::<u32>().ok())
&& num >= self.next_id
{
self.next_id = next_relationship_number(num);
}
}
fn allocate_id(&mut self) -> String {
let mut candidate = self.next_id;
let numeric_space = usize::try_from(u32::MAX).unwrap_or(usize::MAX);
let attempts = self.items.len().saturating_add(1).min(numeric_space);
for _ in 0..attempts {
let id = format!("rId{candidate}");
let next = next_relationship_number(candidate);
if self.items.iter().all(|relationship| relationship.id != id) {
self.next_id = next;
return id;
}
candidate = next;
}
for ordinal in 1u128.. {
let id = format!("rIdGenerated{ordinal}");
if self.items.iter().all(|relationship| relationship.id != id) {
return id;
}
}
unreachable!("the generated relationship id space is unbounded")
}
pub(crate) fn validate_ids(&self) -> Result<()> {
let mut ids = HashSet::with_capacity(self.items.len());
if self
.items
.iter()
.all(|relationship| ids.insert(relationship.id.as_str()))
{
Ok(())
} else {
Err(OpcError::InvalidRelationship)
}
}
}
fn parse_relationship(element: &BytesStart<'_>, decoder: Decoder) -> Result<Relationship> {
let mut id = None;
let mut rel_type = None;
let mut target = None;
let mut target_mode = None;
for attribute in element.attributes() {
let attribute = attribute?;
let value = attribute
.decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)?
.into_owned();
match attribute.key.as_ref() {
b"Id" => id = Some(value),
b"Type" => rel_type = Some(value),
b"Target" => target = Some(value),
b"TargetMode" => target_mode = Some(value),
_ => {}
}
}
match (id, rel_type, target) {
(Some(id), Some(rel_type), Some(target)) => Ok(Relationship {
id,
rel_type,
target,
target_mode,
}),
_ => Err(OpcError::InvalidRelationship),
}
}
fn update_max_relationship_id(id: &str, maximum: &mut u32) {
if let Some(number) = id.strip_prefix("rId").and_then(|value| value.parse().ok()) {
*maximum = (*maximum).max(number);
}
}
fn next_relationship_number(current: u32) -> u32 {
current.checked_add(1).unwrap_or(1).max(1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn relationship_and_content_type_constants_are_unique_and_well_formed() {
const PACKAGE_PREFIX: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/";
const OFFICE_PREFIX: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/";
let package_relationships = [rel_types::CORE_PROPERTIES, rel_types::THUMBNAIL];
let office_relationships = [
rel_types::DOCUMENT,
rel_types::STYLES,
rel_types::NUMBERING,
rel_types::HEADER,
rel_types::FOOTER,
rel_types::IMAGE,
rel_types::SETTINGS,
rel_types::WEB_SETTINGS,
rel_types::FONT_TABLE,
rel_types::FONT,
rel_types::THEME,
rel_types::HYPERLINK,
rel_types::FOOTNOTES,
rel_types::ENDNOTES,
rel_types::GLOSSARY_DOCUMENT,
rel_types::CHART,
rel_types::DIAGRAM_DATA,
rel_types::DIAGRAM_LAYOUT,
rel_types::DIAGRAM_QUICK_STYLE,
rel_types::DIAGRAM_COLORS,
rel_types::PACKAGE,
rel_types::OLE_OBJECT,
rel_types::CONTROL,
rel_types::WORKSHEET,
rel_types::SHARED_STRINGS,
rel_types::EXTENDED_PROPERTIES,
rel_types::CUSTOM_PROPERTIES,
rel_types::SLIDE,
rel_types::SLIDE_LAYOUT,
rel_types::SLIDE_MASTER,
rel_types::NOTES_SLIDE,
rel_types::NOTES_MASTER,
rel_types::PRES_PROPS,
rel_types::VIEW_PROPS,
rel_types::TABLE_STYLES,
rel_types::HANDOUT_MASTER,
];
let strict_relationships = [rel_types::STRICT_OLE_OBJECT, rel_types::STRICT_CONTROL];
let microsoft_relationships = [
rel_types::ACTIVEX_CONTROL_BINARY,
rel_types::VBA_PROJECT,
rel_types::VBA_PROJECT_SIGNATURE,
rel_types::VBA_PROJECT_SIGNATURE_AGILE,
];
let mut relationship_values = std::collections::HashSet::new();
for value in package_relationships {
assert!(value.starts_with(PACKAGE_PREFIX));
assert!(!value.chars().any(char::is_whitespace));
assert!(relationship_values.insert(value));
}
for value in office_relationships {
assert!(value.starts_with(OFFICE_PREFIX));
assert!(!value.chars().any(char::is_whitespace));
assert!(relationship_values.insert(value));
}
for value in strict_relationships {
assert!(value.starts_with("http://purl.oclc.org/ooxml/officeDocument/relationships/"));
assert!(!value.chars().any(char::is_whitespace));
assert!(relationship_values.insert(value));
}
for value in microsoft_relationships {
assert!(value.starts_with("http://schemas.microsoft.com/office/2006/relationships/"));
assert!(!value.chars().any(char::is_whitespace));
assert!(relationship_values.insert(value));
}
for value in [
rel_types::POWERPOINT_COMMENTS,
rel_types::POWERPOINT_AUTHORS,
rel_types::DIAGRAM_DRAWING,
] {
assert!(value.starts_with("http://schemas.microsoft.com/office/"));
assert!(!value.chars().any(char::is_whitespace));
assert!(relationship_values.insert(value));
}
let content_type_values = [
crate::content_types::RELATIONSHIPS,
crate::content_types::XML,
crate::content_types::CORE_PROPERTIES,
crate::content_types::EXTENDED_PROPERTIES,
crate::content_types::CUSTOM_PROPERTIES,
crate::content_types::THEME,
crate::content_types::CHART,
crate::content_types::WORD_GLOSSARY,
crate::content_types::WORD_DOCUMENT,
crate::content_types::WORD_DOCUMENT_MACRO_ENABLED,
crate::content_types::WORD_TEMPLATE,
crate::content_types::WORD_TEMPLATE_MACRO_ENABLED,
crate::content_types::PRESENTATION,
crate::content_types::SLIDESHOW,
crate::content_types::SLIDE,
crate::content_types::SLIDE_LAYOUT,
crate::content_types::SLIDE_MASTER,
crate::content_types::NOTES_SLIDE,
crate::content_types::NOTES_MASTER,
crate::content_types::PRES_PROPS,
crate::content_types::VIEW_PROPS,
crate::content_types::TABLE_STYLES,
crate::content_types::HANDOUT_MASTER,
crate::content_types::POWERPOINT_COMMENTS,
crate::content_types::POWERPOINT_AUTHORS,
crate::content_types::WORKBOOK,
crate::content_types::EMBEDDED_WORKBOOK,
crate::content_types::WORKSHEET,
crate::content_types::SHARED_STRINGS,
crate::content_types::STYLES,
];
let mut content_types = std::collections::HashSet::new();
for value in content_type_values {
let (kind, subtype) = value.split_once('/').expect("valid MIME type");
assert_eq!(kind, "application");
assert!(!subtype.is_empty());
assert!(!subtype.contains('/'));
assert!(!value.chars().any(char::is_whitespace));
assert!(content_types.insert(value));
}
}
#[test]
fn round_trip_relationships() {
let mut rels = Relationships::new();
rels.add(rel_types::DOCUMENT, "word/document.xml");
rels.add(rel_types::STYLES, "word/styles.xml");
let xml = rels.to_xml().unwrap();
let parsed = Relationships::from_xml(&xml).unwrap();
assert_eq!(parsed.items.len(), 2);
assert_eq!(parsed.items[0].id, "rId1");
assert_eq!(parsed.items[0].target, "word/document.xml");
assert_eq!(parsed.items[1].id, "rId2");
}
#[test]
fn find_by_type() {
let mut rels = Relationships::new();
rels.add(rel_types::DOCUMENT, "word/document.xml");
rels.add(rel_types::STYLES, "word/styles.xml");
let doc = rels.get_by_type(rel_types::DOCUMENT).unwrap();
assert_eq!(doc.target, "word/document.xml");
}
#[test]
fn high_numeric_relationship_ids_roll_over_without_collision() {
let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId4294967294" Type="type-a" Target="a.xml"/>
</Relationships>"#;
let mut relationships = Relationships::from_xml(xml).unwrap();
assert_eq!(relationships.add("type-b", "b.xml"), "rId4294967295");
assert_eq!(relationships.add("type-c", "c.xml"), "rId1");
assert_eq!(
relationships.add_external("type-d", "https://example.com"),
"rId2"
);
let ids = relationships
.items
.iter()
.map(|relationship| relationship.id.as_str())
.collect::<std::collections::HashSet<_>>();
assert_eq!(ids.len(), relationships.items.len());
assert!(!ids.contains("rId0"));
}
#[test]
fn parsed_u32_max_relationship_id_rolls_to_first_free_positive_id() {
let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId4294967295" Type="type-max" Target="max.xml"/>
<Relationship Id="rId1" Type="type-one" Target="one.xml"/>
</Relationships>"#;
let mut relationships = Relationships::from_xml(xml).unwrap();
assert_eq!(relationships.add("type-two", "two.xml"), "rId2");
relationships.add_with_id("rId4294967295", "type-max", "replacement.xml");
assert_eq!(relationships.add("type-three", "three.xml"), "rId3");
}
#[test]
fn aliased_and_expanded_relationship_entries_parse_but_foreign_decoys_do_not() {
let xml = br#"<pr:Relationships xmlns:pr="http://schemas.openxmlformats.org/package/2006/relationships" xmlns:x="urn:foreign"><x:Relationship Id="rId9" Type="urn:wrong" Target="wrong.xml"/><pr:Relationship Id="rId2" Type="urn:first" Target="first.xml"></pr:Relationship><pr:Relationship Id="rId3" Type="urn:second" Target="second.xml"/></pr:Relationships>"#;
let parsed = Relationships::from_xml(xml).expect("canonical relationships parse");
assert_eq!(parsed.items.len(), 2);
assert_eq!(parsed.items[0].id, "rId2");
assert_eq!(parsed.items[1].id, "rId3");
}
#[test]
fn duplicate_relationship_ids_are_rejected_on_parse_and_serialize() {
let xml = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="urn:first" Target="first.xml"/><Relationship Id="rId1" Type="urn:second" Target="second.xml"/></Relationships>"#;
assert!(matches!(
Relationships::from_xml(xml),
Err(OpcError::InvalidRelationship)
));
let mut relationships = Relationships::new();
relationships.items.push(Relationship {
id: "producer".to_owned(),
rel_type: "urn:first".to_owned(),
target: "first.xml".to_owned(),
target_mode: None,
});
relationships.items.push(Relationship {
id: "producer".to_owned(),
rel_type: "urn:second".to_owned(),
target: "second.xml".to_owned(),
target_mode: None,
});
assert!(matches!(
relationships.to_xml(),
Err(OpcError::InvalidRelationship)
));
}
#[test]
fn encoded_relationship_values_are_decoded_before_validation_and_escape_once() {
let duplicate = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="urn:first" Target="first.xml"/><Relationship Id="rId1" Type="urn:second" Target="second.xml"/></Relationships>"#;
assert!(matches!(
Relationships::from_xml(duplicate),
Err(OpcError::InvalidRelationship)
));
let xml = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="urn:test?a=1&b=2" Target="folder/a&b.xml"/></Relationships>"#;
let parsed = Relationships::from_xml(xml).unwrap();
assert_eq!(parsed.items[0].id, "rId1");
assert_eq!(parsed.items[0].rel_type, "urn:test?a=1&b=2");
assert_eq!(parsed.items[0].target, "folder/a&b.xml");
let saved = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
assert!(saved.contains("folder/a&b.xml"));
assert!(!saved.contains("&amp;"));
assert_eq!(
Relationships::from_xml(saved.as_bytes()).unwrap().items,
parsed.items
);
}
#[test]
fn unchanged_relationship_parts_serialize_byte_for_byte() {
let xml = br#"<?xml version='1.0' encoding='UTF-8'?>
<r:Relationships xmlns:r="http://schemas.openxmlformats.org/package/2006/relationships">
<r:Relationship Target="word/document.xml" Type="urn:document" Id="producer-id"></r:Relationship>
</r:Relationships>"#;
let mut relationships = Relationships::from_xml(xml).unwrap();
assert_eq!(relationships.to_xml().unwrap(), xml);
relationships.items[0].target = "word/changed.xml".to_owned();
let changed = relationships.to_xml().unwrap();
assert_ne!(changed, xml);
assert!(
String::from_utf8(changed)
.unwrap()
.contains("word/changed.xml")
);
}
}