Skip to main content

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}