// Copyright © 2019-2020 The Radicle Foundation // // This file is part of radicle-link, distributed under the GPLv3 with Radicle // Linking Exception. For full terms see the included LICENSE file. use std::{ borrow::Cow, convert::TryFrom, fmt::{self, Debug, Display}, str::FromStr, }; use git_ext as ext; use multihash::{Multihash, MultihashRef}; use percent_encoding::percent_decode_str; use thiserror::Error; use super::sealed; lazy_static! { pub static ref DEFAULT_PATH: ext::Qualified = ext::Qualified::from(reflike!("refs/rad/id")); } pub mod error { use super::*; #[derive(Debug, Error)] #[non_exhaustive] pub enum DecodeId { #[error("invalid id")] InvalidId(#[source] E), #[error(transparent)] Encoding(#[from] multibase::Error), #[error(transparent)] Multihash(#[from] multihash::DecodeOwnedError), } #[derive(Debug, Error)] #[non_exhaustive] pub enum FromRefLike { #[error("missing {0}")] Missing(&'static str), #[error("must be a fully-qualified ref, ie. start with `refs/namespaces`")] Namespaced(#[from] ext::reference::name::StripPrefixError), #[error("invalid id")] InvalidId(#[source] DecodeId), #[error(transparent)] Path(#[from] ext::reference::name::Error), } #[derive(Debug, Error)] #[non_exhaustive] pub enum FromStr { #[error("missing {0}")] Missing(&'static str), #[error("invalid namespace identifier: {0}, expected `rad::`")] InvalidNID(String), #[error("invalid protocol: {0}, expected `rad::`")] InvalidProto(String), #[error("invalid id: {id}, expected `rad::`")] InvalidId { id: String, #[source] source: DecodeId, }, #[error(transparent)] Path(#[from] ext::reference::name::Error), #[error(transparent)] Utf8(#[from] std::str::Utf8Error), } } pub trait HasProtocol: sealed::Sealed { const PROTOCOL: &'static str; } impl HasProtocol for git_ext::Oid { const PROTOCOL: &'static str = "git"; } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SomeProtocol { Git, } impl minicbor::Encode for SomeProtocol { fn encode( &self, e: &mut minicbor::Encoder, ) -> Result<(), minicbor::encode::Error> { match self { Self::Git => e.u8(0), }?; Ok(()) } } impl<'de> minicbor::Decode<'de> for SomeProtocol { fn decode(d: &mut minicbor::Decoder) -> Result { match d.u8()? { 0 => Ok(Self::Git), _ => Err(minicbor::decode::Error::Message("unknown protocol")), } } } impl TryFrom<&str> for SomeProtocol { type Error = &'static str; fn try_from(s: &str) -> Result { match s { "git" => Ok(SomeProtocol::Git), _ => Err("unknown protocol"), } } } #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Urn { pub id: R, pub path: Option, } impl Urn { pub const fn new(id: R) -> Self { Self { id, path: None } } /// Render [`Self::id`] into the canonical string encoding. pub fn encode_id<'a>(&'a self) -> String where &'a R: Into, { multibase::encode(multibase::Base::Base32Z, (&self.id).into()) } pub fn try_from_id(s: impl AsRef) -> Result> where R: TryFrom, R::Error: std::error::Error + Send + Sync + 'static, { let bytes = multibase::decode(s.as_ref()).map(|x| x.1)?; let mhash = Multihash::from_bytes(bytes)?; let id = R::try_from(mhash).map_err(error::DecodeId::InvalidId)?; Ok(Self::new(id)) } pub fn map(self, f: F) -> Urn where F: FnOnce(R) -> S, { Urn { id: f(self.id), path: self.path, } } pub fn map_path(self, f: F) -> Self where F: FnOnce(Option) -> Option, { Self { id: self.id, path: f(self.path), } } pub fn with_path

