Skip to main content

mdzip_core/
error.rs

1//! Errors with stable codes.
2//!
3//! Codes follow the MDZip specification's error categories (Section 16.5)
4//! where one exists, and match the other MDZip core libraries and the spec's
5//! shared fixtures. A few codes are specific to this library; they're marked
6//! below.
7
8use std::fmt;
9
10/// A stable, machine-readable error code, such as `ERR_ENTRYPOINT_MISSING`.
11///
12/// Compare codes, not messages: messages can change between releases, codes
13/// don't. [`ErrorCode::as_str`] gives the code as the spec writes it.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum ErrorCode {
17    /// Not a valid ZIP archive, or an entry is corrupt or uses an unsupported
18    /// compression method (Section 4).
19    ZipInvalid,
20    /// An entry is encrypted (Section 4).
21    ZipEncrypted,
22    /// An entry path breaks a rule in Section 5.4 not covered by a more
23    /// specific code below (for example an empty or `.` segment).
24    PathInvalid,
25    /// An entry path is empty.
26    PathEmpty,
27    /// An entry path starts with `/`.
28    PathLeadingSlash,
29    /// An entry path has a `..` segment.
30    PathTraversal,
31    /// An entry path has a reserved character: `\ : * ? " < > |`.
32    PathReservedChar,
33    /// An entry path has a control character (U+0000–U+001F, U+007F).
34    PathControlChar,
35    /// Two entries have the same path after NFC normalization.
36    DuplicatePath,
37    /// A file's path is also the directory of another entry, such as `docs`
38    /// and `docs/intro.md`.
39    PathConflict,
40    /// `manifest.json` is unreadable, or a critical field has the wrong type
41    /// or value (Section 6.4).
42    ManifestInvalid,
43    /// The manifest `entryPoint` isn't a valid entry point path (Section 5.5).
44    EntryPointInvalid,
45    /// The manifest `entryPoint` is valid but matches no entry (Section 5.5).
46    EntryPointMissing,
47    /// No unambiguous entry point could be found (Section 5.5, rule 4).
48    EntryPointUnresolved,
49    /// The archive targets a newer major version of the spec (Section 11).
50    VersionUnsupported,
51    /// The manifest `mode` isn't `document` or `project` (Section 5.7.5).
52    ModeUnsupported,
53    /// A resource limit (entry count, size, compression ratio) was exceeded
54    /// (Section 13.1).
55    LimitExceeded,
56    /// Library-specific: an update would change or break which file the entry
57    /// point resolves to (Section 14.1).
58    EntryPointChanged,
59    /// Library-specific: a requested entry isn't in the archive.
60    NotFound,
61    /// Library-specific: an entry read as text isn't valid UTF-8 (Section 7.1).
62    TextInvalid,
63    /// Library-specific: reading or writing a file on disk failed.
64    Io,
65}
66
67impl ErrorCode {
68    /// The code as the spec writes it, for example `"ERR_ZIP_INVALID"`.
69    pub const fn as_str(self) -> &'static str {
70        match self {
71            ErrorCode::ZipInvalid => "ERR_ZIP_INVALID",
72            ErrorCode::ZipEncrypted => "ERR_ZIP_ENCRYPTED",
73            ErrorCode::PathInvalid => "ERR_PATH_INVALID",
74            ErrorCode::PathEmpty => "ERR_PATH_EMPTY",
75            ErrorCode::PathLeadingSlash => "ERR_PATH_LEADING_SLASH",
76            ErrorCode::PathTraversal => "ERR_PATH_TRAVERSAL",
77            ErrorCode::PathReservedChar => "ERR_PATH_RESERVED_CHAR",
78            ErrorCode::PathControlChar => "ERR_PATH_CONTROL_CHAR",
79            ErrorCode::DuplicatePath => "ERR_DUPLICATE_PATH",
80            ErrorCode::PathConflict => "ERR_PATH_CONFLICT",
81            ErrorCode::ManifestInvalid => "ERR_MANIFEST_INVALID",
82            ErrorCode::EntryPointInvalid => "ERR_ENTRYPOINT_INVALID",
83            ErrorCode::EntryPointMissing => "ERR_ENTRYPOINT_MISSING",
84            ErrorCode::EntryPointUnresolved => "ERR_ENTRYPOINT_UNRESOLVED",
85            ErrorCode::VersionUnsupported => "ERR_VERSION_UNSUPPORTED",
86            ErrorCode::ModeUnsupported => "ERR_MODE_UNSUPPORTED",
87            ErrorCode::LimitExceeded => "ERR_LIMIT_EXCEEDED",
88            ErrorCode::EntryPointChanged => "ERR_ENTRYPOINT_CHANGED",
89            ErrorCode::NotFound => "ERR_NOT_FOUND",
90            ErrorCode::TextInvalid => "ERR_TEXT_INVALID",
91            ErrorCode::Io => "ERR_IO",
92        }
93    }
94}
95
96impl fmt::Display for ErrorCode {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.write_str(self.as_str())
99    }
100}
101
102/// An MDZip error: a stable [`ErrorCode`] and a human-readable message.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct MdzError {
105    code: ErrorCode,
106    message: String,
107}
108
109impl MdzError {
110    /// A new error with `code` and `message`.
111    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
112        MdzError {
113            code,
114            message: message.into(),
115        }
116    }
117
118    /// The stable error code.
119    pub fn code(&self) -> ErrorCode {
120        self.code
121    }
122
123    /// The human-readable message, without the code.
124    pub fn message(&self) -> &str {
125        &self.message
126    }
127}
128
129impl fmt::Display for MdzError {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        write!(f, "{}: {}", self.code, self.message)
132    }
133}
134
135impl std::error::Error for MdzError {}
136
137/// `Result` with [`MdzError`].
138pub type Result<T, E = MdzError> = std::result::Result<T, E>;
139
140pub(crate) fn err<T>(code: ErrorCode, message: impl Into<String>) -> Result<T> {
141    Err(MdzError::new(code, message))
142}