use std::time::Duration;
use crate::bolt4;
use crate::cbor::Value;
use crate::connection::{CallError, FrameStream, Session};
use crate::identity::KeyPair;
use crate::manifest::{self, Manifest, Mcid};
pub const CONTENT_REALM: [u8; 32] = [0u8; 32];
const PUT_BLOCK_PROC: &str = "_content.put_block";
const GET_BLOCK_PROC: &str = "_content.get_block";
const PUT_MANIFEST_PROC: &str = "_content.put_manifest";
const GET_MANIFEST_PROC: &str = "_content.get_manifest";
const BLOCK_TIMEOUT: Duration = Duration::from_secs(15);
const MANIFEST_TIMEOUT: Duration = Duration::from_secs(5);
const MAX_ATTEMPTS: u32 = 3;
const RETRY_BACKOFF: Duration = Duration::from_millis(200);
#[derive(Debug)]
pub enum PutError {
OpenStream(quinn::ConnectionError),
Call(CallError),
Remote {
code: u8,
name: String,
detail: Option<String>,
},
UnexpectedReply(Value),
HashMismatch,
}
impl std::fmt::Display for PutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PutError::OpenStream(e) => write!(f, "opening a dedicated stream: {e}"),
PutError::Call(e) => write!(f, "{e}"),
PutError::Remote { code, name, detail } => {
write!(f, "station returned error {code} ({name}): {detail:?}")
}
PutError::UnexpectedReply(v) => write!(f, "unexpected reply shape: {v:?}"),
PutError::HashMismatch => write!(f, "station reported hash_mismatch"),
}
}
}
impl std::error::Error for PutError {}
#[derive(Debug)]
pub enum GetError {
OpenStream(quinn::ConnectionError),
Call(CallError),
Remote {
code: u8,
name: String,
detail: Option<String>,
},
UnexpectedReply(Value),
NotFound,
ManifestDecode(manifest::FromWireError),
HashMismatch,
Verify(manifest::VerifyError),
}
impl std::fmt::Display for GetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GetError::OpenStream(e) => write!(f, "opening a dedicated stream: {e}"),
GetError::Call(e) => write!(f, "{e}"),
GetError::Remote { code, name, detail } => {
write!(f, "station returned error {code} ({name}): {detail:?}")
}
GetError::UnexpectedReply(v) => write!(f, "unexpected reply shape: {v:?}"),
GetError::NotFound => write!(f, "station reported not_found"),
GetError::ManifestDecode(e) => write!(f, "decoding the fetched manifest: {e}"),
GetError::HashMismatch => write!(f, "fetched content does not hash to its MCID"),
GetError::Verify(e) => write!(f, "reassembled content failed verification: {e}"),
}
}
}
impl std::error::Error for GetError {}
pub async fn put(
session: &mut Session,
data: &[u8],
name: impl Into<String>,
identity: &KeyPair,
) -> Result<Mcid, PutError> {
let mut stream = session
.open_dedicated_stream()
.await
.map_err(PutError::OpenStream)?;
if data.len() <= manifest::DEFAULT_CHUNK_SIZE {
let mcid = manifest::block_mcid(data);
put_block(&mut stream, &mcid, data, identity).await?;
return Ok(mcid);
}
let opts = manifest::CreateOptions {
name: name.into(),
..manifest::CreateOptions::default()
};
let (manifest, chunks) = manifest::create(data, &opts);
for (index, chunk) in chunks.iter().enumerate() {
let chunk_mcid = manifest::chunk_mcid(&manifest, index)
.expect("index is in range: it came from iterating manifest.create's own chunks");
put_block(&mut stream, &chunk_mcid, chunk, identity).await?;
}
put_manifest(&mut stream, &manifest, identity).await?;
Ok(manifest.mcid)
}
pub async fn get(
session: &mut Session,
mcid: Mcid,
identity: &KeyPair,
) -> Result<Vec<u8>, GetError> {
let mut stream = session
.open_dedicated_stream()
.await
.map_err(GetError::OpenStream)?;
if !manifest::mcid_is_chunked(&mcid) {
let data = get_block(&mut stream, &mcid, identity).await?;
if manifest::block_mcid(&data) != mcid {
return Err(GetError::HashMismatch);
}
return Ok(data);
}
let manifest = get_manifest(&mut stream, &mcid, identity).await?;
let mut data = Vec::new();
for index in 0..manifest.chunk_count {
let chunk_mcid = manifest::chunk_mcid(&manifest, index)
.expect("index < manifest.chunk_count, so manifest.chunks[index] exists");
let chunk = get_block(&mut stream, &chunk_mcid, identity).await?;
if manifest::block_mcid(&chunk) != chunk_mcid {
return Err(GetError::HashMismatch);
}
data.extend_from_slice(&chunk);
}
manifest::verify(&manifest, &data).map_err(GetError::Verify)?;
Ok(data)
}
async fn put_block(
stream: &mut FrameStream,
mcid: &Mcid,
bytes: &[u8],
identity: &KeyPair,
) -> Result<(), PutError> {
let payload = Value::Map(vec![
(Value::text("mcid"), Value::Bytes(mcid.to_vec())),
(Value::text("payload"), Value::Bytes(bytes.to_vec())),
]);
let response = call_with_retry(stream, PUT_BLOCK_PROC, payload, BLOCK_TIMEOUT, identity)
.await
.map_err(PutError::Call)?;
match response {
crate::frame::CallResponse::Result { payload, .. } => match payload {
Value::Text(t) if t == "ok" => Ok(()),
Value::Text(t) if t == "hash_mismatch" => Err(PutError::HashMismatch),
other => Err(PutError::UnexpectedReply(other)),
},
crate::frame::CallResponse::Error {
code, name, detail, ..
} => Err(PutError::Remote { code, name, detail }),
}
}
async fn put_manifest(
stream: &mut FrameStream,
manifest: &Manifest,
identity: &KeyPair,
) -> Result<(), PutError> {
let payload = Value::Map(vec![(Value::text("manifest"), manifest::to_wire(manifest))]);
let response = call_with_retry(
stream,
PUT_MANIFEST_PROC,
payload,
MANIFEST_TIMEOUT,
identity,
)
.await
.map_err(PutError::Call)?;
match response {
crate::frame::CallResponse::Result { payload, .. } => match payload {
Value::Text(t) if t == "ok" => Ok(()),
other => Err(PutError::UnexpectedReply(other)),
},
crate::frame::CallResponse::Error {
code, name, detail, ..
} => Err(PutError::Remote { code, name, detail }),
}
}
async fn get_block(
stream: &mut FrameStream,
mcid: &Mcid,
identity: &KeyPair,
) -> Result<Vec<u8>, GetError> {
let payload = Value::Map(vec![(Value::text("mcid"), Value::Bytes(mcid.to_vec()))]);
let response = call_with_retry(stream, GET_BLOCK_PROC, payload, BLOCK_TIMEOUT, identity)
.await
.map_err(GetError::Call)?;
match response {
crate::frame::CallResponse::Result { payload, .. } => match payload {
Value::Bytes(b) => Ok(b),
Value::Text(t) if t == "not_found" => Err(GetError::NotFound),
other => Err(GetError::UnexpectedReply(other)),
},
crate::frame::CallResponse::Error {
code, name, detail, ..
} => Err(GetError::Remote { code, name, detail }),
}
}
async fn get_manifest(
stream: &mut FrameStream,
mcid: &Mcid,
identity: &KeyPair,
) -> Result<Manifest, GetError> {
let payload = Value::Map(vec![(Value::text("mcid"), Value::Bytes(mcid.to_vec()))]);
let response = call_with_retry(
stream,
GET_MANIFEST_PROC,
payload,
MANIFEST_TIMEOUT,
identity,
)
.await
.map_err(GetError::Call)?;
match response {
crate::frame::CallResponse::Result { payload, .. } => match payload {
Value::Map(_) => manifest::from_wire(&payload).map_err(GetError::ManifestDecode),
Value::Text(t) if t == "not_found" => Err(GetError::NotFound),
other => Err(GetError::UnexpectedReply(other)),
},
crate::frame::CallResponse::Error {
code, name, detail, ..
} => Err(GetError::Remote { code, name, detail }),
}
}
async fn call_with_retry(
stream: &mut FrameStream,
procedure: &str,
payload: Value,
timeout: Duration,
identity: &KeyPair,
) -> Result<crate::frame::CallResponse, CallError> {
let mut attempt = 0;
loop {
attempt += 1;
let deadline_ms = (now_ms() + timeout.as_millis() as u64) as i128;
let outcome = stream
.call(
procedure,
CONTENT_REALM,
payload.clone(),
deadline_ms,
identity,
timeout,
)
.await;
let should_retry = attempt < MAX_ATTEMPTS
&& matches!(
&outcome,
Ok(crate::frame::CallResponse::Error { code, .. })
if bolt4::Code::from_u8(*code).is_some_and(bolt4::Code::is_retryable)
);
if !should_retry {
return outcome;
}
tokio::time::sleep(RETRY_BACKOFF).await;
}
}
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock after epoch")
.as_millis() as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn small_data_addresses_as_a_single_block() {
let data = vec![7u8; 100];
assert!(data.len() <= manifest::DEFAULT_CHUNK_SIZE);
let mcid = manifest::block_mcid(&data);
assert!(!manifest::mcid_is_chunked(&mcid));
}
#[test]
fn large_data_would_address_as_a_manifest() {
let data = vec![7u8; manifest::DEFAULT_CHUNK_SIZE + 1];
let opts = manifest::CreateOptions::default();
let (manifest, chunks) = manifest::create(&data, &opts);
assert!(chunks.len() > 1);
assert!(manifest::mcid_is_chunked(&manifest.mcid));
}
#[test]
fn call_with_retry_backoff_matches_the_spec() {
assert_eq!(MAX_ATTEMPTS, 3);
assert_eq!(RETRY_BACKOFF, Duration::from_millis(200));
}
}