File size: 6,116 Bytes
02dbc9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
use anyhow::{anyhow, Context, Result};
use memmap2::Mmap;
use quick_xml::events::Event;
use quick_xml::reader::Reader;
use std::fs::File;
use std::io::{BufRead, Cursor, Read};
use std::path::Path;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ParserError {
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Unsupported file format: {0}")]
    UnsupportedFormat(String),
    #[error("Corrupt document: {0}")]
    CorruptDocument(String),
}

#[derive(Debug, Clone)]
pub struct ParsedDocument {
    pub path: String,
    pub title: String,
    pub content: String,
    pub extension: String,
    pub size_bytes: u64,
}

pub struct DocumentParser;

impl DocumentParser {
    /// Ingests a file with zero-copy mmap for large files or standard read for small files.
    pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<ParsedDocument> {
        let path_ref = path.as_ref();
        let metadata = std::fs::metadata(path_ref)
            .with_context(|| format!("Failed to read metadata for {:?}", path_ref))?;
        let size_bytes = metadata.len();

        if size_bytes == 0 {
            return Ok(ParsedDocument {
                path: path_ref.to_string_lossy().to_string(),
                title: Self::extract_title(path_ref),
                content: String::new(),
                extension: Self::get_extension(path_ref),
                size_bytes: 0,
            });
        }

        let extension = Self::get_extension(path_ref);
        let title = Self::extract_title(path_ref);

        let file = File::open(path_ref)?;
        
        // Zero-copy memory map for files >= 16 KB; heap read for smaller files
        let content = if size_bytes >= 16 * 1024 {
            let mmap = unsafe { Mmap::map(&file)? };
            Self::extract_content_from_bytes(&mmap, &extension)?
        } else {
            let mut buffer = Vec::with_capacity(size_bytes as usize);
            let mut reader = std::io::BufReader::new(file);
            reader.read_to_end(&mut buffer)?;
            Self::extract_content_from_bytes(&buffer, &extension)?
        };

        Ok(ParsedDocument {
            path: path_ref.to_string_lossy().to_string(),
            title,
            content,
            extension,
            size_bytes,
        })
    }

    fn extract_content_from_bytes(bytes: &[u8], extension: &str) -> Result<String> {
        match extension.to_lowercase().as_str() {
            "txt" | "md" | "markdown" | "log" | "rs" | "py" | "js" | "ts" | "toml" | "yaml" | "yml" => {
                match std::str::from_utf8(bytes) {
                    Ok(valid_str) => Ok(valid_str.to_string()),
                    Err(_) => Ok(String::from_utf8_lossy(bytes).into_owned()),
                }
            }
            "json" => {
                let val: serde_json::Value = serde_json::from_slice(bytes)
                    .context("Invalid JSON document")?;
                if let Some(text) = val.as_str() {
                    Ok(text.to_string())
                } else {
                    Ok(serde_json::to_string_pretty(&val)?)
                }
            }
            "csv" => Self::parse_csv(bytes),
            "docx" => Self::parse_docx(bytes),
            "pdf" => Self::parse_pdf(bytes),
            ext => Err(ParserError::UnsupportedFormat(ext.to_string()).into()),
        }
    }

    fn parse_csv(bytes: &[u8]) -> Result<String> {
        let mut rdr = csv::ReaderBuilder::new()
            .flexible(true)
            .has_headers(true)
            .from_reader(Cursor::new(bytes));

        let mut output = String::with_capacity(bytes.len());
        
        if let Ok(headers) = rdr.headers() {
            output.push_str(&headers.iter().collect::<Vec<_>>().join(" | "));
            output.push('\n');
        }

        for result in rdr.records() {
            let record = result?;
            output.push_str(&record.iter().collect::<Vec<_>>().join(" | "));
            output.push('\n');
        }

        Ok(output)
    }

    fn parse_docx(bytes: &[u8]) -> Result<String> {
        let cursor = Cursor::new(bytes);
        let mut archive = zip::ZipArchive::new(cursor)
            .context("Failed to open DOCX as ZIP archive")?;

        let mut document_xml = archive
            .by_name("word/document.xml")
            .context("Missing word/document.xml inside DOCX archive")?;

        let mut xml_bytes = Vec::new();
        document_xml.read_to_end(&mut xml_bytes)?;

        let mut reader = Reader::from_reader(Cursor::new(xml_bytes));
        reader.config_mut().trim_text(true);

        let mut txt = String::new();
        let mut buf = Vec::new();
        let mut in_text_node = false;

        loop {
            match reader.read_event_into(&mut buf) {
                Ok(Event::Start(ref e)) if e.name().as_ref() == b"w:t" => {
                    in_text_node = true;
                }
                Ok(Event::End(ref e)) if e.name().as_ref() == b"w:t" => {
                    in_text_node = false;
                }
                Ok(Event::End(ref e)) if e.name().as_ref() == b"w:p" => {
                    txt.push('\n');
                }
                Ok(Event::Text(e)) if in_text_node => {
                    if let Ok(s) = e.unescape() {
                        txt.push_str(&s);
                    }
                }
                Ok(Event::Eof) => break,
                Err(e) => return Err(anyhow!("XML Parsing error in DOCX: {}", e)),
                _ => (),
            }
            buf.clear();
        }

        Ok(txt)
    }

    fn parse_pdf(bytes: &[u8]) -> Result<String> {
        pdf_extract::extract_text_from_mem(bytes)
            .context("Failed to extract text stream from PDF")
    }

    fn extract_title(path: &Path) -> String {
        path.file_name()
            .map(|f| f.to_string_lossy().to_string())
            .unwrap_or_else(|| "Untitled".to_string())
    }

    fn get_extension(path: &Path) -> String {
        path.extension()
            .map(|e| e.to_string_lossy().to_string())
            .unwrap_or_else(|| "txt".to_string())
    }
}