Skip to main content

hashtree_core/
blob_route.rs

1//! One-hop blob routing values and their compact process/network wire codec.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use thiserror::Error;
7
8use crate::{Hash, Store, StoreError};
9
10pub const BLOB_MAX_BYTES: usize = 16 * 1024 * 1024;
11pub const BLOB_REQUEST_BYTES: usize = 36;
12pub const BLOB_REPLY_HEADER_BYTES: usize = 7;
13
14const MAGIC: u8 = 0x48;
15const VERSION: u8 = 1;
16const GET: u8 = 1;
17const DATA: u8 = 1;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct BlobRequest {
21    pub hash: Hash,
22    pub htl: u8,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum BlobReply {
27    Data(Vec<u8>),
28    NoResult,
29}
30
31#[async_trait]
32pub trait BlobRoute: Send + Sync {
33    async fn route(&self, request: BlobRequest) -> Result<BlobReply, StoreError>;
34}
35
36/// A terminal route that performs exactly one lookup and does not interpret HTL.
37pub struct StoreBlobRoute<S: Store + ?Sized> {
38    store: Arc<S>,
39}
40
41impl<S: Store + ?Sized> StoreBlobRoute<S> {
42    pub fn new(store: Arc<S>) -> Self {
43        Self { store }
44    }
45}
46
47#[async_trait]
48impl<S: Store + ?Sized + 'static> BlobRoute for StoreBlobRoute<S> {
49    async fn route(&self, request: BlobRequest) -> Result<BlobReply, StoreError> {
50        Ok(match self.store.get(&request.hash).await? {
51            Some(data) => BlobReply::Data(data),
52            None => BlobReply::NoResult,
53        })
54    }
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum BlobReplyHeader {
59    Data(usize),
60    NoResult,
61}
62
63#[derive(Debug, Error, PartialEq, Eq)]
64pub enum BlobCodecError {
65    #[error("invalid Hashtree blob wire message: {0}")]
66    Invalid(&'static str),
67    #[error("Hashtree blob size {0} exceeds the 16 MiB limit")]
68    BlobTooLarge(usize),
69}
70
71pub fn encode_blob_request(request: &BlobRequest) -> [u8; BLOB_REQUEST_BYTES] {
72    let mut bytes = [0; BLOB_REQUEST_BYTES];
73    bytes[..4].copy_from_slice(&[MAGIC, VERSION, GET, request.htl]);
74    bytes[4..].copy_from_slice(&request.hash);
75    bytes
76}
77
78pub fn decode_blob_request(bytes: &[u8]) -> Result<BlobRequest, BlobCodecError> {
79    if bytes.len() != BLOB_REQUEST_BYTES {
80        return Err(BlobCodecError::Invalid("request has the wrong length"));
81    }
82    if bytes[..3] != [MAGIC, VERSION, GET] {
83        return Err(BlobCodecError::Invalid(
84            "request has an unsupported prelude",
85        ));
86    }
87    let mut hash = [0; 32];
88    hash.copy_from_slice(&bytes[4..]);
89    Ok(BlobRequest {
90        hash,
91        htl: bytes[3],
92    })
93}
94
95pub fn encode_blob_reply_header(
96    reply: &BlobReply,
97) -> Result<[u8; BLOB_REPLY_HEADER_BYTES], BlobCodecError> {
98    let (status, size) = match reply {
99        BlobReply::NoResult => (0, 0),
100        BlobReply::Data(data) => (DATA, data.len()),
101    };
102    if size > BLOB_MAX_BYTES {
103        return Err(BlobCodecError::BlobTooLarge(size));
104    }
105    let mut header = [0; BLOB_REPLY_HEADER_BYTES];
106    header[..3].copy_from_slice(&[MAGIC, VERSION, status]);
107    header[3..].copy_from_slice(&(size as u32).to_be_bytes());
108    Ok(header)
109}
110
111pub fn decode_blob_reply_header(bytes: &[u8]) -> Result<BlobReplyHeader, BlobCodecError> {
112    if bytes.len() != BLOB_REPLY_HEADER_BYTES {
113        return Err(BlobCodecError::Invalid(
114            "response header has the wrong length",
115        ));
116    }
117    if bytes[0] != MAGIC || bytes[1] != VERSION {
118        return Err(BlobCodecError::Invalid(
119            "response has an unsupported prelude",
120        ));
121    }
122    let size = u32::from_be_bytes(bytes[3..].try_into().expect("four-byte length")) as usize;
123    if size > BLOB_MAX_BYTES {
124        return Err(BlobCodecError::BlobTooLarge(size));
125    }
126    match (bytes[2], size) {
127        (0, 0) => Ok(BlobReplyHeader::NoResult),
128        (0, _) => Err(BlobCodecError::Invalid(
129            "NoResult response has a non-zero payload length",
130        )),
131        (DATA, size) => Ok(BlobReplyHeader::Data(size)),
132        _ => Err(BlobCodecError::Invalid(
133            "response has an unsupported status",
134        )),
135    }
136}