Skip to main content

quilt_rs/
error.rs

1use std::path::PathBuf;
2use std::path::StripPrefixError;
3use std::str::Utf8Error;
4
5use aws_smithy_types::byte_stream;
6use reqwest::header::ToStrError;
7use thiserror::Error;
8
9use crate::io::remote::HostChecksums;
10use quilt_uri::Host;
11use quilt_uri::Namespace;
12use quilt_uri::UriError;
13
14#[derive(Error, Debug)]
15#[error("S3 error{}: {kind}", .host.as_ref().map_or(String::new(), |h| format!(" for {h}")))]
16pub struct S3Error {
17    pub host: Option<Host>,
18    #[source]
19    pub kind: S3ErrorKind,
20}
21
22impl S3Error {
23    pub fn new(kind: S3ErrorKind) -> Self {
24        Self { host: None, kind }
25    }
26
27    pub fn is_not_found(&self) -> bool {
28        matches!(self.kind, S3ErrorKind::NotFound(_))
29    }
30}
31
32#[derive(Error, Debug, PartialEq)]
33pub enum S3ErrorKind {
34    #[error("Failed to check object existence: {0}")]
35    Exists(String),
36
37    #[error("Failed to get object: {0}")]
38    GetObject(String),
39
40    #[error("Failed to get object attributes: {0}")]
41    GetObjectAttributes(String),
42
43    #[error("Failed to get object stream: {0}")]
44    GetObjectStream(String),
45
46    #[error("Failed to initialize S3 client: {0}")]
47    Client(String),
48
49    #[error("Failed to list objects: {0}")]
50    ListObjects(String),
51
52    #[error("Failed to put object: {0}")]
53    PutObject(String),
54
55    #[error("Failed to resolve object URL: {0}")]
56    ResolveUrl(String),
57
58    #[error("Failed to upload object: {0}")]
59    UploadFile(String),
60
61    #[error("S3 not found: {0}")]
62    NotFound(String),
63
64    #[error("S3 error: {0}")]
65    Raw(String),
66
67    #[error("Failed to initialize S3 Remote")]
68    RemoteInit,
69
70    #[error("Object key expected to be present")]
71    ObjectKey,
72
73    #[error("Error with upload id: {0}")]
74    UploadId(String),
75
76    #[error("Failed to read RwLock: {0}")]
77    PoisonLock(String),
78}
79
80#[derive(Error, Debug, PartialEq)]
81pub enum AuthError {
82    #[error("Failed to read credentials: {0}")]
83    CredentialsRead(String),
84
85    #[error("Failed to refresh credentials: {0}")]
86    CredentialsRefresh(String),
87
88    #[error("Failed to read tokens: {0}")]
89    TokensRead(String),
90
91    #[error("Failed to refresh tokens: {0}")]
92    TokensRefresh(String),
93
94    #[error("Failed to exchange authorization code for tokens: {0}")]
95    TokensExchange(String),
96}
97
98#[derive(Error, Debug, PartialEq)]
99pub enum InstallPackageError {
100    #[error("The package {0} is already installed")]
101    AlreadyInstalled(Namespace),
102
103    #[error("The given package is not installed: {0}")]
104    NotInstalled(Namespace),
105}
106
107#[derive(Error, Debug, PartialEq)]
108pub enum InstallPathError {
109    #[error("Failed to install path: {}", .0.display())]
110    Install(PathBuf),
111
112    #[error("Some paths are already installed")]
113    AlreadyInstalled,
114
115    #[error("Failed to uninstall path: {}", .0.display())]
116    Uninstall(PathBuf),
117}
118
119#[derive(Error, Debug)]
120pub enum ChecksumError {
121    #[error("Checksum error: {0}")]
122    Mismatch(String),
123
124    #[error("Missing checksum: {0:?}")]
125    Missing(HostChecksums),
126
127    #[error("Malformed checksum: {0}")]
128    Malformed(String),
129
130    #[error("Invalid multihash: {0}")]
131    InvalidMultihash(String),
132
133    #[error("Failed to get checksum from S3: {0}")]
134    NoS3Checksum(String),
135
136    #[error("Multihash error: {0}")]
137    Multihash(#[from] multihash::Error),
138
139    #[error("Multibase error: {0}")]
140    Multibase(#[from] multibase::Error),
141}
142
143#[derive(Error, Debug)]
144pub enum ManifestError {
145    #[error("Manifest header: {0}")]
146    Header(String),
147
148    #[error("Failed to load manifest from {path}: {source}")]
149    Load {
150        path: PathBuf,
151        source: Box<crate::Error>,
152    },
153
154    #[error("Table error: {0}")]
155    Table(String),
156}
157
158#[derive(Error, Debug)]
159pub enum LineageError {
160    #[error("Domain lineage missing, including missing Home directory")]
161    Missing,
162
163    #[error("Domain lineage missing Home directory")]
164    MissingHome,
165
166    #[error("Failed to parse lineage file: {0}")]
167    Parse(serde_json::Error),
168
169    #[error("Operation requires a remote origin, but this is a local-only package")]
170    NoRemote,
171}
172
173#[derive(Error, Debug, PartialEq)]
174pub enum RemoteCatalogError {
175    #[error("Workflow error: {0}")]
176    Workflow(String),
177
178    #[error("Failed to fetch host config: {0}")]
179    HostConfig(String),
180
181    #[error("S3 bucket '{0}' is not reachable — verify the bucket name")]
182    BucketUnreachable(String),
183}
184
185#[derive(Error, Debug, PartialEq)]
186pub enum LoginError {
187    #[error("Login required{}", .0.as_ref().map_or(String::new(), |h| format!(": {h}")))]
188    Required(Option<Host>),
189
190    #[error("Failed to get registry URL from {0}. Does {0}/config.json have it?")]
191    RequiredRegistryUrl(Host),
192}
193
194#[derive(Error, Debug)]
195pub enum FsError {
196    #[error("Failed to read file {path}: {source}")]
197    Read {
198        path: PathBuf,
199        source: std::io::Error,
200    },
201
202    #[error("Failed to write file {path}: {source}")]
203    Write {
204        path: PathBuf,
205        source: std::io::Error,
206    },
207
208    #[error("Failed to copy file from {from} to {to}: {source}")]
209    Copy {
210        from: PathBuf,
211        to: PathBuf,
212        source: std::io::Error,
213    },
214
215    #[error("Failed to create directory {path}: {source}")]
216    DirectoryCreate {
217        path: PathBuf,
218        source: std::io::Error,
219    },
220
221    #[error("File not found: {path}")]
222    NotFound { path: PathBuf },
223
224    #[error("Path prefix not found: {0}")]
225    PathPrefixNotFound(StripPrefixError),
226
227    #[error("ByteStream error: {0}")]
228    ByteStream(#[from] byte_stream::error::Error),
229}
230
231#[derive(Error, Debug, PartialEq)]
232pub enum PackageOpError {
233    #[error("Commit error: {0}")]
234    Commit(String),
235
236    #[error("Push error: {0}")]
237    Push(String),
238
239    #[error("Publish error: {0}")]
240    Publish(String),
241
242    #[error("General error regarding package: {0}")]
243    Package(String),
244}
245
246/// The error type for this library
247#[derive(Error, Debug)]
248pub enum Error {
249    #[error("Authentication failed for {0}: {1}")]
250    Auth(Host, AuthError),
251
252    #[error(transparent)]
253    Checksum(#[from] ChecksumError),
254
255    #[error(transparent)]
256    Fs(#[from] FsError),
257
258    #[error(transparent)]
259    InstallPackage(InstallPackageError),
260
261    #[error(transparent)]
262    InstallPath(InstallPathError),
263
264    #[error("IO error: {0}")]
265    Io(#[from] std::io::Error),
266
267    #[error("JSON error: {0}")]
268    Json(#[from] serde_json::Error),
269
270    #[error(transparent)]
271    Lineage(#[from] LineageError),
272
273    #[error(transparent)]
274    Login(#[from] LoginError),
275
276    #[error(transparent)]
277    Manifest(#[from] ManifestError),
278
279    #[error(transparent)]
280    PackageOp(#[from] PackageOpError),
281
282    #[error("Reqwest error: {0}")]
283    Reqwest(#[from] reqwest::Error),
284
285    #[error(transparent)]
286    RemoteCatalog(#[from] RemoteCatalogError),
287
288    #[error(transparent)]
289    S3(#[from] S3Error),
290
291    #[error("Cannot convert to string: {0}")]
292    ToString(#[from] ToStrError),
293
294    #[error("Integer conversion error: {0}")]
295    TryFromIntError(#[from] std::num::TryFromIntError),
296
297    #[error("Unimplemented")]
298    Unimplemented,
299
300    #[error(transparent)]
301    Uri(#[from] UriError),
302
303    #[error("Error parsing URL: {0}")]
304    UrlParse(#[from] url::ParseError),
305
306    #[error("UTF-8 error: {0}")]
307    Utf8(#[from] Utf8Error),
308
309    #[error("YAML error: {0}")]
310    Yaml(#[from] serde_yaml::Error),
311}
312
313impl Error {
314    /// Returns `true` if this error represents an S3 "not found" (NoSuchKey) response.
315    pub fn is_not_found(&self) -> bool {
316        matches!(self, Error::S3(s3) if s3.is_not_found())
317    }
318}
319
320// Compose `?` across two From hops: external error → focused enum → Error.
321// Rust's `?` only runs one `From::from`, so these bridges make call sites
322// keep working without `.map_err(..)`.
323
324impl From<multihash::Error> for Error {
325    fn from(err: multihash::Error) -> Self {
326        Error::Checksum(ChecksumError::Multihash(err))
327    }
328}
329
330impl From<multibase::Error> for Error {
331    fn from(err: multibase::Error) -> Self {
332        Error::Checksum(ChecksumError::Multibase(err))
333    }
334}
335
336impl From<byte_stream::error::Error> for Error {
337    fn from(err: byte_stream::error::Error) -> Self {
338        FsError::ByteStream(err).into()
339    }
340}
341
342impl From<StripPrefixError> for Error {
343    fn from(err: StripPrefixError) -> Self {
344        Error::Fs(FsError::PathPrefixNotFound(err))
345    }
346}