Create src/parser.rs
Browse files- src/parser.rs +181 -0
src/parser.rs
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use anyhow::{anyhow, Context, Result};
|
| 2 |
+
use memmap2::Mmap;
|
| 3 |
+
use quick_xml::events::Event;
|
| 4 |
+
use quick_xml::reader::Reader;
|
| 5 |
+
use std::fs::File;
|
| 6 |
+
use std::io::{BufRead, Cursor, Read};
|
| 7 |
+
use std::path::Path;
|
| 8 |
+
use thiserror::Error;
|
| 9 |
+
|
| 10 |
+
#[derive(Error, Debug)]
|
| 11 |
+
pub enum ParserError {
|
| 12 |
+
#[error("I/O error: {0}")]
|
| 13 |
+
Io(#[from] std::io::Error),
|
| 14 |
+
#[error("Unsupported file format: {0}")]
|
| 15 |
+
UnsupportedFormat(String),
|
| 16 |
+
#[error("Corrupt document: {0}")]
|
| 17 |
+
CorruptDocument(String),
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
#[derive(Debug, Clone)]
|
| 21 |
+
pub struct ParsedDocument {
|
| 22 |
+
pub path: String,
|
| 23 |
+
pub title: String,
|
| 24 |
+
pub content: String,
|
| 25 |
+
pub extension: String,
|
| 26 |
+
pub size_bytes: u64,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
pub struct DocumentParser;
|
| 30 |
+
|
| 31 |
+
impl DocumentParser {
|
| 32 |
+
/// Ingests a file with zero-copy mmap for large files or standard read for small files.
|
| 33 |
+
pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<ParsedDocument> {
|
| 34 |
+
let path_ref = path.as_ref();
|
| 35 |
+
let metadata = std::fs::metadata(path_ref)
|
| 36 |
+
.with_context(|| format!("Failed to read metadata for {:?}", path_ref))?;
|
| 37 |
+
let size_bytes = metadata.len();
|
| 38 |
+
|
| 39 |
+
if size_bytes == 0 {
|
| 40 |
+
return Ok(ParsedDocument {
|
| 41 |
+
path: path_ref.to_string_lossy().to_string(),
|
| 42 |
+
title: Self::extract_title(path_ref),
|
| 43 |
+
content: String::new(),
|
| 44 |
+
extension: Self::get_extension(path_ref),
|
| 45 |
+
size_bytes: 0,
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
let extension = Self::get_extension(path_ref);
|
| 50 |
+
let title = Self::extract_title(path_ref);
|
| 51 |
+
|
| 52 |
+
let file = File::open(path_ref)?;
|
| 53 |
+
|
| 54 |
+
// Zero-copy memory map for files >= 16 KB; heap read for smaller files
|
| 55 |
+
let content = if size_bytes >= 16 * 1024 {
|
| 56 |
+
let mmap = unsafe { Mmap::map(&file)? };
|
| 57 |
+
Self::extract_content_from_bytes(&mmap, &extension)?
|
| 58 |
+
} else {
|
| 59 |
+
let mut buffer = Vec::with_capacity(size_bytes as usize);
|
| 60 |
+
let mut reader = std::io::BufReader::new(file);
|
| 61 |
+
reader.read_to_end(&mut buffer)?;
|
| 62 |
+
Self::extract_content_from_bytes(&buffer, &extension)?
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
Ok(ParsedDocument {
|
| 66 |
+
path: path_ref.to_string_lossy().to_string(),
|
| 67 |
+
title,
|
| 68 |
+
content,
|
| 69 |
+
extension,
|
| 70 |
+
size_bytes,
|
| 71 |
+
})
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
fn extract_content_from_bytes(bytes: &[u8], extension: &str) -> Result<String> {
|
| 75 |
+
match extension.to_lowercase().as_str() {
|
| 76 |
+
"txt" | "md" | "markdown" | "log" | "rs" | "py" | "js" | "ts" | "toml" | "yaml" | "yml" => {
|
| 77 |
+
match std::str::from_utf8(bytes) {
|
| 78 |
+
Ok(valid_str) => Ok(valid_str.to_string()),
|
| 79 |
+
Err(_) => Ok(String::from_utf8_lossy(bytes).into_owned()),
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
"json" => {
|
| 83 |
+
let val: serde_json::Value = serde_json::from_slice(bytes)
|
| 84 |
+
.context("Invalid JSON document")?;
|
| 85 |
+
if let Some(text) = val.as_str() {
|
| 86 |
+
Ok(text.to_string())
|
| 87 |
+
} else {
|
| 88 |
+
Ok(serde_json::to_string_pretty(&val)?)
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
"csv" => Self::parse_csv(bytes),
|
| 92 |
+
"docx" => Self::parse_docx(bytes),
|
| 93 |
+
"pdf" => Self::parse_pdf(bytes),
|
| 94 |
+
ext => Err(ParserError::UnsupportedFormat(ext.to_string()).into()),
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
fn parse_csv(bytes: &[u8]) -> Result<String> {
|
| 99 |
+
let mut rdr = csv::ReaderBuilder::new()
|
| 100 |
+
.flexible(true)
|
| 101 |
+
.has_headers(true)
|
| 102 |
+
.from_reader(Cursor::new(bytes));
|
| 103 |
+
|
| 104 |
+
let mut output = String::with_capacity(bytes.len());
|
| 105 |
+
|
| 106 |
+
if let Ok(headers) = rdr.headers() {
|
| 107 |
+
output.push_str(&headers.iter().collect::<Vec<_>>().join(" | "));
|
| 108 |
+
output.push('\n');
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
for result in rdr.records() {
|
| 112 |
+
let record = result?;
|
| 113 |
+
output.push_str(&record.iter().collect::<Vec<_>>().join(" | "));
|
| 114 |
+
output.push('\n');
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
Ok(output)
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
fn parse_docx(bytes: &[u8]) -> Result<String> {
|
| 121 |
+
let cursor = Cursor::new(bytes);
|
| 122 |
+
let mut archive = zip::ZipArchive::new(cursor)
|
| 123 |
+
.context("Failed to open DOCX as ZIP archive")?;
|
| 124 |
+
|
| 125 |
+
let mut document_xml = archive
|
| 126 |
+
.by_name("word/document.xml")
|
| 127 |
+
.context("Missing word/document.xml inside DOCX archive")?;
|
| 128 |
+
|
| 129 |
+
let mut xml_bytes = Vec::new();
|
| 130 |
+
document_xml.read_to_end(&mut xml_bytes)?;
|
| 131 |
+
|
| 132 |
+
let mut reader = Reader::from_reader(Cursor::new(xml_bytes));
|
| 133 |
+
reader.config_mut().trim_text(true);
|
| 134 |
+
|
| 135 |
+
let mut txt = String::new();
|
| 136 |
+
let mut buf = Vec::new();
|
| 137 |
+
let mut in_text_node = false;
|
| 138 |
+
|
| 139 |
+
loop {
|
| 140 |
+
match reader.read_event_into(&mut buf) {
|
| 141 |
+
Ok(Event::Start(ref e)) if e.name().as_ref() == b"w:t" => {
|
| 142 |
+
in_text_node = true;
|
| 143 |
+
}
|
| 144 |
+
Ok(Event::End(ref e)) if e.name().as_ref() == b"w:t" => {
|
| 145 |
+
in_text_node = false;
|
| 146 |
+
}
|
| 147 |
+
Ok(Event::End(ref e)) if e.name().as_ref() == b"w:p" => {
|
| 148 |
+
txt.push('\n');
|
| 149 |
+
}
|
| 150 |
+
Ok(Event::Text(e)) if in_text_node => {
|
| 151 |
+
if let Ok(s) = e.unescape() {
|
| 152 |
+
txt.push_str(&s);
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
Ok(Event::Eof) => break,
|
| 156 |
+
Err(e) => return Err(anyhow!("XML Parsing error in DOCX: {}", e)),
|
| 157 |
+
_ => (),
|
| 158 |
+
}
|
| 159 |
+
buf.clear();
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
Ok(txt)
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
fn parse_pdf(bytes: &[u8]) -> Result<String> {
|
| 166 |
+
pdf_extract::extract_text_from_mem(bytes)
|
| 167 |
+
.context("Failed to extract text stream from PDF")
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
fn extract_title(path: &Path) -> String {
|
| 171 |
+
path.file_name()
|
| 172 |
+
.map(|f| f.to_string_lossy().to_string())
|
| 173 |
+
.unwrap_or_else(|| "Untitled".to_string())
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
fn get_extension(path: &Path) -> String {
|
| 177 |
+
path.extension()
|
| 178 |
+
.map(|e| e.to_string_lossy().to_string())
|
| 179 |
+
.unwrap_or_else(|| "txt".to_string())
|
| 180 |
+
}
|
| 181 |
+
}
|