1use std::path::PathBuf;
4
5#[cfg(feature = "git-resolver")]
6use semver::Version;
7use thiserror::Error;
8
9#[cfg(feature = "git-resolver")]
10use crate::hash::ContentHash;
11#[cfg(feature = "git-resolver")]
12use crate::hash::HashError;
13use crate::lockfile::LockfileError;
14use crate::manifest::ManifestError;
15#[cfg(feature = "git-resolver")]
16use crate::module_walk::ModuleWalkError;
17use crate::signing::VerifyingKey;
18#[cfg(feature = "git-resolver")]
19use crate::version_requirement::VersionRequirement;
20
21#[derive(Debug, Error)]
24pub enum ResolverError {
25 #[error("`{name}` is not a declared dependency")]
28 NotADependency {
29 name: String,
31 },
32
33 #[error("{}", missing_file_message(.dep, .path, .kind))]
35 MissingFile {
36 dep: String,
38 path: PathBuf,
40 kind: MissingFileKind,
42 },
43
44 #[cfg(feature = "git-resolver")]
47 #[error("tag `{tag}` points to a `module.json` declaring version `{declared}`")]
48 TagManifestMismatch {
49 tag: String,
51 declared: Version,
53 },
54
55 #[cfg(feature = "git-resolver")]
57 #[error("dependency cycle: {}", format_cycle(.path))]
58 Cycle {
59 path: Vec<String>,
61 },
62
63 #[cfg(feature = "git-resolver")]
66 #[error(
67 "no version satisfies `{dep}` requirement `{requirement}` (considered: {})",
68 format_versions(.considered)
69 )]
70 NoSatisfyingVersion {
71 dep: String,
73 requirement: VersionRequirement,
75 considered: Vec<Version>,
77 },
78
79 #[error("`{dep}` is not in `module-lock.json`; run `sprocket module lock` to update")]
82 NotInLockfile {
83 dep: String,
85 },
86
87 #[error(
90 "`{dep}` manifest source differs from the lockfile; run `sprocket module lock` to update"
91 )]
92 LockfileSourceMismatch {
93 dep: String,
95 },
96
97 #[cfg(feature = "git-resolver")]
100 #[error(
101 "cached `{dep}` content hash does not match the lockfile (expected `{expected}`, observed \
102 `{observed}`)"
103 )]
104 ChecksumMismatch {
105 dep: String,
107 expected: ContentHash,
109 observed: ContentHash,
111 },
112
113 #[error(
116 "signer for `{dep}` has changed since the lockfile was written (run `sprocket module \
117 trust {dep}` to accept the new key)"
118 )]
119 SignerKeyMismatch {
120 dep: String,
122 expected: Box<VerifyingKey>,
124 observed: Box<VerifyingKey>,
126 },
127
128 #[error("`{dep}` was signed when locked but is now unsigned; this may indicate tampering")]
133 SignatureDowngrade {
134 dep: String,
136 expected_signer: Box<VerifyingKey>,
138 },
139
140 #[cfg(feature = "git-resolver")]
143 #[error("`{dep}` selector references unknown {kind} `{name}`")]
144 UnknownGitRef {
145 dep: String,
147 kind: GitRefKind,
149 name: String,
151 },
152
153 #[cfg(feature = "git-resolver")]
156 #[error("`{dep}` `commit` value `{value}` is not a valid Git commit SHA")]
157 InvalidCommit {
158 dep: String,
160 value: String,
162 },
163
164 #[cfg(feature = "git-resolver")]
167 #[error(
168 "`{dep}` signature does not match observed content (signer: `{}`)",
169 signer.to_openssh()
170 )]
171 SignatureVerificationFailed {
172 dep: String,
174 signer: Box<VerifyingKey>,
176 },
177
178 #[cfg(feature = "git-resolver")]
180 #[error("`{dep}` `module.sig` failed to parse")]
181 SignatureParse {
182 dep: String,
184 #[source]
186 source: crate::signing::SignatureFileError,
187 },
188
189 #[cfg(feature = "git-resolver")]
191 #[error("invalid `exclude` pattern `{pattern}`")]
192 InvalidExclude {
193 pattern: String,
195 #[source]
197 source: globset::Error,
198 },
199
200 #[error("`{dep}` is unsigned but `require_signed` is enabled")]
202 RequireSignedViolation {
203 dep: String,
205 },
206
207 #[cfg(feature = "git-resolver")]
210 #[error(
211 "`{dep}` declares a local-path source but is reachable through a non-local parent; only \
212 locally-rooted projects may use local-path dependencies"
213 )]
214 LocalPathInTransitive {
215 dep: String,
217 },
218
219 #[cfg(feature = "git-resolver")]
222 #[error("`{dep}` is declared by the consumer but absent from the freshly-resolved tree")]
223 MissingFreshDependency {
224 dep: String,
226 },
227
228 #[cfg(feature = "git-resolver")]
230 #[error("`{dep}` git URL `{url}` uses scheme `{scheme}` which is not allowed by policy")]
231 GitUrlPolicyViolation {
232 dep: String,
234 url: String,
236 scheme: String,
238 },
239
240 #[cfg(feature = "git-resolver")]
244 #[error("`{dep}` git URL `{url}` host `{host}` could not be resolved")]
245 GitHostResolutionFailed {
246 dep: String,
248 url: String,
250 host: String,
252 },
253
254 #[cfg(feature = "git-resolver")]
256 #[error("`{dep}` git URL `{url}` targets host `{host}` which is not allowed by policy")]
257 GitHostPolicyViolation {
258 dep: String,
260 url: String,
262 host: String,
264 },
265
266 #[cfg(feature = "git-resolver")]
268 #[error(
269 "`{dep}` git URL `{url}` targets host `{host}` which is not in the configured allow list; \
270 to allow it, add `{host}` to `{config_key}` in the `[modules]` section of your \
271 `sprocket.toml`"
272 )]
273 GitHostNotAllowed {
274 dep: String,
276 url: String,
278 host: String,
280 config_key: &'static str,
282 },
283
284 #[cfg(feature = "git-resolver")]
286 #[error("`{dep}` materialized tree exceeds limits (files: {files}, bytes: {bytes})")]
287 MaterializedTreeLimitExceeded {
288 dep: String,
290 files: usize,
292 bytes: u64,
294 },
295
296 #[cfg(feature = "git-resolver")]
298 #[error(transparent)]
299 Git(#[from] crate::resolver::git::GitError),
300
301 #[cfg(feature = "git-resolver")]
304 #[error("`{dep}` materialized path escapes module root: `{path}`")]
305 MaterializedSymlinkEscape {
306 dep: String,
308 path: PathBuf,
310 },
311
312 #[error("i/o error at `{path}`")]
314 Io {
315 path: PathBuf,
317 #[source]
319 source: std::io::Error,
320 },
321
322 #[cfg(feature = "git-resolver")]
324 #[error(transparent)]
325 Walk(#[from] ModuleWalkError),
326
327 #[cfg(feature = "git-resolver")]
329 #[error(transparent)]
330 Hash(#[from] HashError),
331
332 #[error(transparent)]
334 Manifest(#[from] ManifestError),
335
336 #[error(transparent)]
338 Lockfile(#[from] LockfileError),
339
340 #[error(transparent)]
342 RelativePath(#[from] crate::relative_path::RelativePathError),
343}
344
345#[derive(Clone, Copy, Debug, PartialEq, Eq)]
347pub enum GitRefKind {
348 Tag,
350 Branch,
352}
353
354impl std::fmt::Display for GitRefKind {
355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 match self {
357 Self::Tag => f.write_str("tag"),
358 Self::Branch => f.write_str("branch"),
359 }
360 }
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub enum MissingFileKind {
366 Entrypoint,
369 SubPath,
372 Excluded,
374}
375
376fn missing_file_message(dep: &str, path: &std::path::Path, kind: &MissingFileKind) -> String {
378 let p = path.display();
379 match kind {
380 MissingFileKind::Entrypoint => {
381 format!("`{dep}` declares entrypoint `{p}` but the file does not exist")
382 }
383 MissingFileKind::SubPath => format!("`{dep}/{p}` not found"),
384 MissingFileKind::Excluded => format!("`{dep}/{p}` is excluded by the module manifest"),
385 }
386}
387
388#[cfg(feature = "git-resolver")]
390fn format_cycle(path: &[String]) -> String {
391 path.join(" → ")
392}
393
394#[cfg(feature = "git-resolver")]
396fn format_versions(versions: &[Version]) -> String {
397 if versions.is_empty() {
398 return "<none>".to_string();
399 }
400 versions
401 .iter()
402 .map(ToString::to_string)
403 .collect::<Vec<_>>()
404 .join(", ")
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 fn dep() -> String {
412 "foo".to_string()
413 }
414
415 #[test]
416 fn missing_file_kind_renders_distinctly() {
417 let entry = ResolverError::MissingFile {
418 dep: dep(),
419 path: "index.wdl".into(),
420 kind: MissingFileKind::Entrypoint,
421 };
422 let sub = ResolverError::MissingFile {
423 dep: dep(),
424 path: "missing.wdl".into(),
425 kind: MissingFileKind::SubPath,
426 };
427 let excl = ResolverError::MissingFile {
428 dep: dep(),
429 path: "internal/x.wdl".into(),
430 kind: MissingFileKind::Excluded,
431 };
432
433 assert!(entry.to_string().contains("entrypoint"));
434 assert!(sub.to_string().contains("not found"));
435 assert!(excl.to_string().contains("excluded"));
436 }
437}