diff --git a/Cargo.toml b/Cargo.toml index 2f8e319..311d1ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0" async-std = "1.9" async-stream = "0.3" async-trait = "0.1" @@ -36,6 +35,7 @@ sigstore = { version = "0.7.2", default-features = false, features = [ "tuf", "cached-client", ] } +thiserror = "1.0" tracing = "0.1" url = { version = "2.2", features = ["serde"] } walkdir = "2" diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..3848422 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,41 @@ +use thiserror::Error; + +pub type FetcherResult = std::result::Result; + +#[derive(Error, Debug)] +pub enum FetcherError { + #[error("cannot retrieve path from uri: {0}")] + InvalidFilePathError(String), + #[error("invalid wasm file")] + InvalidWasmFileError, + #[error("wasm module cannot be save to {0:?}: {1}")] + CannotWriteWasmModuleFile(String, #[source] std::io::Error), + #[error(transparent)] + PolicyError(#[from] crate::policy::DigestError), + #[error(transparent)] + VerifyError(#[from] crate::verify::errors::VerifyError), + #[error(transparent)] + RegistryError(#[from] crate::registry::errors::RegistryError), + #[error(transparent)] + UrlParserError(#[from] url::ParseError), + #[error(transparent)] + SourceError(#[from] crate::sources::SourceError), + #[error(transparent)] + StoreError(#[from] crate::store::errors::StoreError), + #[error(transparent)] + InvalidURLError(#[from] InvalidURLError), + #[error(transparent)] + CannotCreateStoragePathError(#[from] CannotCreateStoragePathError), +} + +#[derive(thiserror::Error, Debug)] +#[error("{0}")] +pub struct FailedToParseYamlDataError(#[from] pub serde_yaml::Error); + +#[derive(thiserror::Error, Debug)] +#[error("invalid URL: {0}")] +pub struct CannotCreateStoragePathError(#[from] pub std::io::Error); + +#[derive(thiserror::Error, Debug)] +#[error("invalid URL: {0}")] +pub struct InvalidURLError(pub String); diff --git a/src/fetcher.rs b/src/fetcher.rs index eeb614c..be522dd 100644 --- a/src/fetcher.rs +++ b/src/fetcher.rs @@ -1,8 +1,7 @@ -use anyhow::Result; use async_trait::async_trait; use url::Url; -use crate::sources::Certificate; +use crate::{sources::Certificate, sources::SourceResult}; #[derive(Clone, Debug, PartialEq)] pub(crate) enum ClientProtocol { @@ -22,5 +21,5 @@ pub(crate) enum TlsVerificationMode { #[async_trait] pub(crate) trait PolicyFetcher { // Download and return the bytes of the WASM module - async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> Result>; + async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> SourceResult>; } diff --git a/src/https.rs b/src/https.rs index 2963c37..f680643 100644 --- a/src/https.rs +++ b/src/https.rs @@ -1,6 +1,5 @@ #![allow(clippy::upper_case_acronyms)] -use anyhow::{anyhow, Result}; use async_trait::async_trait; use std::{ boxed::Box, @@ -10,27 +9,39 @@ use url::Url; use crate::fetcher::{ClientProtocol, PolicyFetcher, TlsVerificationMode}; use crate::sources::Certificate; +use crate::sources::SourceError; +use crate::sources::SourceResult; // Struct used to reference a WASM module that is hosted on a HTTP(s) server #[derive(Default)] pub(crate) struct Https {} impl TryFrom<&Certificate> for reqwest::Certificate { - type Error = anyhow::Error; + type Error = SourceError; - fn try_from(certificate: &Certificate) -> Result { + fn try_from(certificate: &Certificate) -> SourceResult { match certificate { - Certificate::Der(certificate) => reqwest::Certificate::from_der(certificate) - .map_err(|err| anyhow!("could not load certificate as DER encoded: {}", err)), - Certificate::Pem(certificate) => reqwest::Certificate::from_pem(certificate) - .map_err(|err| anyhow!("could not load certificate as PEM encoded: {}", err)), + Certificate::Der(certificate) => { + reqwest::Certificate::from_der(certificate).map_err(|err| { + SourceError::InvalidCertificateError(format!( + "could not load certificate as DER encoded: {err}" + )) + }) + } + Certificate::Pem(certificate) => { + reqwest::Certificate::from_pem(certificate).map_err(|err| { + SourceError::InvalidCertificateError(format!( + "could not load certificate as PEM encoded: {err}" + )) + }) + } } } } #[async_trait] impl PolicyFetcher for Https { - async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> Result> { + async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> SourceResult> { let mut client_builder = reqwest::Client::builder(); match client_protocol { ClientProtocol::Http => {} diff --git a/src/lib.rs b/src/lib.rs index b85a6aa..5e8c78c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,13 @@ extern crate reqwest; extern crate rustls; extern crate walkdir; -use anyhow::{anyhow, Result}; +use errors::FetcherResult; use std::boxed::Box; use std::fs; +use store::errors::{StoreError, StoreResult}; use url::Url; +pub mod errors; pub mod fetcher; mod https; pub mod policy; @@ -16,6 +18,7 @@ pub mod sources; pub mod store; pub mod verify; +use crate::errors::{CannotCreateStoragePathError, FetcherError}; use crate::fetcher::{ClientProtocol, PolicyFetcher, TlsVerificationMode}; use crate::https::Https; use crate::policy::Policy; @@ -78,7 +81,7 @@ pub async fn fetch_policy( url: &str, destination: PullDestination, sources: Option<&Sources>, -) -> Result { +) -> FetcherResult { let url = parse_url(url)?; match url.scheme() { "file" => { @@ -87,15 +90,17 @@ pub async fn fetch_policy( uri: url.to_string(), local_path: url .to_file_path() - .map_err(|err| anyhow!("cannot retrieve path from uri {}: {:?}", url, err))?, + .map_err(|_| FetcherError::InvalidFilePathError(url.to_string()))?, }); } "registry" | "http" | "https" => Ok(()), - _ => Err(anyhow!("unknown scheme: {}", url.scheme())), + _ => Err(StoreError::UnknownSchemeError(url.scheme().to_owned())), }?; let (store, mut destination) = pull_destination(&url, &destination)?; if let Some(store) = store { - store.ensure(&store.policy_full_path(url.as_str(), store::PolicyPath::PrefixOnly)?)?; + store + .ensure(&store.policy_full_path(url.as_str(), store::PolicyPath::PrefixOnly)?) + .map_err(CannotCreateStoragePathError)?; } match url.scheme() { "registry" => { @@ -134,11 +139,7 @@ pub async fn fetch_policy( { Err(err) => { if !sources.is_insecure_source(&host_and_port(&url)?) { - return Err(anyhow!( - "the policy {} could not be downloaded due to error: {}", - url, - err - )); + return Err(FetcherError::SourceError(err)); } } Ok(bytes) => return create_file_if_valid(&bytes, &destination, url.to_string()), @@ -155,11 +156,14 @@ pub async fn fetch_policy( match policy_fetcher.fetch(&url, ClientProtocol::Http).await { Ok(bytes) => create_file_if_valid(&bytes, &destination, url.to_string()), - Err(e) => Err(anyhow!("could not pull policy {}: {}", url, e)), + Err(e) => Err(FetcherError::SourceError(e)), } } -fn client_protocol(url: &Url, sources: &Sources) -> Result { +fn client_protocol( + url: &Url, + sources: &Sources, +) -> std::result::Result { if let Some(certificates) = sources.source_authority(&host_and_port(url)?) { return Ok(ClientProtocol::Https( TlsVerificationMode::CustomCaCertificates(certificates), @@ -168,7 +172,10 @@ fn client_protocol(url: &Url, sources: &Sources) -> Result { Ok(ClientProtocol::Https(TlsVerificationMode::SystemCa)) } -fn pull_destination(url: &Url, destination: &PullDestination) -> Result<(Option, PathBuf)> { +fn pull_destination( + url: &Url, + destination: &PullDestination, +) -> FetcherResult<(Option, PathBuf)> { Ok(match destination { PullDestination::MainStore => { let store = Store::default(); @@ -196,19 +203,19 @@ fn pull_destination(url: &Url, destination: &PullDestination) -> Result<(Option< // Helper function, takes the URL of the policy and allocates the // right struct to interact with it #[allow(clippy::box_default)] -fn url_fetcher(scheme: &str) -> Result> { +fn url_fetcher(scheme: &str) -> StoreResult> { match scheme { "http" | "https" => Ok(Box::new(Https::default())), "registry" => Ok(Box::new(Registry::new())), - _ => Err(anyhow!("unknown scheme: {}", scheme)), + _ => Err(StoreError::UnknownSchemeError(scheme.to_owned())), } } -pub(crate) fn host_and_port(url: &Url) -> Result { +pub(crate) fn host_and_port(url: &Url) -> std::result::Result { Ok(format!( "{}{}", url.host_str() - .ok_or_else(|| anyhow!("invalid URL {}", url))?, + .ok_or_else(|| errors::InvalidURLError(url.to_string()))?, url.port() .map(|port| format!(":{}", port)) .unwrap_or_default(), @@ -222,12 +229,13 @@ pub(crate) fn host_and_port(url: &Url) -> Result { // https://webassembly.github.io/spec/core/bikeshed/#binary-magic const WASM_MAGIC_NUMBER: [u8; 4] = [0x00, 0x61, 0x73, 0x6D]; -fn create_file_if_valid(bytes: &[u8], destination: &Path, url: String) -> Result { +fn create_file_if_valid(bytes: &[u8], destination: &Path, url: String) -> FetcherResult { if !bytes.starts_with(&WASM_MAGIC_NUMBER) { - return Err(anyhow!("invalid wasm file")); + return Err(FetcherError::InvalidWasmFileError); }; - fs::write(destination, bytes) - .map_err(|e| anyhow!("wasm module cannot be save to {:?}: {}", destination, e))?; + fs::write(destination, bytes).map_err(|e| { + FetcherError::CannotWriteWasmModuleFile(destination.to_string_lossy().to_string(), e) + })?; Ok(Policy { uri: url, @@ -359,7 +367,7 @@ mod tests { port: Some(5000), path: "/kubewarden/policies/test".to_string(), }))] - fn url_parsing(#[case] url: &str, #[case] expected: anyhow::Result) { + fn url_parsing(#[case] url: &str, #[case] expected: FetcherResult) { let res = parse_url(url); println!("{} -> {:?}", url, res); assert_eq!(res.is_ok(), expected.is_ok()); diff --git a/src/policy.rs b/src/policy.rs index b9fc400..ce7ef25 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -9,8 +9,15 @@ pub struct Policy { pub local_path: PathBuf, } +#[derive(thiserror::Error, Debug)] +#[error("cannot retrieve path from uri: {err}")] +pub struct DigestError { + #[from] + err: std::io::Error, +} + impl Policy { - pub fn digest(&self) -> Result { + pub fn digest(&self) -> std::result::Result { let d = Sha256::digest(std::fs::read(&self.local_path)?); Ok(format!("{:x}", d)) } diff --git a/src/registry/errors.rs b/src/registry/errors.rs new file mode 100644 index 0000000..744b5d7 --- /dev/null +++ b/src/registry/errors.rs @@ -0,0 +1,23 @@ +use thiserror::Error; + +use crate::errors::InvalidURLError; + +pub type RegistryResult = std::result::Result; + +#[derive(Error, Debug)] +pub enum RegistryError { + #[error("Fail to interact with OCI registry: {0}")] + OCIRegistryError(#[from] oci_distribution::errors::OciDistributionError), + #[error("Invalid OCI image reference: {0}")] + InvalidOCIImageReferenceError(#[from] oci_distribution::ParseError), + #[error("{0}")] + BuildImmutableReferenceError(String), + #[error("Invalid destination format")] + InvalidDestinationError, + #[error("{0}")] + OtherError(String), + #[error(transparent)] + UrlParserError(#[from] url::ParseError), + #[error(transparent)] + InvalidURLError(#[from] InvalidURLError), +} diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 09cbcf4..2f73774 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -1,4 +1,3 @@ -use anyhow::{anyhow, Result}; use async_trait::async_trait; use docker_credential::DockerCredential; use lazy_static::lazy_static; @@ -17,9 +16,19 @@ use std::str::FromStr; use tracing::{debug, warn}; use url::Url; -use crate::fetcher::{ClientProtocol, PolicyFetcher, TlsVerificationMode}; +use crate::{ + fetcher::TlsVerificationMode, + sources::{Certificate, SourceResult, Sources}, +}; +use crate::{ + fetcher::{ClientProtocol, PolicyFetcher}, + sources::SourceError, +}; +use errors::RegistryError; + +use self::errors::RegistryResult; -use crate::sources::{Certificate, Sources}; +pub mod errors; lazy_static! { static ref SHA256_DIGEST_RE: Regex = Regex::new(r"[A-Fa-f0-9]{64}").unwrap(); @@ -120,7 +129,7 @@ impl Registry { &self, url: &str, sources: Option<&Sources>, - ) -> Result { + ) -> RegistryResult { // Start by building the Reference, this will expand the input url to // ensure it contains also the registry. Example: `busybox` -> // `docker.io/library/busybox:latest` @@ -138,7 +147,11 @@ impl Registry { } /// Fetch the manifest's digest of the OCI object referenced by the given url. - pub async fn manifest_digest(&self, url: &str, sources: Option<&Sources>) -> Result { + pub async fn manifest_digest( + &self, + url: &str, + sources: Option<&Sources>, + ) -> RegistryResult { // Start by building the Reference, this will expand the input url to // ensure it contains also the registry. Example: `busybox` -> // `docker.io/library/busybox:latest` @@ -148,10 +161,9 @@ impl Registry { let sources: Sources = sources.cloned().unwrap_or_default(); let cp = crate::client_protocol(&url, &sources)?; - Registry::client(cp) + Ok(Registry::client(cp) .fetch_manifest_digest(&reference, ®istry_auth) - .await - .map_err(|e| anyhow!(e)) + .await?) } /// Push the policy to the OCI registry specified by `url`. @@ -163,12 +175,13 @@ impl Registry { policy: &[u8], destination: &str, sources: Option<&Sources>, - ) -> Result { - let url = Url::parse(destination).map_err(|_| anyhow!("invalid URL: {}", destination))?; + ) -> RegistryResult { + let url = Url::parse(destination) + .map_err(|_| crate::errors::InvalidURLError(destination.to_owned()))?; let sources: Sources = sources.cloned().unwrap_or_default(); let destination = destination .strip_prefix("registry://") - .ok_or_else(|| anyhow!("Invalid destination format"))?; + .ok_or_else(|| RegistryError::InvalidDestinationError)?; match self .do_push(policy, &url, crate::client_protocol(&url, &sources)?) @@ -179,7 +192,7 @@ impl Registry { } Err(err) => { if !sources.is_insecure_source(&crate::host_and_port(&url)?) { - return Err(anyhow!("could not push policy: {}", err,)); + return Err(err); } } } @@ -195,10 +208,7 @@ impl Registry { return build_immutable_ref(destination, &manifest_url); } - let manifest_url = self - .do_push(policy, &url, ClientProtocol::Http) - .await - .map_err(|_| anyhow!("could not push policy"))?; + let manifest_url = self.do_push(policy, &url, ClientProtocol::Http).await?; build_immutable_ref(destination, &manifest_url) } @@ -208,7 +218,7 @@ impl Registry { policy: &[u8], url: &Url, client_protocol: ClientProtocol, - ) -> Result { + ) -> RegistryResult { let reference = Reference::from_str(url.as_ref().strip_prefix("registry://").unwrap_or_default())?; @@ -227,7 +237,7 @@ impl Registry { }; let image_manifest = manifest::OciImageManifest::build(&layers, &config, None); - Registry::client(client_protocol) + Ok(Registry::client(client_protocol) .push( &reference, &layers, @@ -236,25 +246,18 @@ impl Registry { Some(image_manifest), ) .await - .map(|push_response| push_response.manifest_url) - .map_err(|e| anyhow!("could not push policy: {}", e)) + .map(|push_response| push_response.manifest_url)?) } } -pub(crate) fn build_fully_resolved_reference(url: &str) -> Result { +pub(crate) fn build_fully_resolved_reference(url: &str) -> RegistryResult { let image = url.strip_prefix("registry://").unwrap_or(url); - Reference::try_from(image).map_err(|e| { - anyhow!( - "Cannot parse {} into an OCI Reference object: {:?}", - image, - e - ) - }) + Ok(Reference::try_from(image)?) } #[async_trait] impl PolicyFetcher for Registry { - async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> Result> { + async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> SourceResult> { let reference = Reference::from_str(url.as_ref().strip_prefix("registry://").unwrap_or_default())?; debug!(image=?reference, ?client_protocol, "fetching policy"); @@ -273,7 +276,7 @@ impl PolicyFetcher for Registry { match image_content { Some(image_content) => Ok(image_content), - None => Err(anyhow!("could not pull policy {}: empty layers", url)), + None => Err(SourceError::EmptyLayersError(url.to_string())), } } } @@ -283,36 +286,35 @@ impl PolicyFetcher for Registry { /// * `image ref`: the mutable image reference. For example: `ghcr.io/kubewarden/secure-policy:latest` /// * `manifest_url`: the URL of the manifest, as returned when doing a push operation. For example /// `https://ghcr.io/v2/kubewarden/secure-policy/manifests/sha256:72b4569c3daee67abeaa64192fb53895d0edb2d44fa6e1d9d4c5d3f8ece09f6e` -fn build_immutable_ref(image_ref: &str, manifest_url: &str) -> Result { +fn build_immutable_ref(image_ref: &str, manifest_url: &str) -> RegistryResult { let manifest_digest = manifest_url .rsplit_once('/') .map(|(_, digest)| digest.to_string()) .ok_or_else(|| { - anyhow!( + RegistryError::BuildImmutableReferenceError(format!( "Cannot extract manifest digest from the OCI registry response: {}", manifest_url - ) + )) })?; - let (digest, checksum) = manifest_digest - .split_once(':') - .ok_or_else(|| anyhow!("Invalid digest: {}", manifest_digest))?; + let (digest, checksum) = manifest_digest.split_once(':').ok_or_else(|| { + RegistryError::BuildImmutableReferenceError(format!("Invalid digest: {}", manifest_digest)) + })?; let digest_valid = match digest { "sha256" => Ok(SHA256_DIGEST_RE.is_match(checksum)), "sha512" => Ok(SHA512_DIGEST_RE.is_match(checksum)), - unknown => Err(anyhow!( + unknown => Err(RegistryError::BuildImmutableReferenceError(format!( "unknown algorithm '{}' for manifest {}", - unknown, - manifest_digest - )), + unknown, manifest_digest + ))), }?; if !digest_valid { - return Err(anyhow!( + return Err(RegistryError::BuildImmutableReferenceError(format!( "The digest of the returned manifest is not valid: {}", manifest_digest - )); + ))); } let oci_reference = oci_distribution::Reference::try_from(image_ref)?; @@ -427,31 +429,31 @@ mod tests { case( "ghcr.io/kubewarden/secure-policy:latest", "https://ghcr.io/v2/kubewarden/secure-policy/manifests/sha256:XYZ4569c3daee67abeaa64192fb53895d0edb2d44fa6e1d9d4c5d3f8ece09f6e", - Err(anyhow!("boom")), + Err(RegistryError::OtherError("boom".to_owned())), ), // Error because of shorter digest case( "ghcr.io/kubewarden/secure-policy:latest", "https://ghcr.io/v2/kubewarden/secure-policy/manifests/sha256:72b4569c", - Err(anyhow!("boom")), + Err(RegistryError::OtherError("boom".to_owned())), ), // Error because unknown algorithm case( "ghcr.io/kubewarden/secure-policy:latest", "https://ghcr.io/v2/kubewarden/secure-policy/manifests/sha384:72b4569c3daee67abeaa64192fb53895d0edb2d44fa6e1d9d4c5d3f8ece09f6e", - Err(anyhow!("boom")), + Err(RegistryError::OtherError("boom".to_owned())), ), // Error because invalid url format case( "ghcr.io/kubewarden/secure-policy:latest", "not an url", - Err(anyhow!("boom")), + Err(RegistryError::OtherError("boom".to_owned())), ) )] fn test_extract_manifest_digest( image_ref: &str, manifest_url: &str, - immutable_ref: Result, + immutable_ref: RegistryResult, ) { let actual = build_immutable_ref(image_ref, manifest_url); match immutable_ref { diff --git a/src/sources.rs b/src/sources.rs index d0d1fdb..3c133e4 100644 --- a/src/sources.rs +++ b/src/sources.rs @@ -1,12 +1,34 @@ -use anyhow::{anyhow, Result}; - use serde::{Deserialize, Serialize}; +use thiserror::Error; +use crate::errors::FailedToParseYamlDataError; use std::collections::{HashMap, HashSet}; use std::convert::{TryFrom, TryInto}; use std::path::{Path, PathBuf}; use std::{fs, fs::File}; +pub type SourceResult = std::result::Result; + +#[derive(Error, Debug)] +pub enum SourceError { + #[error(transparent)] + InvalidURLError(#[from] crate::errors::InvalidURLError), + #[error("Fail to interact with OCI registry: {0}")] + OCIRegistryError(#[from] oci_distribution::errors::OciDistributionError), + #[error("Invalid OCI image reference: {0}")] + InvalidOCIImageReferenceError(#[from] oci_distribution::ParseError), + #[error("could not pull policy {0}: empty layers")] + EmptyLayersError(String), + #[error("Invalid certificate: {0}")] + InvalidCertificateError(String), + #[error("Cannot read certificate from file: {0}")] + CannotReadCertificateError(#[from] std::io::Error), + #[error(transparent)] + FailedToParseYamlDataError(#[from] FailedToParseYamlDataError), + #[error("failed to create the http client: {0}")] + FailedToCreateHttpClientError(#[from] reqwest::Error), +} + #[derive(Clone, Default, Deserialize, Debug)] struct RawSourceAuthorities(HashMap>); @@ -29,15 +51,14 @@ enum RawSourceAuthority { } impl TryFrom for RawCertificate { - type Error = anyhow::Error; + type Error = SourceError; - fn try_from(raw_source_authority: RawSourceAuthority) -> Result { + fn try_from(raw_source_authority: RawSourceAuthority) -> SourceResult { match raw_source_authority { RawSourceAuthority::Data { data } => Ok(data), RawSourceAuthority::Path { path } => { - let file_data = fs::read(path.clone()).map_err(|e| { - anyhow!("Cannot read certificate from file '{:?}': {:?}", path, e) - })?; + let file_data = + fs::read(path.clone()).map_err(SourceError::CannotReadCertificateError)?; Ok(RawCertificate(String::from_utf8(file_data).unwrap())) } } @@ -58,9 +79,9 @@ struct RawCertificate(String); struct SourceAuthorities(HashMap>); impl TryFrom for SourceAuthorities { - type Error = anyhow::Error; + type Error = SourceError; - fn try_from(raw_source_authorities: RawSourceAuthorities) -> Result { + fn try_from(raw_source_authorities: RawSourceAuthorities) -> SourceResult { let mut sa = SourceAuthorities::default(); for (host, authorities) in raw_source_authorities.0 { @@ -90,9 +111,9 @@ pub enum Certificate { } impl TryFrom for Sources { - type Error = anyhow::Error; + type Error = SourceError; - fn try_from(sources: RawSources) -> Result { + fn try_from(sources: RawSources) -> SourceResult { Ok(Sources { insecure_sources: sources.insecure_sources.clone(), source_authorities: sources.source_authorities.try_into()?, @@ -101,17 +122,16 @@ impl TryFrom for Sources { } impl TryFrom for Certificate { - type Error = anyhow::Error; + type Error = SourceError; - fn try_from(raw_certificate: RawCertificate) -> Result { + fn try_from(raw_certificate: RawCertificate) -> SourceResult { if reqwest::Certificate::from_pem(raw_certificate.0.as_bytes()).is_ok() { Ok(Certificate::Pem(raw_certificate.0.as_bytes().to_vec())) } else if reqwest::Certificate::from_der(raw_certificate.0.as_bytes()).is_ok() { Ok(Certificate::Der(raw_certificate.0.as_bytes().to_vec())) } else { - Err(anyhow!( - "certificate {:?} is not in PEM nor in DER encoding", - raw_certificate + Err(SourceError::InvalidCertificateError( + "raw certificate is not in PEM nor in DER encoding".to_owned(), )) } } @@ -201,8 +221,10 @@ impl Sources { } } -pub fn read_sources_file(path: &Path) -> Result { - serde_yaml::from_reader::<_, RawSources>(File::open(path)?)?.try_into() +pub fn read_sources_file(path: &Path) -> SourceResult { + serde_yaml::from_reader::<_, RawSources>(File::open(path)?) + .map_err(FailedToParseYamlDataError)? + .try_into() } #[cfg(test)] @@ -283,8 +305,11 @@ Wm7DCfrPNGVwFWUQOmsPue9rZBgO path.push("/boom"); let auth = RawSourceAuthority::Path { path }; - let expected: Result = auth.try_into(); - assert!(expected.is_err()); + let expected: SourceResult = auth.try_into(); + assert!(matches!( + expected, + Err(SourceError::CannotReadCertificateError(_)) + )); } #[test] @@ -299,15 +324,8 @@ Wm7DCfrPNGVwFWUQOmsPue9rZBgO path: path.to_path_buf(), }; - let expected: Result = auth.try_into(); - match expected { - Ok(RawCertificate(data)) => { - assert_eq!(&data, expected_contents); - } - unexpected => { - panic!("Didn't get what I was expecting: {:?}", unexpected); - } - } + let expected: SourceResult = auth.try_into(); + assert!(matches!(expected, Ok(RawCertificate(data)) if &data == expected_contents)); } #[test] @@ -322,8 +340,13 @@ Wm7DCfrPNGVwFWUQOmsPue9rZBgO ); let raw_source_authorities: RawSourceAuthorities = serde_json::from_value(raw).unwrap(); - let actual: Result = raw_source_authorities.try_into(); - assert!(actual.is_ok(), "Got an expected error: {:?}", actual); + let actual: SourceResult = raw_source_authorities.try_into(); + + assert!( + matches!(actual, Ok(_)), + "Got an unexpected error: {:?}", + actual + ); let actual_map = actual.unwrap().0; let actual_certs = actual_map.get("foo.com").unwrap(); diff --git a/src/store/errors.rs b/src/store/errors.rs new file mode 100644 index 0000000..784b675 --- /dev/null +++ b/src/store/errors.rs @@ -0,0 +1,23 @@ +use thiserror::Error; + +pub type StoreResult = std::result::Result; + +#[derive(Error, Debug)] +pub enum StoreError { + #[error(transparent)] + UrlParserError(#[from] url::ParseError), + #[error("faild to read verification file: {0}")] + VerificationFileReadError(#[from] std::io::Error), + #[error("cannot read policy in local storage: {0}")] + FailedToReadPolicyInLocalStorageError(#[from] walkdir::Error), + #[error(transparent)] + PolicyStoragePathError(#[from] std::path::StripPrefixError), + #[error("unknown scheme: {0}")] + UnknownSchemeError(String), + #[error("multiple policies found with the same prefix: {0}")] + MultiplePoliciesFoundError(String), + #[error(transparent)] + DigestErrors(#[from] crate::policy::DigestError), + #[error(transparent)] + DecoderError(#[from] base64::DecodeError), +} diff --git a/src/store/mod.rs b/src/store/mod.rs index a489662..ae3f5dd 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -1,4 +1,3 @@ -use anyhow::{anyhow, Result}; use directories::ProjectDirs; use lazy_static::lazy_static; use path_slash::PathExt; @@ -7,7 +6,11 @@ use url::Url; use walkdir::WalkDir; use crate::policy::Policy; +use errors::StoreError; +use self::errors::StoreResult; + +pub mod errors; pub mod path; mod scheme; @@ -80,7 +83,7 @@ impl Store { /// this store. If `policy_path` is set to `PrefixOnly`, the /// filename of the policy will be omitted, otherwise it will be /// included. - pub fn policy_full_path(&self, url: &str, policy_path: PolicyPath) -> Result { + pub fn policy_full_path(&self, url: &str, policy_path: PolicyPath) -> StoreResult { let path = self.policy_path(url, policy_path)?; Ok(self.root.join(path)) @@ -90,7 +93,7 @@ impl Store { /// without the store. If `policy_path` is set to `PrefixOnly`, the /// filename of the policy will be omitted, otherwise it will be /// included. - pub fn policy_path(&self, url: &str, policy_path: PolicyPath) -> Result { + pub fn policy_path(&self, url: &str, policy_path: PolicyPath) -> StoreResult { let url = Url::parse(url)?; let filename = policy_file_name(&url); let policy_prefix = self.policy_prefix(&url); @@ -120,7 +123,7 @@ impl Store { } /// Lists all policies in this store - pub fn list(&self) -> Result> { + pub fn list(&self) -> StoreResult> { let mut policies = Vec::new(); if !self.root.exists() { @@ -164,11 +167,11 @@ impl Store { } /// Get a policy by its URI, if it exists. - pub fn get_policy_by_uri(&self, uri: &str) -> Result> { + pub fn get_policy_by_uri(&self, uri: &str) -> StoreResult> { let uri = Url::parse(uri)?; if !scheme::is_known_remote_scheme(uri.scheme()) { - return Err(anyhow!("Unknown scheme: {}", uri.scheme())); + return Err(StoreError::UnknownSchemeError(uri.scheme().to_owned())); } let policy_path = self.policy_full_path(uri.as_str(), PolicyPath::PrefixAndFilename)?; @@ -183,16 +186,15 @@ impl Store { } /// Get a policy that matches the given SHA prefix, if it exists. - pub fn get_policy_by_sha_prefix(&self, sha_prefix: &str) -> Result> { + pub fn get_policy_by_sha_prefix(&self, sha_prefix: &str) -> StoreResult> { self.list()?.into_iter().try_fold(None, |acc, policy| { if !policy.digest()?.starts_with(sha_prefix) { return Ok(acc); } if acc.is_some() { - Err(anyhow!( - "Multiple policies found with the same prefix: {}", - sha_prefix + Err(StoreError::MultiplePoliciesFoundError( + sha_prefix.to_owned(), )) } else { Ok(Some(policy)) @@ -267,7 +269,7 @@ mod tests { input_url: &str, input_policy_path: PolicyPath, expected_relative_path: &str, - ) -> Result<()> { + ) -> StoreResult<()> { let default = Store::default(); let path = default.policy_full_path(input_url, input_policy_path)?; assert_eq!( @@ -307,7 +309,7 @@ mod tests { input_url: &str, input_policy_path: PolicyPath, expected_path: &str, - ) -> Result<()> { + ) -> StoreResult<()> { let default = Store::default(); let path = default.policy_path(input_url, input_policy_path)?; assert_eq!(path::encode_path(expected_path), path); diff --git a/src/store/path/default.rs b/src/store/path/default.rs index d99cede..302580e 100644 --- a/src/store/path/default.rs +++ b/src/store/path/default.rs @@ -1,6 +1,7 @@ -use anyhow::Result; use std::path::{Path, PathBuf}; +use crate::store::errors::StoreResult; + /// Encode a path to a format that doesn't contain any invalid characters /// for the target platform. /// This is the default implementation for non-Windows platforms, @@ -15,6 +16,6 @@ pub fn encode_filename(filename: &str) -> String { } /// Retrieve a path that was transformed with `transform_path`. -pub fn decode_path>(path: P) -> Result { +pub fn decode_path>(path: P) -> StoreResult { Ok(path.as_ref().to_path_buf()) } diff --git a/src/store/path/windows.rs b/src/store/path/windows.rs index c38e6f9..5372e7c 100644 --- a/src/store/path/windows.rs +++ b/src/store/path/windows.rs @@ -1,7 +1,7 @@ -use anyhow::Result; use base64::{alphabet, engine::general_purpose, Engine as _}; use std::path::{Path, PathBuf}; +use crate::store::errors::StoreResult; use crate::store::scheme; /// A base64 engine that uses URL_SAFE alphabet and escapes using no padding @@ -42,7 +42,7 @@ pub fn encode_filename(filename: &str) -> String { } /// Decode a path that was transformed with `encode_path`. -pub fn decode_path>(path: P) -> Result { +pub fn decode_path>(path: P) -> StoreResult { let mut decoded_path = PathBuf::new(); for component in path.as_ref().components() { diff --git a/src/verify/config.rs b/src/verify/config.rs index bc3ca98..9117d64 100644 --- a/src/verify/config.rs +++ b/src/verify/config.rs @@ -1,11 +1,12 @@ -use anyhow::{anyhow, Result}; use serde::{Deserialize, Deserializer, Serialize}; use sigstore::cosign::verification_constraint::VerificationConstraint; use std::boxed::Box; use std::{collections::HashMap, fs, path::Path}; use url::Url; -use crate::verify::verification_constraints; +use crate::{errors::FailedToParseYamlDataError, verify::verification_constraints}; + +use super::errors::{VerifyError, VerifyResult}; /// Alias to the type that is currently used to store the /// verification settings. @@ -77,7 +78,7 @@ pub enum Signature { } impl Signature { - pub fn verifier(&self) -> Result> { + pub fn verifier(&self) -> VerifyResult> { match self { Signature::PubKey { owner, @@ -88,8 +89,7 @@ impl Signature { owner.as_ref().map(|r| r.as_str()), key, annotations.as_ref(), - ) - .map_err(|e| anyhow!("Cannot create public key verifier: {}", e))?; + )?; Ok(Box::new(vc)) } Signature::GenericIssuer { @@ -138,7 +138,7 @@ where Ok(url) } -pub fn read_verification_file(path: &Path) -> Result { +pub fn read_verification_file(path: &Path) -> VerifyResult { let config = fs::read_to_string(path)?; build_latest_verification_config(&config) } @@ -151,16 +151,19 @@ pub fn read_verification_file(path: &Path) -> Result { /// For example, when the configuration is missing a required attribute. /// This methods should be used instead of invoking `serde_yaml` deserialization functions against /// the YAML string. -pub fn build_latest_verification_config(config_str: &str) -> Result { - let vc: VerificationConfig = serde_yaml::from_str(config_str)?; +pub fn build_latest_verification_config( + config_str: &str, +) -> VerifyResult { + let vc: VerificationConfig = + serde_yaml::from_str(config_str).map_err(FailedToParseYamlDataError)?; let config = match vc { VerificationConfig::Versioned(versioned_config) => match versioned_config { VersionedVerificationConfig::V1(c) => c, VersionedVerificationConfig::Unsupported => { - return Err(anyhow!( + return Err(VerifyError::InvalidVerifyFileError(format!( "Not a supported configuration version: {:?}", versioned_config - )) + ))); } }, VerificationConfig::Invalid(mut value) => { @@ -183,14 +186,20 @@ pub fn build_latest_verification_config(config_str: &str) -> Result(sanitized_value); - return Err(anyhow!("Not a valid configuration file: {:?}", err)); + let err = serde_yaml::from_value::(sanitized_value).map_err( + |err| { + VerifyError::InvalidVerifyFileError(format!( + "Not a valid configuration file: {err}" + )) + }, + ); + return err; } }; if config.all_of.is_none() && config.any_of.is_none() { - return Err(anyhow!( - "config is missing signatures in both allOf and anyOff list" + return Err(VerifyError::InvalidVerifyFileError( + "config is missing signatures in both allOf and anyOff list".to_owned(), )); } Ok(config) diff --git a/src/verify/errors.rs b/src/verify/errors.rs new file mode 100644 index 0000000..5004eb6 --- /dev/null +++ b/src/verify/errors.rs @@ -0,0 +1,36 @@ +use thiserror::Error; + +use crate::{errors::FailedToParseYamlDataError, registry::errors::RegistryError}; + +pub type VerifyResult = std::result::Result; + +#[derive(Error, Debug)] +pub enum VerifyError { + #[error("faild to read verification file: {0}")] + VerificationFileReadError(#[from] std::io::Error), + #[error("{0}")] + ChecksumVerificationError(String), + #[error("{0}")] + ImageVerificationError(String), + #[error("{0}")] + InvalidVerifyFileError(String), + #[error("Verification only works with OCI images: Not a valid oci image: {0}")] + InvalidOCIImageReferenceError(#[from] oci_distribution::ParseError), + #[error("key verification failure: {0} ")] + KeyVerificationError(#[source] sigstore::errors::SigstoreError), + // The next error is more specialized error based on a sigstore error. It must + // be used explicit. Otherwise, the KeyVerificationError will be used by default + // due the implicit conversion. + #[error("failed to get image trusted layers: {0}")] + FailedToFetchTrustedLayersError(#[from] sigstore::errors::SigstoreError), + #[error("Policy cannot be verified, local wasm file doesn't exist: {0}")] + MissingWasmFileError(String), + #[error(transparent)] + DigestErrors(#[from] crate::policy::DigestError), + #[error(transparent)] + RegistryError(#[from] RegistryError), + #[error("{0}")] + GithubUrlParserError(String), + #[error(transparent)] + FailedToParseYamlDataError(#[from] FailedToParseYamlDataError), +} diff --git a/src/verify/mod.rs b/src/verify/mod.rs index f37f5c5..a1e362c 100644 --- a/src/verify/mod.rs +++ b/src/verify/mod.rs @@ -1,7 +1,6 @@ -use crate::policy::Policy; use crate::sources::Sources; +use crate::{errors::FailedToParseYamlDataError, policy::Policy}; -use anyhow::{anyhow, Result}; use oci_distribution::manifest::WASM_LAYER_MEDIA_TYPE; use sigstore::{ cosign::{self, signature_layers::SignatureLayer, ClientBuilder, CosignCapabilities}, @@ -9,7 +8,9 @@ use sigstore::{ }; use crate::verify::config::Signature; +use crate::verify::errors::VerifyError; use crate::Registry; + use oci_distribution::secrets::RegistryAuth; use oci_distribution::Reference; use sigstore::errors::SigstoreError; @@ -19,6 +20,8 @@ use std::sync::Arc; use tokio::sync::Mutex; use tracing::{debug, error, info, warn}; +use self::errors::VerifyResult; + /// This structure simplifies the process of policy verification /// using Sigstore pub struct Verifier { @@ -27,6 +30,7 @@ pub struct Verifier { } pub mod config; +pub mod errors; pub mod verification_constraints; /// Define how Fulcio and Rekor data are going to be provided to sigstore cosign client @@ -78,7 +82,7 @@ impl Verifier { pub fn new( sources: Option, fulcio_and_rekor_data: Option<&FulcioAndRekorData>, - ) -> Result { + ) -> VerifyResult { let client_config: sigstore::registry::ClientConfig = sources.clone().unwrap_or_default().into(); let mut cosign_client_builder = @@ -116,9 +120,7 @@ impl Verifier { cosign_client_builder = cosign_client_builder.enable_registry_caching(); - let cosign_client = cosign_client_builder - .build() - .map_err(|e| anyhow!("could not build a cosign client: {}", e))?; + let cosign_client = cosign_client_builder.build()?; Ok(Verifier { cosign_client: Arc::new(Mutex::new(cosign_client)), sources, @@ -140,7 +142,7 @@ impl Verifier { &mut self, image_url: &str, verification_config: &config::LatestVerificationConfig, - ) -> Result { + ) -> VerifyResult { let (source_image_digest, trusted_layers) = fetch_sigstore_remote_data(&self.cosign_client, image_url).await?; @@ -166,22 +168,18 @@ impl Verifier { &mut self, policy: &Policy, verified_manifest_digest: &str, - ) -> Result<()> { + ) -> VerifyResult<()> { let image_name = match policy.uri.strip_prefix("registry://") { None => policy.uri.as_str(), Some(url) => url, }; if let Err(e) = Reference::try_from(image_name) { - return Err(anyhow!( - "Verification only works with OCI images: Not a valid oci image {}", - e - )); + return Err(VerifyError::InvalidOCIImageReferenceError(e)); } if !policy.local_path.exists() { - return Err(anyhow!( - "Policy cannot be verified, local wasm file doesn't exist: {:?}", - policy.local_path + return Err(VerifyError::MissingWasmFileError( + policy.local_path.display().to_string(), )); } @@ -215,15 +213,15 @@ impl Verifier { if digests.len() != 1 { error!(manifest = ?manifest, "The manifest is expected to have one WASM layer"); - return Err(anyhow!("Cannot verify local file integrity, the remote manifest doesn't have only one WASM layer")); + return Err(VerifyError::ChecksumVerificationError("Cannot verify local file integrity, the remote manifest doesn't have only one WASM layer".to_owned())); } let expected_digest = digests[0] .strip_prefix("sha256:") - .ok_or_else(|| anyhow!("The checksum inside of the remote manifest is not using the sha256 hashing algorithm as expected."))?; + .ok_or_else(|| VerifyError::ChecksumVerificationError("The checksum inside of the remote manifest is not using the sha256 hashing algorithm as expected.".to_owned()))?; let file_digest = policy.digest()?; if file_digest != expected_digest { - Err(anyhow!("The digest of the local file doesn't match with the one reported inside of the signed manifest. Got {} instead of {}", file_digest, expected_digest)) + Err(VerifyError::ChecksumVerificationError(format!("The digest of the local file doesn't match with the one reported inside of the signed manifest. Got {} instead of {}", file_digest, expected_digest))) } else { info!("Local file checksum verification passed"); Ok(()) @@ -237,13 +235,13 @@ impl Verifier { fn verify_signatures_against_config( verification_config: &config::LatestVerificationConfig, trusted_layers: &[SignatureLayer], -) -> Result<()> { +) -> VerifyResult<()> { // filter trusted_layers against our verification constraints: // if verification_config.all_of.is_none() && verification_config.any_of.is_none() { // deserialized config is already sanitized, and should not reach here anyways - return Err(anyhow!( - "Image verification failed: no signatures to verify" + return Err(VerifyError::ImageVerificationError( + "Image verification failed: no signatures to verify".to_owned(), )); } @@ -278,9 +276,9 @@ fn verify_signatures_against_config( let mut errormsg = "Image verification failed: missing signatures\n".to_string(); errormsg.push_str("The following constraints were not satisfied:\n"); for s in unsatisfied_signatures { - errormsg.push_str(&serde_yaml::to_string(s)?); + errormsg.push_str(&serde_yaml::to_string(s).map_err(FailedToParseYamlDataError)?); } - return Err(anyhow!(errormsg)); + return Err(VerifyError::ImageVerificationError(errormsg)); } } @@ -307,9 +305,10 @@ fn verify_signatures_against_config( format!("Image verification failed: minimum number of signatures not reached: needed {}, got {}", signatures_any_of.minimum_matches, num_satisfied_constraints); errormsg.push_str("\nThe following constraints were not satisfied:\n"); for s in unsatisfied_signatures.iter() { - errormsg.push_str(&serde_yaml::to_string(s)?); + errormsg + .push_str(&serde_yaml::to_string(s).map_err(FailedToParseYamlDataError)?); } - return Err(anyhow!(errormsg)); + return Err(VerifyError::ImageVerificationError(errormsg)); } } } @@ -323,7 +322,7 @@ fn verify_signatures_against_config( pub async fn fetch_sigstore_remote_data( cosign_client_input: &Arc>, image_url: &str, -) -> Result<(String, Vec)> { +) -> VerifyResult<(String, Vec)> { let mut cosign_client = cosign_client_input.lock().await; // obtain image name: @@ -332,10 +331,7 @@ pub async fn fetch_sigstore_remote_data( Some(url) => url, }; if let Err(e) = Reference::try_from(image_name) { - return Err(anyhow!( - "Verification only works with OCI images: Not a valid oci image {}", - e - )); + return Err(VerifyError::InvalidOCIImageReferenceError(e)); } // obtain registry auth: @@ -352,10 +348,12 @@ pub async fn fetch_sigstore_remote_data( // // trusted_signature_layers() will error early if cosign_client using // Fulcio,Rekor certs and signatures are not verified - let image_oci_ref = OciReference::from_str(image_name)?; + let image_oci_ref = + OciReference::from_str(image_name).map_err(VerifyError::FailedToFetchTrustedLayersError)?; let (cosign_signature_image, source_image_digest) = cosign_client .triangulate(&image_oci_ref, &sigstore_auth) - .await?; + .await + .map_err(VerifyError::FailedToFetchTrustedLayersError)?; // get trusted layers let layers = cosign_client @@ -367,9 +365,12 @@ pub async fn fetch_sigstore_remote_data( .await .map_err(|e| match e { SigstoreError::RegistryPullManifestError { image: _, error: _ } => { - anyhow!("no signatures found for image: {} ", image_name) + VerifyError::ImageVerificationError(format!( + "no signatures found for image: {} ", + image_name + )) } - e => anyhow!(e), + e => VerifyError::FailedToFetchTrustedLayersError(e), })?; Ok((source_image_digest, layers)) } diff --git a/src/verify/verification_constraints.rs b/src/verify/verification_constraints.rs index 1036443..368e0c5 100644 --- a/src/verify/verification_constraints.rs +++ b/src/verify/verification_constraints.rs @@ -1,4 +1,3 @@ -use anyhow::anyhow; use std::collections::HashMap; use std::convert::TryFrom; use tracing::debug; @@ -10,6 +9,8 @@ use sigstore::cosign::verification_constraint::{ use sigstore::cosign::{signature_layers::CertificateSubject, SignatureLayer}; use sigstore::errors::{Result, SigstoreError}; +use super::errors::{VerifyError, VerifyResult}; + use super::config::Subject; /// Verification Constraint for public keys and annotations @@ -28,7 +29,7 @@ impl PublicKeyAndAnnotationsVerifier { owner: Option<&str>, key: &str, annotations: Option<&HashMap>, - ) -> Result { + ) -> VerifyResult { let pub_key_verifier = PublicKeyVerifier::try_from(key.as_bytes())?; let annotation_verifier = annotations.map(|a| AnnotationVerifier { annotations: a.to_owned(), @@ -282,22 +283,28 @@ struct GitHubRepo { } impl TryFrom<&str> for GitHubRepo { - type Error = anyhow::Error; + type Error = VerifyError; fn try_from(u: &str) -> std::result::Result { - let u = url::Url::parse(u).map_err(|e| anyhow!("Cannot parse github url: {}", e))?; + let u = url::Url::parse(u).map_err(|e| { + VerifyError::GithubUrlParserError(format!("Cannot parse github url: {}", e)) + })?; if u.host_str() != Some("github.com") { - return Err(anyhow!("Not a GitHub url: host doesn't match")); + return Err(VerifyError::GithubUrlParserError( + "Not a GitHub url: host doesn't match".to_owned(), + )); } - let mut segments = u - .path_segments() - .ok_or_else(|| anyhow!("Cannot parse GitHub url: no path segments"))?; - let owner = segments - .next() - .ok_or_else(|| anyhow!("cannot parse github url: owner not found"))?; - let repo = segments - .next() - .ok_or_else(|| anyhow!("cannot parse github url: repo not found"))?; + let mut segments = u.path_segments().ok_or_else(|| { + VerifyError::GithubUrlParserError( + "Cannot parse GitHub url: no path segments".to_owned(), + ) + })?; + let owner = segments.next().ok_or_else(|| { + VerifyError::GithubUrlParserError("cannot parse github url: owner not found".to_owned()) + })?; + let repo = segments.next().ok_or_else(|| { + VerifyError::GithubUrlParserError("cannot parse github url: repo not found".to_owned()) + })?; Ok(GitHubRepo { owner: owner.to_string(), diff --git a/tests/sources.rs b/tests/sources.rs index 070ce04..2721090 100644 --- a/tests/sources.rs +++ b/tests/sources.rs @@ -1,6 +1,6 @@ mod common; -use anyhow::Error; +use policy_fetcher::sources::SourceResult; use policy_fetcher::sources::{read_sources_file, Certificate, Sources}; use std::io::Write; use tempfile::NamedTempFile; @@ -28,7 +28,7 @@ source_authorities: let expected_cert = Certificate::Pem(common::CERT_DATA.into()); let path = sources_file.path(); - let actual: Result = read_sources_file(path); + let actual: SourceResult = read_sources_file(path); match actual { Ok(_) => { diff --git a/tests/store.rs b/tests/store.rs index 3cb8dd7..7e9aa5e 100644 --- a/tests/store.rs +++ b/tests/store.rs @@ -1,6 +1,5 @@ use std::path::Path; -use anyhow::Result; use policy_fetcher::policy::Policy; use policy_fetcher::store::{path, Store}; use tempfile::tempdir; @@ -160,7 +159,7 @@ fn test_get_policy_by_sha_prefix_duplicate() { assert!(result.is_err()); } -fn setup_store(policies: &[Policy]) -> Result<()> { +fn setup_store(policies: &[Policy]) -> std::result::Result<(), std::io::Error> { for policy in policies { std::fs::create_dir_all(policy.local_path.parent().unwrap())?;