stella-docx-kernel 0.10.0

Bounded DOCX package projection and WordprocessingML scanning
Documentation
use quick_xml::XmlVersion;
use quick_xml::events::{BytesStart, Event};
use quick_xml::name::ResolveResult;
use quick_xml::reader::NsReader;

use crate::ProjectionError;

const PACKAGE_RELATIONSHIPS_TRANSITIONAL: &[u8] =
    b"http://schemas.openxmlformats.org/package/2006/relationships";
const PACKAGE_RELATIONSHIPS_STRICT: &[u8] = b"http://purl.oclc.org/ooxml/package/relationships";
const OFFICE_DOCUMENT_TRANSITIONAL: &str =
    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
const OFFICE_DOCUMENT_STRICT: &str =
    "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument";
const COMMENTS_TRANSITIONAL: &str =
    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
const COMMENTS_STRICT: &str = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments";
const COMMENTS_EXTENDED: &str =
    "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";

#[derive(Debug, Eq, PartialEq)]
pub(super) struct ReviewPartPaths {
    pub comments: Option<Vec<u8>>,
    pub comments_extended: Option<Vec<u8>>,
}

pub(super) fn main_document_path(xml: &[u8]) -> Result<Vec<u8>, ProjectionError> {
    let mut reader = NsReader::from_reader(xml);
    reader.config_mut().check_end_names = true;
    let mut depth = 0_usize;
    let mut root_seen = false;
    let mut target = None;

    loop {
        match reader
            .read_event()
            .map_err(|_| ProjectionError::InvalidPackageRelationships)?
        {
            Event::Start(element) => {
                inspect_element(&reader, &element, depth, &mut root_seen, &mut target)?;
                depth = depth
                    .checked_add(1)
                    .ok_or(ProjectionError::InvalidPackageRelationships)?;
            }
            Event::Empty(element) => {
                inspect_element(&reader, &element, depth, &mut root_seen, &mut target)?;
            }
            Event::End(_) => {
                depth = depth
                    .checked_sub(1)
                    .ok_or(ProjectionError::InvalidPackageRelationships)?;
            }
            Event::Eof => break,
            _ => {}
        }
    }
    if !root_seen || depth != 0 {
        return Err(ProjectionError::InvalidPackageRelationships);
    }
    target.ok_or(ProjectionError::MissingDocumentXml)
}

pub(super) fn document_relationships_path(
    document_path: &[u8],
) -> Result<Vec<u8>, ProjectionError> {
    let path = std::str::from_utf8(document_path)
        .map_err(|_| ProjectionError::InvalidPackageRelationships)?;
    let (parent, file_name) = path.rsplit_once('/').unwrap_or(("", path));
    if file_name.is_empty() {
        return Err(ProjectionError::InvalidPackageRelationships);
    }
    let prefix = if parent.is_empty() {
        String::new()
    } else {
        format!("{parent}/")
    };
    Ok(format!("{prefix}_rels/{file_name}.rels").into_bytes())
}

pub(super) fn review_part_paths(
    xml: &[u8],
    document_path: &[u8],
) -> Result<ReviewPartPaths, ProjectionError> {
    let mut reader = NsReader::from_reader(xml);
    reader.config_mut().check_end_names = true;
    let mut depth = 0_usize;
    let mut root_seen = false;
    let mut paths = ReviewPartPaths {
        comments: None,
        comments_extended: None,
    };

    loop {
        match reader
            .read_event()
            .map_err(|_| ProjectionError::InvalidPackageRelationships)?
        {
            Event::Start(element) => {
                inspect_review_element(
                    &reader,
                    &element,
                    depth,
                    &mut root_seen,
                    &mut paths,
                    document_path,
                )?;
                depth = depth
                    .checked_add(1)
                    .ok_or(ProjectionError::InvalidPackageRelationships)?;
            }
            Event::Empty(element) => inspect_review_element(
                &reader,
                &element,
                depth,
                &mut root_seen,
                &mut paths,
                document_path,
            )?,
            Event::End(_) => {
                depth = depth
                    .checked_sub(1)
                    .ok_or(ProjectionError::InvalidPackageRelationships)?;
            }
            Event::DocType(_) => return Err(ProjectionError::InvalidPackageRelationships),
            Event::Eof => break,
            _ => {}
        }
    }
    if !root_seen || depth != 0 {
        return Err(ProjectionError::InvalidPackageRelationships);
    }
    Ok(paths)
}

fn inspect_review_element(
    reader: &NsReader<&[u8]>,
    element: &BytesStart<'_>,
    depth: usize,
    root_seen: &mut bool,
    paths: &mut ReviewPartPaths,
    document_path: &[u8],
) -> Result<(), ProjectionError> {
    let (namespace, local_name) = reader.resolver().resolve_element(element.name());
    if depth == 0 {
        if *root_seen
            || local_name.as_ref() != b"Relationships"
            || !is_relationships_namespace(&namespace)
        {
            return Err(ProjectionError::InvalidPackageRelationships);
        }
        *root_seen = true;
        return Ok(());
    }
    if depth != 1
        || local_name.as_ref() != b"Relationship"
        || !is_relationships_namespace(&namespace)
    {
        return Ok(());
    }

    let relationship_type = unqualified_attribute(reader, element, b"Type")?;
    let selected = match relationship_type.as_deref() {
        Some(COMMENTS_TRANSITIONAL | COMMENTS_STRICT) => &mut paths.comments,
        Some(COMMENTS_EXTENDED) => &mut paths.comments_extended,
        _ => return Ok(()),
    };
    if selected.is_some() {
        return Err(ProjectionError::InvalidPackageRelationships);
    }
    match unqualified_attribute(reader, element, b"TargetMode")?.as_deref() {
        None | Some("Internal") => {}
        Some(_) => return Err(ProjectionError::InvalidPackageRelationships),
    }
    let target = unqualified_attribute(reader, element, b"Target")?
        .ok_or(ProjectionError::InvalidPackageRelationships)?;
    *selected = Some(normalize_part_target(document_path, &target)?);
    Ok(())
}

