diff --git a/gitoid/src/error.rs b/gitoid/src/error.rs index 559cd13..0492542 100644 --- a/gitoid/src/error.rs +++ b/gitoid/src/error.rs @@ -1,7 +1,8 @@ +use hex::FromHexError as HexError; use std::error::Error as StdError; use std::fmt::{self, Display, Formatter}; use std::io::Error as IoError; -use url::ParseError as UrlError; +use url::{ParseError as UrlError, Url}; /// A `Result` with `gitoid::Error` as the error type. pub(crate) type Result = std::result::Result; @@ -12,6 +13,20 @@ pub enum Error { /// The expected and actual length of the data being read didn't /// match, indicating something has likely gone wrong. BadLength { expected: usize, actual: usize }, + /// Tried to construct a `GitOid` from a `Url` with a scheme besides `gitoid`. + InvalidScheme(Url), + /// Tried to construct a `GitOid` from a `Url` without an `ObjectType` in it. + MissingObjectType(Url), + /// Tried to construct a `GitOid` from a `Url` without a `HashAlgorithm` in it. + MissingHashAlgorithm(Url), + /// Tried to construct a `GitOid` from a `Url` without a hash in it. + MissingHash(Url), + /// Tried to parse an unknown object type. + UnknownObjectType(String), + /// Tried to parse an unknown hash algorithm. + UnknownHashAlgorithm(String), + /// Tried to parse an invalid hex string. + InvalidHex(HexError), /// Could not construct a valid URL based on the `GitOid` data. Url(UrlError), /// Could not perform the IO operations necessary to construct the `GitOid`. @@ -24,6 +39,15 @@ impl Display for Error { Error::BadLength { expected, actual } => { write!(f, "expected length {}, actual length {}", expected, actual) } + Error::InvalidScheme(url) => write!(f, "invalid scheme in URL '{}'", url.scheme()), + Error::MissingObjectType(url) => write!(f, "missing object type in URL '{}'", url), + Error::MissingHashAlgorithm(url) => { + write!(f, "missing hash algorithm in URL '{}'", url) + } + Error::MissingHash(url) => write!(f, "missing hash in URL '{}'", url), + Error::UnknownObjectType(s) => write!(f, "unknown object type '{}'", s), + Error::UnknownHashAlgorithm(s) => write!(f, "unknown hash algorithm '{}'", s), + Error::InvalidHex(_) => write!(f, "invalid hex string"), Error::Url(e) => write!(f, "{}", e), Error::Io(e) => write!(f, "{}", e), } @@ -33,13 +57,26 @@ impl Display for Error { impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { - Error::BadLength { .. } => None, + Error::BadLength { .. } + | Error::InvalidScheme(_) + | Error::MissingObjectType(_) + | Error::MissingHashAlgorithm(_) + | Error::MissingHash(_) + | Error::UnknownObjectType(_) + | Error::UnknownHashAlgorithm(_) => None, + Error::InvalidHex(e) => Some(e), Error::Url(e) => Some(e), Error::Io(e) => Some(e), } } } +impl From for Error { + fn from(e: HexError) -> Error { + Error::InvalidHex(e) + } +} + impl From for Error { fn from(e: UrlError) -> Error { Error::Url(e) diff --git a/gitoid/src/gitoid.rs b/gitoid/src/gitoid.rs index db4b2e3..2370eeb 100644 --- a/gitoid/src/gitoid.rs +++ b/gitoid/src/gitoid.rs @@ -6,6 +6,8 @@ use core::marker::Unpin; use sha2::digest::DynDigest; use std::hash::Hash; use std::io::{BufReader, Read}; +use std::ops::Not as _; +use std::str::FromStr; use tokio::io::AsyncReadExt; use url::Url; @@ -114,12 +116,17 @@ impl GitOid { }) } + /// Construct a new `GitOid` from a `Url`. + pub fn new_from_url(url: Url) -> Result { + url.try_into() + } + //=========================================================================================== // Getters //------------------------------------------------------------------------------------------- /// Get a URL for the current `GitOid`. - pub fn uri(&self) -> Result { + pub fn url(&self) -> Result { let s = format!( "gitoid:{}:{}:{}", self.object_type, @@ -145,6 +152,58 @@ impl GitOid { } } +impl TryFrom for GitOid { + type Error = Error; + + fn try_from(url: Url) -> Result { + use Error::*; + + // Validate the scheme used. + if url.scheme() != "gitoid" { + return Err(InvalidScheme(url)); + } + + // Get the segments as an iterator over string slices. + let mut segments = url.path().split(':'); + + // Parse the object type, if present. + let object_type = { + let part = segments + .next() + .and_then(|p| p.is_empty().not().then_some(p)) + .ok_or_else(|| MissingObjectType(url.clone()))?; + + ObjectType::from_str(part)? + }; + + // Parse the hash algorithm, if present. + let hash_algorithm = { + let part = segments + .next() + .and_then(|p| p.is_empty().not().then_some(p)) + .ok_or_else(|| MissingHashAlgorithm(url.clone()))?; + + HashAlgorithm::from_str(part)? + }; + + // Parse the hash, if present. + let hex_str = segments + .next() + .and_then(|p| p.is_empty().not().then_some(p)) + .ok_or_else(|| MissingHash(url.clone()))?; + let mut value = [0u8; 32]; + hex::decode_to_slice(hex_str, &mut value)?; + + // Construct a new `GitOid` from the parts. + Ok(GitOid { + hash_algorithm, + object_type, + len: value.len(), + value, + }) + } +} + //=============================================================================================== // Helpers //----------------------------------------------------------------------------------------------- diff --git a/gitoid/src/hash_algorithm.rs b/gitoid/src/hash_algorithm.rs index 27002d0..30782bb 100644 --- a/gitoid/src/hash_algorithm.rs +++ b/gitoid/src/hash_algorithm.rs @@ -1,8 +1,10 @@ //! A hash algorithm which can be used to make a `GitOid`. -use core::fmt::{Display, Formatter, Result}; +use crate::{Error, Result}; +use core::fmt::{self, Display, Formatter}; use sha1::Sha1; use sha2::{digest::DynDigest, Digest, Sha256}; +use std::str::FromStr; /// The available algorithms for computing hashes #[derive(Clone, Copy, PartialOrd, Eq, Ord, Debug, Hash, PartialEq)] @@ -32,10 +34,22 @@ impl HashAlgorithm { pub(crate) const NUM_HASH_BYTES: usize = 32; impl Display for HashAlgorithm { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { HashAlgorithm::Sha1 => write!(f, "sha1"), HashAlgorithm::Sha256 => write!(f, "sha256"), } } } + +impl FromStr for HashAlgorithm { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "sha1" => Ok(HashAlgorithm::Sha1), + "sha256" => Ok(HashAlgorithm::Sha256), + _ => Err(Error::UnknownHashAlgorithm(s.to_owned())), + } + } +} diff --git a/gitoid/src/object_type.rs b/gitoid/src/object_type.rs index b4e3605..515581a 100644 --- a/gitoid/src/object_type.rs +++ b/gitoid/src/object_type.rs @@ -1,4 +1,9 @@ -use std::fmt::{self, Display, Formatter}; +use std::{ + fmt::{self, Display, Formatter}, + str::FromStr, +}; + +use crate::Error; /// The types of objects for which a `GitOid` can be made. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -27,3 +32,17 @@ impl Display for ObjectType { ) } } + +impl FromStr for ObjectType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "blob" => Ok(ObjectType::Blob), + "tree" => Ok(ObjectType::Tree), + "commit" => Ok(ObjectType::Commit), + "tag" => Ok(ObjectType::Tag), + _ => Err(Error::UnknownObjectType(s.to_owned())), + } + } +} diff --git a/gitoid/src/tests.rs b/gitoid/src/tests.rs index 1c6e1fc..25844d9 100644 --- a/gitoid/src/tests.rs +++ b/gitoid/src/tests.rs @@ -3,6 +3,7 @@ use hash_algorithm::HashAlgorithm::*; use object_type::ObjectType::*; use std::fs::File; use std::io::BufReader; +use url::Url; #[test] fn generate_sha1_gitoid_from_bytes() { @@ -108,9 +109,90 @@ fn validate_uri() -> Result<()> { let gitoid = GitOid::new_from_bytes(Sha256, Blob, content); assert_eq!( - gitoid.uri()?.to_string(), + gitoid.url()?.to_string(), "gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03" ); Ok(()) } + +#[test] +fn try_from_url_bad_scheme() { + let url = Url::parse( + "whatever:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03", + ) + .unwrap(); + + match GitOid::new_from_url(url) { + Ok(_) => panic!("gitoid parsing should fail"), + Err(e) => assert_eq!(e.to_string(), "invalid scheme in URL 'whatever'"), + } +} + +#[test] +fn try_from_url_missing_object_type() { + let url = Url::parse("gitoid:").unwrap(); + + match GitOid::new_from_url(url) { + Ok(_) => panic!("gitoid parsing should fail"), + Err(e) => assert_eq!(e.to_string(), "missing object type in URL 'gitoid:'"), + } +} + +#[test] +fn try_from_url_bad_object_type() { + let url = Url::parse("gitoid:whatever").unwrap(); + + match GitOid::new_from_url(url) { + Ok(_) => panic!("gitoid parsing should fail"), + Err(e) => assert_eq!(e.to_string(), "unknown object type 'whatever'"), + } +} + +#[test] +fn try_from_url_missing_hash_algorithm() { + let url = Url::parse("gitoid:blob:").unwrap(); + + match GitOid::new_from_url(url) { + Ok(_) => panic!("gitoid parsing should fail"), + Err(e) => assert_eq!( + e.to_string(), + "missing hash algorithm in URL 'gitoid:blob:'" + ), + } +} + +#[test] +fn try_from_url_bad_hash_algorithm() { + let url = Url::parse("gitoid:blob:sha10000").unwrap(); + + match GitOid::new_from_url(url) { + Ok(_) => panic!("gitoid parsing should fail"), + Err(e) => assert_eq!(e.to_string(), "unknown hash algorithm 'sha10000'"), + } +} + +#[test] +fn try_from_url_missing_hash() { + let url = Url::parse("gitoid:blob:sha256:").unwrap(); + + match GitOid::new_from_url(url) { + Ok(_) => panic!("gitoid parsing should fail"), + Err(e) => assert_eq!(e.to_string(), "missing hash in URL 'gitoid:blob:sha256:'"), + } +} + +#[test] +fn try_url_roundtrip() { + let url = Url::parse( + "gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03", + ) + .unwrap(); + let gitoid = GitOid::new_from_url(url.clone()).unwrap(); + let output = gitoid.url().unwrap(); + + eprintln!("{}", url); + eprintln!("{}", output); + + assert_eq!(url, output); +}