1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//! Error type for the EarthSciIO cache/transport/store machinery.
use std::path::PathBuf;
/// Convenience alias for fallible EarthSciIO operations.
pub type Result<T> = std::result::Result<T, Error>;
/// Errors raised by the cache, transports, and stores.
///
/// `CacheMiss` is load-bearing for the offline contract: it carries both the
/// resolved URL and its cache key so a failure names exactly which blob the
/// corpus/cache is missing (see `spec/offline-mode.md` §2).
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// A cache-only read (offline mode, or an offline store) found no blob for
/// the resolved URL's key. Never a silent empty result, never a fallback
/// fetch (`spec/offline-mode.md` §2).
CacheMiss {
/// The resolved URL whose blob is absent.
url: String,
/// The cache key (`sha256(url)`) that was looked up.
key: String,
},
/// An on-disk blob failed its stored `sha256_content` or byte-length check.
Integrity {
/// The cache key of the failing blob.
key: String,
/// What mismatched (size or hash, with both values).
detail: String,
},
/// No transport is registered for a URL scheme — a registration gap, not a
/// Provider change (`spec/registries.md` §1).
UnknownScheme {
/// The unhandled URL scheme.
scheme: String,
/// The URL whose scheme had no transport.
url: String,
},
/// No store is registered under the configured name (`spec/registries.md` §3).
UnknownStore {
/// The configured store name that was not found.
name: String,
},
/// A resolved URL could not be parsed or had no usable scheme.
BadUrl {
/// The offending URL.
url: String,
/// Why it could not be used.
detail: String,
},
/// A transport-level failure (network error, non-success HTTP status, a
/// missing local `file://` source, …).
Transport {
/// The URL being fetched.
url: String,
/// The underlying transport failure.
detail: String,
},
/// The source answered, and the answer is **the object does not exist**
/// (HTTP 404/410, a missing local `file://` path).
///
/// Distinct from [`Error::Transport`] because a *probing* consumer must be
/// able to tell absence from a failure to find out. A zarr store has to
/// report a missing key as `Ok(None)` — zarr opens an array by probing the
/// v3 `zarr.json` before falling back to the v2 `.zarray`, so treating that
/// 404 as an error makes every v2 store unreadable. Reporting "absent" for
/// a timeout or a 5xx would be the opposite mistake: a live store would
/// silently read as empty.
NotFound {
/// The URL that does not exist.
url: String,
/// How the absence was reported (e.g. `HTTP 404`).
detail: String,
},
/// Every source (the primary URL plus any failover mirrors) failed.
AllMirrorsFailed {
/// The canonical resolved URL.
url: String,
/// The last underlying failure encountered.
detail: String,
/// True only when EVERY source reported a definitive absence. One
/// transient failure among the mirrors makes the outcome unknown.
not_found: bool,
},
/// An authenticated realm was requested but no resolver is registered for it.
MissingAuth {
/// The realm with no registered resolver.
realm: String,
},
/// An I/O error, tagged with the path that triggered it when known.
Io {
/// The path the I/O error concerns, if known.
path: Option<PathBuf>,
/// The underlying I/O error.
source: std::io::Error,
},
/// Manifest JSON could not be (de)serialized.
Manifest {
/// The (de)serialization failure detail.
detail: String,
},
/// No reader is registered under the configured format name — a registration
/// gap, not a Provider change (`spec/registries.md` §2).
UnknownFormat {
/// The configured format name that was not found.
name: String,
},
/// A configuration variable this crate reads is set to something it cannot
/// use.
///
/// Deliberately an error and not a fallback to the unconfigured default: the
/// variables that reach here state how a store is to be *reached*, so
/// ignoring a malformed one produces a read that goes out with the wrong
/// identity and fails much later, somewhere that names neither the variable
/// nor the mistake.
BadConfig {
/// The environment variable at fault.
var: String,
/// Why its value could not be used.
detail: String,
},
/// A format reader failed to decode a cached blob (bad magic, truncated file,
/// an unsupported on-disk type, …). Decode parity is `spec/conformance.md` §3.
Format {
/// The reader's format name (e.g. `netcdf`).
format: String,
/// What went wrong while decoding.
detail: String,
},
}
impl Error {
/// Wrap an I/O error together with the path it concerns.
pub fn io(path: Option<PathBuf>, source: std::io::Error) -> Self {
Error::Io { path, source }
}
/// True when this is a [`Error::CacheMiss`] — the offline "absent" signal.
pub fn is_cache_miss(&self) -> bool {
matches!(self, Error::CacheMiss { .. })
}
/// True when the source DEFINITIVELY reported that the object does not
/// exist — a 404/410, a missing local file, or an all-mirrors failure in
/// which every source said so.
///
/// A probing consumer (a zarr store answering "is this key present?")
/// treats this as absence; every other failure stays an error, because
/// there existence is unknown.
pub fn is_not_found(&self) -> bool {
match self {
Error::NotFound { .. } => true,
Error::AllMirrorsFailed { not_found, .. } => *not_found,
// A missing local path surfaces as an ordinary I/O error.
Error::Io { source, .. } => source.kind() == std::io::ErrorKind::NotFound,
_ => false,
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::CacheMiss { url, key } => {
write!(f, "cache miss: no blob for key {key} (url {url})")
}
Error::Integrity { key, detail } => {
write!(f, "integrity failure for key {key}: {detail}")
}
Error::UnknownScheme { scheme, url } => {
write!(
f,
"no transport registered for scheme '{scheme}' (url {url})"
)
}
Error::UnknownStore { name } => write!(f, "no store registered as '{name}'"),
Error::BadUrl { url, detail } => write!(f, "bad url '{url}': {detail}"),
Error::Transport { url, detail } => write!(f, "transport error for {url}: {detail}"),
Error::NotFound { url, detail } => write!(f, "not found: {url} ({detail})"),
Error::AllMirrorsFailed { url, detail, .. } => {
write!(f, "all sources failed for {url}: {detail}")
}
Error::MissingAuth { realm } => {
write!(f, "no auth resolver registered for realm '{realm}'")
}
Error::Io { path, source } => match path {
Some(p) => write!(f, "io error at {}: {source}", p.display()),
None => write!(f, "io error: {source}"),
},
Error::Manifest { detail } => write!(f, "manifest error: {detail}"),
Error::BadConfig { var, detail } => write!(f, "{var} is unusable: {detail}"),
Error::UnknownFormat { name } => write!(f, "no reader registered for format '{name}'"),
Error::Format { format, detail } => {
write!(f, "{format} decode error: {detail}")
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io { source, .. } => Some(source),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(source: std::io::Error) -> Self {
Error::Io { path: None, source }
}
}