From 02a4458aa96e558dd83c4379e22ca6a81c4ece11 Mon Sep 17 00:00:00 2001 From: Andrew Lilley Brinker Date: Tue, 20 Feb 2024 19:45:50 +0000 Subject: [PATCH] feat: initial full ArtifactId impl This commit completes the first implementation of the ArtifactId type, which is really just a wrapper around GitOid. This includes all methods delegating to GitOid under the hood, plus examples run as doc tests which all pass! Signed-off-by: Andrew Lilley Brinker --- gitoid/src/lib.rs | 4 +- omnibor/Cargo.toml | 8 +- omnibor/src/artifact_id.rs | 324 ++++++++++++++++++++++++++++++++++ omnibor/src/error.rs | 39 ++++ omnibor/src/lib.rs | 16 +- omnibor/src/sealed.rs | 1 + omnibor/src/supported_hash.rs | 21 +++ 7 files changed, 404 insertions(+), 9 deletions(-) create mode 100644 omnibor/src/artifact_id.rs create mode 100644 omnibor/src/error.rs create mode 100644 omnibor/src/sealed.rs create mode 100644 omnibor/src/supported_hash.rs diff --git a/gitoid/src/lib.rs b/gitoid/src/lib.rs index 1aba6fc..ce84be5 100644 --- a/gitoid/src/lib.rs +++ b/gitoid/src/lib.rs @@ -94,11 +94,9 @@ //! //! ```rust //! # use gitoid::{Sha256, Blob}; -//! //! type GitOid = gitoid::GitOid; //! -//! let gitoid: GitOid = gitoid::GitOid::from_str("hello, world"); -//! +//! let gitoid = GitOid::from_str("hello, world"); //! println!("gitoid: {}", gitoid); //! ``` //! diff --git a/omnibor/Cargo.toml b/omnibor/Cargo.toml index 0150605..6b1df96 100644 --- a/omnibor/Cargo.toml +++ b/omnibor/Cargo.toml @@ -11,4 +11,10 @@ repository = "https://github.com/omnibor/omnibor-rs" version = "0.1.7" [dependencies] -gitoid = "0.4.0" +gitoid = "0.5.0" +tokio = { version = "1.36.0", features = ["io-util"] } +url = "2.5.0" + +[dev-dependencies] +tokio = { version = "1.36.0", features = ["io-util", "fs"] } +tokio-test = "0.4.3" diff --git a/omnibor/src/artifact_id.rs b/omnibor/src/artifact_id.rs new file mode 100644 index 0000000..73b9028 --- /dev/null +++ b/omnibor/src/artifact_id.rs @@ -0,0 +1,324 @@ +use crate::Error; +use crate::Result; +use crate::SupportedHash; +use gitoid::Blob; +use gitoid::GitOid; +use std::cmp::Ordering; +use std::fmt::Debug; +use std::fmt::Display; +use std::fmt::Formatter; +use std::fmt::Result as FmtResult; +use std::hash::Hash; +use std::hash::Hasher; +use std::io::Read; +use std::io::Seek; +use std::str::FromStr; +use tokio::io::AsyncRead; +use tokio::io::AsyncSeek; +use url::Url; + +/// An OmniBOR Artifact Identifier. +/// +/// This is a content-based unique identifier for any software artifact. +/// +/// It is built around, per the specification, any supported hash algorithm. +/// Currently, only SHA-256 is supported, but others may be added in the future. +pub struct ArtifactId { + #[doc(hidden)] + gitoid: GitOid, +} + +impl ArtifactId { + /// Construct an [`ArtifactId`] from an existing [`GitOid`]. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// # use gitoid::GitOid; + /// let gitoid = GitOid::from_str("hello, world"); + /// let id: ArtifactId = ArtifactId::from_gitoid(gitoid); + /// println!("Artifact ID: {}", id); + /// ``` + pub fn from_gitoid(gitoid: GitOid) -> ArtifactId { + ArtifactId { gitoid } + } + + /// Construct an [`ArtifactId`] from raw bytes. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// let id: ArtifactId = ArtifactId::from_bytes(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + /// println!("Artifact ID: {}", id); + /// ``` + pub fn from_bytes>(content: B) -> ArtifactId { + ArtifactId::from_gitoid(GitOid::from_bytes(content)) + } + + /// Construct an [`ArtifactId`] from a string. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// let id: ArtifactId = ArtifactId::from_str("hello, world"); + /// println!("Artifact ID: {}", id); + /// ``` + #[allow(clippy::should_implement_trait)] + pub fn from_str>(s: S) -> ArtifactId { + ArtifactId::from_gitoid(GitOid::from_str(s)) + } + + /// Construct an [`ArtifactId`] from a synchronous reader. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// # use std::fs::File; + /// let file = File::open("test/data/hello_world.txt").unwrap(); + /// let id: ArtifactId = ArtifactId::from_reader(&file).unwrap(); + /// println!("Artifact ID: {}", id); + /// ``` + pub fn from_reader(reader: R) -> Result> { + let gitoid = GitOid::from_reader(reader)?; + Ok(ArtifactId::from_gitoid(gitoid)) + } + + /// Construct an [`ArtifactId`] from a synchronous reader with an expected length. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// # use std::fs::File; + /// let file = File::open("test/data/hello_world.txt").unwrap(); + /// let id: ArtifactId = ArtifactId::from_reader_with_length(&file, 11).unwrap(); + /// println!("Artifact ID: {}", id); + /// ``` + pub fn from_reader_with_length( + reader: R, + expected_length: usize, + ) -> Result> { + let gitoid = GitOid::from_reader_with_length(reader, expected_length)?; + Ok(ArtifactId::from_gitoid(gitoid)) + } + + /// Construct an [`ArtifactId`] from an asynchronous reader. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// # use tokio::fs::File; + /// # tokio_test::block_on(async { + /// let mut file = File::open("test/data/hello_world.txt").await.unwrap(); + /// let id: ArtifactId = ArtifactId::from_async_reader(&mut file).await.unwrap(); + /// println!("Artifact ID: {}", id); + /// # }) + /// ``` + pub async fn from_async_reader( + reader: R, + ) -> Result> { + let gitoid = GitOid::from_async_reader(reader).await?; + Ok(ArtifactId::from_gitoid(gitoid)) + } + + /// Construct an [`ArtifactId`] from an asynchronous reader with an expected length. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// # use tokio::fs::File; + /// # tokio_test::block_on(async { + /// let mut file = File::open("test/data/hello_world.txt").await.unwrap(); + /// let id: ArtifactId = ArtifactId::from_async_reader_with_length(&mut file, 11).await.unwrap(); + /// println!("Artifact ID: {}", id); + /// # }) + /// ``` + pub async fn from_async_reader_with_length( + reader: R, + expected_length: usize, + ) -> Result> { + let gitoid = GitOid::from_async_reader_with_length(reader, expected_length).await?; + Ok(ArtifactId::from_gitoid(gitoid)) + } + + /// Construct an [`ArtifactId`] from a [`Url`]. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// # use url::Url; + /// let url = Url::parse("gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03").unwrap(); + /// let id: ArtifactId = ArtifactId::from_url(url).unwrap(); + /// println!("Artifact ID: {}", id); + /// ``` + pub fn from_url(url: Url) -> Result> { + ArtifactId::try_from(url) + } + + /// Get the [`Url`] representation of the [`ArtifactId`]. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// let id: ArtifactId = ArtifactId::from_str("hello, world"); + /// println!("Artifact ID URL: {}", id.url()); + /// ``` + pub fn url(&self) -> Url { + self.gitoid.url() + } + + /// Get the underlying bytes of the [`ArtifactId`] hash. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// let id: ArtifactId = ArtifactId::from_str("hello, world"); + /// println!("Artifact ID bytes: {:?}", id.as_bytes()); + /// ``` + pub fn as_bytes(&self) -> &[u8] { + self.gitoid.as_bytes() + } + + /// Get the bytes of the [`ArtifactId`] hash as a hexadecimal string. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// let id: ArtifactId = ArtifactId::from_str("hello, world"); + /// println!("Artifact ID bytes as hex: {}", id.as_hex()); + /// ``` + pub fn as_hex(&self) -> String { + self.gitoid.as_hex() + } + + /// Get the name of the hash algorithm used in the [`ArtifactId`] as a string. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// let id: ArtifactId = ArtifactId::from_str("hello, world"); + /// println!("Artifact ID hash algorithm: {}", id.hash_algorithm()); + /// ``` + pub const fn hash_algorithm(&self) -> &'static str { + self.gitoid.hash_algorithm() + } + + /// Get the object type used in the [`ArtifactId`] as a string. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// let id: ArtifactId = ArtifactId::from_str("hello, world"); + /// println!("Artifact ID object type: {}", id.object_type()); + /// ``` + pub const fn object_type(&self) -> &'static str { + self.gitoid.object_type() + } + + /// Get the length in bytes of the hash used in the [`ArtifactId`]. + /// + /// # Example + /// + /// ```rust + /// # use omnibor::ArtifactId; + /// # use omnibor::Sha256; + /// let id: ArtifactId = ArtifactId::from_str("hello, world"); + /// println!("Artifact ID hash length in bytes: {}", id.hash_len()); + /// ``` + pub fn hash_len(&self) -> usize { + self.gitoid.hash_len() + } +} + +impl FromStr for ArtifactId { + type Err = Error; + + fn from_str(s: &str) -> Result> { + Ok(ArtifactId::from_str(s)) + } +} + +impl Clone for ArtifactId { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for ArtifactId {} + +impl PartialEq> for ArtifactId { + fn eq(&self, other: &Self) -> bool { + self.gitoid == other.gitoid + } +} + +impl Eq for ArtifactId {} + +impl PartialOrd> for ArtifactId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ArtifactId { + fn cmp(&self, other: &Self) -> Ordering { + self.gitoid.cmp(&other.gitoid) + } +} + +impl Hash for ArtifactId { + fn hash

