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 {}