1use std::{
2 fmt,
3 path::{Path, PathBuf},
4 str::FromStr,
5};
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use ulid::Ulid;
10
11use crate::{
12 error::{Error, Result},
13 grpc,
14};
15
16#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub struct Cid {
19 pub id: Ulid,
20 pub hash: [u8; 32],
21}
22
23impl From<Cid> for grpc::Cid {
24 fn from(cid: Cid) -> Self {
25 Self {
26 hash: cid.hash.into(),
27 id: cid.id.into(),
28 }
29 }
30}
31
32impl TryFrom<grpc::Cid> for Cid {
33 type Error = Error;
34
35 fn try_from(cid: grpc::Cid) -> Result<Self> {
36 Ok(Self {
37 hash: cid.hash.try_into().map_err(|_| Error::InvalidFileHash)?,
38 id: cid.id.try_into()?,
39 })
40 }
41}
42
43impl fmt::Display for Cid {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 let ulid_str = self.id.to_string();
46 let hash_hex = hex::encode(self.hash);
47 write!(f, "{ulid_str}-{hash_hex}")
48 }
49}
50
51impl FromStr for Cid {
52 type Err = Error;
53
54 fn from_str(s: &str) -> Result<Self> {
55 let mut parts = s.splitn(2, '-');
56 let ulid_part = parts.next().ok_or(Error::InvalidCidFormat)?;
57 let hash_part = parts.next().ok_or(Error::InvalidCidFormat)?;
58
59 let id = Ulid::from_string(ulid_part).map_err(|_| Error::InvalidCidFormat)?;
60
61 let hash_bytes = hex::decode(hash_part).map_err(|_| Error::InvalidCidFormat)?;
62 if hash_bytes.len() != 32 {
63 return Err(Error::InvalidCidFormat);
64 }
65
66 let mut hash_array = [0u8; 32];
67 hash_array.copy_from_slice(&hash_bytes);
68
69 Ok(Cid {
70 id,
71 hash: hash_array,
72 })
73 }
74}
75
76impl TryFrom<&Path> for grpc::FilePath {
77 type Error = Error;
78
79 fn try_from(path: &Path) -> Result<Self> {
80 Ok(Self {
81 path: path
82 .to_str()
83 .map(ToString::to_string)
84 .ok_or(Error::InvalidFilePath)?,
85 })
86 }
87}
88
89impl TryFrom<PathBuf> for grpc::FilePath {
90 type Error = Error;
91
92 fn try_from(path: PathBuf) -> Result<Self> {
93 Ok(Self {
94 path: path
95 .to_str()
96 .map(ToString::to_string)
97 .ok_or(Error::InvalidFilePath)?,
98 })
99 }
100}
101
102impl From<grpc::FilePath> for PathBuf {
103 fn from(path: grpc::FilePath) -> Self {
104 path.path.into()
105 }
106}
107
108#[derive(Debug, Clone, Hash, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
110pub enum DownloadEvent {
111 Progress(u64),
112 Ready(PathBuf),
113}
114
115impl TryFrom<DownloadEvent> for grpc::DownloadEvent {
116 type Error = Error;
117
118 fn try_from(event: DownloadEvent) -> Result<Self> {
119 let event = match event {
120 DownloadEvent::Progress(progress) => grpc::download_event::Event::Progress(progress),
121 DownloadEvent::Ready(path) => grpc::download_event::Event::Ready(path.try_into()?),
122 };
123
124 Ok(Self { event: Some(event) })
125 }
126}
127
128impl TryFrom<grpc::DownloadEvent> for DownloadEvent {
129 type Error = Error;
130
131 fn try_from(event: grpc::DownloadEvent) -> Result<Self> {
132 Ok(match event.event.ok_or(Error::MissingEvent)? {
133 grpc::download_event::Event::Progress(progress) => DownloadEvent::Progress(progress),
134 grpc::download_event::Event::Ready(path) => DownloadEvent::Ready(path.into()),
135 })
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use std::str::FromStr;
142
143 use super::*;
144
145 #[test]
146 fn test_cid_to_string() {
147 let cid = Cid {
148 id: Ulid::from_string("01GZMC49M8599PQPNGDSAX6X1F").unwrap(),
149 hash: [
150 0xFF, 0xD2, 0x34, 0x1A, 0x00, 0xAB, 0xCD, 0xEF, 0x00, 0x11, 0x33, 0x55, 0x77, 0x99,
151 0xBB, 0xDD, 0xCC, 0xEE, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
152 0xFF, 0x12, 0x34, 0x56,
153 ],
154 };
155
156 let s = cid.to_string();
157
158 let expected_str = "01GZMC49M8599PQPNGDSAX6X1F-ffd2341a00abcdef001133557799bbddccee5566778899aabbccddeeff123456";
159
160 assert_eq!(s, expected_str, "Cid::to_string failed");
161 }
162
163 #[test]
164 fn test_cid_from_str() {
165 let input_str = "01GZMC49M8599PQPNGDSAX6X1F-ffd2341a00abcdef001133557799bbddccee5566778899aabbccddeeff123456";
166
167 let cid = Cid::from_str(input_str).expect("Parsing valid CID");
168
169 assert_eq!(
170 cid.id,
171 Ulid::from_string("01GZMC49M8599PQPNGDSAX6X1F").unwrap(),
172 "The ULID part was incorrectly parsed"
173 );
174
175 let expected_hash = [
176 0xFF, 0xD2, 0x34, 0x1A, 0x00, 0xAB, 0xCD, 0xEF, 0x00, 0x11, 0x33, 0x55, 0x77, 0x99,
177 0xBB, 0xDD, 0xCC, 0xEE, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
178 0xFF, 0x12, 0x34, 0x56,
179 ];
180 assert_eq!(
181 cid.hash, expected_hash,
182 "The hash part was incorrectly parsed"
183 );
184 }
185}