1use crate::content_id::ContentId;
60use crate::error::ContentError;
61use crate::raw_id::RawContentId;
62use core::fmt;
63use core::str::FromStr;
64use ipld_core::cid::Cid;
65use serde::{Deserialize, Deserializer, Serialize, Serializer};
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
79pub struct ForeignCid(Cid);
80
81impl ForeignCid {
82 pub fn new(cid: Cid) -> Result<Self, ContentError> {
90 if ContentId::try_from(cid).is_ok() {
91 return Err(ContentError::InvalidCidProfile {
92 reason: "CID is the dag-cbor/BLAKE3 profile (a ContentId), not foreign".into(),
93 });
94 }
95 if RawContentId::try_from(cid).is_ok() {
96 return Err(ContentError::InvalidCidProfile {
97 reason: "CID is the raw/BLAKE3 profile (a RawContentId), not foreign".into(),
98 });
99 }
100 Ok(ForeignCid(cid))
101 }
102
103 #[must_use]
105 pub fn as_cid(&self) -> &Cid {
106 &self.0
107 }
108
109 #[must_use]
111 pub fn to_bytes(&self) -> Vec<u8> {
112 self.0.to_bytes()
113 }
114
115 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ContentError> {
122 let cid = Cid::read_bytes(bytes).map_err(|e| ContentError::InvalidCid {
123 reason: e.to_string(),
124 source: Box::new(e),
125 })?;
126 Self::new(cid)
127 }
128
129 #[must_use]
131 pub fn codec(&self) -> u64 {
132 self.0.codec()
133 }
134
135 #[must_use]
137 pub fn hash_code(&self) -> u64 {
138 self.0.hash().code()
139 }
140}
141
142impl TryFrom<Cid> for ForeignCid {
143 type Error = ContentError;
144
145 fn try_from(cid: Cid) -> Result<Self, Self::Error> {
146 Self::new(cid)
147 }
148}
149
150impl From<ForeignCid> for Cid {
151 fn from(id: ForeignCid) -> Self {
152 id.0
153 }
154}
155
156impl fmt::Display for ForeignCid {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 fmt::Display::fmt(&self.0, f)
159 }
160}
161
162impl FromStr for ForeignCid {
163 type Err = ContentError;
164
165 fn from_str(s: &str) -> Result<Self, Self::Err> {
166 let cid = Cid::from_str(s).map_err(|e| ContentError::InvalidCid {
167 reason: e.to_string(),
168 source: Box::new(e),
169 })?;
170 Self::new(cid)
171 }
172}
173
174impl Serialize for ForeignCid {
175 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
176 if serializer.is_human_readable() {
177 serializer.serialize_str(&self.to_string())
178 } else {
179 self.0.serialize(serializer)
180 }
181 }
182}
183
184impl<'de> Deserialize<'de> for ForeignCid {
185 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
188 if deserializer.is_human_readable() {
189 let s = String::deserialize(deserializer)?;
190 s.parse::<ForeignCid>()
191 .map_err(<D::Error as serde::de::Error>::custom)
192 } else {
193 let cid = Cid::deserialize(deserializer)?;
194 ForeignCid::new(cid).map_err(<D::Error as serde::de::Error>::custom)
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
202pub enum ClassifiedCid {
203 Content(ContentId),
206 Raw(RawContentId),
209 Foreign(ForeignCid),
213}
214
215impl ClassifiedCid {
216 #[must_use]
219 pub fn from_cid(cid: Cid) -> Self {
220 if let Ok(id) = ContentId::try_from(cid) {
221 return ClassifiedCid::Content(id);
222 }
223 if let Ok(id) = RawContentId::try_from(cid) {
224 return ClassifiedCid::Raw(id);
225 }
226 ClassifiedCid::Foreign(ForeignCid(cid))
227 }
228
229 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ContentError> {
236 let cid = Cid::read_bytes(bytes).map_err(|e| ContentError::InvalidCid {
237 reason: e.to_string(),
238 source: Box::new(e),
239 })?;
240 Ok(Self::from_cid(cid))
241 }
242
243 #[must_use]
245 pub fn as_cid(&self) -> &Cid {
246 match self {
247 ClassifiedCid::Content(id) => id.as_cid(),
248 ClassifiedCid::Raw(id) => id.as_cid(),
249 ClassifiedCid::Foreign(id) => id.as_cid(),
250 }
251 }
252
253 #[must_use]
255 pub fn to_bytes(&self) -> Vec<u8> {
256 self.as_cid().to_bytes()
257 }
258
259 #[must_use]
261 pub fn is_mintable(&self) -> bool {
262 !matches!(self, ClassifiedCid::Foreign(_))
263 }
264
265 #[must_use]
268 pub fn codec(&self) -> u64 {
269 self.as_cid().codec()
270 }
271
272 #[must_use]
274 pub fn hash_code(&self) -> u64 {
275 self.as_cid().hash().code()
276 }
277
278 #[must_use]
280 pub fn as_content(&self) -> Option<&ContentId> {
281 match self {
282 ClassifiedCid::Content(id) => Some(id),
283 _ => None,
284 }
285 }
286
287 #[must_use]
289 pub fn as_raw(&self) -> Option<&RawContentId> {
290 match self {
291 ClassifiedCid::Raw(id) => Some(id),
292 _ => None,
293 }
294 }
295
296 #[must_use]
298 pub fn as_foreign(&self) -> Option<&ForeignCid> {
299 match self {
300 ClassifiedCid::Foreign(id) => Some(id),
301 _ => None,
302 }
303 }
304}
305
306impl From<ContentId> for ClassifiedCid {
307 fn from(id: ContentId) -> Self {
308 ClassifiedCid::Content(id)
309 }
310}
311
312impl From<RawContentId> for ClassifiedCid {
313 fn from(id: RawContentId) -> Self {
314 ClassifiedCid::Raw(id)
315 }
316}
317
318impl From<ForeignCid> for ClassifiedCid {
319 fn from(id: ForeignCid) -> Self {
320 ClassifiedCid::Foreign(id)
321 }
322}
323
324impl From<ClassifiedCid> for Cid {
325 fn from(v: ClassifiedCid) -> Self {
326 *v.as_cid()
327 }
328}
329
330impl fmt::Display for ClassifiedCid {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 fmt::Display::fmt(self.as_cid(), f)
335 }
336}
337
338impl FromStr for ClassifiedCid {
339 type Err = ContentError;
340
341 fn from_str(s: &str) -> Result<Self, Self::Err> {
345 let cid = Cid::from_str(s).map_err(|e| ContentError::InvalidCid {
346 reason: e.to_string(),
347 source: Box::new(e),
348 })?;
349 Ok(Self::from_cid(cid))
350 }
351}
352
353impl Serialize for ClassifiedCid {
354 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
355 if serializer.is_human_readable() {
356 serializer.serialize_str(&self.to_string())
357 } else {
358 self.as_cid().serialize(serializer)
359 }
360 }
361}
362
363impl<'de> Deserialize<'de> for ClassifiedCid {
364 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
367 if deserializer.is_human_readable() {
368 let s = String::deserialize(deserializer)?;
369 s.parse::<ClassifiedCid>()
370 .map_err(<D::Error as serde::de::Error>::custom)
371 } else {
372 let cid = Cid::deserialize(deserializer)?;
373 Ok(ClassifiedCid::from_cid(cid))
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use ipld_core::cid::multihash::Multihash;
382
383 fn digest() -> [u8; 32] {
384 *blake3::hash(b"classify me").as_bytes()
385 }
386
387 fn sha_cid(d: [u8; 32]) -> Cid {
388 Cid::new_v1(0x55, Multihash::wrap(0x12, &d).unwrap())
389 }
390
391 #[test]
392 fn classifies_both_mintable_profiles() {
393 let c = ContentId::from_dag_cbor_digest(digest());
394 let r = RawContentId::from_blake3_digest(digest());
395 assert_eq!(
396 ClassifiedCid::from_cid(*c.as_cid()),
397 ClassifiedCid::Content(c)
398 );
399 assert_eq!(ClassifiedCid::from_cid(*r.as_cid()), ClassifiedCid::Raw(r));
400 assert!(ClassifiedCid::from(c).is_mintable());
401 assert!(ClassifiedCid::from(r).is_mintable());
402 assert_eq!(ClassifiedCid::from(c).as_content(), Some(&c));
403 assert_eq!(ClassifiedCid::from(r).as_raw(), Some(&r));
404 assert!(ClassifiedCid::from(c).as_foreign().is_none());
405 }
406
407 #[test]
408 fn foreign_profiles_are_carried_not_minted() {
409 let sha = sha_cid([7u8; 32]);
410 let v = ClassifiedCid::from_cid(sha);
411 assert_eq!(v, ClassifiedCid::Foreign(ForeignCid::new(sha).unwrap()));
412 assert!(!v.is_mintable());
413 assert_eq!(v.codec(), 0x55);
414 assert_eq!(v.hash_code(), 0x12);
415 assert!(v.as_content().is_none() && v.as_raw().is_none());
416 assert_eq!(v.as_foreign().unwrap().hash_code(), 0x12);
417 assert_eq!(ClassifiedCid::from_bytes(&v.to_bytes()).unwrap(), v);
418 assert_eq!(v.to_string().parse::<ClassifiedCid>().unwrap(), v);
419 }
420
421 #[test]
426 fn foreign_cannot_alias_a_recognized_profile_through_any_door() {
427 let c = ContentId::from_dag_cbor_digest(digest());
428 let r = RawContentId::from_blake3_digest(digest());
429 for cid in [*c.as_cid(), *r.as_cid()] {
430 assert!(matches!(
431 ForeignCid::new(cid),
432 Err(ContentError::InvalidCidProfile { .. })
433 ));
434 assert!(matches!(
435 ForeignCid::try_from(cid),
436 Err(ContentError::InvalidCidProfile { .. })
437 ));
438 assert!(matches!(
439 ForeignCid::from_bytes(&cid.to_bytes()),
440 Err(ContentError::InvalidCidProfile { .. })
441 ));
442 assert!(matches!(
443 cid.to_string().parse::<ForeignCid>(),
444 Err(ContentError::InvalidCidProfile { .. })
445 ));
446 let json = serde_json::to_string(&cid.to_string()).unwrap();
449 assert!(serde_json::from_str::<ForeignCid>(&json).is_err());
450 let cbor = crate::canonical::to_canonical_dagcbor(&cid).unwrap();
451 assert!(crate::canonical::from_canonical_dagcbor_checked::<ForeignCid>(&cbor).is_err());
452 }
453 }
454
455 #[test]
458 fn mintable_variants_cannot_hold_a_foreign_cid() {
459 let sha = sha_cid([3u8; 32]);
460 assert!(ContentId::try_from(sha).is_err());
461 assert!(RawContentId::try_from(sha).is_err());
462 assert!(sha.to_string().parse::<ContentId>().is_err());
463 assert!(sha.to_string().parse::<RawContentId>().is_err());
464 assert!(matches!(
465 ClassifiedCid::from_cid(sha),
466 ClassifiedCid::Foreign(_)
467 ));
468 }
469
470 #[test]
472 fn same_digest_three_identities() {
473 let d = digest();
474 let content = ClassifiedCid::from(ContentId::from_dag_cbor_digest(d));
475 let raw = ClassifiedCid::from(RawContentId::from_blake3_digest(d));
476 let foreign = ClassifiedCid::from_cid(sha_cid(d));
477 assert_ne!(content, raw);
478 assert_ne!(raw, foreign);
479 assert_ne!(content, foreign);
480 assert_ne!(content.to_bytes(), raw.to_bytes());
481 }
482
483 #[test]
484 fn serde_link_reclassifies_from_the_wire() {
485 let r = ClassifiedCid::from(RawContentId::from_content(b"link me"));
486 let cbor = crate::canonical::to_canonical_dagcbor(&r).unwrap();
487 assert_eq!(&cbor[..2], &[0xd8, 0x2a], "must be a tag-42 link");
488 let back: ClassifiedCid = crate::canonical::from_canonical_dagcbor_checked(&cbor).unwrap();
489 assert_eq!(back, r);
490 let json = serde_json::to_string(&r).unwrap();
491 assert_eq!(serde_json::from_str::<ClassifiedCid>(&json).unwrap(), r);
492
493 let f = ClassifiedCid::from_cid(sha_cid([9u8; 32]));
496 let f_cbor = crate::canonical::to_canonical_dagcbor(&f).unwrap();
497 assert_eq!(
498 crate::canonical::from_canonical_dagcbor_checked::<ClassifiedCid>(&f_cbor).unwrap(),
499 f
500 );
501 let c_cbor = crate::canonical::to_canonical_dagcbor(&ClassifiedCid::from(
502 ContentId::from_dag_cbor_digest(digest()),
503 ))
504 .unwrap();
505 assert!(matches!(
506 crate::canonical::from_canonical_dagcbor_checked::<ClassifiedCid>(&c_cbor).unwrap(),
507 ClassifiedCid::Content(_)
508 ));
509 }
510
511 #[test]
512 fn garbage_is_an_error_not_foreign() {
513 assert!(matches!(
514 ClassifiedCid::from_bytes(b"nope"),
515 Err(ContentError::InvalidCid { .. })
516 ));
517 assert!(matches!(
518 "nope".parse::<ClassifiedCid>(),
519 Err(ContentError::InvalidCid { .. })
520 ));
521 assert!(matches!(
522 ForeignCid::from_bytes(b"nope"),
523 Err(ContentError::InvalidCid { .. })
524 ));
525 }
526
527 #[test]
529 fn cidv0_is_foreign() {
530 let v0 = Cid::new_v0(Multihash::wrap(0x12, &[4u8; 32]).unwrap()).unwrap();
531 let c = ClassifiedCid::from_cid(v0);
532 assert!(matches!(c, ClassifiedCid::Foreign(_)));
533 assert!(ForeignCid::new(v0).is_ok());
534 assert_eq!(c.to_string().parse::<ClassifiedCid>().unwrap(), c);
535 }
536}