Skip to main content

thunderstore_engine/
error.rs

1#![deny(missing_docs)]
2
3use std::io;
4use std::num::ParseIntError;
5use thiserror::Error;
6
7/// A type alias for `Result<T, Error>` used throughout the engine.
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// Represents all possible errors that can occur within the Thunderstore engine.
11///
12/// This enum is `#[non_exhaustive]`: new variants may be added in future
13/// releases without a breaking change, so downstream `match`es must include a
14/// wildcard arm.
15#[derive(Error, Debug)]
16#[non_exhaustive]
17pub enum Error {
18  /// Error from the IO system.
19  #[error("IO error: {0}")]
20  Io(#[from] io::Error),
21
22  /// Error from HTTP operations.
23  #[error("HTTP error: {0}")]
24  Http(#[from] reqwest::Error),
25
26  /// Error from JSON parsing.
27  #[error("JSON error: {0}")]
28  Json(#[from] serde_json::Error),
29
30  /// Error from ZIP operations.
31  #[error("ZIP error: {0}")]
32  Zip(#[from] zip::result::ZipError),
33
34  /// Error from header parsing.
35  #[error("Header error: {0}")]
36  Header(#[from] reqwest::header::ToStrError),
37
38  /// Error from parsing integers.
39  #[error("Parse integer error: {0}")]
40  ParseInt(#[from] ParseIntError),
41
42  /// Missing header in response.
43  #[error("Missing header: {0}")]
44  MissingHeader(String),
45
46  /// Error from builder or configuration validation.
47  #[error("Configuration error: {0}")]
48  Config(String),
49
50  /// Error parsing a per-mod `manifest.json`.
51  #[error("manifest parse error: {0}")]
52  ManifestParse(String),
53
54  /// Error reading, writing, or validating the on-disk package-index cache.
55  ///
56  /// Covers cache load/decode/encode, a missing cache, an incompatible or
57  /// corrupt cache layout, and the integrity checks in
58  /// [`PackageIndex::validate`](crate::models::PackageIndex) — which run at the
59  /// cache boundary.
60  #[error("cache error: {0}")]
61  Cache(String),
62
63  /// A network body, manifest file, or decompressed payload exceeded a
64  /// configured size limit.
65  ///
66  /// `actual` is a structured field for programmatic inspection; it is
67  /// deliberately not rendered in `Display` because `thiserror` cannot
68  /// conditionally format an `Option`.
69  #[error("{context} exceeds size limit of {limit} bytes")]
70  SizeLimitExceeded {
71    /// A short label for what was being read (e.g. `"download body"`).
72    context: String,
73    /// The configured byte ceiling.
74    limit: u64,
75    /// The observed size, when known.
76    actual: Option<u64>,
77  },
78
79  /// A download ended before the declared number of bytes arrived.
80  #[error("incomplete download: expected {expected} bytes, received {actual}")]
81  IncompleteDownload {
82    /// The number of bytes the server declared.
83    expected: u64,
84    /// The number of bytes actually received.
85    actual: u64,
86  },
87
88  /// A download filename was unsafe (empty, absolute, or path-traversing).
89  #[error("unsafe name: {0}")]
90  UnsafeName(String),
91
92  /// The on-disk cache was written by an incompatible version of the cache
93  /// format. The client treats this as a cache miss and re-fetches.
94  #[error("unsupported cache format version {found} (expected {expected})")]
95  UnsupportedCacheVersion {
96    /// The format version read from the cache file.
97    found: u32,
98    /// The format version this build understands.
99    expected: u32,
100  },
101
102  /// Error from ecosystem-schema parsing.
103  #[error("Ecosystem error: {0}")]
104  Ecosystem(String),
105
106  /// Error from install routing, e.g. a package that would escape the install root.
107  #[error("Install error: {0}")]
108  Install(String),
109
110  /// Error from the profile layer: an invalid profile name, a malformed
111  /// `mods.yml`/state file, or a profile operation that could not complete.
112  #[error("Profile error: {0}")]
113  Profile(String),
114
115  /// A profile name that could not be used, from any operation that gates on
116  /// [`layout::require_profile`](crate::profile::layout::require_profile).
117  ///
118  /// Carries the reason rather than a sentence so an operation with other
119  /// failure modes, such as
120  /// [`layout::use_profile`](crate::profile::layout::use_profile), can still
121  /// return one `Error` without a caller losing the ability to word the refusal
122  /// itself. Renders exactly as [`Error::Profile`] did for the same refusal.
123  #[error("Profile error: {0}")]
124  BadProfile(#[from] crate::profile::layout::BadProfile),
125
126  /// One or more files in a batch download failed.
127  #[error("{failed} of {total} downloads failed; first error: {first}")]
128  Download {
129    /// How many files failed.
130    failed: usize,
131    /// How many files were attempted.
132    total: usize,
133    /// The first failure's message.
134    first: String,
135  },
136
137  /// A response header was present but could not be parsed, e.g. an
138  /// unparseable `Last-Modified` value.
139  #[error("invalid header: {0}")]
140  InvalidHeader(String),
141
142  /// An archive exceeded a configured extraction limit (entry count or
143  /// uncompressed size), e.g. a zip bomb.
144  #[error("Archive extraction limit exceeded: {0}")]
145  ExtractLimitExceeded(String),
146
147  /// CRC32 verification failed while extracting an archive entry.
148  #[error("CRC32 verification failed for {path}: expected {expected:08x}, got {actual:08x}")]
149  ChecksumMismatch {
150    /// The archive-relative path of the entry that failed verification.
151    path: String,
152    /// The CRC32 the archive recorded for the entry.
153    expected: u32,
154    /// The CRC32 computed over the bytes actually read.
155    actual: u32,
156  },
157}
158
159#[cfg(test)]
160mod tests {
161  use super::*;
162
163  #[test]
164  fn profile_error_displays_message() {
165    let err = Error::Profile("bad profile".to_string());
166
167    assert_eq!(err.to_string(), "Profile error: bad profile");
168  }
169}