(&self, state: &mut H2) + where + H2: Hasher, + { + self.gitoid.hash(state); + } +} + +impl Debug for ArtifactId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.debug_struct("ArtifactId") + .field("gitoid", &self.gitoid) + .finish() + } +} + +impl Display for ArtifactId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{}", self.gitoid) + } +} + +impl TryFrom for ArtifactId { + type Error = Error; + + fn try_from(url: Url) -> Result> { + let gitoid = GitOid::from_url(url)?; + Ok(ArtifactId::from_gitoid(gitoid)) + } +} diff --git a/omnibor/src/error.rs b/omnibor/src/error.rs new file mode 100644 index 0000000..6723bd7 --- /dev/null +++ b/omnibor/src/error.rs @@ -0,0 +1,39 @@ +#[cfg(doc)] +use crate::ArtifactId; +use gitoid::Error as GitOidError; +use std::error::Error as StdError; +use std::fmt::Display; +use std::fmt::Formatter; +use std::fmt::Result as FmtResult; +use std::result::Result as StdResult; + +pub type Result = StdResult; + +/// Errors arising from [`ArtifactId`] use. +#[derive(Debug)] +pub enum Error { + /// An error arising from the underlying `gitoid` crate. + GitOid(GitOidError), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + match self { + Error::GitOid(inner) => write!(f, "{}", inner), + } + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Error::GitOid(inner) => Some(inner), + } + } +} + +impl From for Error { + fn from(inner: GitOidError) -> Error { + Error::GitOid(inner) + } +} diff --git a/omnibor/src/lib.rs b/omnibor/src/lib.rs index aeb8d24..bf9f30a 100644 --- a/omnibor/src/lib.rs +++ b/omnibor/src/lib.rs @@ -1,8 +1,14 @@ //! OmniBOR in Rust. -use gitoid::hash::Sha256; -use gitoid::object::Blob; -use gitoid::GitOid; +pub(crate) mod sealed; -/// An OmniBOR Artifact Identifier. -pub type ArtifactId = GitOid; +mod artifact_id; +mod error; +mod supported_hash; + +pub(crate) use crate::error::Result; + +pub use crate::artifact_id::ArtifactId; +pub use crate::error::Error; +pub use crate::supported_hash::Sha256; +pub use crate::supported_hash::SupportedHash; diff --git a/omnibor/src/sealed.rs b/omnibor/src/sealed.rs new file mode 100644 index 0000000..0650b2f --- /dev/null +++ b/omnibor/src/sealed.rs @@ -0,0 +1 @@ +pub trait Sealed {} diff --git a/omnibor/src/supported_hash.rs b/omnibor/src/supported_hash.rs new file mode 100644 index 0000000..4fe182d --- /dev/null +++ b/omnibor/src/supported_hash.rs @@ -0,0 +1,21 @@ +use crate::sealed::Sealed; +#[cfg(doc)] +use crate::ArtifactId; +use gitoid::HashAlgorithm; + +/// Marker trait for hash algorithms supported for constructing [`ArtifactId`]s. +pub trait SupportedHash: Sealed { + type HashAlgorithm: HashAlgorithm; +} + +/// The SHA-256 hashing algorithm. +pub struct Sha256 { + #[doc(hidden)] + _private: (), +} + +impl Sealed for Sha256 {} + +impl SupportedHash for Sha256 { + type HashAlgorithm = gitoid::Sha256; +}