1use std::time::Duration;
28
29use crate::bolt4;
30use crate::cbor::Value;
31use crate::connection::{CallError, FrameStream, Session};
32use crate::identity::KeyPair;
33use crate::manifest::{self, Manifest, Mcid};
34
35pub const CONTENT_REALM: [u8; 32] = [0u8; 32];
38
39const PUT_BLOCK_PROC: &str = "_content.put_block";
40const GET_BLOCK_PROC: &str = "_content.get_block";
41const PUT_MANIFEST_PROC: &str = "_content.put_manifest";
42const GET_MANIFEST_PROC: &str = "_content.get_manifest";
43
44const BLOCK_TIMEOUT: Duration = Duration::from_secs(15);
46const MANIFEST_TIMEOUT: Duration = Duration::from_secs(5);
48
49const MAX_ATTEMPTS: u32 = 3;
52const RETRY_BACKOFF: Duration = Duration::from_millis(200);
53
54#[derive(Debug)]
55pub enum PutError {
56 OpenStream(quinn::ConnectionError),
59 Call(CallError),
60 Remote {
62 code: u8,
63 name: String,
64 detail: Option<String>,
65 },
66 UnexpectedReply(Value),
69 HashMismatch,
72}
73
74impl std::fmt::Display for PutError {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 PutError::OpenStream(e) => write!(f, "opening a dedicated stream: {e}"),
78 PutError::Call(e) => write!(f, "{e}"),
79 PutError::Remote { code, name, detail } => {
80 write!(f, "station returned error {code} ({name}): {detail:?}")
81 }
82 PutError::UnexpectedReply(v) => write!(f, "unexpected reply shape: {v:?}"),
83 PutError::HashMismatch => write!(f, "station reported hash_mismatch"),
84 }
85 }
86}
87
88impl std::error::Error for PutError {}
89
90#[derive(Debug)]
91pub enum GetError {
92 OpenStream(quinn::ConnectionError),
95 Call(CallError),
96 Remote {
97 code: u8,
98 name: String,
99 detail: Option<String>,
100 },
101 UnexpectedReply(Value),
102 NotFound,
103 ManifestDecode(manifest::FromWireError),
104 HashMismatch,
109 Verify(manifest::VerifyError),
110}
111
112impl std::fmt::Display for GetError {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match self {
115 GetError::OpenStream(e) => write!(f, "opening a dedicated stream: {e}"),
116 GetError::Call(e) => write!(f, "{e}"),
117 GetError::Remote { code, name, detail } => {
118 write!(f, "station returned error {code} ({name}): {detail:?}")
119 }
120 GetError::UnexpectedReply(v) => write!(f, "unexpected reply shape: {v:?}"),
121 GetError::NotFound => write!(f, "station reported not_found"),
122 GetError::ManifestDecode(e) => write!(f, "decoding the fetched manifest: {e}"),
123 GetError::HashMismatch => write!(f, "fetched content does not hash to its MCID"),
124 GetError::Verify(e) => write!(f, "reassembled content failed verification: {e}"),
125 }
126 }
127}
128
129impl std::error::Error for GetError {}
130
131pub async fn put(
139 session: &mut Session,
140 data: &[u8],
141 name: impl Into<String>,
142 identity: &KeyPair,
143) -> Result<Mcid, PutError> {
144 let mut stream = session
145 .open_dedicated_stream()
146 .await
147 .map_err(PutError::OpenStream)?;
148
149 if data.len() <= manifest::DEFAULT_CHUNK_SIZE {
150 let mcid = manifest::block_mcid(data);
151 put_block(&mut stream, &mcid, data, identity).await?;
152 return Ok(mcid);
153 }
154
155 let opts = manifest::CreateOptions {
156 name: name.into(),
157 ..manifest::CreateOptions::default()
158 };
159 let (manifest, chunks) = manifest::create(data, &opts);
160 for (index, chunk) in chunks.iter().enumerate() {
161 let chunk_mcid = manifest::chunk_mcid(&manifest, index)
162 .expect("index is in range: it came from iterating manifest.create's own chunks");
163 put_block(&mut stream, &chunk_mcid, chunk, identity).await?;
164 }
165 put_manifest(&mut stream, &manifest, identity).await?;
166 Ok(manifest.mcid)
167}
168
169pub async fn get(
171 session: &mut Session,
172 mcid: Mcid,
173 identity: &KeyPair,
174) -> Result<Vec<u8>, GetError> {
175 let mut stream = session
176 .open_dedicated_stream()
177 .await
178 .map_err(GetError::OpenStream)?;
179
180 if !manifest::mcid_is_chunked(&mcid) {
181 let data = get_block(&mut stream, &mcid, identity).await?;
182 if manifest::block_mcid(&data) != mcid {
183 return Err(GetError::HashMismatch);
184 }
185 return Ok(data);
186 }
187
188 let manifest = get_manifest(&mut stream, &mcid, identity).await?;
189 let mut data = Vec::new();
199 for index in 0..manifest.chunk_count {
200 let chunk_mcid = manifest::chunk_mcid(&manifest, index)
201 .expect("index < manifest.chunk_count, so manifest.chunks[index] exists");
202 let chunk = get_block(&mut stream, &chunk_mcid, identity).await?;
203 if manifest::block_mcid(&chunk) != chunk_mcid {
204 return Err(GetError::HashMismatch);
205 }
206 data.extend_from_slice(&chunk);
207 }
208 manifest::verify(&manifest, &data).map_err(GetError::Verify)?;
209 Ok(data)
210}
211
212async fn put_block(
213 stream: &mut FrameStream,
214 mcid: &Mcid,
215 bytes: &[u8],
216 identity: &KeyPair,
217) -> Result<(), PutError> {
218 let payload = Value::Map(vec![
219 (Value::text("mcid"), Value::Bytes(mcid.to_vec())),
220 (Value::text("payload"), Value::Bytes(bytes.to_vec())),
221 ]);
222 let response = call_with_retry(stream, PUT_BLOCK_PROC, payload, BLOCK_TIMEOUT, identity)
223 .await
224 .map_err(PutError::Call)?;
225 match response {
226 crate::frame::CallResponse::Result { payload, .. } => match payload {
227 Value::Text(t) if t == "ok" => Ok(()),
228 Value::Text(t) if t == "hash_mismatch" => Err(PutError::HashMismatch),
229 other => Err(PutError::UnexpectedReply(other)),
230 },
231 crate::frame::CallResponse::Error {
232 code, name, detail, ..
233 } => Err(PutError::Remote { code, name, detail }),
234 }
235}
236
237async fn put_manifest(
238 stream: &mut FrameStream,
239 manifest: &Manifest,
240 identity: &KeyPair,
241) -> Result<(), PutError> {
242 let payload = Value::Map(vec![(Value::text("manifest"), manifest::to_wire(manifest))]);
243 let response = call_with_retry(
244 stream,
245 PUT_MANIFEST_PROC,
246 payload,
247 MANIFEST_TIMEOUT,
248 identity,
249 )
250 .await
251 .map_err(PutError::Call)?;
252 match response {
253 crate::frame::CallResponse::Result { payload, .. } => match payload {
254 Value::Text(t) if t == "ok" => Ok(()),
255 other => Err(PutError::UnexpectedReply(other)),
256 },
257 crate::frame::CallResponse::Error {
258 code, name, detail, ..
259 } => Err(PutError::Remote { code, name, detail }),
260 }
261}
262
263async fn get_block(
264 stream: &mut FrameStream,
265 mcid: &Mcid,
266 identity: &KeyPair,
267) -> Result<Vec<u8>, GetError> {
268 let payload = Value::Map(vec![(Value::text("mcid"), Value::Bytes(mcid.to_vec()))]);
269 let response = call_with_retry(stream, GET_BLOCK_PROC, payload, BLOCK_TIMEOUT, identity)
270 .await
271 .map_err(GetError::Call)?;
272 match response {
273 crate::frame::CallResponse::Result { payload, .. } => match payload {
274 Value::Bytes(b) => Ok(b),
275 Value::Text(t) if t == "not_found" => Err(GetError::NotFound),
276 other => Err(GetError::UnexpectedReply(other)),
277 },
278 crate::frame::CallResponse::Error {
279 code, name, detail, ..
280 } => Err(GetError::Remote { code, name, detail }),
281 }
282}
283
284async fn get_manifest(
285 stream: &mut FrameStream,
286 mcid: &Mcid,
287 identity: &KeyPair,
288) -> Result<Manifest, GetError> {
289 let payload = Value::Map(vec![(Value::text("mcid"), Value::Bytes(mcid.to_vec()))]);
290 let response = call_with_retry(
291 stream,
292 GET_MANIFEST_PROC,
293 payload,
294 MANIFEST_TIMEOUT,
295 identity,
296 )
297 .await
298 .map_err(GetError::Call)?;
299 match response {
300 crate::frame::CallResponse::Result { payload, .. } => match payload {
301 Value::Map(_) => manifest::from_wire(&payload).map_err(GetError::ManifestDecode),
302 Value::Text(t) if t == "not_found" => Err(GetError::NotFound),
303 other => Err(GetError::UnexpectedReply(other)),
304 },
305 crate::frame::CallResponse::Error {
306 code, name, detail, ..
307 } => Err(GetError::Remote { code, name, detail }),
308 }
309}
310
311async fn call_with_retry(
318 stream: &mut FrameStream,
319 procedure: &str,
320 payload: Value,
321 timeout: Duration,
322 identity: &KeyPair,
323) -> Result<crate::frame::CallResponse, CallError> {
324 let mut attempt = 0;
325 loop {
326 attempt += 1;
327 let deadline_ms = (now_ms() + timeout.as_millis() as u64) as i128;
328 let outcome = stream
329 .call(
330 procedure,
331 CONTENT_REALM,
332 payload.clone(),
333 deadline_ms,
334 identity,
335 timeout,
336 )
337 .await;
338
339 let should_retry = attempt < MAX_ATTEMPTS
340 && matches!(
341 &outcome,
342 Ok(crate::frame::CallResponse::Error { code, .. })
343 if bolt4::Code::from_u8(*code).is_some_and(bolt4::Code::is_retryable)
344 );
345 if !should_retry {
346 return outcome;
347 }
348 tokio::time::sleep(RETRY_BACKOFF).await;
349 }
350}
351
352fn now_ms() -> u64 {
353 std::time::SystemTime::now()
354 .duration_since(std::time::UNIX_EPOCH)
355 .expect("system clock after epoch")
356 .as_millis() as u64
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn small_data_addresses_as_a_single_block() {
365 let data = vec![7u8; 100];
366 assert!(data.len() <= manifest::DEFAULT_CHUNK_SIZE);
367 let mcid = manifest::block_mcid(&data);
368 assert!(!manifest::mcid_is_chunked(&mcid));
369 }
370
371 #[test]
372 fn large_data_would_address_as_a_manifest() {
373 let data = vec![7u8; manifest::DEFAULT_CHUNK_SIZE + 1];
374 let opts = manifest::CreateOptions::default();
375 let (manifest, chunks) = manifest::create(&data, &opts);
376 assert!(chunks.len() > 1);
377 assert!(manifest::mcid_is_chunked(&manifest.mcid));
378 }
379
380 #[test]
381 fn call_with_retry_backoff_matches_the_spec() {
382 assert_eq!(MAX_ATTEMPTS, 3);
383 assert_eq!(RETRY_BACKOFF, Duration::from_millis(200));
384 }
385}