misukisu commited on
Commit
02dbc9e
·
verified ·
1 Parent(s): 9502b79

Create src/parser.rs

Browse files
Files changed (1) hide show
  1. 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
+ }