1use chrono::NaiveDate;
6use docparser_core::error::{DocError, Result};
7use docparser_core::plugin::{ParseCapability, RawDoc, RawPage};
8use quick_xml::events::Event;
9use quick_xml::Reader;
10
11use crate::zip_read_str;
12
13pub struct XlsxRawBackend;
15
16impl ParseCapability for XlsxRawBackend {
17 fn id(&self) -> &str {
18 "office-raw"
19 }
20 fn formats(&self) -> &[&str] {
21 &["xlsx"]
22 }
23 fn parse(&self, path: &str) -> Result<RawDoc> {
24 let file = std::fs::File::open(path)
25 .map_err(|e| DocError::corrupt(format!("cannot open {path}: {e}")))?;
26 let mut zip = zip::ZipArchive::new(file)
27 .map_err(|e| DocError::corrupt(format!("ZIP open failed: {e}")))?;
28
29 let wb_xml = zip_read_str(&mut zip, "xl/workbook.xml")?
31 .ok_or_else(|| DocError::corrupt("missing xl/workbook.xml"))?;
32 let sheet_names = parse_sheet_names(&wb_xml)?;
33
34 let sst = SharedStrings::new(&mut zip)?;
36
37 let styles = Styles::new(&mut zip)?;
39
40 let mut pages = Vec::new();
42 for (idx, (_rid, name)) in sheet_names.iter().enumerate() {
43 let sheet_path = format!("xl/worksheets/sheet{}.xml", idx + 1);
45 let xml = zip_read_str(&mut zip, &sheet_path)?.unwrap_or_default();
46 let rows = parse_sheet_rows(&xml, &sst, &styles)?;
47 let text = rows
48 .iter()
49 .map(|r| r.join(","))
50 .collect::<Vec<_>>()
51 .join("\n");
52 pages.push(RawPage {
53 name: Some(name.clone()),
54 items: vec![docparser_core::plugin::RawTextItem { text, bbox: None }],
55 ..Default::default()
56 });
57 }
58
59 Ok(RawDoc {
60 pages,
61 ..Default::default()
62 })
63 }
64}
65
66struct SharedStrings {
68 items: Vec<String>,
69}
70
71impl SharedStrings {
72 fn new(zip: &mut zip::ZipArchive<std::fs::File>) -> Result<Self> {
73 let xml = match zip_read_str(zip, "xl/sharedStrings.xml")? {
74 Some(s) => s,
75 None => {
76 return Ok(SharedStrings { items: Vec::new() });
77 }
78 };
79 let items = parse_sst_items(&xml)?;
80 Ok(SharedStrings { items })
81 }
82
83 fn get(&self, idx: usize) -> Option<&str> {
84 self.items.get(idx).map(|s| s.as_str())
85 }
86}
87
88fn parse_sst_items(xml: &str) -> Result<Vec<String>> {
90 let mut reader = Reader::from_str(xml);
91 let mut buf = Vec::new();
93 let mut items = Vec::new();
94 let mut in_t = false;
95 let mut text = String::new();
96
97 loop {
98 match reader.read_event_into(&mut buf) {
99 Ok(Event::Start(ref e)) => match e.name().local_name().as_ref() {
100 crate::tags::SI => {
101 text.clear();
102 }
103 crate::tags::T => {
104 in_t = true;
105 }
106 _ => {}
107 },
108 Ok(Event::End(ref e)) => match e.name().local_name().as_ref() {
109 crate::tags::SI => {
110 items.push(text.trim().to_string());
111 text.clear();
112 }
113 crate::tags::T => {
114 in_t = false;
115 }
116 crate::tags::SST => {
117 break;
118 }
119 _ => {}
120 },
121 Ok(Event::Text(ref e)) => {
122 if in_t {
123 let raw = e.xml10_content();
125 if let Ok(t) = quick_xml::escape::unescape(&raw) {
126 text.push_str(&t);
127 }
128 }
129 }
130 Ok(Event::GeneralRef(ref e)) => {
131 if in_t {
132 crate::push_general_ref(&mut text, e);
133 }
134 }
135 Ok(Event::CData(ref e)) => {
136 if in_t {
137 text.push_str(e.as_ref());
138 }
139 }
140 Ok(Event::Eof) => break,
141 Err(e) => return Err(DocError::corrupt(format!("SST parse error: {e}"))),
142 _ => {}
143 }
144 buf.clear();
145 }
146 Ok(items)
147}
148
149#[derive(Default)]
151struct Styles {
152 num_fmts: std::collections::HashMap<usize, String>,
153 xf_num_fmt_ids: Vec<usize>,
154}
155
156impl Styles {
157 fn new(zip: &mut zip::ZipArchive<std::fs::File>) -> Result<Self> {
158 let xml = match zip_read_str(zip, "xl/styles.xml")? {
159 Some(s) => s,
160 None => return Ok(Styles::default()),
161 };
162 let mut s = Styles::default();
163 s.parse(&xml)?;
164 Ok(s)
165 }
166
167 fn parse(&mut self, xml: &str) -> Result<()> {
168 let mut reader = Reader::from_str(xml);
169 let mut buf = Vec::new();
171 let mut _in_num_fmts = false;
172 let mut _in_cell_xfs = false;
173 let mut _num_fmt_id_val: Option<usize> = None;
174
175 loop {
176 match reader.read_event_into(&mut buf) {
177 Ok(Event::Start(ref e)) => match e.name().local_name().as_ref() {
178 crate::tags::NUM_FMT => {
179 _num_fmt_id_val = None;
180 let mut fmt_id = 0usize;
181 let mut fmt_code = String::new();
182 for attr in e.attributes().filter_map(|a| a.ok()) {
183 match attr.key.as_ref() {
184 "numFmtId" => fmt_id = attr.value.parse().unwrap_or(0),
185 "formatCode" => fmt_code = attr.value.to_string(),
186 _ => {}
187 }
188 }
189 if fmt_id >= 164 {
190 self.num_fmts.insert(fmt_id, fmt_code);
191 }
192 }
193 crate::tags::XF => {
194 let mut num_fmt_id = 0usize;
195 for attr in e.attributes().filter_map(|a| a.ok()) {
196 if attr.key.as_ref() == "numFmtId" {
197 num_fmt_id = attr.value.parse().unwrap_or(0);
198 }
199 }
200 self.xf_num_fmt_ids.push(num_fmt_id);
201 }
202 _ => {}
203 },
204 Ok(Event::Empty(ref e)) => match e.name().local_name().as_ref() {
205 crate::tags::NUM_FMT => {
206 let mut fmt_id = 0usize;
207 let mut fmt_code = String::new();
208 for attr in e.attributes().filter_map(|a| a.ok()) {
209 match attr.key.as_ref() {
210 "numFmtId" => fmt_id = attr.value.parse().unwrap_or(0),
211 "formatCode" => fmt_code = attr.value.to_string(),
212 _ => {}
213 }
214 }
215 if fmt_id >= 164 {
216 self.num_fmts.insert(fmt_id, fmt_code);
217 }
218 }
219 crate::tags::XF => {
220 let mut num_fmt_id = 0usize;
221 for attr in e.attributes().filter_map(|a| a.ok()) {
222 if attr.key.as_ref() == "numFmtId" {
223 num_fmt_id = attr.value.parse().unwrap_or(0);
224 }
225 }
226 self.xf_num_fmt_ids.push(num_fmt_id);
227 }
228 _ => {}
229 },
230 Ok(Event::End(ref e)) => {
231 if e.name().local_name().as_ref() == crate::tags::CELL_XFS {
232 _in_cell_xfs = false;
233 }
234 }
235 Ok(Event::Eof) => break,
236 Err(e) => return Err(DocError::corrupt(format!("Styles parse error: {e}"))),
237 _ => {}
238 }
239 buf.clear();
240 }
241 Ok(())
242 }
243
244 fn is_date_format(&self, xf_idx: Option<usize>) -> bool {
246 let num_fmt_id = xf_idx
247 .and_then(|i| self.xf_num_fmt_ids.get(i))
248 .copied()
249 .unwrap_or(0);
250
251 matches!(num_fmt_id,
253 14..=22 | 27..=36 | 45..=47 | 50..=58 | 71..=81
254 ) || self.num_fmts.contains_key(&num_fmt_id)
255 }
256
257 fn code_for(&self, xf_idx: Option<usize>) -> Option<&str> {
259 let num_fmt_id = xf_idx
260 .and_then(|i| self.xf_num_fmt_ids.get(i))
261 .copied()
262 .unwrap_or(0);
263 if num_fmt_id == 0 {
264 return None;
265 }
266 if let Some(code) = self.num_fmts.get(&num_fmt_id) {
267 return Some(code.as_str());
268 }
269 BUILTIN_FORMATS
270 .iter()
271 .find(|(id, _)| *id == num_fmt_id)
272 .map(|(_, code)| *code)
273 }
274}
275
276const BUILTIN_FORMATS: &[(usize, &str)] = &[
278 (14, "mm-dd-yy"),
279 (15, "d-mmm-yy"),
280 (16, "d-mmm"),
281 (17, "mmm-yy"),
282 (18, "h:mm AM/PM"),
283 (19, "h:mm:ss AM/PM"),
284 (20, "h:mm"),
285 (21, "h:mm:ss"),
286 (22, "m/d/yy h:mm"),
287 (45, "mm:ss"),
288 (46, "[h]:mm:ss"),
289 (47, "mmss.0"),
290];
291
292fn ole_serial_to_date(serial: f64) -> Option<NaiveDate> {
294 let epoch = NaiveDate::from_ymd_opt(1899, 12, 30)?;
295 let mut days = serial as i64;
296 if serial > 0.0 && serial < 60.0 {
298 days += 1;
299 }
300 epoch.checked_add_signed(chrono::Duration::days(days))
301}
302
303fn fmt_to_strftime(fmt: &str) -> String {
305 let is_12h = fmt.contains("AM/PM");
306 let s = fmt
307 .replace("yyyy", "%Y")
308 .replace("yy", "%y")
309 .replace("mmm", "%b")
310 .replace("dd", "%d");
311 let s = s.replace(":mm", ":M_M").replace("mm:", "M_M:");
313 let s = s
314 .replace("mm", "=*=")
315 .replace("m", "=*=")
316 .replace("=*=", "%m");
317 let s = s.replace("M_M", "%M");
318 let s = s.replace("ss", "%S").replace("AM/PM", "%p");
319 if is_12h {
320 s.replace("h", "%I")
321 } else {
322 s.replace("h", "%H")
323 }
324}
325
326fn format_excel_date(serial: f64, format_code: &str) -> String {
328 if let Some(date) = ole_serial_to_date(serial) {
329 let strfmt = fmt_to_strftime(format_code);
330 date.format(&strfmt).to_string()
331 } else {
332 format!("num:{}", serial)
333 }
334}
335
336fn format_number(raw: &str) -> String {
338 let trimmed = raw.trim();
339 if trimmed.is_empty() {
340 return String::new();
341 }
342 if let Ok(num) = trimmed.parse::<f64>() {
343 if num.fract() == 0.0 && num.is_finite() {
344 format!("{}", num as i64)
345 } else {
346 format!("{}", num)
347 }
348 } else {
349 trimmed.to_string()
350 }
351}
352
353fn parse_sheet_names(xml: &str) -> Result<Vec<(String, String)>> {
355 let mut reader = Reader::from_str(xml);
356 let mut buf = Vec::new();
358 let mut sheets = Vec::new();
359
360 loop {
361 match reader.read_event_into(&mut buf) {
362 Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
363 if e.name().local_name().as_ref() == "sheet" {
364 let mut rid = String::new();
365 let mut name = String::new();
366 for attr in e.attributes().filter_map(|a| a.ok()) {
367 match attr.key.as_ref() {
369 "r:id" => rid = attr.value.to_string(),
370 "name" => name = attr.value.to_string(),
371 "id" if rid.is_empty() => rid = attr.value.to_string(),
373 _ => {}
374 }
375 }
376 if !name.is_empty() {
377 sheets.push((rid, name));
378 }
379 }
380 }
381 Ok(Event::End(ref e)) => {
382 if e.name().local_name().as_ref() == "workbook" {
383 break;
384 }
385 }
386 Ok(Event::Eof) => break,
387 Err(e) => return Err(DocError::corrupt(format!("workbook parse error: {e}"))),
388 _ => {}
389 }
390 buf.clear();
391 }
392 Ok(sheets)
393}
394
395fn parse_sheet_rows(xml: &str, sst: &SharedStrings, styles: &Styles) -> Result<Vec<Vec<String>>> {
397 let mut reader = Reader::from_str(xml);
398 let mut buf = Vec::new();
400 let mut rows: Vec<Vec<String>> = Vec::new();
401 let mut current_row: Vec<(usize, String)> = Vec::new(); let mut cell_ref = String::new();
403 let mut cell_type = 's'; let mut cell_style: Option<usize> = None;
405 let mut cell_value = String::new();
406 let mut in_v = false;
407 let mut in_is = false;
408 let mut in_is_t = false;
409
410 fn col_from_ref(r: &str) -> usize {
412 let letters: String = r.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
413 let mut col = 0usize;
414 for c in letters.chars() {
415 col = col * 26 + (c.to_ascii_uppercase() as u8 - b'A' + 1) as usize;
416 }
417 if col > 0 {
418 col - 1
419 } else {
420 0
421 }
422 }
423
424 loop {
425 match reader.read_event_into(&mut buf) {
426 Ok(Event::Start(ref e)) => {
427 match e.name().local_name().as_ref() {
428 crate::tags::C => {
429 cell_type = 's';
430 cell_style = None;
431 cell_value.clear();
432 cell_ref.clear();
433 for attr in e.attributes().filter_map(|a| a.ok()) {
434 match attr.key.as_ref() {
435 "r" => cell_ref = attr.value.to_string(),
436 "t" => {
437 cell_type = match attr.value.as_ref() {
439 "s" => 's', "n" => 'n', "b" => 'b', "e" => 'e', "str" | "inlineStr" => 'i', _ => 's',
445 };
446 }
447 "s" => cell_style = attr.value.parse().ok(),
448 _ => {}
449 }
450 }
451 }
452 crate::tags::V => {
453 in_v = true;
454 }
455 crate::tags::IS => {
456 in_is = true;
457 }
458 crate::tags::T if in_is => in_is_t = true,
459 _ => {}
460 }
461 }
462 Ok(Event::End(ref e)) => {
463 match e.name().local_name().as_ref() {
464 crate::tags::C => {
465 let text = match cell_type {
467 's' => {
468 let idx: usize = cell_value.trim().parse().unwrap_or(0);
469 sst.get(idx).unwrap_or("").to_string()
470 }
471 'i' => {
472 cell_value.trim().to_string()
474 }
475 'b' => {
476 if cell_value.trim() == "1" {
478 "True".to_string()
479 } else {
480 "False".to_string()
481 }
482 }
483 'e' => {
484 format!("ERROR:{}", cell_value.trim())
486 }
487 _ => {
489 let val = cell_value.trim();
490 if val.is_empty() {
491 String::new()
492 } else if styles.is_date_format(cell_style) {
493 if let Ok(num) = val.parse::<f64>() {
495 if let Some(code) = styles.code_for(cell_style) {
496 format_excel_date(num, code)
497 } else {
498 format!("num:{}", val)
499 }
500 } else {
501 format!("num:{}", val)
502 }
503 } else {
504 format_number(val)
506 }
507 }
508 };
509
510 let col = col_from_ref(&cell_ref);
511 current_row.push((col, text));
512 in_v = false;
513 in_is = false;
514 in_is_t = false;
515 }
516 crate::tags::ROW => {
517 current_row.sort_by_key(|(col, _)| *col);
519 let row_str: Vec<String> =
520 current_row.iter().map(|(_, t)| t.clone()).collect();
521 rows.push(row_str);
522 current_row.clear();
523 }
524 crate::tags::V => {
525 in_v = false;
526 }
527 crate::tags::IS => {
528 in_is = false;
529 }
530 crate::tags::T if in_is_t => in_is_t = false,
531 _ => {}
532 }
533 }
534 Ok(Event::Text(ref e)) => {
535 if in_v || in_is_t {
536 let raw = e.xml10_content();
537 if let Ok(t) = quick_xml::escape::unescape(&raw) {
538 cell_value.push_str(&t);
539 }
540 }
541 }
542 Ok(Event::GeneralRef(ref e)) => {
543 if in_v || in_is_t {
544 crate::push_general_ref(&mut cell_value, e);
545 }
546 }
547 Ok(Event::CData(ref e)) => {
548 if in_v || in_is_t {
549 cell_value.push_str(e.as_ref());
550 }
551 }
552 Ok(Event::Eof) => break,
553 Err(e) => return Err(DocError::corrupt(format!("sheet parse error: {e}"))),
554 _ => {}
555 }
556 buf.clear();
557 }
558 Ok(rows)
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn test_sst_empty() {
567 let sst = SharedStrings { items: vec![] };
568 assert_eq!(sst.get(0), None);
569 }
570
571 #[test]
572 fn test_sst_parse() {
573 let xml = r#"<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2">
574 <si><t>Hello</t></si>
575 <si><t>World</t></si>
576 </sst>"#;
577 let items = parse_sst_items(xml).unwrap();
578 assert_eq!(items.len(), 2);
579 assert_eq!(items[0], "Hello");
580 assert_eq!(items[1], "World");
581 }
582
583 #[test]
584 fn test_sheet_names() {
585 let xml = r#"<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
586 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
587 <sheets>
588 <sheet name="Sheet1" r:id="rId1"/>
589 <sheet name="Sheet2" r:id="rId2"/>
590 </sheets>
591 </workbook>"#;
592 let sheets = parse_sheet_names(xml).unwrap();
593 assert_eq!(sheets.len(), 2);
594 assert_eq!(sheets[0].1, "Sheet1");
595 }
596
597 #[test]
601 fn smoke_parse_minimal_xlsx_zip() {
602 use std::io::Write;
603
604 const WORKBOOK: &str = concat!(
605 r#"<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" "#,
606 r#"xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
607 r#"<sheets><sheet name="S1" sheetId="1" r:id="rId1"/></sheets></workbook>"#
608 );
609 const SST: &str = concat!(
610 r#"<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">"#,
611 r#"<si><t>甲</t></si><si><t>乙 & 丙</t></si></sst>"#
612 );
613 const SHEET: &str = concat!(
614 r#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">"#,
615 r#"<sheetData><row r="1">"#,
616 r#"<c r="A1" t="s"><v>0</v></c><c r="B1" t="s"><v>1</v></c>"#,
617 r#"</row></sheetData></worksheet>"#
618 );
619
620 let dir = std::env::temp_dir().join("docparser_office_xlsx_smoke");
621 std::fs::create_dir_all(&dir).unwrap();
622 let path = dir.join("min.xlsx");
623 {
624 let f = std::fs::File::create(&path).unwrap();
625 let mut zw = zip::ZipWriter::new(f);
626 let opts = zip::write::SimpleFileOptions::default();
627 for (name, body) in [
628 ("xl/workbook.xml", WORKBOOK),
629 ("xl/sharedStrings.xml", SST),
630 ("xl/worksheets/sheet1.xml", SHEET),
631 ] {
632 zw.start_file(name, opts).unwrap();
633 zw.write_all(body.as_bytes()).unwrap();
634 }
635 zw.finish().unwrap();
636 }
637
638 let raw = XlsxRawBackend.parse(path.to_str().unwrap()).unwrap();
639 std::fs::remove_file(&path).ok();
640
641 assert_eq!(raw.pages.len(), 1);
642 assert_eq!(raw.pages[0].name.as_deref(), Some("S1"));
643 assert_eq!(raw.pages[0].items.len(), 1);
644 assert_eq!(raw.pages[0].items[0].text, "甲,乙 & 丙");
645 }
646
647 #[test]
648 fn test_col_ref() {
649 fn col(r: &str) -> usize {
650 let letters: String = r.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
651 let mut col = 0usize;
652 for c in letters.chars() {
653 col = col * 26 + (c.to_ascii_uppercase() as u8 - b'A' + 1) as usize;
654 }
655 if col > 0 {
656 col - 1
657 } else {
658 0
659 }
660 }
661 assert_eq!(col("A1"), 0);
662 assert_eq!(col("Z1"), 25);
663 assert_eq!(col("AA1"), 26);
664 assert_eq!(col("AB1"), 27);
665 }
666}