1extern crate alloc;
7
8use alloc::boxed::Box;
9use alloc::format;
10use alloc::string::{String, ToString};
11use core::fmt;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum ErrorPhase {
17 Open,
19 Parse,
21 Style,
23 Layout,
25 Render,
27}
28
29impl fmt::Display for ErrorPhase {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::Open => write!(f, "open"),
33 Self::Parse => write!(f, "parse"),
34 Self::Style => write!(f, "style"),
35 Self::Layout => write!(f, "layout"),
36 Self::Render => write!(f, "render"),
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ErrorLimitContext {
44 pub kind: Box<str>,
46 pub actual: usize,
48 pub limit: usize,
50}
51
52impl ErrorLimitContext {
53 pub fn new(kind: impl Into<String>, actual: usize, limit: usize) -> Self {
55 Self {
56 kind: kind.into().into_boxed_str(),
57 actual,
58 limit,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Eq)]
65pub struct PhaseErrorContext {
66 pub path: Option<Box<str>>,
68 pub href: Option<Box<str>>,
70 pub chapter_index: Option<usize>,
72 pub source: Option<Box<str>>,
74 pub selector: Option<Box<str>>,
76 pub selector_index: Option<usize>,
78 pub declaration: Option<Box<str>>,
80 pub declaration_index: Option<usize>,
82 pub token_offset: Option<usize>,
84 pub limit: Option<Box<ErrorLimitContext>>,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct PhaseError {
91 pub phase: ErrorPhase,
93 pub code: &'static str,
95 pub message: Box<str>,
97 pub context: Option<Box<PhaseErrorContext>>,
99}
100
101impl PhaseError {
102 pub fn new(phase: ErrorPhase, code: &'static str, message: impl Into<String>) -> Self {
104 Self {
105 phase,
106 code,
107 message: message.into().into_boxed_str(),
108 context: None,
109 }
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
115#[non_exhaustive]
116pub enum EpubError {
117 Phase(PhaseError),
119 Zip(ZipError),
121 Parse(String),
123 InvalidEpub(String),
125 Navigation(String),
127 Css(String),
129 Io(String),
131 ChapterOutOfBounds {
133 index: usize,
135 chapter_count: usize,
137 },
138 ManifestItemMissing {
140 idref: String,
142 },
143 ChapterNotUtf8 {
145 href: String,
147 },
148 LimitExceeded {
150 kind: LimitKind,
152 actual: usize,
154 limit: usize,
156 path: Option<String>,
158 },
159 BufferTooSmall {
161 required: usize,
163 provided: usize,
165 context: String,
167 },
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172#[non_exhaustive]
173pub enum LimitKind {
174 FileSize,
176 MemoryBudget,
178 EventCount,
180 NestingDepth,
182 CssSize,
184 FontLimit,
186}
187
188impl fmt::Display for EpubError {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 match self {
191 EpubError::Phase(err) => write!(
192 f,
193 "{} error [{}]: {}",
194 err.phase.to_string().to_ascii_uppercase(),
195 err.code,
196 err.message
197 ),
198 EpubError::Zip(kind) => write!(f, "ZIP error: {}", kind),
199 EpubError::Parse(msg) => write!(f, "Parse error: {}", msg),
200 EpubError::InvalidEpub(msg) => write!(f, "Invalid EPUB: {}", msg),
201 EpubError::Navigation(msg) => write!(f, "Navigation error: {}", msg),
202 EpubError::Css(msg) => write!(f, "CSS error: {}", msg),
203 EpubError::Io(msg) => write!(f, "I/O error: {}", msg),
204 EpubError::ChapterOutOfBounds {
205 index,
206 chapter_count,
207 } => write!(
208 f,
209 "Chapter index {} out of bounds (chapter count: {})",
210 index, chapter_count
211 ),
212 EpubError::ManifestItemMissing { idref } => {
213 write!(f, "Spine item '{}' does not exist in manifest", idref)
214 }
215 EpubError::ChapterNotUtf8 { href } => {
216 write!(f, "Chapter content is not valid UTF-8: {}", href)
217 }
218 EpubError::LimitExceeded {
219 kind,
220 actual,
221 limit,
222 path,
223 } => {
224 write!(
225 f,
226 "{} limit exceeded: {} > {}{}",
227 kind,
228 actual,
229 limit,
230 path.as_ref()
231 .map(|p| format!(" at {}", p))
232 .unwrap_or_default()
233 )
234 }
235 EpubError::BufferTooSmall {
236 required,
237 provided,
238 context,
239 } => {
240 write!(
241 f,
242 "Buffer too small for {}: required {} bytes, provided {}",
243 context, required, provided
244 )
245 }
246 }
247 }
248}
249
250impl fmt::Display for LimitKind {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 match self {
253 LimitKind::FileSize => write!(f, "File size"),
254 LimitKind::MemoryBudget => write!(f, "Memory budget"),
255 LimitKind::EventCount => write!(f, "Event count"),
256 LimitKind::NestingDepth => write!(f, "Nesting depth"),
257 LimitKind::CssSize => write!(f, "CSS size"),
258 LimitKind::FontLimit => write!(f, "Font limit"),
259 }
260 }
261}
262
263#[derive(Debug, Clone, PartialEq, Eq)]
265#[non_exhaustive]
266pub enum ZipErrorKind {
267 FileNotFound,
269 InvalidFormat,
271 UnsupportedCompression,
273 DecompressError,
275 CrcMismatch,
277 IoError,
279 CentralDirFull,
281 BufferTooSmall,
283 FileTooLarge,
285 InvalidMimetype(String),
287 UnsupportedZip64,
289}
290
291pub type ZipError = ZipErrorKind;
293
294impl fmt::Display for ZipErrorKind {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 match self {
297 ZipErrorKind::FileNotFound => write!(f, "file not found in archive"),
298 ZipErrorKind::InvalidFormat => write!(f, "invalid ZIP format"),
299 ZipErrorKind::UnsupportedCompression => write!(f, "unsupported compression method"),
300 ZipErrorKind::DecompressError => write!(f, "decompression failed"),
301 ZipErrorKind::CrcMismatch => write!(f, "CRC32 checksum mismatch"),
302 ZipErrorKind::IoError => write!(f, "I/O error"),
303 ZipErrorKind::CentralDirFull => write!(f, "central directory full"),
304 ZipErrorKind::BufferTooSmall => write!(f, "buffer too small"),
305 ZipErrorKind::FileTooLarge => write!(f, "file too large"),
306 ZipErrorKind::InvalidMimetype(msg) => write!(f, "invalid mimetype: {}", msg),
307 ZipErrorKind::UnsupportedZip64 => write!(f, "ZIP64 is not supported"),
308 }
309 }
310}
311
312#[cfg(feature = "std")]
313impl std::error::Error for EpubError {}
314
315#[cfg(feature = "std")]
316impl std::error::Error for ZipErrorKind {}
317
318impl From<crate::tokenizer::TokenizeError> for EpubError {
319 fn from(err: crate::tokenizer::TokenizeError) -> Self {
320 EpubError::Parse(err.to_string())
321 }
322}
323
324impl From<PhaseError> for EpubError {
325 fn from(err: PhaseError) -> Self {
326 Self::Phase(err)
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_epub_error_display() {
336 let err = EpubError::Parse("bad xml".into());
337 assert_eq!(format!("{}", err), "Parse error: bad xml");
338 }
339
340 #[test]
341 fn test_phase_error_display() {
342 let err = EpubError::Phase(PhaseError::new(
343 ErrorPhase::Style,
344 "STYLE_LIMIT",
345 "limit exceeded",
346 ));
347 assert_eq!(
348 format!("{}", err),
349 "STYLE error [STYLE_LIMIT]: limit exceeded"
350 );
351 }
352
353 #[test]
354 fn test_zip_error_kind_debug() {
355 let kind = ZipErrorKind::FileNotFound;
356 assert_eq!(format!("{:?}", kind), "FileNotFound");
357 }
358
359 #[test]
360 fn test_invalid_mimetype_error() {
361 let err = EpubError::Zip(ZipErrorKind::InvalidMimetype("wrong content type".into()));
362 let display = format!("{}", err);
363 assert!(display.contains("ZIP error"));
364 }
365}