steam_vdf_parser/
error.rs

1//! Error types for VDF parsing.
2
3use alloc::format;
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::fmt;
7
8/// Result type for VDF operations.
9pub type Result<T> = core::result::Result<T, Error>;
10
11/// Create a parse error with truncated input snippet (max 50 chars).
12///
13/// The snippet is limited to 50 characters to keep error messages manageable.
14///
15/// # Parameters
16/// - `input`: The input string being parsed
17/// - `offset`: Byte offset where the error occurred
18/// - `context`: Description of what was expected
19pub fn parse_error(input: &str, offset: usize, context: impl Into<String>) -> Error {
20    let snippet = input.chars().take(50).collect::<String>();
21    Error::ParseError {
22        input: snippet,
23        offset,
24        context: context.into(),
25    }
26}
27
28/// Errors that can occur during VDF parsing.
29#[derive(Debug)]
30pub enum Error {
31    /// Binary format errors
32    /// ---------------------
33
34    /// Invalid magic number in binary VDF header.
35    InvalidMagic {
36        /// The magic number that was found.
37        found: u32,
38        /// Expected magic numbers for this format.
39        expected: &'static [u32],
40    },
41
42    /// Unknown type byte encountered.
43    UnknownType {
44        /// The type byte that was found.
45        type_byte: u8,
46        /// Offset in the input where this occurred.
47        offset: usize,
48    },
49
50    /// Invalid string index into the string table.
51    InvalidStringIndex {
52        /// The index that was requested.
53        index: usize,
54        /// The maximum valid index.
55        max: usize,
56    },
57
58    /// Unexpected end of input while parsing.
59    UnexpectedEndOfInput {
60        /// Description of what was being read.
61        context: &'static str,
62        /// Offset in the input where this occurred.
63        offset: usize,
64        /// Expected minimum number of bytes.
65        expected: usize,
66        /// Actual number of bytes available.
67        actual: usize,
68    },
69
70    /// Invalid UTF-8 sequence in binary data.
71    InvalidUtf8 {
72        /// Offset where the error occurred.
73        offset: usize,
74    },
75
76    /// Invalid UTF-16 sequence in binary data.
77    InvalidUtf16 {
78        /// Offset where the error occurred.
79        offset: usize,
80        /// Position of the unpaired surrogate.
81        position: usize,
82    },
83
84    /// Text format errors
85    /// ------------------
86
87    /// Parse error with context.
88    ParseError {
89        /// A snippet of the input near the error.
90        input: String,
91        /// Offset in the input where this occurred.
92        offset: usize,
93        /// Context describing what was expected.
94        context: String,
95    },
96}
97
98impl fmt::Display for Error {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        match self {
101            Error::InvalidMagic { found, expected } => {
102                let expected_str: String = expected
103                    .iter()
104                    .map(|v| format!("0x{:08x}", v))
105                    .collect::<Vec<_>>()
106                    .join(" or ");
107                write!(
108                    f,
109                    "Invalid magic number: expected {}, found 0x{:08x}",
110                    expected_str, found
111                )
112            }
113            Error::UnknownType { type_byte, offset } => {
114                write!(
115                    f,
116                    "Unknown type byte 0x{:02x} at offset {}",
117                    type_byte, offset
118                )
119            }
120            Error::InvalidStringIndex { index, max } => {
121                if *max == 0 {
122                    write!(f, "Invalid string index {}: string table is empty", index)
123                } else {
124                    write!(
125                        f,
126                        "Invalid string index {}: string table has {} entries (valid range: 0..{})",
127                        index, max, max
128                    )
129                }
130            }
131            Error::UnexpectedEndOfInput {
132                context,
133                offset,
134                expected,
135                actual,
136            } => {
137                write!(
138                    f,
139                    "Unexpected end of input at offset {} while {}: expected {} bytes, found {}",
140                    offset, context, expected, actual
141                )
142            }
143            Error::InvalidUtf8 { offset } => {
144                write!(f, "Invalid UTF-8 sequence at offset {}", offset)
145            }
146            Error::InvalidUtf16 { offset, position } => {
147                write!(
148                    f,
149                    "Invalid UTF-16 sequence at offset {} (surrogate position {})",
150                    offset, position
151                )
152            }
153            Error::ParseError {
154                input,
155                offset,
156                context,
157            } => {
158                let snippet = if input.len() > 50 {
159                    format!("{}...", &input[..50])
160                } else {
161                    input.clone()
162                };
163                write!(
164                    f,
165                    "Parse error at offset {}: {} (near: \"{}\")",
166                    offset, context, snippet
167                )
168            }
169        }
170    }
171}
172
173impl core::error::Error for Error {}
174
175impl Error {
176    /// Adjusts the offset in error variants that contain position information.
177    ///
178    /// This is used to add a base offset when parsing from a sub-slice,
179    /// converting relative offsets to absolute offsets in the original input.
180    fn with_offset(mut self, base: usize) -> Self {
181        match &mut self {
182            Error::UnexpectedEndOfInput { offset, .. } => *offset += base,
183            Error::InvalidUtf8 { offset } => *offset += base,
184            Error::InvalidUtf16 { offset, .. } => *offset += base,
185            Error::UnknownType { offset, .. } => *offset += base,
186            Error::ParseError { offset, .. } => *offset += base,
187            // Other variants don't have offsets to adjust
188            Error::InvalidMagic { .. } | Error::InvalidStringIndex { .. } => {}
189        }
190        self
191    }
192}
193
194/// Returns a closure that adds an offset to an error.
195///
196/// This is used with `.map_err()` to adjust error offsets when parsing from sub-slices.
197pub(crate) fn with_offset(base: usize) -> impl Fn(Error) -> Error {
198    move |err| err.with_offset(base)
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::binary::{APPINFO_MAGIC_40, APPINFO_MAGIC_41};
205    use alloc::format;
206    use alloc::string::ToString;
207
208    #[test]
209    fn test_error_display_invalid_magic() {
210        let err = Error::InvalidMagic {
211            found: 0xDEADBEEF,
212            expected: &[APPINFO_MAGIC_40, APPINFO_MAGIC_41],
213        };
214        let msg = format!("{}", err);
215        assert!(msg.contains("0xdeadbeef"));
216        assert!(msg.contains("0x07564428"));
217    }
218
219    #[test]
220    fn test_error_display_unknown_type() {
221        let err = Error::UnknownType {
222            type_byte: 0xFF,
223            offset: 42,
224        };
225        let msg = format!("{}", err);
226        assert!(msg.contains("0xff"));
227        assert!(msg.contains("42"));
228    }
229
230    #[test]
231    fn test_error_display_invalid_string_index() {
232        let err = Error::InvalidStringIndex { index: 5, max: 3 };
233        let msg = format!("{}", err);
234        assert!(msg.contains("5"));
235        assert!(msg.contains("3"));
236    }
237
238    #[test]
239    fn test_error_display_invalid_string_index_empty() {
240        let err = Error::InvalidStringIndex { index: 0, max: 0 };
241        let msg = format!("{}", err);
242        assert!(msg.contains("empty"));
243    }
244
245    #[test]
246    fn test_error_display_unexpected_end_of_input() {
247        let err = Error::UnexpectedEndOfInput {
248            context: "reading string",
249            offset: 10,
250            expected: 4,
251            actual: 2,
252        };
253        let msg = format!("{}", err);
254        assert!(msg.contains("reading string"));
255        assert!(msg.contains("10"));
256        assert!(msg.contains("expected 4"));
257        assert!(msg.contains("found 2"));
258    }
259
260    #[test]
261    fn test_error_display_invalid_utf8() {
262        let err = Error::InvalidUtf8 { offset: 15 };
263        let msg = format!("{}", err);
264        assert!(msg.contains("15"));
265    }
266
267    #[test]
268    fn test_error_display_invalid_utf16() {
269        let err = Error::InvalidUtf16 {
270            offset: 20,
271            position: 3,
272        };
273        let msg = format!("{}", err);
274        assert!(msg.contains("20"));
275        assert!(msg.contains("3"));
276    }
277
278    #[test]
279    fn test_error_display_parse_error() {
280        let err = Error::ParseError {
281            input: "some very long input that should be truncated in the message".to_string(),
282            offset: 5,
283            context: "expected quote".to_string(),
284        };
285        let msg = format!("{}", err);
286        assert!(msg.contains("5"));
287        assert!(msg.contains("expected quote"));
288        // The input should be truncated to 50 chars, so the message shouldn't be too long
289        assert!(msg.len() < 150);
290    }
291
292    #[test]
293    fn test_error_with_offset_unexpected_end() {
294        let err = Error::UnexpectedEndOfInput {
295            context: "test",
296            offset: 10,
297            expected: 4,
298            actual: 2,
299        };
300        let adjusted = err.with_offset(100);
301        match adjusted {
302            Error::UnexpectedEndOfInput { offset, .. } => {
303                assert_eq!(offset, 110);
304            }
305            _ => panic!("Unexpected error type"),
306        }
307    }
308
309    #[test]
310    fn test_error_with_offset_invalid_utf8() {
311        let err = Error::InvalidUtf8 { offset: 5 };
312        let adjusted = err.with_offset(100);
313        match adjusted {
314            Error::InvalidUtf8 { offset } => {
315                assert_eq!(offset, 105);
316            }
317            _ => panic!("Unexpected error type"),
318        }
319    }
320
321    #[test]
322    fn test_error_with_offset_invalid_utf16() {
323        let err = Error::InvalidUtf16 {
324            offset: 10,
325            position: 2,
326        };
327        let adjusted = err.with_offset(100);
328        match adjusted {
329            Error::InvalidUtf16 { offset, position } => {
330                assert_eq!(offset, 110);
331                assert_eq!(position, 2);
332            }
333            _ => panic!("Unexpected error type"),
334        }
335    }
336
337    #[test]
338    fn test_error_with_offset_unknown_type() {
339        let err = Error::UnknownType {
340            type_byte: 0x42,
341            offset: 7,
342        };
343        let adjusted = err.with_offset(100);
344        match adjusted {
345            Error::UnknownType { type_byte, offset } => {
346                assert_eq!(type_byte, 0x42);
347                assert_eq!(offset, 107);
348            }
349            _ => panic!("Unexpected error type"),
350        }
351    }
352
353    #[test]
354    fn test_error_with_offset_parse_error() {
355        let err = Error::ParseError {
356            input: "test".to_string(),
357            offset: 3,
358            context: "context".to_string(),
359        };
360        let adjusted = err.with_offset(100);
361        match adjusted {
362            Error::ParseError { offset, .. } => {
363                assert_eq!(offset, 103);
364            }
365            _ => panic!("Unexpected error type"),
366        }
367    }
368
369    #[test]
370    fn test_error_with_offset_no_change_for_non_offset_variants() {
371        let err = Error::InvalidMagic {
372            found: 0x12345678,
373            expected: &[APPINFO_MAGIC_40],
374        };
375        let adjusted = err.with_offset(100);
376        // InvalidMagic doesn't have an offset field, so it should be unchanged
377        match adjusted {
378            Error::InvalidMagic { found, .. } => {
379                assert_eq!(found, 0x12345678);
380            }
381            _ => panic!("Unexpected error type"),
382        }
383    }
384
385    #[test]
386    fn test_parse_error_truncates_long_input() {
387        let long_input = "a".repeat(100);
388        let err = parse_error(&long_input, 0, "test context");
389        match err {
390            Error::ParseError { input, .. } => {
391                assert!(input.len() <= 50, "Input should be truncated to 50 chars");
392            }
393            _ => panic!("Expected ParseError variant"),
394        }
395    }
396
397    #[test]
398    fn test_with_offset_closure() {
399        let base_offset = 100;
400        let f = with_offset(base_offset);
401        let err = Error::InvalidUtf8 { offset: 5 };
402        let adjusted = f(err);
403        match adjusted {
404            Error::InvalidUtf8 { offset } => {
405                assert_eq!(offset, 105);
406            }
407            _ => panic!("Unexpected error type"),
408        }
409    }
410}