oxdoc-core 1.0.0

Core OOXML parsing library for oxdoc
Documentation
use thiserror::Error;

pub type Result<T> = std::result::Result<T, OxdocError>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OxdocErrorCode {
    Io,
    CorruptedZip,
    MissingPart,
    UnsupportedEncryptedPart,
    PartTooLarge,
    SuspiciousZipEntry,
    SuspiciousRelationshipTarget,
    MissingCoreRelations,
    MalformedXmlNode,
    InvalidArgument,
}

impl OxdocErrorCode {
    pub fn as_str(self) -> &'static str {
        match self {
            OxdocErrorCode::Io => "E001",
            OxdocErrorCode::CorruptedZip => "E002",
            OxdocErrorCode::MissingPart => "E003",
            OxdocErrorCode::UnsupportedEncryptedPart => "E004",
            OxdocErrorCode::PartTooLarge => "E005",
            OxdocErrorCode::SuspiciousZipEntry => "E006",
            OxdocErrorCode::SuspiciousRelationshipTarget => "E007",
            OxdocErrorCode::MissingCoreRelations => "E008",
            OxdocErrorCode::MalformedXmlNode => "E009",
            OxdocErrorCode::InvalidArgument => "E010",
        }
    }
}

#[derive(Debug, Error)]
pub enum OxdocError {
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    #[error("corrupted or unsupported ZIP container: {0}")]
    CorruptedZip(#[from] zip::result::ZipError),

    #[error("missing required OOXML part: {0}")]
    MissingPart(String),

    #[error("unsupported encrypted OOXML part: {0}")]
    UnsupportedEncryptedPart(String),

    #[error("OOXML part is too large: {path} is {size} bytes; limit is {limit} bytes")]
    PartTooLarge { path: String, size: u64, limit: u64 },

    #[error("suspicious OOXML ZIP entry: {path}: {reason}")]
    SuspiciousZipEntry { path: String, reason: String },

    #[error("suspicious OOXML relationship target in {path}: {target}: {reason}")]
    SuspiciousRelationshipTarget {
        path: String,
        target: String,
        reason: String,
    },

    #[error("missing office document relationship")]
    MissingCoreRelations,

    #[error("malformed XML in {path}: {source}")]
    MalformedXmlNode {
        path: String,
        #[source]
        source: quick_xml::Error,
    },

    #[error("invalid argument: {0}")]
    InvalidArgument(String),
}

impl OxdocError {
    pub fn code(&self) -> OxdocErrorCode {
        match self {
            OxdocError::Io(_) => OxdocErrorCode::Io,
            OxdocError::CorruptedZip(_) => OxdocErrorCode::CorruptedZip,
            OxdocError::MissingPart(_) => OxdocErrorCode::MissingPart,
            OxdocError::UnsupportedEncryptedPart(_) => OxdocErrorCode::UnsupportedEncryptedPart,
            OxdocError::PartTooLarge { .. } => OxdocErrorCode::PartTooLarge,
            OxdocError::SuspiciousZipEntry { .. } => OxdocErrorCode::SuspiciousZipEntry,
            OxdocError::SuspiciousRelationshipTarget { .. } => {
                OxdocErrorCode::SuspiciousRelationshipTarget
            }
            OxdocError::MissingCoreRelations => OxdocErrorCode::MissingCoreRelations,
            OxdocError::MalformedXmlNode { .. } => OxdocErrorCode::MalformedXmlNode,
            OxdocError::InvalidArgument(_) => OxdocErrorCode::InvalidArgument,
        }
    }
}

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

    #[test]
    fn maps_errors_to_stable_codes() {
        let cases = [
            (
                OxdocError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "missing")),
                OxdocErrorCode::Io,
                "E001",
            ),
            (
                OxdocError::CorruptedZip(zip::result::ZipError::FileNotFound),
                OxdocErrorCode::CorruptedZip,
                "E002",
            ),
            (
                OxdocError::MissingPart("word/document.xml".to_owned()),
                OxdocErrorCode::MissingPart,
                "E003",
            ),
            (
                OxdocError::UnsupportedEncryptedPart("word/document.xml".to_owned()),
                OxdocErrorCode::UnsupportedEncryptedPart,
                "E004",
            ),
            (
                OxdocError::PartTooLarge {
                    path: "word/document.xml".to_owned(),
                    size: 2,
                    limit: 1,
                },
                OxdocErrorCode::PartTooLarge,
                "E005",
            ),
            (
                OxdocError::SuspiciousZipEntry {
                    path: "word/document.xml".to_owned(),
                    reason: "directory".to_owned(),
                },
                OxdocErrorCode::SuspiciousZipEntry,
                "E006",
            ),
            (
                OxdocError::SuspiciousRelationshipTarget {
                    path: "_rels/.rels".to_owned(),
                    target: "https://example.invalid/document.xml".to_owned(),
                    reason: "external".to_owned(),
                },
                OxdocErrorCode::SuspiciousRelationshipTarget,
                "E007",
            ),
            (
                OxdocError::MissingCoreRelations,
                OxdocErrorCode::MissingCoreRelations,
                "E008",
            ),
            (
                OxdocError::MalformedXmlNode {
                    path: "_rels/.rels".to_owned(),
                    source: quick_xml::Error::Syntax(quick_xml::errors::SyntaxError::UnclosedTag),
                },
                OxdocErrorCode::MalformedXmlNode,
                "E009",
            ),
            (
                OxdocError::InvalidArgument("bad delimiter".to_owned()),
                OxdocErrorCode::InvalidArgument,
                "E010",
            ),
        ];

        for (error, code, label) in cases {
            assert_eq!(error.code(), code);
            assert_eq!(error.code().as_str(), label);
        }
    }
}