use crate::content_id::ContentId;
use crate::error::ContentError;
use crate::raw_id::RawContentId;
use core::fmt;
use core::str::FromStr;
use ipld_core::cid::Cid;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ForeignCid(Cid);
impl ForeignCid {
pub fn new(cid: Cid) -> Result<Self, ContentError> {
if ContentId::try_from(cid).is_ok() {
return Err(ContentError::InvalidCidProfile {
reason: "CID is the dag-cbor/BLAKE3 profile (a ContentId), not foreign".into(),
});
}
if RawContentId::try_from(cid).is_ok() {
return Err(ContentError::InvalidCidProfile {
reason: "CID is the raw/BLAKE3 profile (a RawContentId), not foreign".into(),
});
}
Ok(ForeignCid(cid))
}
#[must_use]
pub fn as_cid(&self) -> &Cid {
&self.0
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
self.0.to_bytes()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ContentError> {
let cid = Cid::read_bytes(bytes).map_err(|e| ContentError::InvalidCid {
reason: e.to_string(),
source: Box::new(e),
})?;
Self::new(cid)
}
#[must_use]
pub fn codec(&self) -> u64 {
self.0.codec()
}
#[must_use]
pub fn hash_code(&self) -> u64 {
self.0.hash().code()
}
}
impl TryFrom<Cid> for ForeignCid {
type Error = ContentError;
fn try_from(cid: Cid) -> Result<Self, Self::Error> {
Self::new(cid)
}
}
impl From<ForeignCid> for Cid {
fn from(id: ForeignCid) -> Self {
id.0
}
}
impl fmt::Display for ForeignCid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl FromStr for ForeignCid {
type Err = ContentError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let cid = Cid::from_str(s).map_err(|e| ContentError::InvalidCid {
reason: e.to_string(),
source: Box::new(e),
})?;
Self::new(cid)
}
}
impl Serialize for ForeignCid {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
serializer.serialize_str(&self.to_string())
} else {
self.0.serialize(serializer)
}
}
}
impl<'de> Deserialize<'de> for ForeignCid {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
if deserializer.is_human_readable() {
let s = String::deserialize(deserializer)?;
s.parse::<ForeignCid>()
.map_err(<D::Error as serde::de::Error>::custom)
} else {
let cid = Cid::deserialize(deserializer)?;
ForeignCid::new(cid).map_err(<D::Error as serde::de::Error>::custom)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ClassifiedCid {
Content(ContentId),
Raw(RawContentId),
Foreign(ForeignCid),
}
impl ClassifiedCid {
#[must_use]
pub fn from_cid(cid: Cid) -> Self {
if let Ok(id) = ContentId::try_from(cid) {
return ClassifiedCid::Content(id);
}
if let Ok(id) = RawContentId::try_from(cid) {
return ClassifiedCid::Raw(id);
}
ClassifiedCid::Foreign(ForeignCid(cid))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ContentError> {
let cid = Cid::read_bytes(bytes).map_err(|e| ContentError::InvalidCid {
reason: e.to_string(),
source: Box::new(e),
})?;
Ok(Self::from_cid(cid))
}
#[must_use]
pub fn as_cid(&self) -> &Cid {
match self {
ClassifiedCid::Content(id) => id.as_cid(),
ClassifiedCid::Raw(id) => id.as_cid(),
ClassifiedCid::Foreign(id) => id.as_cid(),
}
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
self.as_cid().to_bytes()
}
#[must_use]
pub fn is_mintable(&self) -> bool {
!matches!(self, ClassifiedCid::Foreign(_))
}
#[must_use]
pub fn codec(&self) -> u64 {
self.as_cid().codec()
}
#[must_use]
pub fn hash_code(&self) -> u64 {
self.as_cid().hash().code()
}
#[must_use]
pub fn as_content(&self) -> Option<&ContentId> {
match self {
ClassifiedCid::Content(id) => Some(id),
_ => None,
}
}
#[must_use]
pub fn as_raw(&self) -> Option<&RawContentId> {
match self {
ClassifiedCid::Raw(id) => Some(id),
_ => None,
}
}
#[must_use]
pub fn as_foreign(&self) -> Option<&ForeignCid> {
match self {
ClassifiedCid::Foreign(id) => Some(id),
_ => None,
}
}
}
impl From<ContentId> for ClassifiedCid {
fn from(id: ContentId) -> Self {
ClassifiedCid::Content(id)
}
}
impl From<RawContentId> for ClassifiedCid {
fn from(id: RawContentId) -> Self {
ClassifiedCid::Raw(id)
}
}
impl From<ForeignCid> for ClassifiedCid {
fn from(id: ForeignCid) -> Self {
ClassifiedCid::Foreign(id)
}
}
impl From<ClassifiedCid> for Cid {
fn from(v: ClassifiedCid) -> Self {
*v.as_cid()
}
}
impl fmt::Display for ClassifiedCid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.as_cid(), f)
}
}
impl FromStr for ClassifiedCid {
type Err = ContentError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let cid = Cid::from_str(s).map_err(|e| ContentError::InvalidCid {
reason: e.to_string(),
source: Box::new(e),
})?;
Ok(Self::from_cid(cid))
}
}
impl Serialize for ClassifiedCid {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
serializer.serialize_str(&self.to_string())
} else {
self.as_cid().serialize(serializer)
}
}
}
impl<'de> Deserialize<'de> for ClassifiedCid {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
if deserializer.is_human_readable() {
let s = String::deserialize(deserializer)?;
s.parse::<ClassifiedCid>()
.map_err(<D::Error as serde::de::Error>::custom)
} else {
let cid = Cid::deserialize(deserializer)?;
Ok(ClassifiedCid::from_cid(cid))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ipld_core::cid::multihash::Multihash;
fn digest() -> [u8; 32] {
*blake3::hash(b"classify me").as_bytes()
}
fn sha_cid(d: [u8; 32]) -> Cid {
Cid::new_v1(0x55, Multihash::wrap(0x12, &d).unwrap())
}
#[test]
fn classifies_both_mintable_profiles() {
let c = ContentId::from_dag_cbor_digest(digest());
let r = RawContentId::from_blake3_digest(digest());
assert_eq!(
ClassifiedCid::from_cid(*c.as_cid()),
ClassifiedCid::Content(c)
);
assert_eq!(ClassifiedCid::from_cid(*r.as_cid()), ClassifiedCid::Raw(r));
assert!(ClassifiedCid::from(c).is_mintable());
assert!(ClassifiedCid::from(r).is_mintable());
assert_eq!(ClassifiedCid::from(c).as_content(), Some(&c));
assert_eq!(ClassifiedCid::from(r).as_raw(), Some(&r));
assert!(ClassifiedCid::from(c).as_foreign().is_none());
}
#[test]
fn foreign_profiles_are_carried_not_minted() {
let sha = sha_cid([7u8; 32]);
let v = ClassifiedCid::from_cid(sha);
assert_eq!(v, ClassifiedCid::Foreign(ForeignCid::new(sha).unwrap()));
assert!(!v.is_mintable());
assert_eq!(v.codec(), 0x55);
assert_eq!(v.hash_code(), 0x12);
assert!(v.as_content().is_none() && v.as_raw().is_none());
assert_eq!(v.as_foreign().unwrap().hash_code(), 0x12);
assert_eq!(ClassifiedCid::from_bytes(&v.to_bytes()).unwrap(), v);
assert_eq!(v.to_string().parse::<ClassifiedCid>().unwrap(), v);
}
#[test]
fn foreign_cannot_alias_a_recognized_profile_through_any_door() {
let c = ContentId::from_dag_cbor_digest(digest());
let r = RawContentId::from_blake3_digest(digest());
for cid in [*c.as_cid(), *r.as_cid()] {
assert!(matches!(
ForeignCid::new(cid),
Err(ContentError::InvalidCidProfile { .. })
));
assert!(matches!(
ForeignCid::try_from(cid),
Err(ContentError::InvalidCidProfile { .. })
));
assert!(matches!(
ForeignCid::from_bytes(&cid.to_bytes()),
Err(ContentError::InvalidCidProfile { .. })
));
assert!(matches!(
cid.to_string().parse::<ForeignCid>(),
Err(ContentError::InvalidCidProfile { .. })
));
let json = serde_json::to_string(&cid.to_string()).unwrap();
assert!(serde_json::from_str::<ForeignCid>(&json).is_err());
let cbor = crate::canonical::to_canonical_dagcbor(&cid).unwrap();
assert!(crate::canonical::from_canonical_dagcbor_checked::<ForeignCid>(&cbor).is_err());
}
}
#[test]
fn mintable_variants_cannot_hold_a_foreign_cid() {
let sha = sha_cid([3u8; 32]);
assert!(ContentId::try_from(sha).is_err());
assert!(RawContentId::try_from(sha).is_err());
assert!(sha.to_string().parse::<ContentId>().is_err());
assert!(sha.to_string().parse::<RawContentId>().is_err());
assert!(matches!(
ClassifiedCid::from_cid(sha),
ClassifiedCid::Foreign(_)
));
}
#[test]
fn same_digest_three_identities() {
let d = digest();
let content = ClassifiedCid::from(ContentId::from_dag_cbor_digest(d));
let raw = ClassifiedCid::from(RawContentId::from_blake3_digest(d));
let foreign = ClassifiedCid::from_cid(sha_cid(d));
assert_ne!(content, raw);
assert_ne!(raw, foreign);
assert_ne!(content, foreign);
assert_ne!(content.to_bytes(), raw.to_bytes());
}
#[test]
fn serde_link_reclassifies_from_the_wire() {
let r = ClassifiedCid::from(RawContentId::from_content(b"link me"));
let cbor = crate::canonical::to_canonical_dagcbor(&r).unwrap();
assert_eq!(&cbor[..2], &[0xd8, 0x2a], "must be a tag-42 link");
let back: ClassifiedCid = crate::canonical::from_canonical_dagcbor_checked(&cbor).unwrap();
assert_eq!(back, r);
let json = serde_json::to_string(&r).unwrap();
assert_eq!(serde_json::from_str::<ClassifiedCid>(&json).unwrap(), r);
let f = ClassifiedCid::from_cid(sha_cid([9u8; 32]));
let f_cbor = crate::canonical::to_canonical_dagcbor(&f).unwrap();
assert_eq!(
crate::canonical::from_canonical_dagcbor_checked::<ClassifiedCid>(&f_cbor).unwrap(),
f
);
let c_cbor = crate::canonical::to_canonical_dagcbor(&ClassifiedCid::from(
ContentId::from_dag_cbor_digest(digest()),
))
.unwrap();
assert!(matches!(
crate::canonical::from_canonical_dagcbor_checked::<ClassifiedCid>(&c_cbor).unwrap(),
ClassifiedCid::Content(_)
));
}
#[test]
fn garbage_is_an_error_not_foreign() {
assert!(matches!(
ClassifiedCid::from_bytes(b"nope"),
Err(ContentError::InvalidCid { .. })
));
assert!(matches!(
"nope".parse::<ClassifiedCid>(),
Err(ContentError::InvalidCid { .. })
));
assert!(matches!(
ForeignCid::from_bytes(b"nope"),
Err(ContentError::InvalidCid { .. })
));
}
#[test]
fn cidv0_is_foreign() {
let v0 = Cid::new_v0(Multihash::wrap(0x12, &[4u8; 32]).unwrap()).unwrap();
let c = ClassifiedCid::from_cid(v0);
assert!(matches!(c, ClassifiedCid::Foreign(_)));
assert!(ForeignCid::new(v0).is_ok());
assert_eq!(c.to_string().parse::<ClassifiedCid>().unwrap(), c);
}
}