1use std::collections::HashMap;
20use std::io::{Cursor, Read};
21
22use quick_xml::NsReader;
23use quick_xml::XmlVersion;
24use quick_xml::encoding::Decoder;
25use quick_xml::events::{BytesStart, Event};
26use quick_xml::name::ResolveResult;
27
28use super::{MAX_PART_BYTES, MAX_SHEET_CELLS, MAX_SHEET_COLUMNS, MAX_SHEET_ROWS};
29
30const SPREADSHEET_NAMESPACES: [&[u8]; 2] = [
31 b"http://schemas.openxmlformats.org/spreadsheetml/2006/main",
32 b"http://purl.oclc.org/ooxml/spreadsheetml/main",
33];
34
35const PACKAGE_RELS_NAMESPACE: &[u8] =
36 b"http://schemas.openxmlformats.org/package/2006/relationships";
37
38const DOC_RELS_NAMESPACES: [&[u8]; 2] = [
39 b"http://schemas.openxmlformats.org/officeDocument/2006/relationships",
40 b"http://purl.oclc.org/ooxml/officeDocument/relationships",
41];
42
43const WORKSHEET_REL_TYPES: [&str; 2] = [
44 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
45 "http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet",
46];
47
48const TABLE_NAMESPACE: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:table:1.0";
49
50const OFFICE_NAMESPACE: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:office:1.0";
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum VisibilityError {
55 Limit(String),
57 Unresolved(String),
60}
61
62impl std::fmt::Display for VisibilityError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 VisibilityError::Limit(detail) => write!(f, "{detail}"),
66 VisibilityError::Unresolved(detail) => write!(f, "{detail}"),
67 }
68 }
69}
70
71type VisResult<T> = std::result::Result<T, VisibilityError>;
72
73fn unresolved(detail: impl Into<String>) -> VisibilityError {
74 VisibilityError::Unresolved(detail.into())
75}
76
77fn limit(detail: impl Into<String>) -> VisibilityError {
78 VisibilityError::Limit(detail.into())
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct IndexRange {
84 pub first: u32,
86 pub last: u32,
88}
89
90#[derive(Debug, Clone, Default, PartialEq)]
92pub struct SheetVisibility {
93 hidden_rows: Vec<IndexRange>,
94 hidden_columns: Vec<IndexRange>,
95}
96
97impl SheetVisibility {
98 pub fn row_hidden(&self, row: u32) -> bool {
100 self.hidden_rows
101 .iter()
102 .any(|r| r.first <= row && row <= r.last)
103 }
104
105 pub fn column_hidden(&self, column: u32) -> bool {
107 self.hidden_columns
108 .iter()
109 .any(|r| r.first <= column && column <= r.last)
110 }
111}
112
113#[derive(Debug, Clone, Default)]
115pub struct WorkbookVisibility {
116 by_sheet: HashMap<String, SheetVisibility>,
117}
118
119impl WorkbookVisibility {
120 pub fn sheet(&self, name: &str) -> Option<SheetVisibility> {
124 self.by_sheet.get(name).cloned()
125 }
126}
127
128pub fn read_visibility(bytes: &[u8], format_id: &str) -> VisResult<WorkbookVisibility> {
135 match format_id {
136 "xlsx" | "xlsm" => read_ooxml(bytes),
137 "ods" => read_ods(bytes),
138 "xls" => read_biff(bytes),
139 other => Err(unresolved(format!(
140 "no visibility reader for format {other}"
141 ))),
142 }
143}
144
145fn zip_entry(archive: &mut zip::ZipArchive<Cursor<&[u8]>>, name: &str) -> VisResult<String> {
146 let entry = archive
147 .by_name(name)
148 .map_err(|e| unresolved(format!("archive part {name} unreadable: {e}")))?;
149 let mut text = String::new();
150 entry
151 .take(MAX_PART_BYTES + 1)
152 .read_to_string(&mut text)
153 .map_err(|e| unresolved(format!("archive part {name} unreadable: {e}")))?;
154 if text.len() as u64 > MAX_PART_BYTES {
155 return Err(limit(format!(
156 "archive part {name} exceeds the {MAX_PART_BYTES} byte ceiling"
157 )));
158 }
159 Ok(text)
160}
161
162enum AttrSpace<'a> {
164 None,
166 OneOf(&'a [&'a [u8]]),
168}
169
170fn attribute(
171 reader: &NsReader<&[u8]>,
172 start: &BytesStart,
173 space: AttrSpace,
174 local: &[u8],
175 decoder: Decoder,
176) -> VisResult<Option<String>> {
177 for attr in start.attributes() {
178 let attr = attr.map_err(|e| unresolved(format!("bad attribute: {e}")))?;
179 let (resolution, name) = reader.resolver().resolve_attribute(attr.key);
180 if name.as_ref() != local {
181 continue;
182 }
183 let matches = match &space {
184 AttrSpace::None => matches!(resolution, ResolveResult::Unbound),
185 AttrSpace::OneOf(namespaces) => match resolution {
186 ResolveResult::Bound(bound) => namespaces.iter().any(|ns| *ns == bound.as_ref()),
187 _ => false,
188 },
189 };
190 if !matches {
191 continue;
192 }
193 let value = attr
194 .decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)
195 .map_err(|e| unresolved(format!("bad attribute value: {e}")))?;
196 return Ok(Some(value.into_owned()));
197 }
198 Ok(None)
199}
200
201fn element_in(
202 reader: &NsReader<&[u8]>,
203 start: &BytesStart,
204 namespaces: &[&[u8]],
205 local: &[u8],
206) -> bool {
207 let (resolution, name) = reader.resolver().resolve_element(start.name());
208 if name.as_ref() != local {
209 return false;
210 }
211 match resolution {
212 ResolveResult::Bound(bound) => namespaces.iter().any(|ns| *ns == bound.as_ref()),
213 _ => false,
214 }
215}
216
217fn is_truthy(value: &str) -> bool {
218 value == "1" || value.eq_ignore_ascii_case("true")
219}
220
221fn parse_index(value: &str, what: &str) -> VisResult<u32> {
222 value
223 .parse::<u32>()
224 .map_err(|_| unresolved(format!("bad {what} value {value:?}")))
225}
226
227fn parse_zero_size(value: &str, what: &str) -> VisResult<bool> {
230 let parsed = value
231 .parse::<f64>()
232 .map_err(|_| unresolved(format!("bad {what} value {value:?}")))?;
233 if !parsed.is_finite() {
234 return Err(unresolved(format!("bad {what} value {value:?}")));
235 }
236 Ok(parsed == 0.0)
237}
238
239fn parse_cell_reference(reference: &str) -> VisResult<(u32, u32)> {
241 let letters: String = reference
242 .chars()
243 .take_while(char::is_ascii_alphabetic)
244 .collect();
245 let digits = &reference[letters.len()..];
246 if letters.is_empty() || digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
247 return Err(unresolved(format!("bad cell reference {reference:?}")));
248 }
249 let mut column = 0u64;
250 for c in letters.chars() {
251 column = column * 26 + (c.to_ascii_uppercase() as u64 - 'A' as u64 + 1);
252 if column > u64::from(MAX_SHEET_COLUMNS) {
253 return Err(limit(format!(
254 "cell reference {reference:?} past the column ceiling"
255 )));
256 }
257 }
258 let row = digits
259 .parse::<u32>()
260 .map_err(|_| unresolved(format!("bad cell reference {reference:?}")))?;
261 if row == 0 || row > MAX_SHEET_ROWS {
262 return Err(limit(format!(
263 "cell reference {reference:?} past the row ceiling"
264 )));
265 }
266 Ok((row - 1, column as u32 - 1))
267}
268
269fn check_extent(rows: u32, columns: u32, sheet: &str) -> VisResult<()> {
270 if u64::from(rows) * u64::from(columns) > MAX_SHEET_CELLS {
271 return Err(limit(format!(
272 "sheet {sheet:?} extent {rows} rows by {columns} columns exceeds the {MAX_SHEET_CELLS} cell ceiling"
273 )));
274 }
275 Ok(())
276}
277
278fn normalize_part_target(target: &str) -> VisResult<String> {
281 let (mut stack, path): (Vec<&str>, &str) = match target.strip_prefix('/') {
282 Some(absolute) => (Vec::new(), absolute),
283 None => (vec!["xl"], target),
284 };
285 for segment in path.split('/') {
286 match segment {
287 "" | "." => {}
288 ".." => {
289 if stack.pop().is_none() {
290 return Err(unresolved(format!(
291 "part target {target:?} escapes the package"
292 )));
293 }
294 }
295 other => stack.push(other),
296 }
297 }
298 if stack.is_empty() {
299 return Err(unresolved(format!("empty part target {target:?}")));
300 }
301 Ok(stack.join("/"))
302}
303
304fn read_ooxml(bytes: &[u8]) -> VisResult<WorkbookVisibility> {
308 let mut archive = zip::ZipArchive::new(Cursor::new(bytes))
309 .map_err(|e| unresolved(format!("not a zip archive: {e}")))?;
310
311 let workbook = zip_entry(&mut archive, "xl/workbook.xml")?;
312 let mut sheets: Vec<(String, String)> = Vec::new();
313 let mut reader = NsReader::from_str(&workbook);
314 let decoder = reader.decoder();
315 loop {
316 match reader.read_event() {
317 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
318 if element_in(&reader, &e, &SPREADSHEET_NAMESPACES, b"sheet") {
319 let name = attribute(&reader, &e, AttrSpace::None, b"name", decoder)?
320 .ok_or_else(|| unresolved("sheet element without a name"))?;
321 let rid = attribute(
322 &reader,
323 &e,
324 AttrSpace::OneOf(&DOC_RELS_NAMESPACES),
325 b"id",
326 decoder,
327 )?
328 .ok_or_else(|| {
329 unresolved(format!("sheet {name:?} without a relationship id"))
330 })?;
331 sheets.push((name, rid));
332 }
333 }
334 Ok(Event::Eof) => break,
335 Ok(_) => {}
336 Err(e) => return Err(unresolved(format!("malformed xl/workbook.xml: {e}"))),
337 }
338 }
339
340 let rels = zip_entry(&mut archive, "xl/_rels/workbook.xml.rels")?;
341 let mut targets: HashMap<String, (String, String)> = HashMap::new();
342 let mut reader = NsReader::from_str(&rels);
343 let decoder = reader.decoder();
344 loop {
345 match reader.read_event() {
346 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
347 if element_in(&reader, &e, &[PACKAGE_RELS_NAMESPACE], b"Relationship") {
348 let id = attribute(&reader, &e, AttrSpace::None, b"Id", decoder)?;
349 let target = attribute(&reader, &e, AttrSpace::None, b"Target", decoder)?;
350 let rel_type = attribute(&reader, &e, AttrSpace::None, b"Type", decoder)?;
351 if let (Some(id), Some(target), Some(rel_type)) = (id, target, rel_type) {
352 targets.insert(id, (target, rel_type));
353 }
354 }
355 }
356 Ok(Event::Eof) => break,
357 Ok(_) => {}
358 Err(e) => return Err(unresolved(format!("malformed workbook rels: {e}"))),
359 }
360 }
361
362 let mut by_sheet = HashMap::new();
363 for (name, rid) in sheets {
364 let (target, rel_type) = targets
365 .get(&rid)
366 .ok_or_else(|| unresolved(format!("sheet {name:?} has no relationship target")))?;
367 if !WORKSHEET_REL_TYPES.contains(&rel_type.as_str()) {
368 continue;
372 }
373 let part = normalize_part_target(target)?;
374 let xml = zip_entry(&mut archive, &part)?;
375 by_sheet.insert(name.clone(), read_ooxml_worksheet(&xml, &part, &name)?);
376 }
377 Ok(WorkbookVisibility { by_sheet })
378}
379
380fn read_ooxml_worksheet(xml: &str, part: &str, sheet: &str) -> VisResult<SheetVisibility> {
381 let mut visibility = SheetVisibility::default();
382 let mut max_row = 0u32;
383 let mut max_column = 0u32;
384 let mut reader = NsReader::from_str(xml);
385 let decoder = reader.decoder();
386 loop {
387 match reader.read_event() {
388 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
389 if element_in(&reader, &e, &SPREADSHEET_NAMESPACES, b"col") {
390 let first = attribute(&reader, &e, AttrSpace::None, b"min", decoder)?
391 .ok_or_else(|| unresolved(format!("{part}: col without min")))?;
392 let last = attribute(&reader, &e, AttrSpace::None, b"max", decoder)?
393 .ok_or_else(|| unresolved(format!("{part}: col without max")))?;
394 let hidden = attribute(&reader, &e, AttrSpace::None, b"hidden", decoder)?
395 .is_some_and(|v| is_truthy(&v));
396 let zero_width = attribute(&reader, &e, AttrSpace::None, b"width", decoder)?
397 .map(|w| parse_zero_size(&w, "col width"))
398 .transpose()?
399 .unwrap_or(false);
400 let first = parse_index(&first, "col min")?;
401 let last = parse_index(&last, "col max")?;
402 if first == 0 || last < first || last > MAX_SHEET_COLUMNS {
403 return Err(unresolved(format!("{part}: bad col range {first}..{last}")));
404 }
405 if hidden || zero_width {
406 visibility.hidden_columns.push(IndexRange {
407 first: first - 1,
408 last: last - 1,
409 });
410 }
411 } else if element_in(&reader, &e, &SPREADSHEET_NAMESPACES, b"row") {
412 let index = attribute(&reader, &e, AttrSpace::None, b"r", decoder)?;
413 let hidden = attribute(&reader, &e, AttrSpace::None, b"hidden", decoder)?
414 .is_some_and(|v| is_truthy(&v));
415 let zero_height = attribute(&reader, &e, AttrSpace::None, b"ht", decoder)?
416 .map(|h| parse_zero_size(&h, "row height"))
417 .transpose()?
418 .unwrap_or(false);
419 if let Some(index) = index {
420 let index = parse_index(&index, "row index")?;
421 if index == 0 {
422 return Err(unresolved(format!("{part}: row index 0")));
423 }
424 if index > MAX_SHEET_ROWS {
425 return Err(limit(format!("{part}: row {index} past the row ceiling")));
426 }
427 max_row = max_row.max(index);
428 if hidden || zero_height {
429 visibility.hidden_rows.push(IndexRange {
430 first: index - 1,
431 last: index - 1,
432 });
433 }
434 } else if hidden || zero_height {
435 return Err(unresolved(format!("{part}: hidden row without an index")));
436 }
437 } else if element_in(&reader, &e, &SPREADSHEET_NAMESPACES, b"c")
438 && let Some(reference) = attribute(&reader, &e, AttrSpace::None, b"r", decoder)?
439 {
440 let (row, column) = parse_cell_reference(&reference)?;
441 max_row = max_row.max(row + 1);
442 max_column = max_column.max(column + 1);
443 }
444 }
445 Ok(Event::Eof) => break,
446 Ok(_) => {}
447 Err(e) => return Err(unresolved(format!("malformed {part}: {e}"))),
448 }
449 }
450 check_extent(max_row, max_column, sheet)?;
451 Ok(visibility)
452}
453
454fn read_ods(bytes: &[u8]) -> VisResult<WorkbookVisibility> {
459 let mut archive = zip::ZipArchive::new(Cursor::new(bytes))
460 .map_err(|e| unresolved(format!("not a zip archive: {e}")))?;
461 let content = zip_entry(&mut archive, "content.xml")?;
462
463 struct OpenTable {
464 name: String,
465 visibility: SheetVisibility,
466 next_row: u32,
467 next_column: u32,
468 max_content_row: u32,
472 max_content_column: u32,
473 row_cell_cursor: u32,
474 }
475
476 let mut by_sheet = HashMap::new();
477 let mut table_depth = 0u32;
478 let mut current: Option<OpenTable> = None;
479
480 let mut reader = NsReader::from_str(&content);
481 let decoder = reader.decoder();
482
483 fn repeat_of(
484 reader: &NsReader<&[u8]>,
485 e: &BytesStart,
486 local: &[u8],
487 decoder: Decoder,
488 ) -> VisResult<u32> {
489 let repeat = attribute(
490 reader,
491 e,
492 AttrSpace::OneOf(&[TABLE_NAMESPACE]),
493 local,
494 decoder,
495 )?
496 .map(|v| parse_index(&v, "repeat"))
497 .transpose()?
498 .unwrap_or(1);
499 if repeat == 0 {
500 return Err(unresolved("zero repeat count"));
501 }
502 Ok(repeat)
503 }
504
505 fn advance(cursor: u32, repeat: u32, ceiling: u32, what: &str) -> VisResult<u32> {
506 let next = cursor
507 .checked_add(repeat)
508 .ok_or_else(|| limit(format!("{what} repeat overflows")))?;
509 if next > ceiling {
510 return Err(limit(format!(
511 "{what} extent {next} exceeds the {ceiling} ceiling"
512 )));
513 }
514 Ok(next)
515 }
516
517 loop {
518 let event = reader
519 .read_event()
520 .map_err(|e| unresolved(format!("malformed content.xml: {e}")))?;
521 let (e, empty) = match &event {
522 Event::Start(e) => (e, false),
523 Event::Empty(e) => (e, true),
524 Event::End(e) => {
525 let (resolution, name) = reader.resolver().resolve_element(e.name());
526 let is_table = name.as_ref() == b"table"
527 && matches!(&resolution, ResolveResult::Bound(b) if b.as_ref() == TABLE_NAMESPACE);
528 if is_table && table_depth > 0 {
529 table_depth -= 1;
530 if table_depth == 0
531 && let Some(open) = current.take()
532 {
533 check_extent(open.max_content_row, open.max_content_column, &open.name)?;
534 by_sheet.insert(open.name, open.visibility);
535 }
536 }
537 continue;
538 }
539 Event::Eof => break,
540 _ => continue,
541 };
542
543 if element_in(&reader, e, &[TABLE_NAMESPACE], b"table") {
544 if table_depth == 0 {
545 let name = attribute(
546 &reader,
547 e,
548 AttrSpace::OneOf(&[TABLE_NAMESPACE]),
549 b"name",
550 decoder,
551 )?
552 .ok_or_else(|| unresolved("table without a name"))?;
553 let open = OpenTable {
554 name,
555 visibility: SheetVisibility::default(),
556 next_row: 0,
557 next_column: 0,
558 max_content_row: 0,
559 max_content_column: 0,
560 row_cell_cursor: 0,
561 };
562 if empty {
563 by_sheet.insert(open.name, open.visibility);
564 } else {
565 current = Some(open);
566 table_depth = 1;
567 }
568 } else if !empty {
569 table_depth += 1;
570 }
571 continue;
572 }
573 if table_depth != 1 {
574 continue;
575 }
576 let Some(open) = current.as_mut() else {
577 continue;
578 };
579
580 if element_in(&reader, e, &[TABLE_NAMESPACE], b"table-column") {
581 let repeat = repeat_of(&reader, e, b"number-columns-repeated", decoder)?;
582 let hidden = attribute(
583 &reader,
584 e,
585 AttrSpace::OneOf(&[TABLE_NAMESPACE]),
586 b"visibility",
587 decoder,
588 )?
589 .is_some_and(|v| v == "collapse" || v == "filter");
590 let next = advance(open.next_column, repeat, MAX_SHEET_COLUMNS, "column")?;
591 if hidden {
592 open.visibility.hidden_columns.push(IndexRange {
593 first: open.next_column,
594 last: next - 1,
595 });
596 }
597 open.next_column = next;
598 } else if element_in(&reader, e, &[TABLE_NAMESPACE], b"table-row") {
599 let repeat = repeat_of(&reader, e, b"number-rows-repeated", decoder)?;
600 let hidden = attribute(
601 &reader,
602 e,
603 AttrSpace::OneOf(&[TABLE_NAMESPACE]),
604 b"visibility",
605 decoder,
606 )?
607 .is_some_and(|v| v == "collapse" || v == "filter");
608 let next = advance(open.next_row, repeat, MAX_SHEET_ROWS, "row")?;
609 if hidden {
610 open.visibility.hidden_rows.push(IndexRange {
611 first: open.next_row,
612 last: next - 1,
613 });
614 }
615 open.next_row = next;
616 open.row_cell_cursor = 0;
617 } else if element_in(&reader, e, &[TABLE_NAMESPACE], b"table-cell")
618 || element_in(&reader, e, &[TABLE_NAMESPACE], b"covered-table-cell")
619 {
620 let repeat = repeat_of(&reader, e, b"number-columns-repeated", decoder)?;
621 let next = advance(open.row_cell_cursor, repeat, MAX_SHEET_COLUMNS, "cell")?;
622 let has_content = attribute(
623 &reader,
624 e,
625 AttrSpace::OneOf(&[OFFICE_NAMESPACE]),
626 b"value-type",
627 decoder,
628 )?
629 .is_some();
630 open.row_cell_cursor = next;
631 if has_content {
632 open.max_content_column = open.max_content_column.max(next);
633 open.max_content_row = open.max_content_row.max(open.next_row);
634 }
635 }
636 }
637 if table_depth != 0 {
638 return Err(unresolved("content.xml ends inside a table"));
639 }
640 Ok(WorkbookVisibility { by_sheet })
641}
642
643const BIFF_BOF: u16 = 0x0809;
645const BIFF_EOF: u16 = 0x000A;
646const BIFF_BOUNDSHEET: u16 = 0x0085;
647const BIFF_FILEPASS: u16 = 0x002F;
648const BIFF_CODEPAGE: u16 = 0x0042;
649const BIFF_ROW: u16 = 0x0208;
650const BIFF_COLINFO: u16 = 0x007D;
651
652const BOF_WORKSHEET: u16 = 0x0010;
653
654struct BiffRecord<'a> {
655 opcode: u16,
656 payload: &'a [u8],
657}
658
659struct BiffWalk<'a> {
663 stream: &'a [u8],
664 offset: usize,
665 done: bool,
666}
667
668impl<'a> BiffWalk<'a> {
669 fn new(stream: &'a [u8], from: usize) -> Self {
670 BiffWalk {
671 stream,
672 offset: from,
673 done: false,
674 }
675 }
676}
677
678impl<'a> Iterator for BiffWalk<'a> {
679 type Item = VisResult<BiffRecord<'a>>;
680
681 fn next(&mut self) -> Option<Self::Item> {
682 if self.done {
683 return None;
684 }
685 if self.offset + 4 > self.stream.len() {
686 self.done = true;
687 return Some(Err(unresolved(format!(
688 "BIFF stream ends without EOF at offset {}",
689 self.offset
690 ))));
691 }
692 let opcode = u16::from_le_bytes([self.stream[self.offset], self.stream[self.offset + 1]]);
693 let len = u16::from_le_bytes([self.stream[self.offset + 2], self.stream[self.offset + 3]])
694 as usize;
695 let start = self.offset + 4;
696 if start + len > self.stream.len() {
697 self.done = true;
698 return Some(Err(unresolved(format!(
699 "truncated BIFF record 0x{opcode:04X} at {}",
700 self.offset
701 ))));
702 }
703 self.offset = start + len;
704 if opcode == BIFF_EOF {
705 self.done = true;
706 }
707 Some(Ok(BiffRecord {
708 opcode,
709 payload: &self.stream[start..start + len],
710 }))
711 }
712}
713
714fn field_u16(payload: &[u8], at: usize, what: &str) -> VisResult<u16> {
715 if at + 2 > payload.len() {
716 return Err(unresolved(format!("short BIFF {what} record")));
717 }
718 Ok(u16::from_le_bytes([payload[at], payload[at + 1]]))
719}
720
721#[derive(Clone, Copy, PartialEq)]
722enum BiffVersion {
723 Biff8,
724 Biff5,
725}
726
727fn decode_narrow(bytes: &[u8], codepage: u16) -> VisResult<String> {
731 let mut out = String::with_capacity(bytes.len());
732 for byte in bytes {
733 if byte.is_ascii() {
734 out.push(*byte as char);
735 } else if codepage == 1252 {
736 out.push(cp1252_char(*byte));
737 } else {
738 return Err(unresolved(format!(
739 "sheet name in unsupported codepage {codepage}"
740 )));
741 }
742 }
743 Ok(out)
744}
745
746fn cp1252_char(byte: u8) -> char {
747 const HIGH: [char; 32] = [
749 '\u{20AC}', '\u{81}', '\u{201A}', '\u{192}', '\u{201E}', '\u{2026}', '\u{2020}',
750 '\u{2021}', '\u{2C6}', '\u{2030}', '\u{160}', '\u{2039}', '\u{152}', '\u{8D}', '\u{17D}',
751 '\u{8F}', '\u{90}', '\u{2018}', '\u{2019}', '\u{201C}', '\u{201D}', '\u{2022}', '\u{2013}',
752 '\u{2014}', '\u{2DC}', '\u{2122}', '\u{161}', '\u{203A}', '\u{153}', '\u{9D}', '\u{17E}',
753 '\u{178}',
754 ];
755 if (0x80..0xA0).contains(&byte) {
756 HIGH[(byte - 0x80) as usize]
757 } else {
758 byte as char
759 }
760}
761
762fn boundsheet_name(payload: &[u8], version: BiffVersion, codepage: u16) -> VisResult<String> {
763 match version {
764 BiffVersion::Biff8 => {
765 if payload.len() < 8 {
766 return Err(unresolved("short BOUNDSHEET record"));
767 }
768 let len = payload[6] as usize;
769 let wide = payload[7] & 0x01 != 0;
770 let name_bytes = &payload[8..];
771 if wide {
772 if name_bytes.len() < len * 2 {
773 return Err(unresolved("short BOUNDSHEET name"));
774 }
775 let units: Vec<u16> = name_bytes[..len * 2]
776 .chunks_exact(2)
777 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
778 .collect();
779 String::from_utf16(&units).map_err(|_| unresolved("bad BOUNDSHEET name"))
780 } else {
781 if name_bytes.len() < len {
782 return Err(unresolved("short BOUNDSHEET name"));
783 }
784 Ok(name_bytes[..len].iter().map(|b| *b as char).collect())
786 }
787 }
788 BiffVersion::Biff5 => {
789 if payload.len() < 7 {
790 return Err(unresolved("short BOUNDSHEET record"));
791 }
792 let len = payload[6] as usize;
793 let name_bytes = &payload[7..];
794 if name_bytes.len() < len {
795 return Err(unresolved("short BOUNDSHEET name"));
796 }
797 decode_narrow(&name_bytes[..len], codepage)
798 }
799 }
800}
801
802fn read_biff(bytes: &[u8]) -> VisResult<WorkbookVisibility> {
806 let mut compound = cfb::CompoundFile::open(Cursor::new(bytes))
807 .map_err(|e| unresolved(format!("not a compound file: {e}")))?;
808 let stream_name = ["/Workbook", "/Book"]
809 .into_iter()
810 .find(|name| compound.exists(name))
811 .ok_or_else(|| unresolved("no Workbook stream"))?;
812 let mut stream = Vec::new();
813 compound
814 .open_stream(stream_name)
815 .map_err(|e| unresolved(format!("cannot open {stream_name}: {e}")))?
816 .take(MAX_PART_BYTES + 1)
817 .read_to_end(&mut stream)
818 .map_err(|e| unresolved(format!("cannot read {stream_name}: {e}")))?;
819 if stream.len() as u64 > MAX_PART_BYTES {
820 return Err(limit(format!(
821 "workbook stream exceeds the {MAX_PART_BYTES} byte ceiling"
822 )));
823 }
824
825 let mut walker = BiffWalk::new(&stream, 0);
828 let first = walker
829 .next()
830 .ok_or_else(|| unresolved("empty workbook stream"))??;
831 if first.opcode != BIFF_BOF {
832 return Err(unresolved("workbook stream does not start with BOF"));
833 }
834 let version = match field_u16(first.payload, 0, "BOF")? {
835 0x0600 => BiffVersion::Biff8,
836 0x0500 => BiffVersion::Biff5,
837 other => {
838 return Err(unresolved(format!(
839 "unsupported BIFF version 0x{other:04X}"
840 )));
841 }
842 };
843 let mut codepage = 1252u16;
844 let mut sheets: Vec<(String, usize)> = Vec::new();
845 let mut saw_globals_eof = false;
846 for record in walker.by_ref() {
847 let record = record?;
848 match record.opcode {
849 BIFF_FILEPASS => return Err(unresolved("encrypted workbook")),
850 BIFF_CODEPAGE => codepage = field_u16(record.payload, 0, "CODEPAGE")?,
851 BIFF_BOUNDSHEET => {
852 if record.payload.len() < 6 {
853 return Err(unresolved("short BOUNDSHEET record"));
854 }
855 let position = u32::from_le_bytes([
856 record.payload[0],
857 record.payload[1],
858 record.payload[2],
859 record.payload[3],
860 ]) as usize;
861 let sheet_type = record.payload[5];
862 if sheet_type == 0 {
863 sheets.push((
864 boundsheet_name(record.payload, version, codepage)?,
865 position,
866 ));
867 }
868 }
869 BIFF_EOF => saw_globals_eof = true,
870 _ => {}
871 }
872 }
873 if !saw_globals_eof {
874 return Err(unresolved("globals end without EOF"));
875 }
876 let globals_end = walker.offset;
877
878 let mut furthest_end = globals_end;
882 let mut by_sheet = HashMap::new();
883 for (name, position) in sheets {
884 if position < globals_end || position >= stream.len() {
885 return Err(unresolved(format!(
886 "sheet {name:?} substream offset {position} is outside the sheet area"
887 )));
888 }
889 let mut visibility = SheetVisibility::default();
890 let mut walker = BiffWalk::new(&stream, position);
891 let first = walker
892 .next()
893 .ok_or_else(|| unresolved(format!("sheet {name:?} substream is empty")))??;
894 if first.opcode != BIFF_BOF {
895 return Err(unresolved(format!(
896 "sheet {name:?} substream does not start with BOF"
897 )));
898 }
899 if field_u16(first.payload, 2, "BOF")? != BOF_WORKSHEET {
900 return Err(unresolved(format!(
901 "sheet {name:?} substream BOF is not a worksheet"
902 )));
903 }
904 let mut saw_eof = false;
905 for record in walker.by_ref() {
906 let record = record?;
907 match record.opcode {
908 BIFF_ROW => {
909 let row = field_u16(record.payload, 0, "ROW")?;
910 let height = field_u16(record.payload, 6, "ROW")?;
911 let flags = field_u16(record.payload, 12, "ROW")?;
912 let zero_height = height & 0x8000 == 0 && height & 0x7FFF == 0;
913 if flags & 0x0020 != 0 || zero_height {
914 visibility.hidden_rows.push(IndexRange {
915 first: row as u32,
916 last: row as u32,
917 });
918 }
919 }
920 BIFF_COLINFO => {
921 let first = field_u16(record.payload, 0, "COLINFO")?;
922 let last = field_u16(record.payload, 2, "COLINFO")?;
923 let width = field_u16(record.payload, 4, "COLINFO")?;
924 let flags = field_u16(record.payload, 8, "COLINFO")?;
925 if first > last || last > 255 {
926 return Err(unresolved(format!(
927 "COLINFO range {first}..{last} outside the column limit"
928 )));
929 }
930 if flags & 0x0001 != 0 || width == 0 {
931 visibility.hidden_columns.push(IndexRange {
932 first: first as u32,
933 last: last as u32,
934 });
935 }
936 }
937 BIFF_EOF => saw_eof = true,
938 _ => {}
939 }
940 }
941 if !saw_eof {
942 return Err(unresolved(format!("sheet {name:?} substream has no EOF")));
943 }
944 furthest_end = furthest_end.max(walker.offset);
945 by_sheet.insert(name, visibility);
946 }
947 if furthest_end != stream.len() {
948 return Err(unresolved(format!(
949 "{} trailing bytes after the last substream",
950 stream.len() - furthest_end
951 )));
952 }
953 Ok(WorkbookVisibility { by_sheet })
954}
955
956#[cfg(test)]
957mod tests {
958 use super::*;
959
960 pub(super) fn record(opcode: u16, payload: &[u8]) -> Vec<u8> {
961 let mut out = Vec::new();
962 out.extend_from_slice(&opcode.to_le_bytes());
963 out.extend_from_slice(&(payload.len() as u16).to_le_bytes());
964 out.extend_from_slice(payload);
965 out
966 }
967
968 fn bof(version: u16, dt: u16) -> Vec<u8> {
969 let mut payload = vec![0u8; 16];
970 payload[0..2].copy_from_slice(&version.to_le_bytes());
971 payload[2..4].copy_from_slice(&dt.to_le_bytes());
972 record(BIFF_BOF, &payload)
973 }
974
975 fn row_record(row: u16, height: u16, flags: u16) -> Vec<u8> {
976 let mut payload = vec![0u8; 16];
977 payload[0..2].copy_from_slice(&row.to_le_bytes());
978 payload[6..8].copy_from_slice(&height.to_le_bytes());
979 payload[12..14].copy_from_slice(&flags.to_le_bytes());
980 record(BIFF_ROW, &payload)
981 }
982
983 fn colinfo_record(first: u16, last: u16, width: u16, flags: u16) -> Vec<u8> {
984 let mut payload = vec![0u8; 12];
985 payload[0..2].copy_from_slice(&first.to_le_bytes());
986 payload[2..4].copy_from_slice(&last.to_le_bytes());
987 payload[4..6].copy_from_slice(&width.to_le_bytes());
988 payload[8..10].copy_from_slice(&flags.to_le_bytes());
989 record(BIFF_COLINFO, &payload)
990 }
991
992 fn boundsheet_biff8(name: &str, position: u32) -> Vec<u8> {
993 let mut payload = vec![0u8; 8];
994 payload[0..4].copy_from_slice(&position.to_le_bytes());
995 payload[6] = name.len() as u8;
996 payload.extend_from_slice(name.as_bytes());
997 record(BIFF_BOUNDSHEET, &payload)
998 }
999
1000 fn biff_stream(sheet_records: &[Vec<u8>]) -> Vec<u8> {
1002 let globals = [
1003 bof(0x0600, 0x0005),
1004 boundsheet_biff8("Data", 0),
1005 record(BIFF_EOF, &[]),
1006 ];
1007 let globals_len: usize = globals.iter().map(Vec::len).sum();
1008 let mut stream = Vec::new();
1009 stream.extend(bof(0x0600, 0x0005));
1010 stream.extend(boundsheet_biff8("Data", globals_len as u32));
1011 stream.extend(record(BIFF_EOF, &[]));
1012 stream.extend(bof(0x0600, BOF_WORKSHEET));
1013 for sheet_record in sheet_records {
1014 stream.extend(sheet_record.clone());
1015 }
1016 stream.extend(record(BIFF_EOF, &[]));
1017 stream
1018 }
1019
1020 fn biff_file(stream: &[u8]) -> Vec<u8> {
1021 let cursor = Cursor::new(Vec::new());
1022 let mut compound = cfb::CompoundFile::create(cursor).unwrap();
1023 {
1024 use std::io::Write;
1025 let mut entry = compound.create_stream("/Workbook").unwrap();
1026 entry.write_all(stream).unwrap();
1027 }
1028 compound.into_inner().into_inner()
1029 }
1030
1031 #[test]
1032 fn biff_walk_finds_hidden_rows_and_columns() {
1033 let stream = biff_stream(&[
1034 row_record(0, 300, 0x0000),
1035 row_record(3, 300, 0x0020),
1036 row_record(5, 0x0000, 0x0000),
1037 colinfo_record(1, 2, 2048, 0x0001),
1038 colinfo_record(4, 4, 0, 0x0000),
1039 ]);
1040 let visibility = read_biff(&biff_file(&stream)).unwrap();
1041 let sheet = visibility.sheet("Data").unwrap();
1042 assert!(!sheet.row_hidden(0));
1043 assert!(sheet.row_hidden(3));
1044 assert!(sheet.row_hidden(5));
1045 assert!(sheet.column_hidden(1));
1046 assert!(sheet.column_hidden(2));
1047 assert!(!sheet.column_hidden(3));
1048 assert!(sheet.column_hidden(4));
1049 }
1050
1051 #[test]
1052 fn truncated_biff_stream_names_the_defect() {
1053 let mut stream = biff_stream(&[row_record(3, 300, 0x0020)]);
1054 stream.truncate(stream.len() - 7);
1055 let err = read_biff(&biff_file(&stream)).unwrap_err();
1056 assert!(err.to_string().contains("truncated"));
1057 }
1058
1059 #[test]
1060 fn trailing_bytes_after_the_last_substream_are_an_error() {
1061 let mut stream = biff_stream(&[row_record(3, 300, 0x0020)]);
1062 stream.extend_from_slice(&[0x00, 0x01, 0x02]);
1063 let err = read_biff(&biff_file(&stream)).unwrap_err();
1064 assert!(err.to_string().contains("trailing bytes"), "{err}");
1065 }
1066
1067 #[test]
1068 fn misdirected_substream_offset_is_refused() {
1069 let globals = [
1071 bof(0x0600, 0x0005),
1072 boundsheet_biff8("Data", 0),
1073 record(BIFF_EOF, &[]),
1074 ];
1075 let globals_len: usize = globals.iter().map(Vec::len).sum();
1076 let row = row_record(3, 300, 0x0020);
1077 let target = globals_len + bof(0x0600, BOF_WORKSHEET).len();
1078
1079 let mut stream = Vec::new();
1080 stream.extend(bof(0x0600, 0x0005));
1081 stream.extend(boundsheet_biff8("Data", target as u32));
1082 stream.extend(record(BIFF_EOF, &[]));
1083 stream.extend(bof(0x0600, BOF_WORKSHEET));
1084 stream.extend(row);
1085 stream.extend(record(BIFF_EOF, &[]));
1086
1087 let err = read_biff(&biff_file(&stream)).unwrap_err();
1088 assert!(err.to_string().contains("does not start with BOF"), "{err}");
1089 }
1090
1091 #[test]
1092 fn substream_inside_the_globals_is_refused() {
1093 let mut stream = Vec::new();
1094 stream.extend(bof(0x0600, 0x0005));
1095 stream.extend(boundsheet_biff8("Data", 0));
1096 stream.extend(record(BIFF_EOF, &[]));
1097 stream.extend(bof(0x0600, BOF_WORKSHEET));
1098 stream.extend(record(BIFF_EOF, &[]));
1099 let err = read_biff(&biff_file(&stream)).unwrap_err();
1100 assert!(err.to_string().contains("outside the sheet area"), "{err}");
1101 }
1102
1103 #[test]
1104 fn biff5_names_parse_with_their_own_layout() {
1105 let mut boundsheet = vec![0u8; 7];
1107 boundsheet[6] = 4;
1108 boundsheet.extend_from_slice(b"Data");
1109 let globals = [
1110 bof(0x0500, 0x0005),
1111 record(BIFF_BOUNDSHEET, &boundsheet),
1112 record(BIFF_EOF, &[]),
1113 ];
1114 let globals_len: usize = globals.iter().map(Vec::len).sum();
1115 let mut patched = vec![0u8; 7];
1116 patched[0..4].copy_from_slice(&(globals_len as u32).to_le_bytes());
1117 patched[6] = 4;
1118 patched.extend_from_slice(b"Data");
1119
1120 let mut stream = Vec::new();
1121 stream.extend(bof(0x0500, 0x0005));
1122 stream.extend(record(BIFF_BOUNDSHEET, &patched));
1123 stream.extend(record(BIFF_EOF, &[]));
1124 stream.extend(bof(0x0500, BOF_WORKSHEET));
1125 stream.extend(row_record(2, 300, 0x0020));
1126 stream.extend(record(BIFF_EOF, &[]));
1127
1128 let visibility = read_biff(&biff_file(&stream)).unwrap();
1129 assert!(visibility.sheet("Data").unwrap().row_hidden(2));
1130 }
1131
1132 #[test]
1133 fn encrypted_biff_stream_is_refused() {
1134 let mut stream = Vec::new();
1135 stream.extend(bof(0x0600, 0x0005));
1136 stream.extend(record(BIFF_FILEPASS, &[0u8; 6]));
1137 stream.extend(record(BIFF_EOF, &[]));
1138 let err = read_biff(&biff_file(&stream)).unwrap_err();
1139 assert!(err.to_string().contains("encrypted"));
1140 }
1141
1142 #[test]
1143 fn colinfo_past_the_column_limit_is_refused() {
1144 let stream = biff_stream(&[colinfo_record(5, 3, 2048, 0x0001)]);
1145 let err = read_biff(&biff_file(&stream)).unwrap_err();
1146 assert!(err.to_string().contains("COLINFO"), "{err}");
1147 }
1148
1149 fn zip_of(entries: &[(&str, &[u8])]) -> Vec<u8> {
1150 use std::io::Write;
1151 let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
1152 for (name, bytes) in entries {
1153 writer
1154 .start_file(*name, zip::write::SimpleFileOptions::default())
1155 .unwrap();
1156 writer.write_all(bytes).unwrap();
1157 }
1158 writer.finish().unwrap().into_inner()
1159 }
1160
1161 fn ods_of(content: &str) -> Vec<u8> {
1162 zip_of(&[
1163 (
1164 "mimetype",
1165 b"application/vnd.oasis.opendocument.spreadsheet",
1166 ),
1167 ("content.xml", content.as_bytes()),
1168 ])
1169 }
1170
1171 const ODS_OPEN: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
1172<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:evil="urn:example:evil">
1173<office:body><office:spreadsheet>"#;
1174 const ODS_CLOSE: &str = "</office:spreadsheet></office:body></office:document-content>";
1175
1176 #[test]
1177 fn ods_repeat_overflow_is_a_limit_error_not_a_panic() {
1178 let content = format!(
1179 "{ODS_OPEN}<table:table table:name=\"D\">\
1180<table:table-row table:number-rows-repeated=\"4294967290\"/>\
1181<table:table-row table:number-rows-repeated=\"4294967290\"/>\
1182</table:table>{ODS_CLOSE}"
1183 );
1184 let err = read_ods(&ods_of(&content)).unwrap_err();
1185 assert!(matches!(err, VisibilityError::Limit(_)), "{err}");
1186 }
1187
1188 #[test]
1189 fn ods_zero_repeat_is_refused() {
1190 let content = format!(
1191 "{ODS_OPEN}<table:table table:name=\"D\">\
1192<table:table-row table:number-rows-repeated=\"0\"/>\
1193</table:table>{ODS_CLOSE}"
1194 );
1195 let err = read_ods(&ods_of(&content)).unwrap_err();
1196 assert!(err.to_string().contains("zero repeat"), "{err}");
1197 }
1198
1199 #[test]
1200 fn ods_spoofed_foreign_visibility_changes_nothing() {
1201 let content = format!(
1203 "{ODS_OPEN}<table:table table:name=\"D\">\
1204<table:table-row evil:visibility=\"collapse\"/>\
1205<table:table-row evil:visibility=\"visible\" table:visibility=\"collapse\"/>\
1206</table:table>{ODS_CLOSE}"
1207 );
1208 let sheet = read_ods(&ods_of(&content)).unwrap().sheet("D").unwrap();
1209 assert!(!sheet.row_hidden(0));
1210 assert!(sheet.row_hidden(1));
1211 }
1212
1213 #[test]
1214 fn ods_nested_tables_neither_open_sheets_nor_shift_rows() {
1215 let content = format!(
1218 "{ODS_OPEN}<table:table table:name=\"Outer\">\
1219<table:table-row/>\
1220<evil:table><table:table-row table:visibility=\"collapse\"/></evil:table>\
1221<table:table><table:table-row table:visibility=\"collapse\"/></table:table>\
1222<table:table-row table:visibility=\"collapse\"/>\
1223</table:table>{ODS_CLOSE}"
1224 );
1225 let visibility = read_ods(&ods_of(&content)).unwrap();
1226 let outer = visibility.sheet("Outer").unwrap();
1227 assert!(!outer.row_hidden(0));
1228 assert!(outer.row_hidden(2), "outer hidden row keeps its index");
1229 assert!(
1230 visibility.sheet("").is_none(),
1231 "nested table is not a sheet"
1232 );
1233 }
1234
1235 fn xlsx_of(sheet_xml: &str) -> Vec<u8> {
1236 let workbook = r#"<?xml version="1.0"?>
1237<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
1238<sheets><sheet name="Data" sheetId="1" r:id="rId1"/></sheets></workbook>"#;
1239 let rels = r#"<?xml version="1.0"?>
1240<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1241<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
1242</Relationships>"#;
1243 zip_of(&[
1244 ("xl/workbook.xml", workbook.as_bytes()),
1245 ("xl/_rels/workbook.xml.rels", rels.as_bytes()),
1246 ("xl/worksheets/sheet1.xml", sheet_xml.as_bytes()),
1247 ])
1248 }
1249
1250 const SHEET_OPEN: &str = r#"<?xml version="1.0"?>
1251<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:evil="urn:example:evil"><sheetData>"#;
1252 const SHEET_CLOSE: &str = "</sheetData></worksheet>";
1253
1254 #[test]
1255 fn xlsx_spoofed_foreign_hidden_changes_nothing() {
1256 let sheet = format!(
1257 "{SHEET_OPEN}<row r=\"1\" evil:hidden=\"1\"/>\
1258<row r=\"2\" evil:hidden=\"0\" hidden=\"1\"/>{SHEET_CLOSE}"
1259 );
1260 let visibility = read_ooxml(&xlsx_of(&sheet)).unwrap();
1261 let data = visibility.sheet("Data").unwrap();
1262 assert!(!data.row_hidden(0));
1263 assert!(data.row_hidden(1));
1264 }
1265
1266 #[test]
1267 fn xlsx_cell_extent_product_is_refused() {
1268 let sheet = format!(
1269 "{SHEET_OPEN}<row r=\"1\"><c r=\"A1\"><v>1</v></c></row>\
1270<row r=\"1048576\"><c r=\"XFD1048576\"><v>2</v></c></row>{SHEET_CLOSE}"
1271 );
1272 let err = read_ooxml(&xlsx_of(&sheet)).unwrap_err();
1273 assert!(matches!(err, VisibilityError::Limit(_)), "{err}");
1274 }
1275
1276 #[test]
1277 fn xlsx_non_numeric_sizes_are_refused() {
1278 let sheet = format!("{SHEET_OPEN}<row r=\"1\" ht=\"NaN\"/>{SHEET_CLOSE}");
1279 let err = read_ooxml(&xlsx_of(&sheet)).unwrap_err();
1280 assert!(err.to_string().contains("row height"), "{err}");
1281
1282 let sheet = r#"<?xml version="1.0"?>
1283<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1284<cols><col min="1" max="1" width="wide"/></cols><sheetData/></worksheet>"#;
1285 let err = read_ooxml(&xlsx_of(sheet)).unwrap_err();
1286 assert!(err.to_string().contains("col width"), "{err}");
1287 }
1288
1289 #[test]
1290 fn worksheet_at_an_unusual_part_path_resolves_by_relationship_type() {
1291 let workbook = r#"<?xml version="1.0"?>
1292<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
1293<sheets><sheet name="Data" sheetId="1" r:id="rId1"/></sheets></workbook>"#;
1294 let rels = r#"<?xml version="1.0"?>
1295<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1296<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="parts/sheet1.xml"/>
1297</Relationships>"#;
1298 let sheet = r#"<?xml version="1.0"?>
1299<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1300<sheetData><row r="2" hidden="1"/></sheetData></worksheet>"#;
1301 let archive = zip_of(&[
1302 ("xl/workbook.xml", workbook.as_bytes()),
1303 ("xl/_rels/workbook.xml.rels", rels.as_bytes()),
1304 ("xl/parts/sheet1.xml", sheet.as_bytes()),
1305 ]);
1306 let visibility = read_ooxml(&archive).unwrap();
1307 assert!(visibility.sheet("Data").unwrap().row_hidden(1));
1308 }
1309
1310 #[test]
1311 fn oversized_archive_part_is_a_limit_error() {
1312 let mut huge = String::with_capacity(MAX_PART_BYTES as usize + 1024);
1315 huge.push_str(r#"<?xml version="1.0"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>"#);
1316 while huge.len() as u64 <= MAX_PART_BYTES {
1317 huge.push_str(" ");
1318 }
1319 huge.push_str("</sheetData></worksheet>");
1320 let err = read_ooxml(&xlsx_of(&huge)).unwrap_err();
1321 assert!(matches!(err, VisibilityError::Limit(_)), "{err}");
1322 }
1323
1324 #[test]
1325 fn unknown_format_has_no_reader() {
1326 let err = read_visibility(b"", "xlsb").unwrap_err();
1327 assert!(err.to_string().contains("no visibility reader"));
1328 }
1329
1330 #[test]
1331 fn unresolved_sheet_lookup_returns_none() {
1332 let visibility = WorkbookVisibility::default();
1333 assert!(visibility.sheet("Ghost").is_none());
1334 }
1335
1336 #[test]
1337 fn cell_references_parse_and_cap() {
1338 assert_eq!(parse_cell_reference("A1").unwrap(), (0, 0));
1339 assert_eq!(
1340 parse_cell_reference("XFD1048576").unwrap(),
1341 (1048575, 16383)
1342 );
1343 assert!(parse_cell_reference("XFE1").is_err());
1344 assert!(parse_cell_reference("A0").is_err());
1345 assert!(parse_cell_reference("11").is_err());
1346 }
1347
1348 #[test]
1349 fn part_targets_normalize() {
1350 assert_eq!(
1351 normalize_part_target("worksheets/sheet1.xml").unwrap(),
1352 "xl/worksheets/sheet1.xml"
1353 );
1354 assert_eq!(
1355 normalize_part_target("/xl/parts/sheet1.xml").unwrap(),
1356 "xl/parts/sheet1.xml"
1357 );
1358 assert_eq!(
1359 normalize_part_target("./sub/../parts/s.xml").unwrap(),
1360 "xl/parts/s.xml"
1361 );
1362 assert!(normalize_part_target("../../evil.xml").is_err());
1363 }
1364}