diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6d8ea72 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Windows test file that should _always_ use DOS-style newlines, +# regardless of the current system. Used for tests to validate +# newline normalization is working. +windows_line.txt text eol=crlf + diff --git a/gitoid/Cargo.toml b/gitoid/Cargo.toml index da7e350..252ce75 100644 --- a/gitoid/Cargo.toml +++ b/gitoid/Cargo.toml @@ -30,13 +30,13 @@ sha2 = { version = "0.10.8", default-features = false, optional = true } # std-requiring dependencies. -format-bytes = { version = "0.3.0", optional = true } hex = { version = "0.4.3", optional = true } serde = { version = "1.0.197", optional = true } tokio = { version = "1.36.0", features = ["io-util"], optional = true } url = { version = "2.4.1", optional = true } boring = { version = "4.6.0", optional = true } openssl = { version = "0.10.66", optional = true } +bytecount = "0.6.8" [dev-dependencies] @@ -101,13 +101,7 @@ sha256 = ["dep:sha2"] # # This feature is enabled by default. You can disable it to run in # environments without `std`, usually embedded environments. -std = [ - "digest/std", - "sha1?/std", - "sha1collisiondetection?/std", - "sha2?/std", - "dep:format-bytes", -] +std = ["digest/std", "sha1?/std", "sha1collisiondetection?/std", "sha2?/std"] # Get the ability to construct and get out URLs. # diff --git a/gitoid/src/backend/boringssl.rs b/gitoid/src/backend/boringssl.rs index 245a26e..30a3ef7 100644 --- a/gitoid/src/backend/boringssl.rs +++ b/gitoid/src/backend/boringssl.rs @@ -1,18 +1,12 @@ //! BoringSSL-based cryptography backend. -use crate::impl_hash_algorithm; -use crate::sealed::Sealed; -use crate::HashAlgorithm; +use crate::{impl_hash_algorithm, sealed::Sealed, HashAlgorithm}; use boring::sha; -use digest::consts::U20; -use digest::consts::U32; -use digest::generic_array::GenericArray; -use digest::Digest; -use digest::FixedOutput; -use digest::HashMarker; -use digest::Output; -use digest::OutputSizeUser; -use digest::Update; +use digest::{ + consts::{U20, U32}, + generic_array::GenericArray, + Digest, FixedOutput, HashMarker, Output, OutputSizeUser, Update, +}; #[cfg(feature = "sha1")] /// SHA-1 algorithm diff --git a/gitoid/src/backend/openssl.rs b/gitoid/src/backend/openssl.rs index 284e241..7711129 100644 --- a/gitoid/src/backend/openssl.rs +++ b/gitoid/src/backend/openssl.rs @@ -1,17 +1,11 @@ //! OpenSSL-based cryptography backend. -use crate::impl_hash_algorithm; -use crate::sealed::Sealed; -use crate::HashAlgorithm; -use digest::consts::U20; -use digest::consts::U32; -use digest::generic_array::GenericArray; -use digest::Digest; -use digest::FixedOutput; -use digest::HashMarker; -use digest::Output; -use digest::OutputSizeUser; -use digest::Update; +use crate::{impl_hash_algorithm, sealed::Sealed, HashAlgorithm}; +use digest::{ + consts::{U20, U32}, + generic_array::GenericArray, + Digest, FixedOutput, HashMarker, Output, OutputSizeUser, Update, +}; use openssl::sha; #[cfg(feature = "sha1")] diff --git a/gitoid/src/backend/rustcrypto.rs b/gitoid/src/backend/rustcrypto.rs index effdfdc..b039df4 100644 --- a/gitoid/src/backend/rustcrypto.rs +++ b/gitoid/src/backend/rustcrypto.rs @@ -1,13 +1,10 @@ //! RustCrypto-based cryptography backend. -use crate::impl_hash_algorithm; -use crate::sealed::Sealed; +use crate::{impl_hash_algorithm, sealed::Sealed, HashAlgorithm}; +use digest::{generic_array::GenericArray, Digest, OutputSizeUser}; + #[cfg(doc)] use crate::GitOid; -use crate::HashAlgorithm; -use digest::generic_array::GenericArray; -use digest::Digest; -use digest::OutputSizeUser; #[cfg(feature = "sha1")] /// SHA-1 algorithm, diff --git a/gitoid/src/error.rs b/gitoid/src/error.rs index e7961cc..43923c4 100644 --- a/gitoid/src/error.rs +++ b/gitoid/src/error.rs @@ -1,19 +1,18 @@ //! Error arising from `GitOid` construction or use. -use core::fmt::Display; -use core::fmt::Formatter; -use core::fmt::Result as FmtResult; -use core::result::Result as StdResult; +use core::{ + fmt::{Display, Formatter, Result as FmtResult}, + result::Result as StdResult, +}; + #[cfg(feature = "hex")] use hex::FromHexError as HexError; + #[cfg(feature = "std")] -use std::error::Error as StdError; -#[cfg(feature = "std")] -use std::io::Error as IoError; -#[cfg(feature = "url")] -use url::ParseError as UrlError; +use std::{error::Error as StdError, io::Error as IoError}; + #[cfg(feature = "url")] -use url::Url; +use url::{ParseError as UrlError, Url}; /// A `Result` with `gitoid::Error` as the error type. pub(crate) type Result = StdResult; diff --git a/gitoid/src/gitoid.rs b/gitoid/src/gitoid.rs index 6db51d0..1c15165 100644 --- a/gitoid/src/gitoid.rs +++ b/gitoid/src/gitoid.rs @@ -1,45 +1,38 @@ //! A gitoid representing a single artifact. -use crate::Error; -use crate::HashAlgorithm; -use crate::ObjectType; -use crate::Result; -use core::cmp::Ordering; -use core::fmt::Debug; +use crate::{ + internal::{gitoid_from_async_reader, gitoid_from_buffer, gitoid_from_reader}, + util::stream_len::{async_stream_len, stream_len}, + Error, HashAlgorithm, ObjectType, Result, +}; +use core::{ + cmp::Ordering, + fmt::{Debug, Formatter, Result as FmtResult}, + hash::{Hash, Hasher}, + marker::PhantomData, +}; +use digest::OutputSizeUser; + +#[cfg(feature = "async")] +use tokio::io::{AsyncRead, AsyncSeek}; + #[cfg(feature = "hex")] use core::fmt::Display; -use core::fmt::Formatter; -use core::fmt::Result as FmtResult; -use core::hash::Hash; -use core::hash::Hasher; -use core::marker::PhantomData; -use core::ops::Not as _; -#[cfg(feature = "serde")] -use core::result::Result as StdResult; -#[cfg(feature = "url")] -use core::str::FromStr; -#[cfg(feature = "url")] -use core::str::Split; -#[cfg(feature = "std")] -use digest::block_buffer::generic_array::sequence::GenericSequence; -use digest::block_buffer::generic_array::GenericArray; -use digest::Digest; -use digest::OutputSizeUser; -#[cfg(feature = "std")] -use format_bytes::format_bytes; + #[cfg(feature = "serde")] -use serde::{ - de::{Deserializer, Error as DeserializeError, Visitor}, - Deserialize, Serialize, Serializer, +use { + core::result::Result as StdResult, + serde::{ + de::{Deserializer, Error as DeserializeError, Visitor}, + Deserialize, Serialize, Serializer, + }, }; + #[cfg(feature = "std")] -use std::io::{BufRead, BufReader, Read, Seek, SeekFrom}; -#[cfg(feature = "async")] -use tokio::io::{ - AsyncBufReadExt as _, AsyncRead, AsyncSeek, AsyncSeekExt as _, BufReader as AsyncBufReader, -}; +use std::io::{Read, Seek}; + #[cfg(feature = "url")] -use url::Url; +use {crate::gitoid_url_parser::GitOidUrlParser, core::str::FromStr, url::Url}; /// A struct that computes [gitoids][g] based on the selected algorithm /// @@ -51,14 +44,14 @@ where O: ObjectType, { #[doc(hidden)] - _phantom: PhantomData, + pub(crate) _phantom: PhantomData, #[doc(hidden)] - value: H::Array, + pub(crate) value: H::Array, } #[cfg(feature = "url")] -const GITOID_URL_SCHEME: &str = "gitoid"; +pub(crate) const GITOID_URL_SCHEME: &str = "gitoid"; impl GitOid where @@ -101,11 +94,11 @@ where #[cfg(feature = "std")] /// Generate a `GitOid` from a reader, providing an expected length in bytes. - pub fn id_reader_with_length( - reader: R, - expected_length: usize, - ) -> Result> { - gitoid_from_buffer(H::new(), reader, expected_length) + pub fn id_reader_with_length(reader: R, expected_length: usize) -> Result> + where + R: Read + Seek, + { + gitoid_from_reader(H::new(), reader, expected_length) } #[cfg(feature = "async")] @@ -119,11 +112,11 @@ where #[cfg(feature = "async")] /// Generate a `GitOid` from an asynchronous reader, providing an expected length in bytes. - pub async fn id_async_reader_with_length( + pub async fn id_async_reader_with_length( reader: R, expected_length: usize, ) -> Result> { - gitoid_from_async_buffer(H::new(), reader, expected_length).await + gitoid_from_async_reader(H::new(), reader, expected_length).await } #[cfg(feature = "url")] @@ -255,6 +248,8 @@ where { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.debug_struct("GitOid") + .field("object_type", &O::NAME) + .field("hash_algorithm", &H::NAME) .field("value", &self.value) .finish() } @@ -307,7 +302,7 @@ where // Deserialize self from the URL string. struct GitOidVisitor(PhantomData, PhantomData); - impl<'de, H: HashAlgorithm, O: ObjectType> Visitor<'de> for GitOidVisitor { + impl Visitor<'_> for GitOidVisitor { type Value = GitOid; fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { @@ -328,114 +323,6 @@ where } } -#[cfg(feature = "url")] -struct GitOidUrlParser<'u, H, O> -where - H: HashAlgorithm, - O: ObjectType, -{ - url: &'u Url, - - segments: Split<'u, char>, - - #[doc(hidden)] - _hash_algorithm: PhantomData, - - #[doc(hidden)] - _object_type: PhantomData, -} - -#[allow(dead_code)] -fn some_if_not_empty(s: &str) -> Option<&str> { - s.is_empty().not().then_some(s) -} - -#[cfg(feature = "url")] -impl<'u, H, O> GitOidUrlParser<'u, H, O> -where - H: HashAlgorithm, - O: ObjectType, -{ - fn new(url: &'u Url) -> GitOidUrlParser<'u, H, O> { - GitOidUrlParser { - url, - segments: url.path().split(':'), - _hash_algorithm: PhantomData, - _object_type: PhantomData, - } - } - - fn parse(&mut self) -> Result> { - self.validate_url_scheme() - .and_then(|_| self.validate_object_type()) - .and_then(|_| self.validate_hash_algorithm()) - .and_then(|_| self.parse_hash()) - .map(|hash| GitOid { - _phantom: PhantomData, - value: H::array_from_generic(hash), - }) - } - - fn validate_url_scheme(&self) -> Result<()> { - if self.url.scheme() != GITOID_URL_SCHEME { - return Err(Error::InvalidScheme(self.url.clone())); - } - - Ok(()) - } - - fn validate_object_type(&mut self) -> Result<()> { - let object_type = self - .segments - .next() - .and_then(some_if_not_empty) - .ok_or_else(|| Error::MissingObjectType(self.url.clone()))?; - - if object_type != O::NAME { - return Err(Error::MismatchedObjectType { expected: O::NAME }); - } - - Ok(()) - } - - fn validate_hash_algorithm(&mut self) -> Result<()> { - let hash_algorithm = self - .segments - .next() - .and_then(some_if_not_empty) - .ok_or_else(|| Error::MissingHashAlgorithm(self.url.clone()))?; - - if hash_algorithm != H::NAME { - return Err(Error::MismatchedHashAlgorithm { expected: H::NAME }); - } - - Ok(()) - } - - fn parse_hash(&mut self) -> Result::OutputSize>> { - let hex_str = self - .segments - .next() - .and_then(some_if_not_empty) - .ok_or_else(|| Error::MissingHash(self.url.clone()))?; - - // TODO(alilleybrinker): When `sha1` et al. move to generic-array 1.0, - // update this to use the `arr!` macro. - let mut value = GenericArray::generate(|_| 0); - hex::decode_to_slice(hex_str, &mut value)?; - - let expected_size = ::output_size(); - if value.len() != expected_size { - return Err(Error::UnexpectedHashLength { - expected: expected_size, - observed: value.len(), - }); - } - - Ok(value) - } -} - #[cfg(feature = "url")] impl TryFrom for GitOid where @@ -448,299 +335,3 @@ where GitOidUrlParser::new(&url).parse() } } - -#[cfg(feature = "std")] -/// Generate a GitOid by reading from an arbitrary reader. -fn gitoid_from_buffer( - digester: H::Alg, - reader: R, - expected_read_length: usize, -) -> Result> -where - H: HashAlgorithm, - O: ObjectType, - R: Read, -{ - let expected_hash_length = ::output_size(); - let (hash, amount_read) = - hash_from_buffer::(digester, reader, expected_read_length)?; - - if amount_read != expected_read_length { - return Err(Error::UnexpectedReadLength { - expected: expected_read_length, - observed: amount_read, - }); - } - - if hash.len() != expected_hash_length { - return Err(Error::UnexpectedHashLength { - expected: expected_hash_length, - observed: hash.len(), - }); - } - - Ok(GitOid { - _phantom: PhantomData, - value: H::array_from_generic(hash), - }) -} - -#[cfg(not(feature = "std"))] -/// Generate a GitOid from data in a buffer of bytes. -fn gitoid_from_buffer( - digester: H::Alg, - reader: &[u8], - expected_read_length: usize, -) -> Result> -where - H: HashAlgorithm, - O: ObjectType, -{ - let expected_hash_length = ::output_size(); - let (hash, amount_read) = - hash_from_buffer::(digester, reader, expected_read_length)?; - - if amount_read != expected_read_length { - return Err(Error::UnexpectedReadLength { - expected: expected_read_length, - observed: amount_read, - }); - } - - if hash.len() != expected_hash_length { - return Err(Error::UnexpectedHashLength { - expected: expected_hash_length, - observed: hash.len(), - }); - } - - Ok(GitOid { - _phantom: PhantomData, - value: H::array_from_generic(hash), - }) -} - -#[cfg(feature = "std")] -// Helper extension trait to give a convenient way to iterate over -// chunks sized to the size of the internal buffer of the reader. -trait ForEachChunk: BufRead { - // Takes a function to apply to each chunk, and returns if any - // errors arose along with the number of bytes read in total. - fn for_each_chunk(&mut self, f: impl FnMut(&[u8])) -> Result; -} - -#[cfg(feature = "std")] -impl ForEachChunk for R { - fn for_each_chunk(&mut self, mut f: impl FnMut(&[u8])) -> Result { - let mut total_read = 0; - - loop { - let buffer = self.fill_buf()?; - let amount_read = buffer.len(); - - if amount_read == 0 { - break; - } - - f(buffer); - - self.consume(amount_read); - total_read += amount_read; - } - - Ok(total_read) - } -} - -#[cfg(feature = "std")] -/// Helper function which actually applies the [`GitOid`] construction rules. -/// -/// This function handles actually constructing the hash with the GitOID prefix, -/// and delegates to a buffered reader for performance of the chunked reading. -fn hash_from_buffer( - mut digester: D, - reader: R, - expected_read_length: usize, -) -> Result<(GenericArray, usize)> -where - D: Digest, - O: ObjectType, - R: Read, -{ - digester.update(format_bytes!( - b"{} {}\0", - O::NAME.as_bytes(), - expected_read_length - )); - let amount_read = BufReader::new(reader).for_each_chunk(|b| digester.update(b))?; - let hash = digester.finalize(); - Ok((hash, amount_read)) -} - -#[cfg(not(feature = "std"))] -/// Helper function which actually applies the [`GitOid`] construction rules. -/// -/// This function handles actually constructing the hash with the GitOID prefix, -/// and delegates to a buffered reader for performance of the chunked reading. -fn hash_from_buffer( - mut digester: D, - reader: &[u8], - expected_read_length: usize, -) -> Result<(GenericArray, usize)> -where - D: Digest, - O: ObjectType, -{ - // Manually write out the prefix - digester.update(O::NAME.as_bytes()); - digester.update(b" "); - digester.update(expected_read_length.to_ne_bytes()); - digester.update(b"\0"); - - // It's in memory, so we know the exact size up front. - let amount_read = reader.len(); - digester.update(reader); - let hash = digester.finalize(); - Ok((hash, amount_read)) -} - -#[cfg(feature = "async")] -/// Async version of `gitoid_from_buffer`. -async fn gitoid_from_async_buffer( - digester: H::Alg, - reader: R, - expected_read_length: usize, -) -> Result> -where - H: HashAlgorithm, - O: ObjectType, - R: AsyncRead + Unpin, -{ - let expected_hash_length = ::output_size(); - let (hash, amount_read) = - hash_from_async_buffer::(digester, reader, expected_read_length).await?; - - if amount_read != expected_read_length { - return Err(Error::UnexpectedHashLength { - expected: expected_read_length, - observed: amount_read, - }); - } - - if hash.len() != expected_hash_length { - return Err(Error::UnexpectedHashLength { - expected: expected_hash_length, - observed: hash.len(), - }); - } - - Ok(GitOid { - _phantom: PhantomData, - value: H::array_from_generic(hash), - }) -} - -#[cfg(feature = "async")] -/// Async version of `hash_from_buffer`. -async fn hash_from_async_buffer( - mut digester: D, - reader: R, - expected_read_length: usize, -) -> Result<(GenericArray, usize)> -where - D: Digest, - O: ObjectType, - R: AsyncRead + Unpin, -{ - digester.update(format_bytes!( - b"{} {}\0", - O::NAME.as_bytes(), - expected_read_length - )); - - let mut reader = AsyncBufReader::new(reader); - - let mut total_read = 0; - - loop { - let buffer = reader.fill_buf().await?; - let amount_read = buffer.len(); - - if amount_read == 0 { - break; - } - - digester.update(buffer); - - reader.consume(amount_read); - total_read += amount_read; - } - - let hash = digester.finalize(); - Ok((hash, total_read)) -} - -// Adapted from the Rust standard library's unstable implementation -// of `Seek::stream_len`. -// -// TODO(abrinker): Remove this when `Seek::stream_len` is stabilized. -// -// License reproduction: -// -// Permission is hereby granted, free of charge, to any -// person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the -// Software without restriction, including without -// limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software -// is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice -// shall be included in all copies or substantial portions -// of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -#[cfg(feature = "std")] -fn stream_len(mut stream: R) -> Result -where - R: Seek, -{ - let old_pos = stream.stream_position()?; - let len = stream.seek(SeekFrom::End(0))?; - - // Avoid seeking a third time when we were already at the end of the - // stream. The branch is usually way cheaper than a seek operation. - if old_pos != len { - stream.seek(SeekFrom::Start(old_pos))?; - } - - Ok(len) -} - -#[cfg(feature = "async")] -/// An async equivalent of `stream_len`. -async fn async_stream_len(mut stream: R) -> Result -where - R: AsyncSeek + Unpin, -{ - let old_pos = stream.stream_position().await?; - let len = stream.seek(SeekFrom::End(0)).await?; - - // Avoid seeking a third time when we were already at the end of the - // stream. The branch is usually way cheaper than a seek operation. - if old_pos != len { - stream.seek(SeekFrom::Start(old_pos)).await?; - } - - Ok(len) -} diff --git a/gitoid/src/gitoid_url_parser.rs b/gitoid/src/gitoid_url_parser.rs new file mode 100644 index 0000000..2880302 --- /dev/null +++ b/gitoid/src/gitoid_url_parser.rs @@ -0,0 +1,119 @@ +//! A gitoid representing a single artifact. + +use crate::{gitoid::GITOID_URL_SCHEME, Error, GitOid, HashAlgorithm, ObjectType, Result}; +use core::{marker::PhantomData, ops::Not as _}; +use digest::{block_buffer::generic_array::GenericArray, OutputSizeUser}; + +#[cfg(feature = "std")] +use digest::block_buffer::generic_array::sequence::GenericSequence; + +#[cfg(feature = "url")] +use {core::str::Split, url::Url}; + +#[cfg(feature = "url")] +pub(crate) struct GitOidUrlParser<'u, H, O> +where + H: HashAlgorithm, + O: ObjectType, +{ + url: &'u Url, + + segments: Split<'u, char>, + + #[doc(hidden)] + _hash_algorithm: PhantomData, + + #[doc(hidden)] + _object_type: PhantomData, +} + +#[allow(dead_code)] +fn some_if_not_empty(s: &str) -> Option<&str> { + s.is_empty().not().then_some(s) +} + +#[cfg(feature = "url")] +impl<'u, H, O> GitOidUrlParser<'u, H, O> +where + H: HashAlgorithm, + O: ObjectType, +{ + pub(crate) fn new(url: &'u Url) -> GitOidUrlParser<'u, H, O> { + GitOidUrlParser { + url, + segments: url.path().split(':'), + _hash_algorithm: PhantomData, + _object_type: PhantomData, + } + } + + pub(crate) fn parse(&mut self) -> Result> { + self.validate_url_scheme() + .and_then(|_| self.validate_object_type()) + .and_then(|_| self.validate_hash_algorithm()) + .and_then(|_| self.parse_hash()) + .map(|hash| GitOid { + _phantom: PhantomData, + value: H::array_from_generic(hash), + }) + } + + fn validate_url_scheme(&self) -> Result<()> { + if self.url.scheme() != GITOID_URL_SCHEME { + return Err(Error::InvalidScheme(self.url.clone())); + } + + Ok(()) + } + + fn validate_object_type(&mut self) -> Result<()> { + let object_type = self + .segments + .next() + .and_then(some_if_not_empty) + .ok_or_else(|| Error::MissingObjectType(self.url.clone()))?; + + if object_type != O::NAME { + return Err(Error::MismatchedObjectType { expected: O::NAME }); + } + + Ok(()) + } + + fn validate_hash_algorithm(&mut self) -> Result<()> { + let hash_algorithm = self + .segments + .next() + .and_then(some_if_not_empty) + .ok_or_else(|| Error::MissingHashAlgorithm(self.url.clone()))?; + + if hash_algorithm != H::NAME { + return Err(Error::MismatchedHashAlgorithm { expected: H::NAME }); + } + + Ok(()) + } + + fn parse_hash(&mut self) -> Result::OutputSize>> { + let hex_str = self + .segments + .next() + .and_then(some_if_not_empty) + .ok_or_else(|| Error::MissingHash(self.url.clone()))?; + + // TODO(alilleybrinker): When `sha1` et al. move to generic-array 1.0, + // update this to use the `arr!` macro. + let mut value = GenericArray::generate(|_| 0); + hex::decode_to_slice(hex_str, &mut value)?; + + let expected_size = ::output_size(); + if value.len() != expected_size { + return Err(Error::UnexpectedHashLength { + expected: expected_size, + observed: value.len(), + }); + } + + Ok(value) + } +} diff --git a/gitoid/src/hash_algorithm.rs b/gitoid/src/hash_algorithm.rs index b530b70..2a11e1e 100644 --- a/gitoid/src/hash_algorithm.rs +++ b/gitoid/src/hash_algorithm.rs @@ -1,14 +1,11 @@ //! Trait specifying valid [`GitOid`] hash algorithms. use crate::sealed::Sealed; +use core::{fmt::Debug, hash::Hash, ops::Deref}; +use digest::{block_buffer::generic_array::GenericArray, Digest, OutputSizeUser}; + #[cfg(doc)] use crate::GitOid; -use core::fmt::Debug; -use core::hash::Hash; -use core::ops::Deref; -use digest::block_buffer::generic_array::GenericArray; -use digest::Digest; -use digest::OutputSizeUser; /// Hash algorithms that can be used to make a [`GitOid`]. /// diff --git a/gitoid/src/internal.rs b/gitoid/src/internal.rs new file mode 100644 index 0000000..baeaf83 --- /dev/null +++ b/gitoid/src/internal.rs @@ -0,0 +1,309 @@ +//! A gitoid representing a single artifact. + +use crate::{Error, GitOid, HashAlgorithm, ObjectType, Result}; +use core::marker::PhantomData; +use digest::{block_buffer::generic_array::GenericArray, Digest, OutputSizeUser}; + +/// Generate a GitOid from data in a buffer of bytes. +/// +/// If data is small enough to fit in memory, then generating a GitOid for it +/// this way should be much faster, as it doesn't require seeking. +pub(crate) fn gitoid_from_buffer( + digester: H::Alg, + reader: &[u8], + expected_len: usize, +) -> Result> +where + H: HashAlgorithm, + O: ObjectType, +{ + let expected_hash_length = ::output_size(); + let (hash, amount_read) = hash_from_buffer::(digester, reader, expected_len)?; + + if amount_read != expected_len { + return Err(Error::UnexpectedReadLength { + expected: expected_len, + observed: amount_read, + }); + } + + if hash.len() != expected_hash_length { + return Err(Error::UnexpectedHashLength { + expected: expected_hash_length, + observed: hash.len(), + }); + } + + Ok(GitOid { + _phantom: PhantomData, + value: H::array_from_generic(hash), + }) +} + +#[cfg(feature = "std")] +pub(crate) use standard_impls::gitoid_from_reader; + +#[cfg(feature = "async")] +pub(crate) use async_impls::gitoid_from_async_reader; + +/// Helper function which actually applies the [`GitOid`] construction rules. +/// +/// This function handles actually constructing the hash with the GitOID prefix, +/// and delegates to a buffered reader for performance of the chunked reading. +fn hash_from_buffer( + mut digester: D, + buffer: &[u8], + expected_len: usize, +) -> Result<(GenericArray, usize)> +where + D: Digest, + O: ObjectType, +{ + let hashed_len = expected_len - num_carriage_returns_in_buffer(buffer); + digest_gitoid_header(&mut digester, O::NAME, hashed_len); + digest_with_normalized_newlines(&mut digester, buffer); + Ok((digester.finalize(), expected_len)) +} + +/// Digest the "header" required for a GitOID. +#[inline] +fn digest_gitoid_header(digester: &mut D, object_type: &str, object_len: usize) +where + D: Digest, +{ + digester.update(object_type.as_bytes()); + digester.update(b" "); + digester.update(object_len.to_string().as_bytes()); + digester.update(b"\0"); +} + +/// Update a hash digest with data in a buffer, normalizing newlines. +#[inline] +fn digest_with_normalized_newlines(digester: &mut D, buf: &[u8]) +where + D: Digest, +{ + for chunk in buf.chunk_by(|char1, _| *char1 != b'\r') { + let chunk = match chunk.last() { + // Omit the carriage return at the end of the chunk. + Some(b'\r') => &chunk[0..(chunk.len() - 1)], + _ => chunk, + }; + + digester.update(chunk) + } +} + +/// Count carriage returns in an in-memory buffer. +#[inline(always)] +fn num_carriage_returns_in_buffer(buffer: &[u8]) -> usize { + bytecount::count(buffer, b'\r') +} + +#[cfg(feature = "std")] +mod standard_impls { + use crate::{ + util::for_each_buf_fill::ForEachBufFill as _, Error, GitOid, HashAlgorithm, ObjectType, + Result, + }; + use core::marker::PhantomData; + use digest::{block_buffer::generic_array::GenericArray, Digest, OutputSizeUser}; + use std::io::{BufReader, Read, Seek, SeekFrom}; + + /// Generate a GitOid by reading from an arbitrary reader. + pub(crate) fn gitoid_from_reader( + digester: H::Alg, + reader: R, + expected_len: usize, + ) -> Result> + where + H: HashAlgorithm, + O: ObjectType, + R: Read + Seek, + { + let expected_hash_length = ::output_size(); + let (hash, amount_read) = hash_from_reader::(digester, reader, expected_len)?; + + if amount_read != expected_len { + return Err(Error::UnexpectedReadLength { + expected: expected_len, + observed: amount_read, + }); + } + + if hash.len() != expected_hash_length { + return Err(Error::UnexpectedHashLength { + expected: expected_hash_length, + observed: hash.len(), + }); + } + + Ok(GitOid { + _phantom: PhantomData, + value: H::array_from_generic(hash), + }) + } + + /// Read a seek-able stream and reset to the beginning when done. + fn read_and_reset(reader: R, f: F) -> Result<(usize, R)> + where + R: Read + Seek, + F: Fn(R) -> Result<(usize, R)>, + { + let (data, mut reader) = f(reader)?; + reader.seek(SeekFrom::Start(0))?; + Ok((data, reader)) + } + + /// Count carriage returns in a reader. + fn num_carriage_returns_in_reader(reader: R) -> Result<(usize, R)> + where + R: Read + Seek, + { + read_and_reset(reader, |reader| { + let mut buf_reader = BufReader::new(reader); + let mut total_dos_newlines = 0; + + buf_reader.for_each_buf_fill(|buf| { + // The number of separators is the number of chunks minus one. + total_dos_newlines += buf.chunk_by(|char1, _| *char1 != b'\r').count() - 1 + })?; + + Ok((total_dos_newlines, buf_reader.into_inner())) + }) + } + + /// Helper function which actually applies the [`GitOid`] construction rules. + /// + /// This function handles actually constructing the hash with the GitOID prefix, + /// and delegates to a buffered reader for performance of the chunked reading. + fn hash_from_reader( + mut digester: D, + reader: R, + expected_len: usize, + ) -> Result<(GenericArray, usize)> + where + D: Digest, + O: ObjectType, + R: Read + Seek, + { + let (num_carriage_returns, reader) = num_carriage_returns_in_reader(reader)?; + let hashed_len = expected_len - num_carriage_returns; + + super::digest_gitoid_header(&mut digester, O::NAME, hashed_len); + let amount_read = BufReader::new(reader) + .for_each_buf_fill(|b| super::digest_with_normalized_newlines(&mut digester, b))?; + + Ok((digester.finalize(), amount_read)) + } +} + +#[cfg(feature = "async")] +mod async_impls { + use crate::{Error, GitOid, HashAlgorithm, ObjectType, Result}; + use core::marker::PhantomData; + use digest::{block_buffer::generic_array::GenericArray, Digest, OutputSizeUser}; + use std::io::SeekFrom; + use tokio::io::{ + AsyncBufReadExt as _, AsyncRead, AsyncSeek, AsyncSeekExt as _, BufReader as AsyncBufReader, + }; + + use super::digest_with_normalized_newlines; + + /// Async version of `gitoid_from_reader`. + pub(crate) async fn gitoid_from_async_reader( + digester: H::Alg, + reader: R, + expected_len: usize, + ) -> Result> + where + H: HashAlgorithm, + O: ObjectType, + R: AsyncRead + AsyncSeek + Unpin, + { + let expected_hash_len = ::output_size(); + let (hash, amount_read) = + hash_from_async_buffer::(digester, reader, expected_len).await?; + + if amount_read != expected_len { + return Err(Error::UnexpectedHashLength { + expected: expected_len, + observed: amount_read, + }); + } + + if hash.len() != expected_hash_len { + return Err(Error::UnexpectedHashLength { + expected: expected_hash_len, + observed: hash.len(), + }); + } + + Ok(GitOid { + _phantom: PhantomData, + value: H::array_from_generic(hash), + }) + } + + /// Async version of `hash_from_buffer`. + async fn hash_from_async_buffer( + mut digester: D, + reader: R, + expected_len: usize, + ) -> Result<(GenericArray, usize)> + where + D: Digest, + O: ObjectType, + R: AsyncRead + AsyncSeek + Unpin, + { + let (num_carriage_returns, reader) = num_carriage_returns_in_async_reader(reader).await?; + let hashed_len = expected_len - num_carriage_returns; + + super::digest_gitoid_header(&mut digester, O::NAME, hashed_len); + + let mut reader = AsyncBufReader::new(reader); + let mut total_read = 0; + + loop { + let buffer = reader.fill_buf().await?; + let amount_read = buffer.len(); + + if amount_read == 0 { + break; + } + + digest_with_normalized_newlines(&mut digester, buffer); + + reader.consume(amount_read); + total_read += amount_read; + } + + Ok((digester.finalize(), total_read)) + } + + /// Count carriage returns in a reader. + async fn num_carriage_returns_in_async_reader(reader: R) -> Result<(usize, R)> + where + R: AsyncRead + AsyncSeek + Unpin, + { + let mut reader = AsyncBufReader::new(reader); + let mut total_dos_newlines = 0; + + loop { + let buffer = reader.fill_buf().await?; + let amount_read = buffer.len(); + + if amount_read == 0 { + break; + } + + total_dos_newlines += buffer.chunk_by(|char1, _| *char1 != b'\r').count() - 1; + + reader.consume(amount_read); + } + + let (data, mut reader) = (total_dos_newlines, reader.into_inner()); + reader.seek(SeekFrom::Start(0)).await?; + Ok((data, reader)) + } +} diff --git a/gitoid/src/lib.rs b/gitoid/src/lib.rs index ba49450..648985c 100644 --- a/gitoid/src/lib.rs +++ b/gitoid/src/lib.rs @@ -132,11 +132,14 @@ compile_error!( mod backend; mod error; mod gitoid; +mod gitoid_url_parser; mod hash_algorithm; +mod internal; mod object_type; pub(crate) mod sealed; #[cfg(test)] mod tests; +mod util; #[cfg(feature = "boringssl")] pub use crate::backend::boringssl; diff --git a/gitoid/src/object_type.rs b/gitoid/src/object_type.rs index 248d4ee..2d81d90 100644 --- a/gitoid/src/object_type.rs +++ b/gitoid/src/object_type.rs @@ -1,6 +1,7 @@ //! The types of objects for which a `GitOid` can be made. use crate::sealed::Sealed; + #[cfg(doc)] use crate::GitOid; diff --git a/gitoid/src/tests.rs b/gitoid/src/tests.rs index e2b9825..26ed863 100644 --- a/gitoid/src/tests.rs +++ b/gitoid/src/tests.rs @@ -1,5 +1,3 @@ -#![allow(unused_imports)] - use super::*; #[cfg(all(feature = "sha1", feature = "rustcrypto"))] use crate::rustcrypto::Sha1; @@ -8,9 +6,7 @@ use crate::rustcrypto::Sha256; #[cfg(feature = "std")] use std::fs::File; #[cfg(feature = "async")] -use tokio::fs::File as AsyncFile; -#[cfg(feature = "async")] -use tokio::runtime::Runtime; +use tokio::{fs::File as AsyncFile, runtime::Runtime}; #[cfg(feature = "url")] use url::Url; #[cfg(feature = "serde")] @@ -20,18 +16,24 @@ use { serde_test::{assert_tokens, Token}, }; +/// SHA-1 hash of a file containing "hello world" +/// +/// Taken from a Git repo as ground truth. +const GITOID_HELLO_WORLD_SHA1: &str = "gitoid:blob:sha1:95d09f2b10159347eece71399a7e2e907ea3df4f"; + +/// SHA-256 hash of a file containing "hello world" +/// +/// Taken from a Git repo as ground truth. +const GITOID_HELLO_WORLD_SHA256: &str = + "gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03"; + #[cfg(all(feature = "sha1", feature = "rustcrypto", feature = "hex"))] #[test] fn generate_sha1_gitoid_from_bytes() { let input = b"hello world"; let result = GitOid::::id_bytes(input); - assert_eq!(result.as_hex(), "95d09f2b10159347eece71399a7e2e907ea3df4f"); - - assert_eq!( - result.to_string(), - "gitoid:blob:sha1:95d09f2b10159347eece71399a7e2e907ea3df4f" - ); + assert_eq!(result.to_string(), GITOID_HELLO_WORLD_SHA1); } #[cfg(all(feature = "sha1", feature = "rustcrypto", feature = "std"))] @@ -40,12 +42,7 @@ fn generate_sha1_gitoid_from_buffer() -> Result<()> { let reader = File::open("test/data/hello_world.txt")?; let result = GitOid::::id_reader(reader)?; - assert_eq!(result.as_hex(), "95d09f2b10159347eece71399a7e2e907ea3df4f"); - - assert_eq!( - result.to_string(), - "gitoid:blob:sha1:95d09f2b10159347eece71399a7e2e907ea3df4f" - ); + assert_eq!(result.to_string(), GITOID_HELLO_WORLD_SHA1); Ok(()) } @@ -56,15 +53,7 @@ fn generate_sha256_gitoid_from_bytes() { let input = b"hello world"; let result = GitOid::::id_bytes(input); - assert_eq!( - result.as_hex(), - "fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03" - ); - - assert_eq!( - result.to_string(), - "gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03" - ); + assert_eq!(result.to_string(), GITOID_HELLO_WORLD_SHA256); } #[cfg(all(feature = "sha256", feature = "rustcrypto", feature = "std"))] @@ -73,15 +62,7 @@ fn generate_sha256_gitoid_from_buffer() -> Result<()> { let reader = File::open("test/data/hello_world.txt")?; let result = GitOid::::id_reader(reader)?; - assert_eq!( - result.as_hex(), - "fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03" - ); - - assert_eq!( - result.to_string(), - "gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03" - ); + assert_eq!(result.to_string(), GITOID_HELLO_WORLD_SHA256); Ok(()) } @@ -94,30 +75,64 @@ fn generate_sha256_gitoid_from_async_buffer() -> Result<()> { let reader = AsyncFile::open("test/data/hello_world.txt").await?; let result = GitOid::::id_async_reader(reader).await?; - assert_eq!( - result.as_hex(), - "fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03" - ); + assert_eq!(result.to_string(), GITOID_HELLO_WORLD_SHA256); + + Ok(()) + }) +} + +#[cfg(all(feature = "sha256", feature = "rustcrypto"))] +#[test] +fn newline_normalization_from_file() -> Result<()> { + let unix_file = File::open("test/data/unix_line.txt")?; + let windows_file = File::open("test/data/windows_line.txt")?; + + let unix_gitoid = GitOid::::id_reader(unix_file)?; + let windows_gitoid = GitOid::::id_reader(windows_file)?; + + assert_eq!(unix_gitoid.to_string(), windows_gitoid.to_string()); - assert_eq!( - result.to_string(), - "gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03" - ); + Ok(()) +} + +#[cfg(all(feature = "sha256", feature = "rustcrypto", feature = "async"))] +#[test] +fn newline_normalization_from_async_file() -> Result<()> { + let runtime = Runtime::new()?; + runtime.block_on(async { + let unix_file = AsyncFile::open("test/data/unix_line.txt").await?; + let windows_file = AsyncFile::open("test/data/windows_line.txt").await?; + + let unix_gitoid = GitOid::::id_async_reader(unix_file).await?; + let windows_gitoid = GitOid::::id_async_reader(windows_file).await?; + + assert_eq!(unix_gitoid.to_string(), windows_gitoid.to_string()); Ok(()) }) } +#[cfg(all(feature = "sha256", feature = "rustcrypto"))] +#[test] +fn newline_normalization_in_memory() -> Result<()> { + let with_crlf = b"some\r\nstring\r\n"; + let wout_crlf = b"some\nstring\n"; + + let with_crlf_gitoid = GitOid::::id_bytes(&with_crlf[..]); + let wout_crlf_gitoid = GitOid::::id_bytes(&wout_crlf[..]); + + assert_eq!(with_crlf_gitoid.to_string(), wout_crlf_gitoid.to_string()); + + Ok(()) +} + #[cfg(all(feature = "sha256", feature = "rustcrypto"))] #[test] fn validate_uri() -> Result<()> { let content = b"hello world"; let gitoid = GitOid::::id_bytes(content); - assert_eq!( - gitoid.url().to_string(), - "gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03" - ); + assert_eq!(gitoid.url().to_string(), GITOID_HELLO_WORLD_SHA256); Ok(()) } @@ -125,14 +140,11 @@ fn validate_uri() -> Result<()> { #[cfg(all(feature = "sha256", feature = "rustcrypto", feature = "url"))] #[test] fn try_from_url_bad_scheme() { - let url = Url::parse( - "whatever:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03", - ) - .unwrap(); + let url = Url::parse("gitiod:blob:sha1:95d09f2b10159347eece71399a7e2e907ea3df4f").unwrap(); match GitOid::::try_from_url(url) { Ok(_) => panic!("gitoid parsing should fail"), - Err(e) => assert_eq!(e.to_string(), "invalid scheme in URL 'whatever'"), + Err(e) => assert_eq!(e.to_string(), "invalid scheme in URL 'gitiod'"), } } @@ -197,25 +209,16 @@ fn try_from_url_missing_hash() { #[cfg(all(feature = "sha256", feature = "rustcrypto", feature = "url"))] #[test] fn try_url_roundtrip() { - let url = Url::parse( - "gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03", - ) - .unwrap(); + let url = Url::parse(GITOID_HELLO_WORLD_SHA256).unwrap(); let gitoid = GitOid::::try_from_url(url.clone()).unwrap(); let output = gitoid.url(); assert_eq!(url, output); } +// Validate serialization and deserialization work as expected. #[cfg(all(feature = "serde", feature = "sha256", feature = "rustcrypto"))] #[test] fn valid_gitoid_ser_de() { - let id = GitOid::::id_str("hello, world"); - - // This validates both serialization and deserialization. - assert_tokens( - &id, - &[Token::Str( - "gitoid:blob:sha256:7d0be525d6521168c74051e5ab1b99e3b6d1c962fba763818f1954ab9e1c821a", - )], - ); + let id = GitOid::::id_str("hello world"); + assert_tokens(&id, &[Token::Str(GITOID_HELLO_WORLD_SHA256)]); } diff --git a/gitoid/src/util/for_each_buf_fill.rs b/gitoid/src/util/for_each_buf_fill.rs new file mode 100644 index 0000000..12e0cde --- /dev/null +++ b/gitoid/src/util/for_each_buf_fill.rs @@ -0,0 +1,36 @@ +use crate::Result; + +#[cfg(feature = "std")] +use std::io::BufRead; + +#[cfg(feature = "std")] +/// Helper extension trait to give a convenient way to iterate over +/// chunks sized to the size of the internal buffer of the reader. +pub(crate) trait ForEachBufFill: BufRead { + /// Takes a function to apply to each buffer fill, and returns if any + /// errors arose along with the number of bytes read in total. + fn for_each_buf_fill(&mut self, f: impl FnMut(&[u8])) -> Result; +} + +#[cfg(feature = "std")] +impl ForEachBufFill for R { + fn for_each_buf_fill(&mut self, mut f: impl FnMut(&[u8])) -> Result { + let mut total_read = 0; + + loop { + let buffer = self.fill_buf()?; + let amount_read = buffer.len(); + + if amount_read == 0 { + break; + } + + f(buffer); + + self.consume(amount_read); + total_read += amount_read; + } + + Ok(total_read) + } +} diff --git a/gitoid/src/util/mod.rs b/gitoid/src/util/mod.rs new file mode 100644 index 0000000..ab70c22 --- /dev/null +++ b/gitoid/src/util/mod.rs @@ -0,0 +1,4 @@ +//! Helper modules for use internally. + +pub(crate) mod for_each_buf_fill; +pub(crate) mod stream_len; diff --git a/gitoid/src/util/stream_len.rs b/gitoid/src/util/stream_len.rs new file mode 100644 index 0000000..993cbb3 --- /dev/null +++ b/gitoid/src/util/stream_len.rs @@ -0,0 +1,72 @@ +use crate::Result; + +#[cfg(feature = "async")] +use tokio::io::{AsyncSeek, AsyncSeekExt as _}; + +#[cfg(feature = "std")] +use std::io::{Seek, SeekFrom}; + +// Adapted from the Rust standard library's unstable implementation +// of `Seek::stream_len`. +// +// TODO(abrinker): Remove this when `Seek::stream_len` is stabilized. +// +// License reproduction: +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +#[cfg(feature = "std")] +pub(crate) fn stream_len(mut stream: R) -> Result +where + R: Seek, +{ + let old_pos = stream.stream_position()?; + let len = stream.seek(SeekFrom::End(0))?; + + // Avoid seeking a third time when we were already at the end of the + // stream. The branch is usually way cheaper than a seek operation. + if old_pos != len { + stream.seek(SeekFrom::Start(old_pos))?; + } + + Ok(len) +} + +/// An async equivalent of `stream_len`. +#[cfg(feature = "async")] +pub(crate) async fn async_stream_len(mut stream: R) -> Result +where + R: AsyncSeek + Unpin, +{ + let old_pos = stream.stream_position().await?; + let len = stream.seek(SeekFrom::End(0)).await?; + + // Avoid seeking a third time when we were already at the end of the + // stream. The branch is usually way cheaper than a seek operation. + if old_pos != len { + stream.seek(SeekFrom::Start(old_pos)).await?; + } + + Ok(len) +} diff --git a/gitoid/test/data/unix_line.txt b/gitoid/test/data/unix_line.txt new file mode 100644 index 0000000..94954ab --- /dev/null +++ b/gitoid/test/data/unix_line.txt @@ -0,0 +1,2 @@ +hello +world diff --git a/gitoid/test/data/windows_line.txt b/gitoid/test/data/windows_line.txt new file mode 100644 index 0000000..94954ab --- /dev/null +++ b/gitoid/test/data/windows_line.txt @@ -0,0 +1,2 @@ +hello +world diff --git a/omnibor-cli/tests/snapshots/test__artifact_id_json.snap b/omnibor-cli/tests/snapshots/test__artifact_id_json.snap index 62e96b5..71a074b 100644 --- a/omnibor-cli/tests/snapshots/test__artifact_id_json.snap +++ b/omnibor-cli/tests/snapshots/test__artifact_id_json.snap @@ -13,6 +13,6 @@ info: success: true exit_code: 0 ----- stdout ----- -{"id":"","path":"tests/data/main.c"} +{"id":"gitoid:blob:sha256:93561f4501717b4c4a2f3eb5776f03231d32ec2a1f709a611ad3d8dcf931dc1b","path":"tests/data/main.c"} ----- stderr ----- diff --git a/omnibor-cli/tests/snapshots/test__artifact_id_plain.snap b/omnibor-cli/tests/snapshots/test__artifact_id_plain.snap index ce746ea..14eeadc 100644 --- a/omnibor-cli/tests/snapshots/test__artifact_id_plain.snap +++ b/omnibor-cli/tests/snapshots/test__artifact_id_plain.snap @@ -13,6 +13,6 @@ info: success: true exit_code: 0 ----- stdout ----- -tests/data/main.c => +tests/data/main.c => gitoid:blob:sha256:93561f4501717b4c4a2f3eb5776f03231d32ec2a1f709a611ad3d8dcf931dc1b ----- stderr ----- diff --git a/omnibor-cli/tests/snapshots/test__artifact_id_short.snap b/omnibor-cli/tests/snapshots/test__artifact_id_short.snap index 57421b6..4da9f62 100644 --- a/omnibor-cli/tests/snapshots/test__artifact_id_short.snap +++ b/omnibor-cli/tests/snapshots/test__artifact_id_short.snap @@ -13,6 +13,6 @@ info: success: true exit_code: 0 ----- stdout ----- - +gitoid:blob:sha256:93561f4501717b4c4a2f3eb5776f03231d32ec2a1f709a611ad3d8dcf931dc1b ----- stderr ----- diff --git a/omnibor-cli/tests/test.rs b/omnibor-cli/tests/test.rs index 4903df8..1d130ea 100644 --- a/omnibor-cli/tests/test.rs +++ b/omnibor-cli/tests/test.rs @@ -6,7 +6,6 @@ macro_rules! settings { ($block:expr) => { let mut settings = Settings::clone_current(); settings.add_filter(r#"omnibor(?:\.exe)?"#, "omnibor"); - settings.add_filter(r#"gitoid:blob:sha256:[a-f0-9]{64}"#, ""); settings.bind(|| $block); }; } diff --git a/omnibor/src/artifact_id.rs b/omnibor/src/artifact_id.rs index 6463c58..f8ff32b 100644 --- a/omnibor/src/artifact_id.rs +++ b/omnibor/src/artifact_id.rs @@ -178,10 +178,10 @@ impl ArtifactId { /// let id: ArtifactId = ArtifactId::id_reader_with_length(&file, 11).unwrap(); /// println!("Artifact ID: {}", id); /// ``` - pub fn id_reader_with_length( - reader: R, - expected_length: usize, - ) -> Result> { + pub fn id_reader_with_length(reader: R, expected_length: usize) -> Result> + where + R: Read + Seek, + { let gitoid = GitOid::id_reader_with_length(reader, expected_length)?; Ok(ArtifactId::from_gitoid(gitoid)) } @@ -268,7 +268,7 @@ impl ArtifactId { /// println!("Artifact ID: {}", id); /// # }) /// ``` - pub async fn id_async_reader_with_length( + pub async fn id_async_reader_with_length( reader: R, expected_length: usize, ) -> Result> { diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 056385d..cde5aaf 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -20,4 +20,4 @@ semver = "1.0.22" serde = { version = "1.0.197", features = ["derive"] } toml = "0.8.10" which = "6.0.0" -xshell = "0.2.5" +xshell = "0.2.7"