(self, path: P) -> Self where P: Into>, { self.map_path(|_| path.into()) } } impl From for Urn { fn from(r: R) -> Self { Self::new(r) } } impl<'a, R: Clone> From> for Cow<'a, Urn> { fn from(urn: Urn) -> Self { Cow::Owned(urn) } } impl<'a, R: Clone> From<&'a Urn> for Cow<'a, Urn> { fn from(urn: &'a Urn) -> Self { Cow::Borrowed(urn) } } // FIXME: For some inexplicable reason, rustc rejects an impl for Urn, // claiming that the blanket impl `impl TryFrom for T where U: Into` // overlaps. We absolutely do not have `Into> for ext::RefLike`. impl TryFrom for Urn { type Error = error::FromRefLike; fn try_from(refl: ext::RefLike) -> Result { let refl = refl.strip_prefix("refs/namespaces/")?; let mut suf = refl.split('/'); let ns = suf.next().ok_or(Self::Error::Missing("namespace"))?; let urn = Self::try_from_id(ns).map_err(Self::Error::InvalidId)?; let path = { let path = suf.collect::>().join("/"); if path.is_empty() { Ok(None) } else { ext::RefLike::try_from(path).map(Some) } }?; Ok(urn.with_path(path)) } } impl From> for ext::RefLike where R: HasProtocol, for<'a> &'a R: Into, { fn from(urn: Urn) -> Self { Self::from(&urn) } } // FIXME: this is not kosher -- doesn't include `refs/namespaces`, but // everything after that. Should have a better type for that. impl<'a, R> From<&'a Urn> for ext::RefLike where R: HasProtocol, &'a R: Into, { fn from(urn: &'a Urn) -> Self { let refl = Self::try_from(urn.encode_id()).unwrap(); match &urn.path { None => refl, Some(path) => refl.join(ext::Qualified::from(path.clone())), } } } #[cfg(feature = "git-ref-format")] impl<'a, R> From<&'a Urn> for git_ref_format::Component<'_> where R: HasProtocol, &'a R: Into, { #[inline] fn from(urn: &'a Urn) -> Self { use git_ref_format::RefString; let rs = RefString::try_from(urn.encode_id()).expect("urn id is a valid ref string"); Self::from_refstring(rs).expect("urn id is a valid ref component") } } impl Display for Urn where R: HasProtocol, for<'a> &'a R: Into, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "rad:{}:{}", R::PROTOCOL, self.encode_id())?; if let Some(path) = &self.path { write!(f, "/{}", path.percent_encode())?; } Ok(()) } } impl FromStr for Urn where R: HasProtocol + TryFrom, E: std::error::Error + Send + Sync + 'static, { type Err = error::FromStr; fn from_str(s: &str) -> Result { let mut components = s.split(':'); components .next() .ok_or(Self::Err::Missing("namespace")) .and_then(|nid| { (nid == "rad") .then(|| ()) .ok_or_else(|| Self::Err::InvalidNID(nid.to_string())) })?; components .next() .ok_or(Self::Err::Missing("protocol")) .and_then(|proto| { (R::PROTOCOL == proto) .then(|| ()) .ok_or_else(|| Self::Err::InvalidProto(proto.to_string())) })?; components .next() .ok_or(Self::Err::Missing("id[/path]")) .and_then(|s| { let decoded = percent_decode_str(s).decode_utf8()?; let mut iter = decoded.splitn(2, '/'); let id = iter.next().ok_or(Self::Err::Missing("id"))?; let urn = Self::try_from_id(id).map_err(|err| Self::Err::InvalidId { id: id.to_string(), source: err, })?; let path = iter.next().map(ext::RefLike::try_from).transpose()?; Ok(urn.with_path(path)) }) } } impl serde::Serialize for Urn where R: HasProtocol, for<'a> &'a R: Into, { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.to_string().serialize(serializer) } } impl<'de, R, E> serde::Deserialize<'de> for Urn where R: HasProtocol + TryFrom, E: std::error::Error + Send + Sync + 'static, { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s: &str = serde::Deserialize::deserialize(deserializer)?; s.parse().map_err(serde::de::Error::custom) } } #[derive(Debug, PartialEq, minicbor::Encode, minicbor::Decode)] #[cbor(array)] struct AsCbor<'a> { #[b(0)] #[cbor(with = "minicbor::bytes")] id: &'a [u8], #[n(1)] proto: SomeProtocol, #[b(2)] path: Option<&'a str>, } impl minicbor::Encode for Urn where R: HasProtocol, for<'a> &'a R: Into, { fn encode( &self, e: &mut minicbor::Encoder, ) -> Result<(), minicbor::encode::Error> { let id: Multihash = (&self.id).into(); e.encode(AsCbor { id: id.as_bytes(), proto: SomeProtocol::try_from(R::PROTOCOL).unwrap(), path: self.path.as_ref().map(|path| path.as_str()), })?; Ok(()) } } impl<'de, R> minicbor::Decode<'de> for Urn where for<'a> R: HasProtocol + TryFrom>, { fn decode(d: &mut minicbor::Decoder) -> Result { use minicbor::decode::Error::Message as Error; let AsCbor { id, path, .. } = d.decode()?; let id = { let mhash = MultihashRef::from_slice(id).or(Err(Error("invalid multihash")))?; R::try_from(mhash).or(Err(Error("invalid id"))) }?; let path = path .map(ext::RefLike::try_from) .transpose() .or(Err(Error("invalid path")))?; Ok(Self { id, path }) } } pub mod test { use super::*; /// Fake `id` of a `Urn`. /// /// Not cryptographically secure, but cheap to create for tests. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct FakeId(pub usize); impl sealed::Sealed for FakeId {} impl HasProtocol for FakeId { const PROTOCOL: &'static str = "test"; } impl From for FakeId { fn from(sz: usize) -> FakeId { Self(sz) } } impl From for Multihash { fn from(id: FakeId) -> Self { Self::from(&id) } } impl From<&FakeId> for Multihash { fn from(id: &FakeId) -> Self { multihash::wrap(multihash::Code::Identity, &id.0.to_be_bytes()) } } }