Skip to main content

kc/
error.rs

1//! Errors for keychain parsing, crypto, and item lookup.
2
3use std::path::Path;
4
5pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(Debug, thiserror::Error)]
8pub enum Error {
9    #[error("not a keychain database: the file does not start with \"kych\"")]
10    NotAKeychain,
11
12    #[error("keychain is truncated: wanted {length} bytes at offset {offset}")]
13    Truncated { offset: usize, length: usize },
14
15    #[error("malformed keychain: {0}")]
16    Format(String),
17
18    #[error("keychain has no {0} table")]
19    MissingTable(&'static str),
20
21    #[error("incorrect password, or this keychain was not encrypted with one")]
22    WrongPassword,
23
24    #[error("the keychain is locked; unlock it with a password first")]
25    Locked,
26
27    /// The item's wrapped key is missing from the keychain's key table.
28    #[error("no key in this keychain can decrypt that item (label {label})")]
29    MissingItemKey { label: String },
30
31    #[error("{0}")]
32    Crypto(&'static str),
33
34    #[error("no item matched")]
35    NoSuchItem,
36
37    #[error("an item with those attributes already exists")]
38    DuplicateItem,
39
40    #[error("{context}: {source}")]
41    Io {
42        context: String,
43        #[source]
44        source: std::io::Error,
45    },
46
47    #[error("{0}")]
48    Other(String),
49}
50
51impl Error {
52    pub fn truncated(offset: usize, length: usize) -> Self {
53        Self::Truncated { offset, length }
54    }
55
56    pub fn format(message: impl Into<String>) -> Self {
57        Self::Format(message.into())
58    }
59
60    pub fn other(message: impl Into<String>) -> Self {
61        Self::Other(message.into())
62    }
63
64    pub fn io(context: impl Into<String>, source: std::io::Error) -> Self {
65        Self::Io {
66            context: context.into(),
67            source,
68        }
69    }
70
71    pub fn reading(path: &Path, source: std::io::Error) -> Self {
72        Self::io(format!("could not read {}", path.display()), source)
73    }
74
75    /// Exit code for the CLI: distinguishes "no match" and "wrong password"
76    /// from everything else, the way `security` does.
77    pub fn exit_code(&self) -> u8 {
78        match self {
79            Self::NoSuchItem => 44,
80            Self::WrongPassword | Self::Locked => 45,
81            Self::DuplicateItem => 46,
82            _ => 1,
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn exit_codes_separate_the_common_outcomes() {
93        assert_eq!(Error::NoSuchItem.exit_code(), 44);
94        assert_eq!(Error::WrongPassword.exit_code(), 45);
95        assert_eq!(Error::DuplicateItem.exit_code(), 46);
96        assert_eq!(Error::NotAKeychain.exit_code(), 1);
97    }
98
99    #[test]
100    fn messages_name_the_problem() {
101        assert!(Error::truncated(12, 4).to_string().contains("offset 12"));
102        assert!(Error::NotAKeychain.to_string().contains("kych"));
103    }
104}