hyprforge_image/error.rs
1//! What can go wrong, kept apart.
2//!
3//! Three states, and collapsing any two of them costs the user the one
4//! sentence that would have told them what to do — the rule CLAUDE.md
5//! records about never reading "this file could not be read" as "there is
6//! nothing here".
7
8use std::path::PathBuf;
9
10/// Why a picture could not be shown.
11#[derive(Debug, thiserror::Error)]
12pub enum ImageError {
13 /// The file could not be read at all: gone, or not permitted.
14 #[error("couldn't read {path}: {source}")]
15 Unreadable {
16 path: PathBuf,
17 #[source]
18 source: std::io::Error,
19 },
20
21 /// The file is there and readable, and is not a picture this build
22 /// can decode — a format nobody enabled, or bytes that are not an
23 /// image at all.
24 ///
25 /// Distinct from [`ImageError::Unreadable`] because the two need
26 /// different sentences: one is "this file is missing", the other is
27 /// "this file is not something I can open", and a viewer that says
28 /// the first about the second sends the user looking for a problem
29 /// with their disk.
30 #[error("{path} isn't an image this build can decode: {source}")]
31 Undecodable {
32 path: PathBuf,
33 #[source]
34 source: image::ImageError,
35 },
36
37 /// The header parsed and claims dimensions past what may be decoded
38 /// at all — see [`crate::budget`].
39 ///
40 /// Its own case rather than an `Undecodable`, because nothing is
41 /// wrong with the file: this is a refusal, and it is the one failure
42 /// here that a bigger machine would not have.
43 #[error("{path} is {width}x{height}, which is too large to decode safely")]
44 TooLarge { path: PathBuf, width: u32, height: u32 },
45}
46
47impl ImageError {
48 /// The file this is about, for a message that names it.
49 pub fn path(&self) -> &std::path::Path {
50 match self {
51 ImageError::Unreadable { path, .. }
52 | ImageError::Undecodable { path, .. }
53 | ImageError::TooLarge { path, .. } => path,
54 }
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use std::io::ErrorKind;
62
63 /// The distinction this module exists for, pinned as text: the two
64 /// messages must not read the same, or the state they describe has
65 /// been collapsed in practice however separate the types are.
66 #[test]
67 fn a_missing_file_and_an_unsupported_one_do_not_say_the_same_thing() {
68 let missing = ImageError::Unreadable {
69 path: PathBuf::from("/x/gone.png"),
70 source: std::io::Error::new(ErrorKind::NotFound, "no such file"),
71 };
72 let unsupported = ImageError::TooLarge {
73 path: PathBuf::from("/x/huge.png"),
74 width: 200_000,
75 height: 4,
76 };
77 assert_ne!(missing.to_string(), unsupported.to_string());
78 assert!(missing.to_string().contains("gone.png"));
79 assert!(unsupported.to_string().contains("200000"));
80 }
81
82 /// Every message names the file. A viewer with several pictures open
83 /// and a message that does not say which one is a message about
84 /// nothing.
85 #[test]
86 fn every_failure_names_the_file_it_is_about() {
87 let e = ImageError::Unreadable {
88 path: PathBuf::from("/photos/a.jpg"),
89 source: std::io::Error::new(ErrorKind::PermissionDenied, "denied"),
90 };
91 assert_eq!(e.path(), std::path::Path::new("/photos/a.jpg"));
92 assert!(e.to_string().contains("/photos/a.jpg"));
93 }
94}