1use std::{error::Error as StdError, fmt, io};
2
3#[derive(Debug, Clone, PartialEq)]
9pub enum PuzWarning {
10 SkippedExtension { section: String, reason: String },
12 EncodingIssue { context: String, recovered: bool },
14 DataRecovery { field: String, issue: String },
16 ScrambledPuzzle { version: String },
18}
19
20#[derive(Debug)]
25pub struct ParseResult<T> {
26 pub result: T,
28 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#[derive(Debug, Clone, PartialEq)]
54pub enum PuzError {
55 InvalidMagic { found: Vec<u8> },
57
58 InvalidChecksum {
60 expected: u16,
61 found: u16,
62 context: String,
63 },
64
65 InvalidDimensions { width: u8, height: u8 },
67
68 InvalidClueCount { expected: u16, found: usize },
70
71 SectionSizeMismatch {
73 section: String,
74 expected: usize,
75 found: usize,
76 },
77
78 ParseError {
80 message: String,
81 position: Option<u64>,
82 context: String,
83 },
84
85 IoError {
87 message: String,
88 kind: io::ErrorKind,
89 position: Option<u64>,
90 },
91
92 InvalidUtf8 {
94 message: String,
95 position: Option<u64>,
96 },
97
98 MissingData {
100 field: String,
101 position: Option<u64>,
102 },
103
104 UnsupportedVersion { version: String },
106
107 InvalidGrid { reason: String },
109
110 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 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 _ => {} }
217 self
218 }
219
220 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, }
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}