1use std::borrow::Cow;
31use std::collections::HashSet;
32
33use super::cursor::{attr, element_text, skip_element, Cursor, Event};
34use super::{
35 emit_formula, NameInfo, Row, SheetInfo, SheetStats, Sink, Source, Summary, TableInfo, Value,
36 Visibility,
37};
38use crate::intrinsics::fold;
39use crate::messages::{raise, Refusal};
40use crate::refers;
41use crate::sheet::merge::part_text_raw;
42use crate::sheet::zip::{self, Entry};
43use crate::sheet::{cell_ref, parse_a1_range, xlfn};
44
45const WORKBOOK: &str = "xl/workbook.xml";
48const WORKBOOK_RELS: &str = "xl/_rels/workbook.xml.rels";
49const SHARED_STRINGS: &str = "xl/sharedStrings.xml";
50
51struct Rel {
52 id: String,
53 kind: String,
54 target: String,
55}
56
57pub struct Package<'a> {
61 label: String,
62 bytes: &'a [u8],
63 entries: Vec<Entry>,
64 sheets: Vec<SheetInfo>,
65 names: Vec<NameInfo>,
66 tables: Vec<TableInfo>,
67 external_books: Vec<String>,
68 strings: Vec<String>,
69 string_bytes: usize,
70}
71
72impl std::fmt::Debug for Package<'_> {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("Package")
75 .field("label", &self.label)
76 .field("bytes", &self.bytes.len())
77 .field("sheets", &self.sheets)
78 .field("names", &self.names)
79 .field("tables", &self.tables)
80 .field("external_books", &self.external_books)
81 .field("strings", &self.strings.len())
82 .finish()
83 }
84}
85
86pub(crate) fn not_a_workbook(label: &str, why: &str) -> Refusal {
87 raise("reflect-not-a-workbook", &[("path", label), ("why", why)])
88}
89
90pub(crate) fn xml_refused(label: &str, part: &str, why: &str) -> Refusal {
91 raise(
92 "reflect-xml-refused",
93 &[("path", label), ("part", part), ("why", why)],
94 )
95}
96
97fn unsupported(label: &str, part: &str, why: &str) -> Refusal {
98 raise(
99 "reflect-unsupported",
100 &[("path", label), ("part", part), ("why", why)],
101 )
102}
103
104pub(crate) fn step<'t>(
106 cursor: &mut Cursor<'t>,
107 label: &str,
108 part: &str,
109) -> Result<Option<Event<'t>>, Refusal> {
110 cursor
111 .next_event()
112 .map_err(|why| xml_refused(label, part, why))
113}
114
115fn inner_text(
117 cursor: &mut Cursor,
118 skip: &[&str],
119 excel_escapes: bool,
120 label: &str,
121 part: &str,
122) -> Result<String, Refusal> {
123 element_text(cursor, skip, excel_escapes).map_err(|why| xml_refused(label, part, why))
124}
125
126fn resolve(base_dir: &str, target: &str) -> String {
131 let joined = match target.strip_prefix('/') {
132 Some(absolute) => absolute.to_string(),
133 None => format!("{base_dir}{target}"),
134 };
135 let mut parts: Vec<&str> = Vec::new();
136 for segment in joined.split('/') {
137 match segment {
138 "" | "." => {}
139 ".." => {
140 parts.pop();
141 }
142 s => parts.push(s),
143 }
144 }
145 parts.join("/")
146}
147
148fn dir_of(part: &str) -> &str {
151 match part.rfind('/') {
152 Some(i) => &part[..=i],
153 None => "",
154 }
155}
156
157fn rels_of(part: &str) -> String {
159 let dir = dir_of(part);
160 format!("{dir}_rels/{}.rels", &part[dir.len()..])
161}
162
163fn file_name_of(target: &str) -> String {
166 let last = target.rsplit(['/', '\\']).next().unwrap_or(target);
167 let bytes = last.as_bytes();
168 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
169 let mut i = 0;
170 while i < bytes.len() {
171 if bytes[i] == b'%' && i + 2 < bytes.len() {
172 if let Ok(v) = u8::from_str_radix(&last[i + 1..i + 3], 16) {
173 out.push(v);
174 i += 3;
175 continue;
176 }
177 }
178 out.push(bytes[i]);
179 i += 1;
180 }
181 String::from_utf8(out).unwrap_or_else(|_| last.to_string())
182}
183
184fn read_rels(text: &str, label: &str, part: &str) -> Result<Vec<Rel>, Refusal> {
185 let mut cursor = Cursor::new(text);
186 let mut out = Vec::new();
187 while let Some(event) = step(&mut cursor, label, part)? {
188 if let Event::Start {
189 name: "Relationship",
190 attrs,
191 ..
192 } = event
193 {
194 out.push(Rel {
195 id: attr(attrs, "Id").unwrap_or_default().into_owned(),
196 kind: attr(attrs, "Type").unwrap_or_default().into_owned(),
197 target: attr(attrs, "Target").unwrap_or_default().into_owned(),
198 });
199 }
200 }
201 Ok(out)
202}
203
204fn read_strings(text: &str, label: &str, part: &str) -> Result<Vec<String>, Refusal> {
205 let mut cursor = Cursor::new(text);
206 let mut out = Vec::new();
207 while let Some(event) = step(&mut cursor, label, part)? {
208 if let Event::Start {
209 name: "si", empty, ..
210 } = event
211 {
212 if empty {
213 out.push(String::new());
214 } else {
215 out.push(inner_text(&mut cursor, &["rPh"], true, label, part)?);
216 }
217 }
218 }
219 Ok(out)
220}
221
222fn read_table(
223 text: &str,
224 label: &str,
225 part: &str,
226 sheet: usize,
227) -> Result<Option<TableInfo>, Refusal> {
228 let mut cursor = Cursor::new(text);
229 let mut info: Option<TableInfo> = None;
230 while let Some(event) = step(&mut cursor, label, part)? {
231 match event {
232 Event::Start {
233 name: "table",
234 attrs,
235 ..
236 } => {
237 let name = attr(attrs, "displayName")
238 .or_else(|| attr(attrs, "name"))
239 .unwrap_or_default()
240 .into_owned();
241 info = Some(TableInfo {
242 name,
243 sheet,
244 range: attr(attrs, "ref").unwrap_or_default().into_owned(),
245 columns: Vec::new(),
246 });
247 }
248 Event::Start {
249 name: "tableColumn",
250 attrs,
251 ..
252 } => {
253 if let Some(t) = info.as_mut() {
254 t.columns
255 .push(attr(attrs, "name").unwrap_or_default().into_owned());
256 }
257 }
258 _ => {}
259 }
260 }
261 Ok(info)
262}
263
264impl<'a> Package<'a> {
265 pub fn open(bytes: &'a [u8], label: &str) -> Result<Package<'a>, Refusal> {
268 let entries = zip::entries(bytes).ok_or_else(|| {
269 not_a_workbook(
270 label,
271 zip::why_not_an_archive(bytes)
272 .unwrap_or("it is not a zip archive this reader knows"),
273 )
274 })?;
275 let has = |name: &str| entries.iter().any(|e| e.name == name);
276 if !has(WORKBOOK) {
277 return Err(not_a_workbook(
278 label,
279 "it has no xl/workbook.xml part, so it is not a spreadsheet package",
280 ));
281 }
282 let text = |name: &str| -> Result<Option<String>, Refusal> {
283 part_text_raw(bytes, &entries, name).map_err(|why| not_a_workbook(label, &why))
284 };
285 let workbook = text(WORKBOOK)?.unwrap_or_default();
286 let rels_text = text(WORKBOOK_RELS)?
287 .ok_or_else(|| not_a_workbook(label, "it has no xl/_rels/workbook.xml.rels part"))?;
288 let rels = read_rels(&rels_text, label, WORKBOOK_RELS)?;
289 let target_of = |rid: &str| {
290 rels.iter()
291 .find(|r| r.id == rid)
292 .map(|r| resolve("xl/", &r.target))
293 };
294
295 let mut sheets: Vec<SheetInfo> = Vec::new();
297 let mut names: Vec<NameInfo> = Vec::new();
298 let mut external_rids: Vec<String> = Vec::new();
299 let mut cursor = Cursor::new(&workbook);
300 while let Some(event) = step(&mut cursor, label, WORKBOOK)? {
301 match event {
302 Event::Start {
303 name: "sheet",
304 attrs,
305 ..
306 } => {
307 let sheet_name = attr(attrs, "name").unwrap_or_default().into_owned();
308 let visibility = match attr(attrs, "state").as_deref() {
309 Some("hidden") => Visibility::Hidden,
310 Some("veryHidden") => Visibility::VeryHidden,
311 _ => Visibility::Visible,
312 };
313 let rid = attr(attrs, "id").unwrap_or_default();
314 let part = target_of(&rid).ok_or_else(|| {
315 unsupported(
316 label,
317 WORKBOOK,
318 &format!(
319 "sheet {sheet_name} names a relationship the workbook does not have"
320 ),
321 )
322 })?;
323 sheets.push(SheetInfo {
324 name: sheet_name,
325 visibility,
326 part,
327 });
328 }
329 Event::Start {
330 name: "definedName",
331 attrs,
332 empty,
333 } => {
334 let name = attr(attrs, "name").unwrap_or_default().into_owned();
335 let sheet = attr(attrs, "localSheetId").and_then(|v| v.parse::<usize>().ok());
336 let hidden =
337 matches!(attr(attrs, "hidden").as_deref(), Some("1") | Some("true"));
338 let refers_to = if empty {
339 String::new()
340 } else {
341 inner_text(&mut cursor, &[], false, label, WORKBOOK)?
342 };
343 let placeholder = fold(&name).starts_with("_xlfn.");
344 names.push(NameInfo {
345 name,
346 refers_to,
347 sheet,
348 hidden,
349 placeholder,
350 });
351 }
352 Event::Start {
353 name: "externalReference",
354 attrs,
355 ..
356 } => {
357 if let Some(rid) = attr(attrs, "id") {
358 external_rids.push(rid.into_owned());
359 }
360 }
361 _ => {}
362 }
363 }
364 if sheets.is_empty() {
365 return Err(unsupported(label, WORKBOOK, "it lists no sheet"));
366 }
367
368 let strings_part = rels
370 .iter()
371 .find(|r| r.kind.ends_with("/sharedStrings"))
372 .map(|r| resolve("xl/", &r.target))
373 .unwrap_or_else(|| SHARED_STRINGS.to_string());
374 let (strings, string_bytes) = match text(&strings_part)? {
375 None => (Vec::new(), 0),
376 Some(t) => (read_strings(&t, label, &strings_part)?, t.len()),
377 };
378
379 let mut tables: Vec<TableInfo> = Vec::new();
381 for (i, sheet) in sheets.iter().enumerate() {
382 let rels_part = rels_of(&sheet.part);
383 let Some(rels_text) = text(&rels_part)? else {
384 continue;
385 };
386 for rel in read_rels(&rels_text, label, &rels_part)? {
387 if !rel.kind.ends_with("/table") {
388 continue;
389 }
390 let table_part = resolve(dir_of(&sheet.part), &rel.target);
391 let Some(table_text) = text(&table_part)? else {
392 continue;
393 };
394 if let Some(table) = read_table(&table_text, label, &table_part, i)? {
395 tables.push(table);
396 }
397 }
398 }
399
400 let mut external_books: Vec<String> = Vec::new();
402 for rid in external_rids {
403 let Some(link_part) = target_of(&rid) else {
404 continue;
405 };
406 let link_rels = rels_of(&link_part);
407 let name = match text(&link_rels)? {
408 Some(rels_text) => read_rels(&rels_text, label, &link_rels)?
409 .into_iter()
410 .find(|r| r.kind.ends_with("/externalLinkPath"))
411 .map(|r| file_name_of(&r.target))
412 .unwrap_or_default(),
413 None => String::new(),
414 };
415 external_books.push(name);
416 }
417
418 Ok(Package {
419 label: label.to_string(),
420 bytes,
421 entries,
422 sheets,
423 names,
424 tables,
425 external_books,
426 strings,
427 string_bytes,
428 })
429 }
430}
431
432impl<'a> Source for Package<'a> {
433 fn label(&self) -> &str {
434 &self.label
435 }
436
437 fn sheets(&self) -> &[SheetInfo] {
439 &self.sheets
440 }
441
442 fn names(&self) -> &[NameInfo] {
444 &self.names
445 }
446
447 fn tables(&self) -> &[TableInfo] {
449 &self.tables
450 }
451
452 fn external_books(&self) -> &[String] {
455 &self.external_books
456 }
457
458 fn summary(&self) -> Summary {
459 let placeholders = self.names.iter().filter(|n| n.placeholder).count();
460 Summary {
461 names: self.names.len() - placeholders,
462 placeholders,
463 tables: self.tables.len(),
464 external_books: self.external_books.len(),
465 strings: self.strings.len(),
466 string_bytes: self.string_bytes,
467 }
468 }
469
470 fn walk_sheet_with(
476 &self,
477 index: usize,
478 sink: &mut dyn Sink,
479 book_distinct: &mut HashSet<String>,
480 ) -> Result<SheetStats, Refusal> {
481 let info = &self.sheets[index];
482 let mut sheet_distinct: HashSet<String> = HashSet::new();
483 let part = info.part.as_str();
484 let label = self.label.as_str();
485 let text = part_text_raw(self.bytes, &self.entries, part)
486 .map_err(|why| not_a_workbook(label, &why))?
487 .ok_or_else(|| {
488 unsupported(label, part, "the sheet's part is missing from the package")
489 })?;
490 let mut stats = SheetStats {
491 part_bytes: text.len(),
492 ..SheetStats::default()
493 };
494 let mut cursor = Cursor::new(&text);
495 let mut shared: Vec<Option<(String, u32, u32)>> = Vec::new();
497 let mut in_data = false;
498 let mut row_no: u32 = 0;
499 let mut col_no: u32 = 0;
500 while let Some(event) = step(&mut cursor, label, part)? {
501 match event {
502 Event::Start {
503 name: "sheetData",
504 empty,
505 ..
506 } => {
507 if empty {
508 break;
509 }
510 in_data = true;
511 }
512 Event::End { name: "sheetData" } => break,
513 Event::Start {
514 name: "row", attrs, ..
515 } if in_data => {
516 row_no = attr(attrs, "r")
517 .and_then(|v| v.parse::<u32>().ok())
518 .unwrap_or(row_no + 1);
519 col_no = 0;
520 }
521 Event::Start {
522 name: "c",
523 attrs,
524 empty,
525 } if in_data => {
526 let (row, col) = match attr(attrs, "r").and_then(|r| parse_a1_range(&r)) {
527 Some(a) => (a.top, a.left),
528 None => (row_no, col_no + 1),
529 };
530 row_no = row;
531 col_no = col;
532 let kind: Option<Cow<str>> = attr(attrs, "t");
533 let mut formula_part: Option<(Option<String>, Option<String>, String)> = None;
534 let mut value_text: Option<String> = None;
535 let mut inline: Option<String> = None;
536 if !empty {
537 loop {
538 match step(&mut cursor, label, part)? {
539 None => {
540 return Err(xml_refused(label, part, "a cell never closes"))
541 }
542 Some(Event::Start {
543 name: "f",
544 attrs: f_attrs,
545 empty: f_empty,
546 }) => {
547 let t = attr(f_attrs, "t").map(Cow::into_owned);
548 let si = attr(f_attrs, "si").map(Cow::into_owned);
549 let f_text = if f_empty {
550 String::new()
551 } else {
552 inner_text(&mut cursor, &[], false, label, part)?
553 };
554 formula_part = Some((t, si, f_text));
555 }
556 Some(Event::Start {
557 name: "v",
558 empty: v_empty,
559 ..
560 }) => {
561 value_text = Some(if v_empty {
562 String::new()
563 } else {
564 inner_text(&mut cursor, &[], false, label, part)?
565 });
566 }
567 Some(Event::Start {
568 name: "is",
569 empty: is_empty,
570 ..
571 }) => {
572 inline = Some(if is_empty {
573 String::new()
574 } else {
575 inner_text(&mut cursor, &["rPh"], true, label, part)?
576 });
577 }
578 Some(Event::Start { empty: false, .. }) => {
579 skip_element(&mut cursor)
580 .map_err(|why| xml_refused(label, part, why))?;
581 }
582 Some(Event::End { name: "c" }) => break,
583 _ => {}
584 }
585 }
586 }
587 let addr = cell_ref(row, col);
588 let formula = match formula_part {
589 None => None,
590 Some((t, si, f_text)) => {
591 match t.as_deref() {
592 Some("shared") => {
593 let i: usize = si
594 .as_deref()
595 .and_then(|s| s.trim().parse().ok())
596 .ok_or_else(|| {
597 unsupported(
598 label,
599 part,
600 &format!("cell {addr} holds a shared formula with no index"),
601 )
602 })?;
603 if f_text.is_empty() {
604 let (master, m_row, m_col) =
605 shared.get(i).and_then(|m| m.as_ref()).ok_or_else(|| {
606 unsupported(
607 label,
608 part,
609 &format!("cell {addr} continues a shared formula whose first cell has not been read"),
610 )
611 })?;
612 Some(refers::shift_a1_references(
613 master,
614 i64::from(row) - i64::from(*m_row),
615 i64::from(col) - i64::from(*m_col),
616 ))
617 } else {
618 if shared.len() <= i {
619 shared.resize(i + 1, None);
620 }
621 shared[i] = Some((f_text.clone(), row, col));
622 Some(f_text)
623 }
624 }
625 Some("array") => {
626 if f_text.is_empty() {
627 None
628 } else {
629 stats.array_anchors += 1;
630 Some(f_text)
631 }
632 }
633 Some("dataTable") => {
634 return Err(unsupported(
635 label,
636 part,
637 &format!("cell {addr} holds a data-table formula"),
638 ))
639 }
640 _ => {
641 if f_text.is_empty() {
642 None
643 } else {
644 Some(f_text)
645 }
646 }
647 }
648 }
649 };
650 let value_text = value_text.filter(|s| !s.is_empty());
651 let value = match kind.as_deref() {
652 Some("s") => match value_text {
653 Some(s) => {
654 let i: usize = s.trim().parse().map_err(|_| {
655 unsupported(
656 label,
657 part,
658 &format!("cell {addr} names a shared string by {s}, which is not an index"),
659 )
660 })?;
661 let text = self.strings.get(i).cloned().ok_or_else(|| {
662 unsupported(
663 label,
664 part,
665 &format!("cell {addr} names shared string {i}, past the end of the table"),
666 )
667 })?;
668 Some(Value::Text(text))
669 }
670 None => None,
671 },
672 Some("str") => value_text.map(Value::Text),
673 Some("b") => value_text.map(|s| {
674 let s = s.trim();
675 Value::Bool(s == "1" || s.eq_ignore_ascii_case("true"))
676 }),
677 Some("e") => value_text.map(|s| Value::Error(s.trim().to_string())),
678 Some("d") => value_text.map(|s| Value::Date(s.trim().to_string())),
679 Some("inlineStr") => inline.map(Value::Text),
680 _ => value_text.map(|s| Value::Number(s.trim().to_string())),
681 };
682 if value.is_none() && formula.is_none() {
683 continue;
684 }
685 stats.cells += 1;
686 stats.rows = stats.rows.max(row);
687 stats.columns = stats.columns.max(col);
688 if let Some(value) = &value {
689 sink.row(&Row::Cell {
690 sheet: &info.name,
691 addr: &addr,
692 value,
693 });
694 }
695 if let Some(f_text) = formula {
696 let text = format!("={}", xlfn::strip_future_prefixes(&f_text));
697 let text = refers::resolve_books(&text, &self.external_books);
698 emit_formula(
699 sink,
700 &info.name,
701 &addr,
702 &text,
703 row,
704 col,
705 &mut stats,
706 &mut sheet_distinct,
707 book_distinct,
708 );
709 }
710 }
711 _ => {}
712 }
713 }
714 stats.distinct_r1c1 = sheet_distinct.len() as u64;
715 Ok(stats)
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722 use crate::reflect::reflect_text;
723
724 const NS: &str = "xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\"";
725 const REL_SHEET: &str =
726 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";
727 const REL_SST: &str =
728 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
729 const REL_TABLE: &str =
730 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table";
731
732 fn one_sheet(sheet_data: &str, sst: Option<&str>) -> Vec<u8> {
734 let mut parts: Vec<(String, Vec<u8>)> = vec![
735 ("xl/workbook.xml".to_string(), format!("<workbook {NS}><sheets><sheet name=\"Model\" sheetId=\"1\" r:id=\"rId1\"/></sheets></workbook>").into_bytes()),
736 ("xl/_rels/workbook.xml.rels".to_string(), format!("<Relationships><Relationship Id=\"rId1\" Type=\"{REL_SHEET}\" Target=\"worksheets/sheet1.xml\"/><Relationship Id=\"rId2\" Type=\"{REL_SST}\" Target=\"sharedStrings.xml\"/></Relationships>").into_bytes()),
737 ("xl/worksheets/sheet1.xml".to_string(), format!("<worksheet {NS}><dimension ref=\"A1\"/><sheetData>{sheet_data}</sheetData><pageMargins left=\"0.7\"/></worksheet>").into_bytes()),
738 ];
739 if let Some(s) = sst {
740 parts.push((
741 "xl/sharedStrings.xml".to_string(),
742 format!("<sst {NS}>{s}</sst>").into_bytes(),
743 ));
744 }
745 zip::write_stored(&parts).unwrap()
746 }
747
748 #[test]
749 fn each_kind_of_cell_reads() {
750 let sst = "<si><t>Revenue</t></si><si><r><t>Rate </t></r><r><rPr><b/></rPr><t>applies</t></r><rPh sb=\"0\" eb=\"1\"><t>x</t></rPh></si>";
751 let data = concat!(
752 "<row r=\"1\"><c r=\"A1\" t=\"s\"><v>0</v></c><c r=\"B1\"><v>1200</v></c><c r=\"C1\" s=\"3\"/><c r=\"D1\" t=\"inlineStr\"><is><t>in & line</t></is></c></row>",
753 "<row r=\"2\"><c r=\"A2\" t=\"s\"><v>1</v></c><c r=\"B2\" t=\"b\"><v>1</v></c><c r=\"C2\" t=\"e\"><f>1/0</f><v>#DIV/0!</v></c><c r=\"D2\" t=\"d\"><v>2026-10-03</v></c></row>",
754 "<row r=\"3\"><c r=\"A3\" t=\"str\"><f>A1&\"!\"</f><v>Revenue!</v></c><c r=\"B3\"><f>B1*2</f><v>2400</v></c><c r=\"C3\"><f t=\"shared\" ref=\"C3:C4\" si=\"0\">B3+1</f><v>2401</v></c></row>",
755 "<row r=\"4\"><c r=\"C4\"><f t=\"shared\" si=\"0\"/><v>1</v></c><c r=\"D4\" cm=\"1\"><f t=\"array\" ref=\"D4:D5\">_xlfn.SEQUENCE(2)</f><v>1</v></c></row>",
756 "<row r=\"5\"><c r=\"D5\"><v>2</v></c><c><v>7</v></c><c t=\"s\"><v>0</v></c></row>",
757 "<row><c r=\"A6\"><f></f><v>0.30000000000000004</v></c><c r=\"B6\"><v></v></c></row>",
758 );
759 let bytes = one_sheet(data, Some(sst));
760 let text = reflect_text(&bytes, "t.xlsx").unwrap();
761 assert_eq!(
762 text,
763 concat!(
764 "(sheet \"Model\" visible)\n",
765 "(cell \"Model\" \"A1\" \"Revenue\")\n",
766 "(cell \"Model\" \"B1\" 1200)\n",
767 "(cell \"Model\" \"D1\" \"in & line\")\n",
768 "(cell \"Model\" \"A2\" \"Rate applies\")\n",
769 "(cell \"Model\" \"B2\" true)\n",
770 "(cell \"Model\" \"C2\" (error \"#DIV/0!\"))\n",
771 "(formula \"Model\" \"C2\" \"=1/0\")\n",
772 "(cell \"Model\" \"D2\" (date \"2026-10-03\"))\n",
773 "(cell \"Model\" \"A3\" \"Revenue!\")\n",
774 "(formula \"Model\" \"A3\" \"=A1&\\\"!\\\"\")\n",
775 "(refers \"Model!A3\" \"Model!A1\")\n",
776 "(cell \"Model\" \"B3\" 2400)\n",
777 "(formula \"Model\" \"B3\" \"=B1*2\")\n",
778 "(refers \"Model!B3\" \"Model!B1\")\n",
779 "(cell \"Model\" \"C3\" 2401)\n",
780 "(formula \"Model\" \"C3\" \"=B3+1\")\n",
781 "(refers \"Model!C3\" \"Model!B3\")\n",
782 "(cell \"Model\" \"C4\" 1)\n",
783 "(formula \"Model\" \"C4\" \"=B4+1\")\n",
784 "(refers \"Model!C4\" \"Model!B4\")\n",
785 "(cell \"Model\" \"D4\" 1)\n",
786 "(formula \"Model\" \"D4\" \"=SEQUENCE(2)\")\n",
787 "(cell \"Model\" \"D5\" 2)\n",
788 "(cell \"Model\" \"E5\" 7)\n",
789 "(cell \"Model\" \"F5\" \"Revenue\")\n",
790 "(cell \"Model\" \"A6\" 0.30000000000000004)\n",
791 )
792 );
793 let package = Package::open(&bytes, "t.xlsx").unwrap();
794 let stats = package.walk_sheet(0, &mut super::super::Discard).unwrap();
795 assert_eq!(stats.cells, 16);
796 assert_eq!(stats.formulas, 6);
797 assert_eq!(stats.array_anchors, 1);
798 assert_eq!((stats.distinct_r1c1, stats.unreadable), (5, 0));
800 assert_eq!((stats.rows, stats.columns), (6, 6));
801 assert!(stats.part_bytes > 500);
802 let summary = package.summary();
803 assert_eq!((summary.strings, summary.names, summary.tables), (2, 0, 0));
804 }
805
806 #[test]
807 fn the_header_rows_come_in_the_fixed_order() {
808 let parts: Vec<(String, Vec<u8>)> = vec![
809 ("xl/workbook.xml".to_string(), format!("<workbook {NS}><sheets><sheet name=\"Model\" sheetId=\"1\" r:id=\"rId1\"/><sheet name=\"Q1 Data\" sheetId=\"3\" state=\"hidden\" r:id=\"rId2\"/><sheet name=\"Secret\" sheetId=\"4\" state=\"veryHidden\" r:id=\"rId3\"/></sheets><externalReferences><externalReference r:id=\"rId9\"/></externalReferences><definedNames><definedName name=\"rate\">0.2</definedName><definedName name=\"_xlfn.SINGLE\" hidden=\"1\">#NAME?</definedName><definedName name=\"Local\" localSheetId=\"1\">'Q1 Data'!$B$2</definedName><definedName name=\"Broken\" hidden=\"1\">Model!#REF!</definedName><definedName name=\"Alpha\">Model!$A$1</definedName></definedNames></workbook>").into_bytes()),
810 ("xl/_rels/workbook.xml.rels".to_string(), format!("<Relationships><Relationship Id=\"rId1\" Type=\"{REL_SHEET}\" Target=\"worksheets/sheet1.xml\"/><Relationship Id=\"rId2\" Type=\"{REL_SHEET}\" Target=\"/xl/worksheets/sheet2.xml\"/><Relationship Id=\"rId3\" Type=\"{REL_SHEET}\" Target=\"worksheets/sheet3.xml\"/><Relationship Id=\"rId9\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink\" Target=\"externalLinks/externalLink1.xml\"/></Relationships>").into_bytes()),
811 ("xl/worksheets/sheet1.xml".to_string(), format!("<worksheet {NS}><sheetData><row r=\"1\"><c r=\"A1\"><v>1</v></c></row></sheetData><tableParts count=\"2\"><tablePart r:id=\"rId1\"/><tablePart r:id=\"rId2\"/></tableParts></worksheet>").into_bytes()),
812 ("xl/worksheets/_rels/sheet1.xml.rels".to_string(), format!("<Relationships><Relationship Id=\"rId1\" Type=\"{REL_TABLE}\" Target=\"../tables/table1.xml\"/><Relationship Id=\"rId2\" Type=\"{REL_TABLE}\" Target=\"../tables/table2.xml\"/></Relationships>").into_bytes()),
813 ("xl/tables/table1.xml".to_string(), format!("<table {NS} id=\"1\" name=\"Table1\" displayName=\"Sales\" ref=\"A1:B4\"><tableColumns count=\"2\"><tableColumn id=\"1\" name=\"Item\"/><tableColumn id=\"2\" name=\"Amount\"/></tableColumns></table>").into_bytes()),
814 ("xl/tables/table2.xml".to_string(), format!("<table {NS} id=\"2\" name=\"Costs\" displayName=\"Costs\" ref=\"D1:D9\"><tableColumns count=\"1\"><tableColumn id=\"1\" name=\"Cost\"/></tableColumns></table>").into_bytes()),
815 ("xl/worksheets/sheet2.xml".to_string(), format!("<worksheet {NS}><sheetData/></worksheet>").into_bytes()),
816 ("xl/worksheets/sheet3.xml".to_string(), format!("<worksheet {NS}><sheetData><row r=\"2\"><c r=\"B2\"><v>10</v></c></row></sheetData></worksheet>").into_bytes()),
817 ("xl/externalLinks/externalLink1.xml".to_string(), format!("<externalLink {NS}><externalBook r:id=\"rId1\"><sheetNames><sheetName val=\"Sheet1\"/></sheetNames></externalBook></externalLink>").into_bytes()),
818 ("xl/externalLinks/_rels/externalLink1.xml.rels".to_string(), "<Relationships><Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath\" Target=\"file:///C:/Models/Rates%20Q1.xlsx\" TargetMode=\"External\"/></Relationships>".to_string().into_bytes()),
819 ];
820 let bytes = zip::write_stored(&parts).unwrap();
821 let text = reflect_text(&bytes, "t.xlsx").unwrap();
822 assert_eq!(
823 text,
824 concat!(
825 "(sheet \"Model\" visible)\n",
826 "(sheet \"Q1 Data\" hidden)\n",
827 "(sheet \"Secret\" very-hidden)\n",
828 "(name \"'Q1 Data'!Local\" \"'Q1 Data'!$B$2\")\n",
829 "(name \"Alpha\" \"Model!$A$1\")\n",
830 "(name \"Broken\" \"Model!#REF!\")\n",
831 "(name \"rate\" \"0.2\")\n",
832 "(table \"Costs\" \"Model\" \"D1:D9\")\n",
833 "(table \"Sales\" \"Model\" \"A1:B4\")\n",
834 "(cell \"Model\" \"A1\" 1)\n",
835 "(cell \"Secret\" \"B2\" 10)\n",
836 )
837 );
838 let package = Package::open(&bytes, "t.xlsx").unwrap();
839 assert_eq!(package.external_books(), ["Rates Q1.xlsx"]);
840 assert_eq!(package.tables()[0].columns, ["Item", "Amount"]);
841 let summary = package.summary();
842 assert_eq!(
843 (
844 summary.names,
845 summary.placeholders,
846 summary.tables,
847 summary.external_books
848 ),
849 (4, 1, 2, 1)
850 );
851 assert!(package
852 .names()
853 .iter()
854 .any(|n| n.name == "Broken" && n.hidden));
855 }
856
857 #[test]
858 fn what_is_refused_is_named_through_the_catalogue() {
859 let r = Package::open(b"nope", "x.xlsx").unwrap_err();
860 assert_eq!(r.id, "reflect-not-a-workbook");
861 assert!(r.text.starts_with("x.xlsx is not a workbook frazaro reflect can read: it has no end-of-central-directory record"), "{}", r.text);
862 let ole = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1, 0, 0, 0, 0];
863 assert!(Package::open(&ole, "old.xls")
864 .unwrap_err()
865 .text
866 .contains("older .xls"));
867 let no_wb = zip::write_stored(&[("a.txt".to_string(), b"hi".to_vec())]).unwrap();
868 assert!(Package::open(&no_wb, "x.zip")
869 .unwrap_err()
870 .text
871 .contains("no xl/workbook.xml"));
872 let ods = zip::write_stored(&[
875 (
876 "mimetype".to_string(),
877 b"application/vnd.oasis.opendocument.spreadsheet".to_vec(),
878 ),
879 (
880 "content.xml".to_string(),
881 b"<office:document-content/>".to_vec(),
882 ),
883 ])
884 .unwrap();
885 assert!(Package::open(&ods, "x.ods")
886 .unwrap_err()
887 .text
888 .contains("no xl/workbook.xml"));
889 assert!(crate::reflect::odf::is_opendocument(&ods));
890 let doctype = one_sheet(
892 "<!DOCTYPE x><row r=\"1\"><c r=\"A1\"><v>1</v></c></row>",
893 None,
894 );
895 let r = reflect_text(&doctype, "d.xlsx").unwrap_err();
896 assert_eq!(r.id, "reflect-xml-refused");
897 assert!(
898 r.text
899 .contains("xl/worksheets/sheet1.xml, it declares a DOCTYPE"),
900 "{}",
901 r.text
902 );
903 let table = one_sheet("<row r=\"1\"><c r=\"B2\"><f t=\"dataTable\" ref=\"B2:B3\" dt2D=\"0\" r1=\"A1\"/><v>1</v></c></row>", None);
905 let r = reflect_text(&table, "d.xlsx").unwrap_err();
906 assert_eq!(r.id, "reflect-unsupported");
907 assert!(
908 r.text.contains("cell B2 holds a data-table formula"),
909 "{}",
910 r.text
911 );
912 let orphan = one_sheet(
913 "<row r=\"1\"><c r=\"A1\"><f t=\"shared\" si=\"4\"/><v>1</v></c></row>",
914 None,
915 );
916 assert!(reflect_text(&orphan, "d.xlsx")
917 .unwrap_err()
918 .text
919 .contains("cell A1 continues a shared formula"));
920 let past = one_sheet(
921 "<row r=\"1\"><c r=\"A1\" t=\"s\"><v>3</v></c></row>",
922 Some("<si><t>a</t></si>"),
923 );
924 assert!(reflect_text(&past, "d.xlsx")
925 .unwrap_err()
926 .text
927 .contains("shared string 3, past the end"));
928 let mut damaged = one_sheet("<row r=\"1\"><c r=\"A1\"><v>1</v></c></row>", None);
930 let e = zip::entries(&damaged)
931 .unwrap()
932 .into_iter()
933 .find(|e| e.name == "xl/worksheets/sheet1.xml")
934 .unwrap();
935 damaged[e.data_offset + 10] ^= 1;
936 let r = reflect_text(&damaged, "d.xlsx").unwrap_err();
937 assert_eq!(r.id, "reflect-not-a-workbook");
938 assert!(r.text.contains("checksum does not match"), "{}", r.text);
939 }
940
941 #[test]
947 fn the_reflect_goldens_are_reproduced() {
948 let rows: [(&str, &[u8], &str); 8] = [
949 (
953 "scripts/reflect/opendocument_saved.ods",
954 include_bytes!("../../../scripts/reflect/opendocument_saved.ods"),
955 include_str!("../../../scripts/reflect/opendocument_saved_relations.vla"),
956 ),
957 (
959 "scripts/reflect/changed.xlsx",
960 include_bytes!("../../../scripts/reflect/changed.xlsx"),
961 include_str!("../../../scripts/reflect/changed_relations.vla"),
962 ),
963 (
964 "scripts/reflect/opendocument.ods",
965 include_bytes!("../../../scripts/reflect/opendocument.ods"),
966 include_str!("../../../scripts/reflect/opendocument_relations.vla"),
967 ),
968 (
972 "scripts/reflect/saved.xlsx",
973 include_bytes!("../../../scripts/reflect/saved.xlsx"),
974 include_str!("../../../scripts/reflect/saved_relations.vla"),
975 ),
976 (
977 "scripts/reflect/fixture.xlsx",
978 include_bytes!("../../../scripts/reflect/fixture.xlsx"),
979 include_str!("../../../scripts/reflect/fixture_relations.vla"),
980 ),
981 (
982 "scripts/build/fixture_golden.xlsx",
983 include_bytes!("../../../scripts/build/fixture_golden.xlsx"),
984 include_str!("../../../scripts/reflect/build_fixture_relations.vla"),
985 ),
986 (
987 "scripts/build/into_golden.xlsx",
988 include_bytes!("../../../scripts/build/into_golden.xlsx"),
989 include_str!("../../../scripts/reflect/build_into_relations.vla"),
990 ),
991 (
992 "scripts/build/model.xlsx",
993 include_bytes!("../../../scripts/build/model.xlsx"),
994 include_str!("../../../scripts/reflect/model_relations.vla"),
995 ),
996 ];
997 for (label, bytes, golden) in rows {
998 let got = reflect_text(bytes, label)
999 .unwrap_or_else(|r| panic!("{label} did not read: {}", r.text));
1000 let want = golden.replace("\r\n", "\n");
1001 if got != want {
1002 let line = got
1003 .lines()
1004 .zip(want.lines())
1005 .position(|(a, b)| a != b)
1006 .map(|i| i + 1)
1007 .unwrap_or(got.lines().count().min(want.lines().count()) + 1);
1008 panic!("{label}: the relations differ from the golden at line {line}");
1009 }
1010 }
1011 }
1012
1013 #[test]
1014 fn paths_resolve_as_the_package_names_them() {
1015 assert_eq!(
1016 resolve("xl/", "worksheets/sheet1.xml"),
1017 "xl/worksheets/sheet1.xml"
1018 );
1019 assert_eq!(
1020 resolve("xl/", "/xl/worksheets/sheet1.xml"),
1021 "xl/worksheets/sheet1.xml"
1022 );
1023 assert_eq!(
1024 resolve("xl/worksheets/", "../tables/table1.xml"),
1025 "xl/tables/table1.xml"
1026 );
1027 assert_eq!(
1028 resolve("xl/worksheets/", "./a/../b.xml"),
1029 "xl/worksheets/b.xml"
1030 );
1031 assert_eq!(rels_of("xl/workbook.xml"), "xl/_rels/workbook.xml.rels");
1032 assert_eq!(
1033 rels_of("xl/worksheets/sheet1.xml"),
1034 "xl/worksheets/_rels/sheet1.xml.rels"
1035 );
1036 assert_eq!(
1037 file_name_of("file:///C:/Models/Rates%20Q1.xlsx"),
1038 "Rates Q1.xlsx"
1039 );
1040 assert_eq!(file_name_of("Rates.xlsx"), "Rates.xlsx");
1041 assert_eq!(file_name_of("..\\Shared\\Rates.xlsx"), "Rates.xlsx");
1042 assert_eq!(file_name_of("odd%2"), "odd%2");
1043 }
1044}