stella-docx-kernel 0.7.0

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

use crate::ProjectionError;
use crate::projection::namespaces::{OoxmlNamespace, is_fully_supported_namespace};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum CompatibilityAction {
    Process,
    Skip,
}

#[derive(Default)]
struct AlternateContent {
    branch_selected: bool,
    branch_active: bool,
    fallback_seen: bool,
}

#[derive(Default)]
pub(super) struct MarkupCompatibility {
    alternate_content: Vec<AlternateContent>,
    suppressed_depth: usize,
}

impl MarkupCompatibility {
    pub(super) fn start(
        &mut self,
        reader: &NsReader<&[u8]>,
        namespace: OoxmlNamespace,
        element: &BytesStart<'_>,
    ) -> Result<CompatibilityAction, ProjectionError> {
        if self.suppressed_depth > 0 {
            self.suppressed_depth = self
                .suppressed_depth
                .checked_add(1)
                .ok_or(ProjectionError::InvalidDocumentXml)?;
            return Ok(CompatibilityAction::Skip);
        }

        if namespace != OoxmlNamespace::MarkupCompatibility {
            if self
                .alternate_content
                .last()
                .is_some_and(|content| !content.branch_active)
            {
                self.suppressed_depth = 1;
                return Ok(CompatibilityAction::Skip);
            }
            return Ok(CompatibilityAction::Process);
        }

        match element.local_name().as_ref() {
            b"AlternateContent" => {
                if self
                    .alternate_content
                    .last()
                    .is_some_and(|content| !content.branch_active)
                {
                    return Err(ProjectionError::InvalidDocumentXml);
                }
                self.alternate_content.push(AlternateContent::default());
            }
            b"Choice" => {
                let supported = choice_is_supported(reader, element)?;
                self.start_branch(supported, false)?;
            }
            b"Fallback" => self.start_branch(true, true)?,
            _ => self.suppressed_depth = 1,
        }
        Ok(CompatibilityAction::Skip)
    }

    pub(super) fn empty(
        &mut self,
        reader: &NsReader<&[u8]>,
        namespace: OoxmlNamespace,
        element: &BytesStart<'_>,
    ) -> Result<CompatibilityAction, ProjectionError> {
        if self.suppressed_depth > 0 {
            return Ok(CompatibilityAction::Skip);
        }
        if namespace != OoxmlNamespace::MarkupCompatibility {
            return Ok(
                if self
                    .alternate_content
                    .last()
                    .is_some_and(|content| !content.branch_active)
                {
                    CompatibilityAction::Skip
                } else {
                    CompatibilityAction::Process
                },
            );
        }

        match element.local_name().as_ref() {
            b"AlternateContent" => {
                if self
                    .alternate_content
                    .last()
                    .is_some_and(|content| !content.branch_active)
                {
                    return Err(ProjectionError::InvalidDocumentXml);
                }
            }
            b"Choice" => {
                let supported = choice_is_supported(reader, element)?;
                self.select_empty_branch(supported, false)?;
            }
            b"Fallback" => self.select_empty_branch(true, true)?,
            _ => {}
        }
        Ok(CompatibilityAction::Skip)
    }

    pub(super) fn end(
        &mut self,
        namespace: OoxmlNamespace,
        local_name: &[u8],
    ) -> Result<CompatibilityAction, ProjectionError> {
        if self.suppressed_depth > 0 {
            self.suppressed_depth = self
                .suppressed_depth
                .checked_sub(1)
                .ok_or(ProjectionError::InvalidDocumentXml)?;
            return Ok(CompatibilityAction::Skip);
        }
        if namespace != OoxmlNamespace::MarkupCompatibility {
            return Ok(CompatibilityAction::Process);
        }

        match local_name {
            b"AlternateContent" => {
                let content = self
                    .alternate_content
                    .pop()
                    .ok_or(ProjectionError::InvalidDocumentXml)?;
                if content.branch_active {
                    return Err(ProjectionError::InvalidDocumentXml);
                }
            }
            b"Choice" | b"Fallback" => {
                let content = self
                    .alternate_content
                    .last_mut()
                    .ok_or(ProjectionError::InvalidDocumentXml)?;
                if !content.branch_active {
                    return Err(ProjectionError::InvalidDocumentXml);
                }
                content.branch_active = false;
            }
            _ => {}
        }
        Ok(CompatibilityAction::Skip)
    }

    pub(super) const fn is_suppressed(&self) -> bool {
        self.suppressed_depth > 0
    }

    pub(super) const fn is_complete(&self) -> bool {
        self.suppressed_depth == 0 && self.alternate_content.is_empty()
    }

    fn start_branch(&mut self, supported: bool, fallback: bool) -> Result<(), ProjectionError> {
        let content = self
            .alternate_content
            .last_mut()
            .ok_or(ProjectionError::InvalidDocumentXml)?;
        validate_branch_order(content, fallback)?;
        let active = !content.branch_selected && supported;
        if active {
            content.branch_selected = true;
            content.branch_active = true;
        } else {
            self.suppressed_depth = 1;
        }
        Ok(())
    }

    fn select_empty_branch(
        &mut self,
        supported: bool,
        fallback: bool,
    ) -> Result<(), ProjectionError> {
        let content = self
            .alternate_content
            .last_mut()
            .ok_or(ProjectionError::InvalidDocumentXml)?;
        validate_branch_order(content, fallback)?;
        if !content.branch_selected && supported {
            content.branch_selected = true;
        }
        Ok(())
    }
}

const fn validate_branch_order(
    content: &mut AlternateContent,
    fallback: bool,
) -> Result<(), ProjectionError> {
    if content.branch_active || content.fallback_seen {
        return Err(ProjectionError::InvalidDocumentXml);
    }
    if fallback {
        content.fallback_seen = true;
    }
    Ok(())
}

fn choice_is_supported(
    reader: &NsReader<&[u8]>,
    element: &BytesStart<'_>,
) -> Result<bool, ProjectionError> {
    let mut requires = None;
    for attribute in element.attributes() {
        let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
        let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
        if namespace == ResolveResult::Unbound && local_name.as_ref() == b"Requires" {
            if requires.is_some() {
                return Err(ProjectionError::InvalidDocumentXml);
            }
            requires = Some(
                attribute
                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
                    .map_err(|_| ProjectionError::InvalidDocumentXml)?
                    .into_owned(),
            );
        }
    }
    let requires = requires.ok_or(ProjectionError::InvalidDocumentXml)?;
    let mut prefixes = requires.split_ascii_whitespace().peekable();
    if prefixes.peek().is_none() {
        return Err(ProjectionError::InvalidDocumentXml);
    }
    Ok(prefixes.all(|required| {
        reader.resolver().bindings().any(|(prefix, namespace)| {
            matches!(prefix, PrefixDeclaration::Named(name) if name == required.as_bytes())
                && is_fully_supported_namespace(namespace.as_ref())
        })
    }))
}