Skip to main content

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}