hashtree_core/
blob_route.rs1use 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_MAX_HTL: u8 = 10;
13pub const BLOB_DEFAULT_HTL: u8 = BLOB_MAX_HTL;
15pub const BLOB_REQUEST_BYTES: usize = 36;
16pub const BLOB_REPLY_HEADER_BYTES: usize = 7;
17
18const MAGIC: u8 = 0x48;
19const VERSION: u8 = 1;
20const GET: u8 = 1;
21const DATA: u8 = 1;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct BlobRequest {
25 pub hash: Hash,
26 pub htl: u8,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum BlobReply {
31 Data(Vec<u8>),
32 NoResult,
33}
34
35#[async_trait]
36pub trait BlobRoute: Send + Sync {
37 async fn route(&self, request: BlobRequest) -> Result<BlobReply, StoreError>;
38}
39
40pub struct StoreBlobRoute<S: Store + ?Sized> {
42 store: Arc<S>,
43}
44
45impl<S: Store + ?Sized> StoreBlobRoute<S> {
46 pub fn new(store: Arc<S>) -> Self {
47 Self { store }
48 }
49}
50
51#[async_trait]
52impl<S: Store + ?Sized + 'static> BlobRoute for StoreBlobRoute<S> {
53 async fn route(&self, request: BlobRequest) -> Result<BlobReply, StoreError> {
54 Ok(match self.store.get(&request.hash).await? {
55 Some(data) if data.len() > BLOB_MAX_BYTES => {
56 return Err(StoreError::Other(format!(
57 "blob route returned {} bytes, exceeding the {BLOB_MAX_BYTES}-byte limit",
58 data.len()
59 )));
60 }
61 Some(data) if crate::sha256(&data) != request.hash => {
62 return Err(StoreError::Other(
63 "blob route returned content with the wrong hash".to_string(),
64 ));
65 }
66 Some(data) => BlobReply::Data(data),
67 None => BlobReply::NoResult,
68 })
69 }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum BlobReplyHeader {
74 Data(usize),
75 NoResult,
76}
77
78#[derive(Debug, Error, PartialEq, Eq)]
79pub enum BlobCodecError {
80 #[error("invalid Hashtree blob wire message: {0}")]
81 Invalid(&'static str),
82 #[error("Hashtree blob size {0} exceeds the 16 MiB limit")]
83 BlobTooLarge(usize),
84 #[error("Hashtree blob HTL {0} exceeds the maximum of {BLOB_MAX_HTL}")]
85 HtlTooLarge(u8),
86}
87
88pub fn encode_blob_request(request: &BlobRequest) -> [u8; BLOB_REQUEST_BYTES] {
89 let mut bytes = [0; BLOB_REQUEST_BYTES];
90 bytes[..4].copy_from_slice(&[MAGIC, VERSION, GET, request.htl]);
91 bytes[4..].copy_from_slice(&request.hash);
92 bytes
93}
94
95pub fn decode_blob_request(bytes: &[u8]) -> Result<BlobRequest, BlobCodecError> {
96 if bytes.len() != BLOB_REQUEST_BYTES {
97 return Err(BlobCodecError::Invalid("request has the wrong length"));
98 }
99 if bytes[..3] != [MAGIC, VERSION, GET] {
100 return Err(BlobCodecError::Invalid(
101 "request has an unsupported prelude",
102 ));
103 }
104 if bytes[3] > BLOB_MAX_HTL {
105 return Err(BlobCodecError::HtlTooLarge(bytes[3]));
106 }
107 let mut hash = [0; 32];
108 hash.copy_from_slice(&bytes[4..]);
109 Ok(BlobRequest {
110 hash,
111 htl: bytes[3],
112 })
113}
114
115pub fn encode_blob_reply_header(
116 reply: &BlobReply,
117) -> Result<[u8; BLOB_REPLY_HEADER_BYTES], BlobCodecError> {
118 let (status, size) = match reply {
119 BlobReply::NoResult => (0, 0),
120 BlobReply::Data(data) => (DATA, data.len()),
121 };
122 if size > BLOB_MAX_BYTES {
123 return Err(BlobCodecError::BlobTooLarge(size));
124 }
125 let mut header = [0; BLOB_REPLY_HEADER_BYTES];
126 header[..3].copy_from_slice(&[MAGIC, VERSION, status]);
127 header[3..].copy_from_slice(&(size as u32).to_be_bytes());
128 Ok(header)
129}
130
131pub fn decode_blob_reply_header(bytes: &[u8]) -> Result<BlobReplyHeader, BlobCodecError> {
132 if bytes.len() != BLOB_REPLY_HEADER_BYTES {
133 return Err(BlobCodecError::Invalid(
134 "response header has the wrong length",
135 ));
136 }
137 if bytes[0] != MAGIC || bytes[1] != VERSION {
138 return Err(BlobCodecError::Invalid(
139 "response has an unsupported prelude",
140 ));
141 }
142 let size = u32::from_be_bytes(bytes[3..].try_into().expect("four-byte length")) as usize;
143 if size > BLOB_MAX_BYTES {
144 return Err(BlobCodecError::BlobTooLarge(size));
145 }
146 match (bytes[2], size) {
147 (0, 0) => Ok(BlobReplyHeader::NoResult),
148 (0, _) => Err(BlobCodecError::Invalid(
149 "NoResult response has a non-zero payload length",
150 )),
151 (DATA, size) => Ok(BlobReplyHeader::Data(size)),
152 _ => Err(BlobCodecError::Invalid(
153 "response has an unsupported status",
154 )),
155 }
156}