use std::path::PathBuf;
use semver::Version;
use thiserror::Error;
use crate::hash::ContentHash;
use crate::hash::HashError;
use crate::lockfile::LockfileError;
use crate::manifest::ManifestError;
use crate::module_walk::ModuleWalkError;
use crate::signing::VerifyingKey;
use crate::version_requirement::VersionRequirement;
#[derive(Debug, Error)]
pub enum ResolverError {
#[error("`{name}` is not a declared dependency")]
NotADependency {
name: String,
},
#[error("{}", missing_file_message(.dep, .path, .kind))]
MissingFile {
dep: String,
path: PathBuf,
kind: MissingFileKind,
},
#[error("tag `{tag}` points to a `module.json` declaring version `{declared}`")]
TagManifestMismatch {
tag: String,
declared: Version,
},
#[error("dependency cycle: {}", format_cycle(.path))]
Cycle {
path: Vec<String>,
},
#[error(
"no version satisfies `{dep}` requirement `{requirement}` (considered: {})",
format_versions(.considered)
)]
NoSatisfyingVersion {
dep: String,
requirement: VersionRequirement,
considered: Vec<Version>,
},
#[error("`{dep}` is not in `module-lock.json`; run `sprocket module lock` to update")]
NotInLockfile {
dep: String,
},
#[error(
"`{dep}` manifest source differs from the lockfile; run `sprocket module lock` to update"
)]
LockfileSourceMismatch {
dep: String,
},
#[error(
"cached `{dep}` content hash does not match the lockfile (expected `{expected}`, observed \
`{observed}`)"
)]
ChecksumMismatch {
dep: String,
expected: ContentHash,
observed: ContentHash,
},
#[error(
"signer for `{dep}` has changed since the lockfile was written (run `sprocket module \
trust {dep}` to accept the new key)"
)]
SignerKeyMismatch {
dep: String,
expected: Box<VerifyingKey>,
observed: Box<VerifyingKey>,
},
#[error("`{dep}` was signed when locked but is now unsigned; this may indicate tampering")]
SignatureDowngrade {
dep: String,
expected_signer: Box<VerifyingKey>,
},
#[error("`{dep}` selector references unknown {kind} `{name}`")]
UnknownGitRef {
dep: String,
kind: GitRefKind,
name: String,
},
#[error("`{dep}` `commit` value `{value}` is not a valid Git commit SHA")]
InvalidCommit {
dep: String,
value: String,
},
#[error(
"`{dep}` signature does not match observed content (signer: `{}`)",
signer.to_openssh()
)]
SignatureVerificationFailed {
dep: String,
signer: Box<VerifyingKey>,
},
#[error("`{dep}` `module.sig` failed to parse")]
SignatureParse {
dep: String,
#[source]
source: crate::signing::SignatureFileError,
},
#[error("invalid `exclude` pattern `{pattern}`")]
InvalidExclude {
pattern: String,
#[source]
source: globset::Error,
},
#[error("`{dep}` is unsigned but `require_signed` is enabled")]
RequireSignedViolation {
dep: String,
},
#[error(
"`{dep}` declares a local-path source but is reachable through a non-local parent; only \
locally-rooted projects may use local-path dependencies"
)]
LocalPathInTransitive {
dep: String,
},
#[error("`{dep}` is declared by the consumer but absent from the freshly-resolved tree")]
MissingFreshDependency {
dep: String,
},
#[error("`{dep}` git URL `{url}` uses scheme `{scheme}` which is not allowed by policy")]
GitUrlPolicyViolation {
dep: String,
url: String,
scheme: String,
},
#[error("`{dep}` git URL `{url}` host `{host}` could not be resolved")]
GitHostResolutionFailed {
dep: String,
url: String,
host: String,
},
#[error("`{dep}` git URL `{url}` targets host `{host}` which is not allowed by policy")]
GitHostPolicyViolation {
dep: String,
url: String,
host: String,
},
#[error(
"`{dep}` git URL `{url}` targets host `{host}` which is not in the configured allow list; \
to allow it, add `{host}` to `{config_key}` in the `[modules]` section of your \
`sprocket.toml`"
)]
GitHostNotAllowed {
dep: String,
url: String,
host: String,
config_key: &'static str,
},
#[error("`{dep}` materialized tree exceeds limits (files: {files}, bytes: {bytes})")]
MaterializedTreeLimitExceeded {
dep: String,
files: usize,
bytes: u64,
},
#[error(transparent)]
Git(#[from] crate::resolver::git::GitError),
#[error("`{dep}` materialized path escapes module root: `{path}`")]
MaterializedSymlinkEscape {
dep: String,
path: PathBuf,
},
#[error("i/o error at `{path}`")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error(transparent)]
Walk(#[from] ModuleWalkError),
#[error(transparent)]
Hash(#[from] HashError),
#[error(transparent)]
Manifest(#[from] ManifestError),
#[error(transparent)]
Lockfile(#[from] LockfileError),
#[error(transparent)]
RelativePath(#[from] crate::relative_path::RelativePathError),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GitRefKind {
Tag,
Branch,
}
impl std::fmt::Display for GitRefKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Tag => f.write_str("tag"),
Self::Branch => f.write_str("branch"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MissingFileKind {
Entrypoint,
SubPath,
Excluded,
}
fn missing_file_message(dep: &str, path: &std::path::Path, kind: &MissingFileKind) -> String {
let p = path.display();
match kind {
MissingFileKind::Entrypoint => {
format!("`{dep}` declares entrypoint `{p}` but the file does not exist")
}
MissingFileKind::SubPath => format!("`{dep}/{p}` not found"),
MissingFileKind::Excluded => format!("`{dep}/{p}` is excluded by the module manifest"),
}
}
fn format_cycle(path: &[String]) -> String {
path.join(" → ")
}
fn format_versions(versions: &[Version]) -> String {
if versions.is_empty() {
return "<none>".to_string();
}
versions
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
fn dep() -> String {
"foo".to_string()
}
#[test]
fn missing_file_kind_renders_distinctly() {
let entry = ResolverError::MissingFile {
dep: dep(),
path: "index.wdl".into(),
kind: MissingFileKind::Entrypoint,
};
let sub = ResolverError::MissingFile {
dep: dep(),
path: "missing.wdl".into(),
kind: MissingFileKind::SubPath,
};
let excl = ResolverError::MissingFile {
dep: dep(),
path: "internal/x.wdl".into(),
kind: MissingFileKind::Excluded,
};
assert!(entry.to_string().contains("entrypoint"));
assert!(sub.to_string().contains("not found"));
assert!(excl.to_string().contains("excluded"));
}
}