| //! Interfaces for interacting with different types of TUF repositories. |
| |
| use crate::crypto::{self, HashAlgorithm, HashValue}; |
| use crate::interchange::DataInterchange; |
| use crate::metadata::{ |
| Metadata, MetadataPath, MetadataVersion, RawSignedMetadata, TargetDescription, TargetPath, |
| }; |
| use crate::util::SafeAsyncRead; |
| use crate::{Error, Result}; |
| |
| use futures_io::AsyncRead; |
| use futures_util::future::BoxFuture; |
| use futures_util::io::AsyncReadExt; |
| use std::marker::PhantomData; |
| use std::sync::Arc; |
| |
| mod file_system; |
| pub use self::file_system::{ |
| FileSystemBatchUpdate, FileSystemRepository, FileSystemRepositoryBuilder, |
| }; |
| |
| #[cfg(any(feature = "hyper_013", feature = "hyper_014"))] |
| mod http; |
| |
| #[cfg(any(feature = "hyper_013", feature = "hyper_014"))] |
| pub use self::http::{HttpRepository, HttpRepositoryBuilder}; |
| |
| mod ephemeral; |
| pub use self::ephemeral::{EphemeralBatchUpdate, EphemeralRepository}; |
| |
| #[cfg(test)] |
| mod error_repo; |
| #[cfg(test)] |
| pub(crate) use self::error_repo::ErrorRepository; |
| |
| #[cfg(test)] |
| mod track_repo; |
| #[cfg(test)] |
| pub(crate) use self::track_repo::{Track, TrackRepository}; |
| |
| /// A readable TUF repository. |
| pub trait RepositoryProvider<D> |
| where |
| D: DataInterchange + Sync, |
| { |
| /// Fetch signed metadata identified by `meta_path`, `version`, and |
| /// [`D::extension()`][extension]. |
| /// |
| /// Implementations may ignore `max_length` and `hash_data` as [`Client`][Client] will verify |
| /// these constraints itself. However, it may be more efficient for an implementation to detect |
| /// invalid metadata and fail the fetch operation before streaming all of the bytes of the |
| /// metadata. |
| /// |
| /// [extension]: crate::interchange::DataInterchange::extension |
| /// [Client]: crate::client::Client |
| fn fetch_metadata<'a>( |
| &'a self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>>; |
| |
| /// Fetch the given target. |
| /// |
| /// Implementations may ignore the `length` and `hashes` fields in `target_description` as |
| /// [`Client`][Client] will verify these constraints itself. However, it may be more efficient |
| /// for an implementation to detect invalid targets and fail the fetch operation before |
| /// streaming all of the bytes. |
| /// |
| /// [Client]: crate::client::Client |
| fn fetch_target<'a>( |
| &'a self, |
| target_path: &TargetPath, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>>; |
| } |
| |
| /// Test helper to help read a metadata file from a repository into a string. |
| #[cfg(test)] |
| pub(crate) async fn fetch_metadata_to_string<D, R>( |
| repo: &R, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| ) -> Result<String> |
| where |
| D: DataInterchange + Sync, |
| R: RepositoryProvider<D>, |
| { |
| let mut reader = repo.fetch_metadata(meta_path, version).await?; |
| let mut buf = String::new(); |
| reader.read_to_string(&mut buf).await.unwrap(); |
| Ok(buf) |
| } |
| |
| /// Test helper to help read a target file from a repository into a string. |
| #[cfg(test)] |
| pub(crate) async fn fetch_target_to_string<D, R>( |
| repo: &R, |
| target_path: &TargetPath, |
| ) -> Result<String> |
| where |
| D: DataInterchange + Sync, |
| R: RepositoryProvider<D>, |
| { |
| let mut reader = repo.fetch_target(target_path).await?; |
| let mut buf = String::new(); |
| reader.read_to_string(&mut buf).await.unwrap(); |
| Ok(buf) |
| } |
| |
| /// A writable TUF repository. Most implementors of this trait should also implement |
| /// `RepositoryProvider`. |
| pub trait RepositoryStorage<D> |
| where |
| D: DataInterchange + Sync, |
| { |
| /// Store the provided `metadata` in a location identified by `meta_path`, `version`, and |
| /// [`D::extension()`][extension], overwriting any existing metadata at that location. |
| /// |
| /// [extension]: crate::interchange::DataInterchange::extension |
| fn store_metadata<'a>( |
| &'a mut self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), |
| ) -> BoxFuture<'a, Result<()>>; |
| |
| /// Store the provided `target` in a location identified by `target_path`, overwriting any |
| /// existing target at that location. |
| fn store_target<'a>( |
| &'a mut self, |
| target_path: &TargetPath, |
| target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), |
| ) -> BoxFuture<'a, Result<()>>; |
| } |
| |
| /// A subtrait of both RepositoryStorage and RepositoryProvider. This is useful to create |
| /// trait objects that implement both traits. |
| pub trait RepositoryStorageProvider<D>: RepositoryStorage<D> + RepositoryProvider<D> |
| where |
| D: DataInterchange + Sync, |
| { |
| } |
| |
| impl<D, T> RepositoryStorageProvider<D> for T |
| where |
| D: DataInterchange + Sync, |
| T: RepositoryStorage<D> + RepositoryProvider<D>, |
| { |
| } |
| |
| impl<T, D> RepositoryProvider<D> for &T |
| where |
| T: RepositoryProvider<D>, |
| D: DataInterchange + Sync, |
| { |
| fn fetch_metadata<'a>( |
| &'a self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_metadata(meta_path, version) |
| } |
| |
| fn fetch_target<'a>( |
| &'a self, |
| target_path: &TargetPath, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_target(target_path) |
| } |
| } |
| |
| impl<T, D> RepositoryProvider<D> for &mut T |
| where |
| T: RepositoryProvider<D>, |
| D: DataInterchange + Sync, |
| { |
| fn fetch_metadata<'a>( |
| &'a self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_metadata(meta_path, version) |
| } |
| |
| fn fetch_target<'a>( |
| &'a self, |
| target_path: &TargetPath, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_target(target_path) |
| } |
| } |
| |
| impl<T, D> RepositoryStorage<D> for &mut T |
| where |
| T: RepositoryStorage<D>, |
| D: DataInterchange + Sync, |
| { |
| fn store_metadata<'a>( |
| &'a mut self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), |
| ) -> BoxFuture<'a, Result<()>> { |
| (**self).store_metadata(meta_path, version, metadata) |
| } |
| |
| fn store_target<'a>( |
| &'a mut self, |
| target_path: &TargetPath, |
| target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), |
| ) -> BoxFuture<'a, Result<()>> { |
| (**self).store_target(target_path, target) |
| } |
| } |
| |
| impl<T, D> RepositoryStorage<D> for Box<T> |
| where |
| T: RepositoryStorage<D> + ?Sized, |
| D: DataInterchange + Sync, |
| { |
| fn store_metadata<'a>( |
| &'a mut self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), |
| ) -> BoxFuture<'a, Result<()>> { |
| (**self).store_metadata(meta_path, version, metadata) |
| } |
| |
| fn store_target<'a>( |
| &'a mut self, |
| target_path: &TargetPath, |
| target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), |
| ) -> BoxFuture<'a, Result<()>> { |
| (**self).store_target(target_path, target) |
| } |
| } |
| |
| impl<T, D> RepositoryProvider<D> for Box<T> |
| where |
| T: RepositoryProvider<D> + ?Sized, |
| D: DataInterchange + Sync, |
| { |
| fn fetch_metadata<'a>( |
| &'a self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_metadata(meta_path, version) |
| } |
| |
| fn fetch_target<'a>( |
| &'a self, |
| target_path: &TargetPath, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_target(target_path) |
| } |
| } |
| |
| impl<D, T> RepositoryProvider<D> for Arc<T> |
| where |
| D: DataInterchange + Sync, |
| T: RepositoryProvider<D> + ?Sized, |
| { |
| fn fetch_metadata<'a>( |
| &'a self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_metadata(meta_path, version) |
| } |
| |
| fn fetch_target<'a>( |
| &'a self, |
| target_path: &TargetPath, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_target(target_path) |
| } |
| } |
| |
| impl<D> RepositoryProvider<D> for &dyn RepositoryProvider<D> |
| where |
| D: DataInterchange + Sync, |
| { |
| fn fetch_metadata<'a>( |
| &'a self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_metadata(meta_path, version) |
| } |
| |
| fn fetch_target<'a>( |
| &'a self, |
| target_path: &TargetPath, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_target(target_path) |
| } |
| } |
| |
| impl<D> RepositoryProvider<D> for &mut dyn RepositoryProvider<D> |
| where |
| D: DataInterchange + Sync, |
| { |
| fn fetch_metadata<'a>( |
| &'a self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_metadata(meta_path, version) |
| } |
| |
| fn fetch_target<'a>( |
| &'a self, |
| target_path: &TargetPath, |
| ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> { |
| (**self).fetch_target(target_path) |
| } |
| } |
| |
| impl<D> RepositoryStorage<D> for &mut dyn RepositoryStorage<D> |
| where |
| D: DataInterchange + Sync, |
| { |
| fn store_metadata<'a>( |
| &'a mut self, |
| meta_path: &MetadataPath, |
| version: MetadataVersion, |
| metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), |
| ) -> BoxFuture<'a, Result<()>> { |
| (**self).store_metadata(meta_path, version, metadata) |
| } |
| |
| fn store_target<'a>( |
| &'a mut self, |
| target_path: &TargetPath, |
| target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), |
| ) -> BoxFuture<'a, Result<()>> { |
| (**self).store_target(target_path, target) |
| } |
| } |
| |
| /// A wrapper around an implementation of [`RepositoryProvider`] and/or [`RepositoryStorage`] tied |
| /// to a specific [`DataInterchange`](crate::interchange::DataInterchange) that will enforce |
| /// provided length limits and hash checks. |
| #[derive(Debug, Clone)] |
| pub(crate) struct Repository<R, D> { |
| repository: R, |
| _interchange: PhantomData<D>, |
| } |
| |
| impl<R, D> Repository<R, D> { |
| /// Creates a new [`Repository`] wrapping `repository`. |
| pub(crate) fn new(repository: R) -> Self { |
| Self { |
| repository, |
| _interchange: PhantomData, |
| } |
| } |
| |
| /// Perform a sanity check that `M`, `Role`, and `MetadataPath` all describe the same entity. |
| fn check<M>(meta_path: &MetadataPath) -> Result<()> |
| where |
| M: Metadata, |
| { |
| if !M::ROLE.fuzzy_matches_path(meta_path) { |
| return Err(Error::IllegalArgument(format!( |
| "Role {} does not match path {:?}", |
| M::ROLE, |
| meta_path |
| ))); |
| } |
| |
| Ok(()) |
| } |
| |
| pub(crate) fn into_inner(self) -> R { |
| self.repository |
| } |
| |
| pub(crate) fn as_inner(&self) -> &R { |
| &self.repository |
| } |
| |
| pub(crate) fn as_inner_mut(&mut self) -> &mut R { |
| &mut self.repository |
| } |
| } |
| |
| impl<R, D> Repository<R, D> |
| where |
| R: RepositoryProvider<D>, |
| D: DataInterchange + Sync, |
| { |
| /// Fetch metadata identified by `meta_path`, `version`, and [`D::extension()`][extension]. |
| /// |
| /// If `max_length` is provided, this method will return an error if the metadata exceeds |
| /// `max_length` bytes. If `hash_data` is provided, this method will return and error if the |
| /// hashed bytes of the metadata do not match `hash_data`. |
| /// |
| /// [extension]: crate::interchange::DataInterchange::extension |
| pub(crate) async fn fetch_metadata<'a, M>( |
| &'a self, |
| meta_path: &'a MetadataPath, |
| version: MetadataVersion, |
| max_length: Option<usize>, |
| hashes: Vec<(&'static HashAlgorithm, HashValue)>, |
| ) -> Result<RawSignedMetadata<D, M>> |
| where |
| M: Metadata, |
| { |
| Self::check::<M>(meta_path)?; |
| |
| // Fetch the metadata, verifying max_length and hashes (if provided), as |
| // the repository implementation should only be trusted to use those as |
| // hints to fail early. |
| let mut reader = self |
| .repository |
| .fetch_metadata(meta_path, version) |
| .await? |
| .check_length_and_hash(max_length.unwrap_or(::std::usize::MAX) as u64, hashes)?; |
| |
| let mut buf = Vec::new(); |
| reader.read_to_end(&mut buf).await?; |
| |
| Ok(RawSignedMetadata::new(buf)) |
| } |
| |
| /// Fetch the target identified by `target_path` through the returned `AsyncRead`, verifying |
| /// that the target matches the preferred hash specified in `target_description` and that it is |
| /// the expected length. Such verification errors will be provided by a read failure on the |
| /// provided `AsyncRead`. |
| /// |
| /// It is **critical** that none of the bytes from the returned `AsyncRead` are used until it |
| /// has been fully consumed as the data is untrusted. |
| pub(crate) async fn fetch_target( |
| &self, |
| consistent_snapshot: bool, |
| target_path: &TargetPath, |
| target_description: TargetDescription, |
| ) -> Result<impl AsyncRead + Send + Unpin + '_> { |
| // https://theupdateframework.github.io/specification/v1.0.26/#fetch-target 5.7.3: |
| // |
| // [...] download the target (up to the number of bytes specified in the targets metadata), |
| // and verify that its hashes match the targets metadata. |
| let length = target_description.length(); |
| let hashes = crypto::retain_supported_hashes(target_description.hashes()); |
| if hashes.is_empty() { |
| return Err(Error::NoSupportedHashAlgorithm); |
| } |
| |
| // https://theupdateframework.github.io/specification/v1.0.26/#fetch-target 5.7.3: |
| // |
| // [...] If consistent snapshots are not used (see § 6.2 Consistent snapshots), then the |
| // filename used to download the target file is of the fixed form FILENAME.EXT (e.g., |
| // foobar.tar.gz). Otherwise, the filename is of the form HASH.FILENAME.EXT [...] |
| let target = if consistent_snapshot { |
| let mut hashes = hashes.iter(); |
| loop { |
| if let Some((_, hash)) = hashes.next() { |
| let target_path = target_path.with_hash_prefix(hash)?; |
| match self.repository.fetch_target(&target_path).await { |
| Ok(target) => break target, |
| Err(Error::NotFound) => {} |
| Err(err) => return Err(err), |
| } |
| } else { |
| return Err(Error::NotFound); |
| } |
| } |
| } else { |
| self.repository.fetch_target(target_path).await? |
| }; |
| |
| target.check_length_and_hash(length, hashes) |
| } |
| } |
| |
| impl<R, D> Repository<R, D> |
| where |
| R: RepositoryStorage<D>, |
| D: DataInterchange + Sync, |
| { |
| /// Store the provided `metadata` in a location identified by `meta_path`, `version`, and |
| /// [`D::extension()`][extension], overwriting any existing metadata at that location. |
| /// |
| /// [extension]: crate::interchange::DataInterchange::extension |
| pub async fn store_metadata<'a, M>( |
| &'a mut self, |
| path: &'a MetadataPath, |
| version: MetadataVersion, |
| metadata: &'a RawSignedMetadata<D, M>, |
| ) -> Result<()> |
| where |
| M: Metadata + Sync, |
| { |
| Self::check::<M>(path)?; |
| |
| self.repository |
| .store_metadata(path, version, &mut metadata.as_bytes()) |
| .await |
| } |
| |
| /// Store the provided `target` in a location identified by `target_path`. |
| pub async fn store_target<'a>( |
| &'a mut self, |
| target_path: &'a TargetPath, |
| target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), |
| ) -> Result<()> { |
| self.repository.store_target(target_path, target).await |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use crate::interchange::Json; |
| use crate::metadata::{MetadataPath, MetadataVersion, RootMetadata, SnapshotMetadata}; |
| use crate::repository::EphemeralRepository; |
| use assert_matches::assert_matches; |
| use futures_executor::block_on; |
| |
| #[test] |
| fn repository_forwards_not_found_error() { |
| block_on(async { |
| let repo = Repository::<_, Json>::new(EphemeralRepository::new()); |
| |
| assert_matches!( |
| repo.fetch_metadata::<RootMetadata>( |
| &MetadataPath::root(), |
| MetadataVersion::None, |
| None, |
| vec![], |
| ) |
| .await, |
| Err(Error::NotFound) |
| ); |
| }); |
| } |
| |
| #[test] |
| fn repository_rejects_mismatched_path() { |
| block_on(async { |
| let mut repo = Repository::<_, Json>::new(EphemeralRepository::new()); |
| let fake_metadata = RawSignedMetadata::<Json, RootMetadata>::new(vec![]); |
| |
| repo.store_metadata(&MetadataPath::root(), MetadataVersion::None, &fake_metadata) |
| .await |
| .unwrap(); |
| |
| assert_matches!( |
| repo.store_metadata( |
| &MetadataPath::snapshot(), |
| MetadataVersion::None, |
| &fake_metadata, |
| ) |
| .await, |
| Err(Error::IllegalArgument(_)) |
| ); |
| |
| assert_matches!( |
| repo.fetch_metadata::<SnapshotMetadata>( |
| &MetadataPath::root(), |
| MetadataVersion::None, |
| None, |
| vec![], |
| ) |
| .await, |
| Err(Error::IllegalArgument(_)) |
| ); |
| }); |
| } |
| |
| #[test] |
| fn repository_verifies_metadata_hash() { |
| block_on(async { |
| let path = MetadataPath::root(); |
| let version = MetadataVersion::None; |
| let data: &[u8] = b"valid metadata"; |
| let _metadata = RawSignedMetadata::<Json, RootMetadata>::new(data.to_vec()); |
| let data_hash = crypto::calculate_hash(data, &HashAlgorithm::Sha256); |
| |
| let mut repo = EphemeralRepository::new(); |
| repo.store_metadata(&path, version, &mut &*data) |
| .await |
| .unwrap(); |
| |
| let client = Repository::<_, Json>::new(repo); |
| |
| assert_matches!( |
| client |
| .fetch_metadata::<RootMetadata>( |
| &path, |
| version, |
| None, |
| vec![(&HashAlgorithm::Sha256, data_hash)], |
| ) |
| .await, |
| Ok(_metadata) |
| ); |
| }) |
| } |
| |
| #[test] |
| fn repository_rejects_corrupt_metadata() { |
| block_on(async { |
| let path = MetadataPath::root(); |
| let version = MetadataVersion::None; |
| let data: &[u8] = b"corrupt metadata"; |
| |
| let mut repo = EphemeralRepository::new(); |
| repo.store_metadata(&path, version, &mut &*data) |
| .await |
| .unwrap(); |
| |
| let client = Repository::<_, Json>::new(repo); |
| |
| assert_matches!( |
| client |
| .fetch_metadata::<RootMetadata>( |
| &path, |
| version, |
| None, |
| vec![(&HashAlgorithm::Sha256, HashValue::new(vec![]))], |
| ) |
| .await, |
| Err(_) |
| ); |
| }) |
| } |
| |
| #[test] |
| fn repository_verifies_metadata_size() { |
| block_on(async { |
| let path = MetadataPath::root(); |
| let version = MetadataVersion::None; |
| let data: &[u8] = b"reasonably sized metadata"; |
| let _metadata = RawSignedMetadata::<Json, RootMetadata>::new(data.to_vec()); |
| |
| let mut repo = EphemeralRepository::new(); |
| repo.store_metadata(&path, version, &mut &*data) |
| .await |
| .unwrap(); |
| |
| let client = Repository::<_, Json>::new(repo); |
| |
| assert_matches!( |
| client |
| .fetch_metadata::<RootMetadata>(&path, version, Some(100), vec![]) |
| .await, |
| Ok(_metadata) |
| ); |
| }) |
| } |
| |
| #[test] |
| fn repository_rejects_oversized_metadata() { |
| block_on(async { |
| let path = MetadataPath::root(); |
| let version = MetadataVersion::None; |
| let data: &[u8] = b"very big metadata"; |
| |
| let mut repo = EphemeralRepository::new(); |
| repo.store_metadata(&path, version, &mut &*data) |
| .await |
| .unwrap(); |
| |
| let client = Repository::<_, Json>::new(repo); |
| |
| assert_matches!( |
| client |
| .fetch_metadata::<RootMetadata>(&path, version, Some(4), vec![]) |
| .await, |
| Err(_) |
| ); |
| }) |
| } |
| |
| #[test] |
| fn repository_rejects_corrupt_targets() { |
| block_on(async { |
| let repo = EphemeralRepository::new(); |
| let mut client = Repository::<_, Json>::new(repo); |
| |
| let data: &[u8] = b"like tears in the rain"; |
| let target_description = |
| TargetDescription::from_slice(data, &[HashAlgorithm::Sha256]).unwrap(); |
| let path = TargetPath::new("batty").unwrap(); |
| client.store_target(&path, &mut &*data).await.unwrap(); |
| |
| let mut read = client |
| .fetch_target(false, &path, target_description.clone()) |
| .await |
| .unwrap(); |
| let mut buf = Vec::new(); |
| read.read_to_end(&mut buf).await.unwrap(); |
| assert_eq!(buf.as_slice(), data); |
| drop(read); |
| |
| let bad_data: &[u8] = b"you're in a desert"; |
| client.store_target(&path, &mut &*bad_data).await.unwrap(); |
| let mut read = client |
| .fetch_target(false, &path, target_description) |
| .await |
| .unwrap(); |
| assert!(read.read_to_end(&mut buf).await.is_err()); |
| }) |
| } |
| |
| #[test] |
| fn repository_takes_trait_objects() { |
| block_on(async { |
| let repo: Box<dyn RepositoryStorageProvider<Json>> = |
| Box::new(EphemeralRepository::new()); |
| let mut client = Repository::<_, Json>::new(repo); |
| |
| let data: &[u8] = b"like tears in the rain"; |
| let target_description = |
| TargetDescription::from_slice(data, &[HashAlgorithm::Sha256]).unwrap(); |
| let path = TargetPath::new("batty").unwrap(); |
| client.store_target(&path, &mut &*data).await.unwrap(); |
| |
| let mut read = client |
| .fetch_target(false, &path, target_description) |
| .await |
| .unwrap(); |
| let mut buf = Vec::new(); |
| read.read_to_end(&mut buf).await.unwrap(); |
| assert_eq!(buf.as_slice(), data); |
| }) |
| } |
| |
| #[test] |
| fn repository_dyn_impls_repository_traits() { |
| let mut repo = EphemeralRepository::new(); |
| |
| fn storage<T: RepositoryStorage<Json>>(_t: T) {} |
| fn provider<T: RepositoryProvider<Json>>(_t: T) {} |
| |
| provider(&repo as &dyn RepositoryProvider<Json>); |
| provider(&mut repo as &mut dyn RepositoryProvider<Json>); |
| storage(&mut repo as &mut dyn RepositoryStorage<Json>); |
| } |
| } |