jw-hwp-core 0.1.0

Read-only parser for Hancom HWP 5.0 (binary CFB) and HWPX (OWPML) documents
Documentation
use std::path::PathBuf;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("path not found: {0}")]
    NotFound(PathBuf),
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
    #[error("container error: {0}")]
    Container(String),
    #[error("required stream missing: {0}")]
    MissingStream(String),
    #[error("decompression failed in stream {stream}: {source}")]
    Decompress {
        stream: String,
        #[source]
        source: std::io::Error,
    },
    #[error("invalid file header: {0}")]
    InvalidHeader(String),
    #[error("record parse error: {0}")]
    Record(String),
}

#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
pub struct Warning {
    pub code: WarningCode,
    pub message: String,
    pub location: Option<Location>,
}

#[derive(Debug, Clone, Copy, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum WarningCode {
    UnknownRecordTag,
    TruncatedRecord,
    UnsupportedContent,
    ShapeRefMissing,
}

#[derive(Debug, Clone, Default, serde::Serialize, PartialEq, Eq)]
pub struct Location {
    pub section: Option<usize>,
    pub paragraph: Option<usize>,
    pub offset: Option<u64>,
}

#[derive(Debug, Default)]
pub struct WarningCollector {
    pub warnings: Vec<Warning>,
}

impl WarningCollector {
    pub fn push(
        &mut self,
        code: WarningCode,
        message: impl Into<String>,
        location: Option<Location>,
    ) {
        self.warnings.push(Warning {
            code,
            message: message.into(),
            location,
        });
    }
    pub fn take(&mut self) -> Vec<Warning> {
        std::mem::take(&mut self.warnings)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn collector_accumulates_and_drains() {
        let mut c = WarningCollector::default();
        c.push(WarningCode::UnknownRecordTag, "tag=0x999", None);
        c.push(
            WarningCode::TruncatedRecord,
            "ran out at offset 42",
            Some(Location {
                section: Some(0),
                paragraph: Some(3),
                offset: Some(42),
            }),
        );
        assert_eq!(c.warnings.len(), 2);
        let taken = c.take();
        assert_eq!(taken.len(), 2);
        assert!(c.warnings.is_empty());
    }

    #[test]
    fn warning_serializes_with_screaming_code() {
        let w = Warning {
            code: WarningCode::UnknownRecordTag,
            message: "x".into(),
            location: None,
        };
        let s = serde_json::to_string(&w).unwrap();
        assert!(s.contains("\"UNKNOWN_RECORD_TAG\""));
    }
}