Skip to main content

alpm_types/
error.rs

1use std::path::PathBuf;
2
3use alpm_parsers::error::{Input, ParseStack};
4use fluent_i18n::t;
5use winnow::error::ParseError;
6
7use crate::Architecture;
8
9/// The library's error type
10///
11/// These errors are usually parsing errors and they each contain a context
12/// about why the error has occurred and the value that caused the error.
13///
14/// The original error is also included in the variants that have the `source` field.
15/// You can access it using the `source()` method.
16/// See [Error::source](https://doc.rust-lang.org/std/error/trait.Error.html#method.source) for
17/// more information.
18#[derive(Debug, thiserror::Error)]
19pub enum Error {
20    /// An error occurred when handling a database.
21    #[error("{msg}", msg = t!("error-database", { "error" => .0.to_string() }))]
22    Database(#[from] crate::database::Error),
23
24    /// Combination of architectures that is invalid.
25    #[error("{msg}", msg = t!("error-invalid-architectures", {
26        "architectures" => format!("{architectures:?}"),
27        "context" => context
28    }))]
29    InvalidArchitectures {
30        /// The invalid architectures combination.
31        architectures: Vec<Architecture>,
32        /// The reason why the architectures are invalid.
33        context: &'static str,
34    },
35
36    /// An invalid integer
37    #[error("{msg}", msg = t!("error-invalid-integer", { "kind" => format!("{kind:?}") }))]
38    InvalidInteger {
39        /// The reason for the invalid integer.
40        kind: std::num::IntErrorKind,
41    },
42
43    /// An invalid enum variant
44    #[error("{msg}", msg = t!("error-invalid-variant", { "error" => .0.to_string() }))]
45    InvalidVariant(#[from] strum::ParseError),
46
47    /// An invalid email address
48    #[error("{msg}", msg = t!("error-invalid-email", { "error" => .0.to_string() }))]
49    InvalidEmail(#[from] email_address::Error),
50
51    /// An invalid URL
52    #[error("{msg}", msg = t!("error-invalid-url", { "error" => .0.to_string() }))]
53    InvalidUrl(#[from] url::ParseError),
54
55    /// An invalid license
56    #[error("{msg}", msg = t!("error-invalid-license", { "error" => .0.to_string() }))]
57    InvalidLicense(#[from] spdx::ParseError),
58
59    /// An invalid semantic version string
60    ///
61    /// This error occurs when a semantic version cannot be parsed from a string.
62    /// We cannot use `#[source] semver::Error` here because it does not implement `PartialEq`.
63    /// See: <https://github.com/dtolnay/semver/issues/326>
64    ///
65    /// TODO: Use the error source when the issue above is resolved.
66    #[error("{msg}", msg = t!("error-invalid-semver", { "kind" => kind }))]
67    InvalidSemver {
68        /// The reason for the invalid semantic version.
69        kind: String,
70    },
71
72    /// An I/O error occurred at a path.
73    #[error("{msg}", msg = t!("error-io-path", {
74        "path" => path.display().to_string(),
75        "context" => context,
76        "source" => source.to_string(),
77    }))]
78    IoPath {
79        /// The path at which the error occurred.
80        path: PathBuf,
81        /// The context in which the error occurred.
82        ///
83        /// This is meant to complete the sentence "I/O error at path while ".
84        context: String,
85        /// The source error.
86        source: std::io::Error,
87    },
88
89    /// Value contains invalid characters
90    #[error("{msg}", msg = t!("error-invalid-chars", { "invalid_char" => invalid_char.to_string() }))]
91    ValueContainsInvalidChars {
92        /// The invalid character
93        invalid_char: char,
94    },
95
96    /// Value length is incorrect
97    #[error("{msg}", msg = t!("error-incorrect-length", { "length" => length, "expected" => expected }))]
98    IncorrectLength {
99        /// The incorrect length.
100        length: usize,
101        /// The expected length.
102        expected: usize,
103    },
104
105    /// Value is missing a delimiter character
106    #[error("{msg}", msg = t!("error-delimiter-not-found", { "delimiter" => delimiter.to_string() }))]
107    DelimiterNotFound {
108        /// The required delimiter.
109        delimiter: char,
110    },
111
112    /// Value does not match the restrictions
113    #[error("{msg}", msg = t!("error-restrictions-not-met", {
114        "restrictions" => format!("{restrictions:?}")
115    }))]
116    ValueDoesNotMatchRestrictions {
117        /// The list of restrictions that cannot be met.
118        restrictions: Vec<String>,
119    },
120
121    /// A validation regex does not match the value
122    #[error("{msg}", msg = t!("error-regex-mismatch", {
123        "value" => value,
124        "regex_type" => regex_type,
125        "regex" => regex
126    }))]
127    RegexDoesNotMatch {
128        /// The value that does not match.
129        value: String,
130        /// The type of regular expression applied to the `value`.
131        regex_type: String,
132        /// The regular expression applied to the `value`.
133        regex: String,
134    },
135
136    /// A winnow parser for a type didn't work and produced an error.
137    #[error("{msg}", msg = t!("error-parse", { "error" => .0 }))]
138    ParseError(String),
139
140    /// Missing field in a value
141    #[error("{msg}", msg = t!("error-missing-component", { "component" => component }))]
142    MissingComponent {
143        /// The component that is missing.
144        component: &'static str,
145    },
146
147    /// An invalid absolute path (i.e. does not start with a `/`)
148    #[error("{msg}", msg = t!("error-path-not-absolute", { "path" => .0 }))]
149    PathNotAbsolute(PathBuf),
150
151    /// An invalid relative path (i.e. starts with a `/`)
152    #[error("{msg}", msg = t!("error-path-not-relative", { "path" => .0 }))]
153    PathNotRelative(PathBuf),
154
155    /// An invalid relative sibling path (i.e. starts with a `./` or contains directory)
156    #[error("{msg}", msg = t!("error-path-not-sibling", { "path" => .0 }))]
157    PathNotSibling(PathBuf),
158
159    /// Expected a file, but got a directory
160    #[error("{msg}", msg = t!("error-path-not-file", { "path" => .0 }))]
161    PathIsNotAFile(PathBuf),
162
163    /// File name contains invalid characters
164    #[error("{msg}", msg = t!("error-filename-invalid-chars", {
165        "path" => .0,
166        "invalid_char" => .1.to_string()
167    }))]
168    FileNameContainsInvalidChars(PathBuf, char),
169
170    /// File name is empty
171    #[error("{msg}", msg = t!("error-filename-empty"))]
172    FileNameIsEmpty,
173
174    /// A deprecated license
175    #[error("{msg}", msg = t!("error-deprecated-license", { "license" => .0 }))]
176    DeprecatedLicense(String),
177
178    /// A component is invalid and cannot be used.
179    #[error("{msg}", msg = t!("error-invalid-component", {
180        "component" => component,
181        "context" => context
182    }))]
183    InvalidComponent {
184        /// The invalid component.
185        component: &'static str,
186        /// The context in which the error occurs.
187        ///
188        /// This is meant to complete the sentence
189        /// "Invalid component {component} encountered while ".
190        context: String,
191    },
192
193    /// An invalid OpenPGP v4 fingerprint
194    #[error("{msg}", msg = t!("error-invalid-pgp-fingerprint"))]
195    InvalidOpenPGPv4Fingerprint,
196
197    /// An invalid OpenPGP key ID
198    #[error("{msg}", msg = t!("error-invalid-pgp-keyid", { "keyid" => .0 }))]
199    InvalidOpenPGPKeyId(String),
200
201    /// An invalid OpenPGP signature
202    #[error("{msg}", msg = t!("error-invalid-base64-encoding", { "expected_item" => expected_item }))]
203    InvalidBase64Encoding {
204        /// The expected item that could not be decoded.
205        expected_item: String,
206    },
207
208    /// An invalid shared object name (v1)
209    #[error("{msg}", msg = t!("error-invalid-soname-v1", { "name" => .0 }))]
210    InvalidSonameV1(&'static str),
211
212    /// A package data error.
213    #[error("{msg}", msg = t!("error-package", { "error" => .0.to_string() }))]
214    Package(#[from] crate::PackageError),
215
216    /// A string represents an unknown compression algorithm file extension.
217    #[error("{msg}", msg = t!("error-unknown-compression", { "value" => value }))]
218    UnknownCompressionAlgorithmFileExtension {
219        /// A string representing an unknown compression algorithm file extension.
220        value: String,
221    },
222
223    /// A string represents an unknown file type identifier.
224    #[error("{msg}", msg = t!("error-unknown-filetype", { "value" => value }))]
225    UnknownFileTypeIdentifier {
226        /// A string representing an unknown file type identifier.
227        value: String,
228    },
229}
230
231impl From<std::num::ParseIntError> for crate::error::Error {
232    /// Converts a [`std::num::ParseIntError`] into an [`Error::InvalidInteger`].
233    fn from(e: std::num::ParseIntError) -> Self {
234        Self::InvalidInteger { kind: *e.kind() }
235    }
236}
237
238impl<'a> From<ParseError<Input<'a>, ParseStack<'a>>> for crate::error::Error {
239    /// Converts a [`ParseError`] into an [`Error::ParseError`].
240    fn from(value: ParseError<Input<'a>, ParseStack<'a>>) -> Self {
241        // Only take the **inner** parser error of our own ParseStack error type.
242        // We don't want to hit the `Display` imple of `ParseError`.
243        Self::ParseError(value.into_inner().to_string())
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use std::num::IntErrorKind;
250
251    use rstest::rstest;
252
253    use super::*;
254
255    #[rstest]
256    #[case(
257        "Invalid integer (caused by InvalidDigit)",
258        Error::InvalidInteger {
259            kind: IntErrorKind::InvalidDigit
260        }
261    )]
262    #[case(
263        "Invalid integer (caused by InvalidDigit)",
264        Error::InvalidInteger {
265            kind: IntErrorKind::InvalidDigit
266        }
267    )]
268    #[case(
269        "Invalid integer (caused by PosOverflow)",
270        Error::InvalidInteger {
271            kind: IntErrorKind::PosOverflow
272        }
273    )]
274    #[allow(deprecated)]
275    #[case(
276        "Invalid integer (caused by InvalidDigit)",
277        Error::InvalidInteger {
278            kind: IntErrorKind::InvalidDigit
279        }
280    )]
281    #[case(
282        "Invalid e-mail (Missing separator character '@'.)",
283        email_address::Error::MissingSeparator.into()
284    )]
285    #[case(
286        "Invalid integer (caused by InvalidDigit)",
287        Error::InvalidInteger {
288            kind: IntErrorKind::InvalidDigit
289        }
290    )]
291    fn error_format_string(#[case] error_str: &str, #[case] error: Error) {
292        assert_eq!(error_str, format!("{error}"));
293    }
294}