puz_parse/
error.rs

1use std::{error::Error as StdError, fmt, io};
2
3/// Warnings that can occur during parsing but don't prevent puzzle creation.
4///
5/// These indicate non-critical issues that were encountered during parsing
6/// but were handled gracefully. The parsing can continue and produce a valid
7/// puzzle, but some information might be missing or using fallback values.
8#[derive(Debug, Clone, PartialEq)]
9pub enum PuzWarning {
10    /// An optional extension section was skipped due to parsing issues
11    SkippedExtension { section: String, reason: String },
12    /// Character encoding issues were encountered but handled
13    EncodingIssue { context: String, recovered: bool },
14    /// Invalid data was found but default values were used
15    DataRecovery { field: String, issue: String },
16    /// Puzzle is scrambled and may not display correctly
17    ScrambledPuzzle { version: String },
18}
19
20/// Result type for parsing that includes warnings.
21///
22/// This is returned by the main [`parse`](crate::parse) function and contains
23/// both the successfully parsed puzzle and any warnings that occurred during parsing.
24#[derive(Debug)]
25pub struct ParseResult<T> {
26    /// The successfully parsed puzzle
27    pub result: T,
28    /// Any warnings that occurred during parsing
29    pub warnings: Vec<PuzWarning>,
30}
31
32impl<T> ParseResult<T> {
33    pub fn new(result: T) -> Self {
34        Self {
35            result,
36            warnings: Vec::new(),
37        }
38    }
39
40    pub fn with_warnings(result: T, warnings: Vec<PuzWarning>) -> Self {
41        Self { result, warnings }
42    }
43
44    pub fn add_warning(&mut self, warning: PuzWarning) {
45        self.warnings.push(warning);
46    }
47}
48
49/// Errors that can occur when parsing a .puz file.
50///
51/// These represent critical issues that prevent successful parsing of the puzzle.
52/// Unlike [`PuzWarning`], these errors cause parsing to fail completely.
53#[derive(Debug, Clone, PartialEq)]
54pub enum PuzError {
55    /// The file magic header is invalid
56    InvalidMagic { found: Vec<u8> },
57
58    /// Checksum validation failed
59    InvalidChecksum {
60        expected: u16,
61        found: u16,
62        context: String,
63    },
64
65    /// Puzzle dimensions are invalid
66    InvalidDimensions { width: u8, height: u8 },
67
68    /// Clue count doesn't match expected value
69    InvalidClueCount { expected: u16, found: usize },
70
71    /// Extension section size mismatch
72    SectionSizeMismatch {
73        section: String,
74        expected: usize,
75        found: usize,
76    },
77
78    /// Parse error with position context
79    ParseError {
80        message: String,
81        position: Option<u64>,
82        context: String,
83    },
84
85    /// An I/O error occurred while reading the file
86    IoError {
87        message: String,
88        kind: io::ErrorKind,
89        position: Option<u64>,
90    },
91
92    /// The file contains invalid UTF-8 data
93    InvalidUtf8 {
94        message: String,
95        position: Option<u64>,
96    },
97
98    /// Required data is missing from the file
99    MissingData {
100        field: String,
101        position: Option<u64>,
102    },
103
104    /// The file version is not supported
105    UnsupportedVersion { version: String },
106
107    /// Grid validation failed
108    InvalidGrid { reason: String },
109
110    /// Clue processing failed
111    InvalidClues { reason: String },
112}
113
114impl fmt::Display for PuzError {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            PuzError::InvalidMagic { found } => {
118                write!(f, "Invalid .puz file magic header. Expected 'ACROSS&DOWN\\0', found: {found:?}. This file may be corrupted or not a .puz file.")
119            }
120            PuzError::InvalidChecksum {
121                expected,
122                found,
123                context,
124            } => {
125                write!(f, "Checksum validation failed in {context}: expected 0x{expected:04X}, found 0x{found:04X}. The file may be corrupted.")
126            }
127            PuzError::InvalidDimensions { width, height } => {
128                write!(
129                    f,
130                    "Invalid puzzle dimensions: {width}x{height}. Dimensions must be between 1 and 255."
131                )
132            }
133            PuzError::InvalidClueCount { expected, found } => {
134                write!(
135                    f,
136                    "Clue count mismatch: expected {expected} clues, found {found}. The file may be corrupted."
137                )
138            }
139            PuzError::SectionSizeMismatch {
140                section,
141                expected,
142                found,
143            } => {
144                write!(f, "Extension section '{section}' size mismatch: expected {expected} bytes, found {found}. The section may be corrupted.")
145            }
146            PuzError::ParseError {
147                message,
148                position,
149                context,
150            } => match position {
151                Some(pos) => write!(f, "Parse error at position {pos}: {message} ({context})"),
152                None => write!(f, "Parse error: {message} ({context})"),
153            },
154            PuzError::IoError {
155                message,
156                kind,
157                position,
158            } => match position {
159                Some(pos) => write!(f, "I/O error at position {pos}: {message} ({kind:?})"),
160                None => write!(f, "I/O error: {message} ({kind:?})"),
161            },
162            PuzError::InvalidUtf8 { message, position } => match position {
163                Some(pos) => write!(f, "Invalid UTF-8 data at position {pos}: {message}"),
164                None => write!(f, "Invalid UTF-8 data: {message}"),
165            },
166            PuzError::MissingData { field, position } => match position {
167                Some(pos) => write!(f, "Missing required data '{field}' at position {pos}"),
168                None => write!(f, "Missing required data: {field}"),
169            },
170            PuzError::UnsupportedVersion { version } => {
171                write!(
172                    f,
173                    "Unsupported .puz file version: '{version}'. Only standard versions are supported."
174                )
175            }
176            PuzError::InvalidGrid { reason } => {
177                write!(f, "Invalid puzzle grid: {reason}")
178            }
179            PuzError::InvalidClues { reason } => {
180                write!(f, "Invalid clues: {reason}")
181            }
182        }
183    }
184}
185
186impl StdError for PuzError {}
187
188impl From<io::Error> for PuzError {
189    fn from(error: io::Error) -> Self {
190        PuzError::IoError {
191            message: format!("I/O operation failed: {error}"),
192            kind: error.kind(),
193            position: None,
194        }
195    }
196}
197
198impl From<std::str::Utf8Error> for PuzError {
199    fn from(error: std::str::Utf8Error) -> Self {
200        PuzError::InvalidUtf8 {
201            message: format!("UTF-8 decoding failed: {error}"),
202            position: None,
203        }
204    }
205}
206
207impl PuzError {
208    /// Add position context to an existing error
209    pub fn with_position(mut self, position: u64) -> Self {
210        match &mut self {
211            PuzError::IoError { position: pos, .. } => *pos = Some(position),
212            PuzError::InvalidUtf8 { position: pos, .. } => *pos = Some(position),
213            PuzError::MissingData { position: pos, .. } => *pos = Some(position),
214            PuzError::ParseError { position: pos, .. } => *pos = Some(position),
215            _ => {} // Other error types don't have position fields
216        }
217        self
218    }
219
220    /// Add context to an existing error
221    pub fn with_context(self, context: &str) -> Self {
222        match self {
223            PuzError::IoError {
224                message,
225                kind,
226                position,
227            } => PuzError::IoError {
228                message: format!("{context}: {message}"),
229                kind,
230                position,
231            },
232            PuzError::InvalidUtf8 { message, position } => PuzError::InvalidUtf8 {
233                message: format!("{context}: {message}"),
234                position,
235            },
236            PuzError::ParseError {
237                message,
238                position,
239                context: existing_context,
240            } => PuzError::ParseError {
241                message,
242                position,
243                context: format!("{context}: {existing_context}"),
244            },
245            other => other, // For other types, return as-is or convert to ParseError
246        }
247    }
248}
249
250impl fmt::Display for PuzWarning {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        match self {
253            PuzWarning::SkippedExtension { section, reason } => {
254                write!(f, "Skipped extension section '{section}': {reason}")
255            }
256            PuzWarning::EncodingIssue { context, recovered } => {
257                write!(
258                    f,
259                    "Encoding issue in {}: {}",
260                    context,
261                    if *recovered {
262                        "recovered using fallback"
263                    } else {
264                        "could not recover"
265                    }
266                )
267            }
268            PuzWarning::DataRecovery { field, issue } => {
269                write!(f, "Data recovery for '{field}': {issue}")
270            }
271            PuzWarning::ScrambledPuzzle { version } => {
272                write!(f, "Puzzle is scrambled (version {version}). Solution may not be readable without descrambling.")
273            }
274        }
275    }
276}