use std::io::Cursor;
use quick_xml::events::Event;
use quick_xml::Reader;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct Relationship {
pub id: String,
pub rel_type: String,
pub target: String,
pub target_mode: TargetMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetMode {
Internal,
External,
}
impl std::fmt::Display for TargetMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TargetMode::Internal => write!(f, "Internal"),
TargetMode::External => write!(f, "External"),
}
}
}
pub fn parse_relationships(xml_data: &[u8]) -> Result<Vec<Relationship>> {
let mut reader = Reader::from_reader(Cursor::new(xml_data));
reader.config_mut().trim_text(true);
let mut relationships = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
let local_name =
String::from_utf8_lossy(e.local_name().as_ref()).to_string();
if local_name == "Relationship" {
let mut id = String::new();
let mut rel_type = String::new();
let mut target = String::new();
let mut target_mode = TargetMode::Internal;
for attr in e.attributes().flatten() {
let key =
String::from_utf8_lossy(attr.key.local_name().as_ref()).to_string();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key.as_str() {
"Id" => id = value,
"Type" => rel_type = value,
"Target" => target = value,
"TargetMode" => {
if value.eq_ignore_ascii_case("External") {
target_mode = TargetMode::External;
}
}
_ => {}
}
}
relationships.push(Relationship {
id,
rel_type,
target,
target_mode,
});
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(Error::XmlParsing(format!(
"Error parsing .rels: {e}"
)));
}
_ => {}
}
buf.clear();
}
Ok(relationships)
}
pub fn find_external_relationships(rels: &[Relationship]) -> Vec<&Relationship> {
rels.iter()
.filter(|r| r.target_mode == TargetMode::External)
.collect()
}
pub mod rel_types {
pub const OFFICE_DOCUMENT: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
pub const VBA_PROJECT: &str =
"http://schemas.microsoft.com/office/2006/relationships/vbaProject";
pub const OLE_OBJECT: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject";
pub const HYPERLINK: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
pub const IMAGE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
pub const FRAME: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/frame";
pub const EXTERNAL_LINK: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink";
pub const ATTACHED_TEMPLATE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/attachedTemplate";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_relationships() {
let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://evil.com/payload" TargetMode="External"/>
</Relationships>"#;
let rels = parse_relationships(xml).unwrap();
assert_eq!(rels.len(), 2);
assert_eq!(rels[0].id, "rId1");
assert_eq!(rels[0].target, "word/document.xml");
assert_eq!(rels[0].target_mode, TargetMode::Internal);
assert_eq!(rels[1].id, "rId2");
assert_eq!(rels[1].target, "https://evil.com/payload");
assert_eq!(rels[1].target_mode, TargetMode::External);
}
#[test]
fn test_find_external() {
let xml = br#"<?xml version="1.0"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="test" Target="internal.xml"/>
<Relationship Id="rId2" Type="test" Target="http://evil.com" TargetMode="External"/>
<Relationship Id="rId3" Type="test" Target="http://also-evil.com" TargetMode="External"/>
</Relationships>"#;
let rels = parse_relationships(xml).unwrap();
let external = find_external_relationships(&rels);
assert_eq!(external.len(), 2);
}
#[test]
fn test_empty_relationships() {
let xml = br#"<?xml version="1.0"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>"#;
let rels = parse_relationships(xml).unwrap();
assert!(rels.is_empty());
}
#[test]
fn test_target_mode_display() {
assert_eq!(TargetMode::Internal.to_string(), "Internal");
assert_eq!(TargetMode::External.to_string(), "External");
}
}