ojak/portable/
hashlink.rs1use alloc::string::String;
11use alloc::vec::Vec;
12use core::fmt;
13use sha2::{Digest, Sha256};
14
15const SHA2_256: [u8; 2] = [0x12, 0x20];
17
18#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct Media {
21 pub content_type: String,
23 pub bytes: Vec<u8>,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq, Hash)]
29pub struct Hashlink {
30 digest: [u8; 32],
31}
32
33impl Hashlink {
34 #[must_use]
36 pub fn of(bytes: &[u8]) -> Self {
37 Self {
38 digest: Sha256::digest(bytes).into(),
39 }
40 }
41
42 #[must_use]
45 pub fn parse(hashlink: &str) -> Option<Self> {
46 let (scheme, digest) = hashlink.split_once(':')?;
47 if !scheme.eq_ignore_ascii_case("hl") {
48 return None;
49 }
50 Self::from_multibase(digest)
51 }
52
53 #[must_use]
56 pub fn from_multibase(multibase: &str) -> Option<Self> {
57 let encoded = multibase.strip_prefix('z')?;
58 let decoded: Vec<u8> = bs58::decode(encoded).into_vec().ok()?;
59 let digest = decoded.strip_prefix(&SHA2_256)?;
60 Some(Self {
61 digest: digest.try_into().ok()?,
62 })
63 }
64
65 #[must_use]
67 pub fn digest(&self) -> &[u8; 32] {
68 &self.digest
69 }
70
71 #[must_use]
73 pub fn multibase(&self) -> String {
74 let mut multihash = Vec::with_capacity(34);
75 multihash.extend_from_slice(&SHA2_256);
76 multihash.extend_from_slice(&self.digest);
77 let mut multibase = String::from("z");
78 multibase.push_str(&bs58::encode(multihash).into_string());
79 multibase
80 }
81
82 #[must_use]
84 pub fn matches(&self, bytes: &[u8]) -> bool {
85 Self::of(bytes) == *self
86 }
87}
88
89impl fmt::Display for Hashlink {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 write!(f, "hl:{}", self.multibase())
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 const EXAMPLE: &str = "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n";
102
103 #[test]
104 fn a_hashlink_is_its_digest() {
105 let link = Hashlink::parse(&alloc::format!("hl:{EXAMPLE}")).unwrap();
106 assert_eq!(link, Hashlink::from_multibase(EXAMPLE).unwrap());
107 assert_eq!(link.multibase(), EXAMPLE);
108 assert_eq!(link.to_string(), alloc::format!("hl:{EXAMPLE}"));
109 }
110
111 #[test]
112 fn bytes_are_what_their_digest_names() {
113 let link = Hashlink::of(b"hello");
114 assert!(link.matches(b"hello"));
115 assert!(!link.matches(b"hello!"));
116 assert_eq!(
118 link.multibase(),
119 "zQmRN6wdp1S2A5EtjW9A3M1vKSBuQQGcgvuhoMUoEz4iiT5"
120 );
121 assert_eq!(Hashlink::parse(&link.to_string()), Some(link));
122 }
123
124 #[test]
125 fn what_is_not_a_hashlink() {
126 for hashlink in [
127 "",
128 "hl:",
129 EXAMPLE,
130 &alloc::format!("https:{EXAMPLE}"),
131 &alloc::format!("hl:{}", &EXAMPLE[1..]),
132 &alloc::format!(
133 "hl:{EXAMPLE}:zCwPSdabLuj3jue1qYujzunnKwpL4myKdyeqySyFhnzZ8qdfW3bb6W8dVdRu"
134 ),
135 "hl:z5dtAYsVQU6SojhprzGZSsEvMGpE1VW",
137 "hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1",
138 ] {
139 assert!(Hashlink::parse(hashlink).is_none(), "{hashlink}");
140 }
141 }
142}