Skip to main content

easydoc_reader/extractor/
text.rs

1//! Plain text extraction from DOCX/DOC files via `office_oxide`.
2
3use std::path::Path;
4
5use easydoc_core::{DocError, Result};
6
7use crate::extractor::{DocumentFormat, detect_format_from_bytes};
8
9/// Extracts all plain text from a document using `office_oxide`.
10///
11/// Supports DOCX, DOC, and all other formats `office_oxide` can read.
12///
13/// # Errors
14///
15/// Returns I/O or format errors if the file cannot be opened or parsed.
16pub fn extract_text(path: &Path) -> Result<String> {
17    let doc = office_oxide::Document::open(path)
18        .map_err(|e| DocError::Document(format!("failed to open document: {e}")))?;
19    Ok(doc.plain_text())
20}
21
22/// Extracts all plain text from in-memory document bytes.
23///
24/// Detects DOCX/DOC from magic bytes and parses without touching the
25/// filesystem — suitable for fuzzing and embedded/streaming callers.
26///
27/// # Errors
28///
29/// Returns format errors if the bytes are not a supported document, or
30/// parse errors from `office_oxide`.
31pub fn extract_text_from_bytes(bytes: &[u8]) -> Result<String> {
32    let format = detect_format_from_bytes(bytes).ok_or_else(|| {
33        DocError::Format("unsupported document: could not detect DOCX/DOC magic bytes".to_owned())
34    })?;
35    let doc =
36        office_oxide::Document::from_reader(std::io::Cursor::new(bytes.to_vec()), to_oxide(format))
37            .map_err(|e| DocError::Document(format!("failed to open document from bytes: {e}")))?;
38    Ok(doc.plain_text())
39}
40
41/// Maps easydoc's format enum to `office_oxide`'s.
42fn to_oxide(format: DocumentFormat) -> office_oxide::DocumentFormat {
43    match format {
44        DocumentFormat::Docx => office_oxide::DocumentFormat::Docx,
45        DocumentFormat::Doc => office_oxide::DocumentFormat::Doc,
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use easydoc_writer::DocBuilder;
53
54    fn minimal_docx_bytes() -> Vec<u8> {
55        DocBuilder::new("memory.docx")
56            .add_paragraph(easydoc_writer::Paragraph::new().add_text("hello bytes"))
57            .save_to_bytes()
58            .expect("writer should produce valid DOCX")
59    }
60
61    #[test]
62    fn detect_format_from_bytes_docx() {
63        let bytes = minimal_docx_bytes();
64        assert_eq!(detect_format_from_bytes(&bytes), Some(DocumentFormat::Docx));
65    }
66
67    #[test]
68    fn detect_format_from_bytes_doc_magic() {
69        let ole2: [u8; 8] = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
70        assert_eq!(detect_format_from_bytes(&ole2), Some(DocumentFormat::Doc));
71    }
72
73    #[test]
74    fn detect_format_from_bytes_garbage() {
75        assert_eq!(detect_format_from_bytes(b"not a document"), None);
76        assert_eq!(detect_format_from_bytes(b""), None);
77        assert_eq!(detect_format_from_bytes(&[0xd0, 0xcf]), None); // too short
78    }
79
80    #[test]
81    fn extract_text_from_bytes_roundtrip() {
82        let bytes = minimal_docx_bytes();
83        let text = extract_text_from_bytes(&bytes).expect("should parse");
84        assert!(text.contains("hello bytes"));
85    }
86
87    #[test]
88    fn extract_text_from_bytes_garbage_errors() {
89        assert!(extract_text_from_bytes(b"garbage").is_err());
90    }
91}