rakata_formats/archive.rs
1//! Shared vocabulary for the lazy archive indexes.
2//!
3//! ERF, RIM, and BIF indexes all face the same question: what should happen
4//! when an archive is damaged? The answer is graded by how much of the file
5//! is still trustworthy.
6//!
7//! - **The table will not parse.** Nothing about the archive is knowable, so
8//! indexing fails outright. Whatever is mounting it records that and moves
9//! on to the other sources it has.
10//! - **The table parses but an entry points outside the file.** Everything
11//! else in the archive is still readable, so the index keeps going and
12//! marks that one entry with an [`EntryDefect`](crate::archive::EntryDefect). Reading it returns the
13//! error; reading its neighbours works.
14//! - **A read fails at read time.** Already covered by the per-read
15//! `Result`.
16//!
17//! The middle case is why this module exists. Tools that inspect installs and
18//! salvage damaged saves are most useful precisely when the data is broken,
19//! so refusing to open an archive over one bad entry disables them exactly
20//! when they are needed. Serving the healthy remainder is only safe because
21//! the defect is recorded rather than swallowed: a caller that wants
22//! strictness checks the defects and refuses on its own terms.
23
24use std::fmt::{Display, Formatter};
25
26/// Why an indexed entry cannot be read.
27///
28/// Attached to an entry at index time. The entry keeps its identity and its
29/// declared byte range, so a caller can still report what the archive
30/// *claimed* about the resource even though the bytes are unreachable.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum EntryDefect {
33 /// The declared byte range extends past the end of the archive.
34 OutOfBounds {
35 /// Byte offset the entry table declared.
36 offset: u64,
37 /// Byte length the entry table declared.
38 size: u64,
39 /// Length of the archive the entry lives in.
40 archive_len: u64,
41 },
42}
43
44impl Display for EntryDefect {
45 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::OutOfBounds {
48 offset,
49 size,
50 archive_len,
51 } => write!(
52 formatter,
53 "declared byte range {offset}..{} extends past the archive end ({archive_len})",
54 offset.saturating_add(*size)
55 ),
56 }
57 }
58}
59
60impl EntryDefect {
61 /// Returns a defect when `offset..offset + size` escapes `archive_len`.
62 ///
63 /// Overflowing the addition counts as escaping: no real entry can span
64 /// past `u64::MAX`.
65 pub(crate) fn check_bounds(offset: u64, size: u64, archive_len: u64) -> Option<Self> {
66 match offset.checked_add(size) {
67 Some(end) if end <= archive_len => None,
68 _ => Some(Self::OutOfBounds {
69 offset,
70 size,
71 archive_len,
72 }),
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn a_range_inside_the_archive_is_not_a_defect() {
83 assert!(EntryDefect::check_bounds(10, 20, 100).is_none());
84 assert!(EntryDefect::check_bounds(0, 100, 100).is_none());
85 assert!(EntryDefect::check_bounds(100, 0, 100).is_none());
86 }
87
88 #[test]
89 fn a_range_past_the_end_is_a_defect() {
90 let defect = EntryDefect::check_bounds(90, 20, 100).expect("range escapes");
91
92 assert_eq!(
93 defect,
94 EntryDefect::OutOfBounds {
95 offset: 90,
96 size: 20,
97 archive_len: 100
98 }
99 );
100 }
101
102 #[test]
103 fn an_overflowing_range_is_a_defect_rather_than_a_wrap() {
104 assert!(EntryDefect::check_bounds(u64::MAX, 1, u64::MAX).is_some());
105 }
106
107 #[test]
108 fn the_message_names_the_range_and_the_limit() {
109 let message = EntryDefect::check_bounds(90, 20, 100)
110 .expect("range escapes")
111 .to_string();
112
113 assert!(message.contains("90..110"), "{message}");
114 assert!(message.contains("100"), "{message}");
115 }
116}