Skip to main content

sherlock_nsf_parser/
error.rs

1//! Error taxonomy for the parser.
2//!
3//! Variants are stable across 0.x; new variants will be added rather than
4//! existing ones renamed.
5
6use std::fmt;
7
8/// Top-level parser error.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum NsfError {
11    /// File is shorter than the minimum size required to even hold a
12    /// valid NSF file header (6 bytes).
13    TooShort {
14        /// Bytes actually available.
15        actual: usize,
16        /// Bytes required for the operation that failed.
17        required: usize,
18    },
19    /// The 2-byte file-header signature did not match the NSF LSIG marker
20    /// (`0x1A 0x00`). Likely not an NSF file at all.
21    BadFileSignature {
22        /// The two bytes that were read at file offset 0.
23        observed: [u8; 2],
24    },
25    /// The database-header-size field in the file header is implausible
26    /// (zero, larger than any documented NSF, or larger than the file
27    /// itself).
28    BadHeaderSize {
29        /// The size value read from the file header.
30        size: u32,
31    },
32    /// A subrecord (superblock, bucket descriptor block, ...) signature
33    /// check failed. Distinct from [`Self::BadFileSignature`] so the
34    /// error message can identify which structure failed validation.
35    BadSubrecordSignature {
36        /// Short human-readable name of the structure whose signature
37        /// failed (e.g. "superblock", "BDB header").
38        kind: &'static str,
39        /// Expected signature bytes (typically 2 bytes).
40        expected: [u8; 2],
41        /// Observed bytes at the signature position.
42        observed: [u8; 2],
43    },
44    /// A structure required for the requested operation is stored
45    /// compressed and the parser does not yet implement the compression
46    /// scheme. The canonical case: on modern ODS the superblock body
47    /// (which carries the bucket-descriptor array that maps a
48    /// `bucket_index` to a file offset) is stored with Domino "CX"
49    /// compression. Resolving an [`crate::RrvLocation::BucketSlot`] entry
50    /// to bytes requires decompressing that body first. Until the CX
51    /// decompressor lands, bucket-slot resolution returns this error
52    /// rather than guessing - in a forensic context a wrong decompressor
53    /// silently corrupts evidence, which is worse than an explicit
54    /// not-yet-supported signal.
55    CompressionUnsupported {
56        /// Structure whose body is compressed (e.g. "superblock body").
57        structure: &'static str,
58        /// The compression-type value read from the structure header.
59        compression_type: u16,
60    },
61    /// A `bucket_index` from an RRV entry is past the end of the parsed
62    /// bucket-descriptor array. Indicates either corruption or a stale
63    /// (non-freshest) superblock being consulted.
64    BucketIndexOutOfRange {
65        /// The bucket index requested.
66        requested: u32,
67        /// The number of bucket descriptors actually present.
68        available: usize,
69    },
70    /// A `slot_index` from an RRV entry is outside the bucket's slot
71    /// table. Slot indices are 1-based on disk; zero is never valid.
72    SlotIndexOutOfRange {
73        /// The slot index requested (1-based as stored on disk).
74        requested: u16,
75        /// The number of slots the bucket actually declares.
76        available: u32,
77    },
78    /// Decompression of a compressed structure failed: the compressed
79    /// stream was truncated, a back-reference pointed before the start of
80    /// the output, or the declared output size was exceeded. Distinct from
81    /// [`Self::CompressionUnsupported`] (which means "scheme not
82    /// implemented"); this means "scheme implemented, but this input did
83    /// not decode cleanly".
84    DecompressionFailed {
85        /// Short description of which invariant the stream violated.
86        detail: &'static str,
87    },
88}
89
90impl fmt::Display for NsfError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::TooShort { actual, required } => write!(
94                f,
95                "file too short: {actual} bytes available, {required} required"
96            ),
97            Self::BadFileSignature { observed } => write!(
98                f,
99                "not an NSF file: expected file-header signature 1A 00, got {:02X} {:02X}",
100                observed[0], observed[1]
101            ),
102            Self::BadHeaderSize { size } => write!(
103                f,
104                "implausible database-header size in file header: {size}"
105            ),
106            Self::BadSubrecordSignature {
107                kind,
108                expected,
109                observed,
110            } => write!(
111                f,
112                "bad {kind} signature: expected {:02X} {:02X}, got {:02X} {:02X}",
113                expected[0], expected[1], observed[0], observed[1]
114            ),
115            Self::CompressionUnsupported {
116                structure,
117                compression_type,
118            } => write!(
119                f,
120                "{structure} is compressed (compression type {compression_type}); \
121                 decompression not yet implemented"
122            ),
123            Self::BucketIndexOutOfRange {
124                requested,
125                available,
126            } => write!(
127                f,
128                "bucket index {requested} out of range: {available} bucket descriptors present"
129            ),
130            Self::SlotIndexOutOfRange {
131                requested,
132                available,
133            } => write!(
134                f,
135                "slot index {requested} out of range: bucket declares {available} slots"
136            ),
137            Self::DecompressionFailed { detail } => {
138                write!(f, "decompression failed: {detail}")
139            }
140        }
141    }
142}
143
144impl std::error::Error for NsfError {}