fn inspect_element(
    reader: &NsReader<&[u8]>,
    element: &BytesStart<'_>,
    depth: usize,
    root_seen: &mut bool,
    target: &mut Option<Vec<u8>>,
) -> Result<(), ProjectionError> {
    let (namespace, local_name) = reader.resolver().resolve_element(element.name());
    if depth == 0 {
        if *root_seen
            || local_name.as_ref() != b"Relationships"
            || !is_relationships_namespace(&namespace)
        {
            return Err(ProjectionError::InvalidPackageRelationships);
        }
        *root_seen = true;
        return Ok(());
    }
    if depth != 1
        || local_name.as_ref() != b"Relationship"
        || !is_relationships_namespace(&namespace)
    {
        return Ok(());
    }

    let relationship_type = unqualified_attribute(reader, element, b"Type")?;
    if !matches!(
        relationship_type.as_deref(),
        Some(OFFICE_DOCUMENT_TRANSITIONAL | OFFICE_DOCUMENT_STRICT)
    ) {
        return Ok(());
    }
    if target.is_some() {
        return Err(ProjectionError::DuplicateDocumentXml);
    }
    match unqualified_attribute(reader, element, b"TargetMode")?.as_deref() {
        None | Some("Internal") => {}
        Some(_) => return Err(ProjectionError::InvalidPackageRelationships),
    }
    let value = unqualified_attribute(reader, element, b"Target")?
        .ok_or(ProjectionError::InvalidPackageRelationships)?;
    *target = Some(normalize_root_target(&value)?);
    Ok(())
}

fn is_relationships_namespace(namespace: &ResolveResult<'_>) -> bool {
    matches!(
        namespace,
        ResolveResult::Bound(value)
            if matches!(
                value.as_ref(),
                PACKAGE_RELATIONSHIPS_TRANSITIONAL | PACKAGE_RELATIONSHIPS_STRICT
            )
    )
}

fn unqualified_attribute(
    reader: &NsReader<&[u8]>,
    element: &BytesStart<'_>,
    name: &[u8],
) -> Result<Option<String>, ProjectionError> {
    let mut value = None;
    for attribute in element.attributes() {
        let attribute = attribute.map_err(|_| ProjectionError::InvalidPackageRelationships)?;
        let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
        if namespace == ResolveResult::Unbound && local_name.as_ref() == name {
            if value.is_some() {
                return Err(ProjectionError::InvalidPackageRelationships);
            }
            value = Some(
                attribute
                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
                    .map_err(|_| ProjectionError::InvalidPackageRelationships)?
                    .into_owned(),
            );
        }
    }
    Ok(value)
}

fn normalize_root_target(target: &str) -> Result<Vec<u8>, ProjectionError> {
    if target.is_empty()
        || target.contains(['\\', '?', '#', '\0'])
        || target.contains(':')
        || !target.is_ascii()
    {
        return Err(ProjectionError::InvalidPackageRelationships);
    }
    let mut segments = Vec::new();
    for segment in target.trim_start_matches('/').split('/') {
        match segment {
            "" | "." => {}
            ".." => {
                if segments.pop().is_none() {
                    return Err(ProjectionError::InvalidPackageRelationships);
                }
            }
            _ => segments.push(segment),
        }
    }
    if segments.is_empty() {
        return Err(ProjectionError::InvalidPackageRelationships);
    }
    Ok(segments.join("/").into_bytes())
}

fn normalize_part_target(source_path: &[u8], target: &str) -> Result<Vec<u8>, ProjectionError> {
    let source = std::str::from_utf8(source_path)
        .map_err(|_| ProjectionError::InvalidPackageRelationships)?;
    if target.starts_with('/') {
        return normalize_root_target(target);
    }
    let parent = source.rsplit_once('/').map_or("", |(parent, _)| parent);
    let combined = if parent.is_empty() {
        target.to_owned()
    } else {
        format!("{parent}/{target}")
    };
    normalize_root_target(&combined)
}

#[cfg(test)]
mod tests {
    use super::{document_relationships_path, review_part_paths};

    #[test]
    fn derives_relationship_and_relative_part_paths_at_any_package_depth() {
        assert_eq!(
            document_relationships_path(b"document.xml"),
            Ok(b"_rels/document.xml.rels".to_vec())
        );
        assert_eq!(
            document_relationships_path(b"custom/main.xml"),
            Ok(b"custom/_rels/main.xml.rels".to_vec())
        );
        let relationships = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="../review/comments.xml"/></Relationships>"#;
        assert_eq!(
            review_part_paths(relationships, b"custom/main.xml").map(|paths| paths.comments),
            Ok(Some(b"review/comments.xml".to_vec()))
        );
    }

    #[test]
    fn rejects_external_duplicate_and_package_escaping_review_targets() {
        for relationship in [
            r#"<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="comments.xml" TargetMode="External"/>"#,
            r#"<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="comments.xml"/><Relationship Type="http://purl.oclc.org/ooxml/officeDocument/relationships/comments" Target="other.xml"/>"#,
            r#"<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="../../comments.xml"/>"#,
        ] {
            let xml = format!(
                r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{relationship}</Relationships>"#
            );
            assert!(review_part_paths(xml.as_bytes(), b"custom/main.xml").is_err());
        }
    }
}