Skip to main content

easydoc_reader/extractor/
mod.rs

1//! Content extractors for DOCX/DOC files.
2
3pub mod image;
4pub mod numbering;
5pub mod sax;
6pub(crate) mod semantic;
7pub mod table;
8pub mod text;
9
10use std::path::Path;
11
12/// Detects the document format from file extension and magic bytes.
13#[must_use]
14pub fn detect_format(path: &Path) -> Option<DocumentFormat> {
15    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
16        match ext.to_lowercase().as_str() {
17            "docx" => return Some(DocumentFormat::Docx),
18            "doc" => return Some(DocumentFormat::Doc),
19            _ => {}
20        }
21    }
22
23    std::fs::read(path)
24        .ok()
25        .and_then(|bytes| detect_format_from_bytes(&bytes))
26}
27
28/// Detects the document format from magic bytes only.
29///
30/// Used by the `from_bytes` read APIs and fuzz targets where no file path is
31/// available. ZIP magic (`PK\x03\x04`) maps to DOCX; the OLE2 container magic
32/// maps to legacy DOC.
33#[must_use]
34pub fn detect_format_from_bytes(bytes: &[u8]) -> Option<DocumentFormat> {
35    if bytes.len() >= 4 && &bytes[0..4] == b"PK\x03\x04" {
36        return Some(DocumentFormat::Docx);
37    }
38    if bytes.len() >= 8 {
39        let ole2_magic: [u8; 8] = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
40        if bytes[0..8] == ole2_magic {
41            return Some(DocumentFormat::Doc);
42        }
43    }
44    None
45}
46
47/// Supported document formats.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[non_exhaustive]
50pub enum DocumentFormat {
51    /// Office Open XML (.docx).
52    Docx,
53    /// Legacy Word Binary (.doc).
54    Doc,
55}
56
57impl std::fmt::Display for DocumentFormat {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Self::Docx => write!(f, "DOCX"),
61            Self::Doc => write!(f, "DOC"),
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use std::io::Write;
70
71    #[test]
72    fn detect_format_docx_extension() {
73        let path = Path::new("test.docx");
74        assert_eq!(detect_format(path), Some(DocumentFormat::Docx));
75    }
76
77    #[test]
78    fn detect_format_doc_extension() {
79        let path = Path::new("test.doc");
80        assert_eq!(detect_format(path), Some(DocumentFormat::Doc));
81    }
82
83    #[test]
84    fn detect_format_unknown_extension() {
85        let path = Path::new("test.pdf");
86        assert_eq!(detect_format(path), None);
87    }
88
89    #[test]
90    fn detect_format_docx_magic_bytes() {
91        let dir = std::env::temp_dir().join("easydoc_test_fmt1");
92        std::fs::create_dir_all(&dir).unwrap();
93        let path = dir.join("test.unknown");
94        let mut f = std::fs::File::create(&path).unwrap();
95        f.write_all(b"PK").unwrap();
96        drop(f);
97        assert_eq!(detect_format(&path), Some(DocumentFormat::Docx));
98        std::fs::remove_file(&path).ok();
99    }
100
101    #[test]
102    fn detect_format_doc_magic_bytes() {
103        let dir = std::env::temp_dir().join("easydoc_test_fmt2");
104        std::fs::create_dir_all(&dir).unwrap();
105        let path = dir.join("test.unknown");
106        let mut f = std::fs::File::create(&path).unwrap();
107        f.write_all(&[0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1])
108            .unwrap();
109        drop(f);
110        assert_eq!(detect_format(&path), Some(DocumentFormat::Doc));
111        std::fs::remove_file(&path).ok();
112    }
113
114    #[test]
115    fn detect_format_short_file() {
116        let dir = std::env::temp_dir().join("easydoc_test_fmt3");
117        std::fs::create_dir_all(&dir).unwrap();
118        let path = dir.join("test.short");
119        std::fs::write(&path, b"AB").unwrap();
120        assert_eq!(detect_format(&path), None);
121        std::fs::remove_file(&path).ok();
122    }
123
124    #[test]
125    fn document_format_display() {
126        assert_eq!(format!("{}", DocumentFormat::Docx), "DOCX");
127        assert_eq!(format!("{}", DocumentFormat::Doc), "DOC");
128    }
129
130    #[test]
131    fn document_format_debug() {
132        assert_eq!(format!("{:?}", DocumentFormat::Docx), "Docx");
133        assert_eq!(format!("{:?}", DocumentFormat::Doc), "Doc");
134    }
135
136    #[test]
137    fn document_format_clone_eq() {
138        let f = DocumentFormat::Docx;
139        let f2 = f;
140        assert_eq!(f, f2);
141    }
142}