Skip to main content

bao_tree/io/
mixed.rs

1//! Read from sync, send to tokio sender
2use std::{future::Future, result};
3
4use blake3;
5use bytes::Bytes;
6use serde::{Deserialize, Serialize};
7use smallvec::SmallVec;
8
9use super::{sync::Outboard, EncodeError, Leaf, Parent};
10use crate::{
11    iter::BaoChunk, rec::truncate_ranges, split_inner, ChunkNum, ChunkRangesRef, HashMode, TreeNode,
12};
13
14/// A content item for the bao streaming protocol.
15#[derive(Debug, Serialize, Deserialize)]
16pub enum EncodedItem {
17    /// total data size, will be the first item
18    Size(u64),
19    /// a parent node
20    Parent(Parent),
21    /// a leaf node
22    Leaf(Leaf),
23    /// an error, will be the last item
24    Error(EncodeError),
25    /// done, will be the last item
26    Done,
27}
28
29impl From<Leaf> for EncodedItem {
30    fn from(l: Leaf) -> Self {
31        Self::Leaf(l)
32    }
33}
34
35impl From<Parent> for EncodedItem {
36    fn from(p: Parent) -> Self {
37        Self::Parent(p)
38    }
39}
40
41impl From<EncodeError> for EncodedItem {
42    fn from(e: EncodeError) -> Self {
43        Self::Error(e)
44    }
45}
46
47/// Abstract sender trait for sending encoded items
48pub trait Sender {
49    /// Error type
50    type Error;
51    /// Send an item
52    fn send(
53        &mut self,
54        item: EncodedItem,
55    ) -> impl Future<Output = std::result::Result<(), Self::Error>> + '_;
56}
57
58impl Sender for tokio::sync::mpsc::Sender<EncodedItem> {
59    type Error = tokio::sync::mpsc::error::SendError<EncodedItem>;
60    fn send(
61        &mut self,
62        item: EncodedItem,
63    ) -> impl Future<Output = std::result::Result<(), Self::Error>> + '_ {
64        tokio::sync::mpsc::Sender::send(self, item)
65    }
66}
67
68/// Traverse ranges relevant to a query from a reader and outboard to a stream
69///
70/// This function validates the data before writing.
71///
72/// It is possible to encode ranges from a partial file and outboard.
73/// This will either succeed if the requested ranges are all present, or fail
74/// as soon as a range is missing.
75pub async fn traverse_ranges_validated<D, O, F>(
76    data: D,
77    outboard: O,
78    ranges: &ChunkRangesRef,
79    send: &mut F,
80) -> std::result::Result<(), F::Error>
81where
82    D: ReadBytesAt,
83    O: Outboard,
84    F: Sender,
85{
86    traverse_ranges_validated_impl(data, outboard, ranges, send, HashMode::Standard).await
87}
88
89/// Traverse ranges relevant to a query from a reader and keyed outboard to a stream
90///
91/// This function validates the data before writing, using BLAKE3 keyed hashing.
92///
93/// It is possible to encode ranges from a partial file and outboard.
94/// This will either succeed if the requested ranges are all present, or fail
95/// as soon as a range is missing.
96pub async fn keyed_traverse_ranges_validated<D, O, F>(
97    data: D,
98    outboard: O,
99    ranges: &ChunkRangesRef,
100    send: &mut F,
101    key: &[u8; 32],
102) -> std::result::Result<(), F::Error>
103where
104    D: ReadBytesAt,
105    O: Outboard,
106    F: Sender,
107{
108    traverse_ranges_validated_impl(data, outboard, ranges, send, HashMode::Keyed(*key)).await
109}
110
111async fn traverse_ranges_validated_impl<D, O, F>(
112    data: D,
113    outboard: O,
114    ranges: &ChunkRangesRef,
115    send: &mut F,
116    mode: HashMode,
117) -> std::result::Result<(), F::Error>
118where
119    D: ReadBytesAt,
120    O: Outboard,
121    F: Sender,
122{
123    send.send(EncodedItem::Size(outboard.tree().size())).await?;
124    let res: result::Result<std::result::Result<(), F::Error>, EncodeError> = async {
125        if ranges.is_empty() {
126            return Ok(Ok(()));
127        }
128        let mut stack: SmallVec<[_; 10]> = SmallVec::<[blake3::Hash; 10]>::new();
129        stack.push(outboard.root());
130        let data = data;
131        let tree = outboard.tree();
132        // canonicalize ranges
133        let ranges = truncate_ranges(ranges, tree.size());
134        for item in tree.ranges_pre_order_chunks_iter_ref(ranges, 0) {
135            match item {
136                BaoChunk::Parent {
137                    is_root,
138                    left,
139                    right,
140                    node,
141                    ..
142                } => {
143                    let (l_hash, r_hash) = outboard.load(node)?.unwrap();
144                    let actual = mode.parent_cv(&l_hash, &r_hash, is_root);
145                    let expected = stack.pop().unwrap();
146                    if actual != expected {
147                        return Err(EncodeError::ParentHashMismatch(node));
148                    }
149                    if right {
150                        stack.push(r_hash);
151                    }
152                    if left {
153                        stack.push(l_hash);
154                    }
155                    let item = Parent {
156                        node,
157                        pair: (l_hash, r_hash),
158                    };
159                    if let Err(e) = send.send(item.into()).await {
160                        return Ok(Err(e));
161                    }
162                }
163                BaoChunk::Leaf {
164                    start_chunk,
165                    size,
166                    is_root,
167                    ranges,
168                    ..
169                } => {
170                    let expected = stack.pop().unwrap();
171                    let start = start_chunk.to_bytes();
172                    let buffer = data.read_bytes_at(start, size)?;
173                    if !ranges.is_all() {
174                        // we need to encode just a part of the data
175                        //
176                        // write into an out buffer to ensure we detect mismatches
177                        // before writing to the output.
178                        let mut out_buf = Vec::new();
179                        let actual = traverse_selected_rec_impl(
180                            start_chunk,
181                            buffer,
182                            is_root,
183                            ranges,
184                            tree.block_size.to_u32(),
185                            true,
186                            &mut out_buf,
187                            mode,
188                        );
189                        if actual != expected {
190                            return Err(EncodeError::LeafHashMismatch(start_chunk));
191                        }
192                        for item in out_buf.into_iter() {
193                            if let Err(e) = send.send(item).await {
194                                return Ok(Err(e));
195                            }
196                        }
197                    } else {
198                        let actual = mode.hash_subtree(start_chunk.0, &buffer, is_root);
199                        #[allow(clippy::redundant_slicing)]
200                        if actual != expected {
201                            return Err(EncodeError::LeafHashMismatch(start_chunk));
202                        }
203                        let item = Leaf {
204                            data: buffer,
205                            offset: start_chunk.to_bytes(),
206                        };
207                        if let Err(e) = send.send(item.into()).await {
208                            return Ok(Err(e));
209                        }
210                    };
211                }
212            }
213        }
214        Ok(Ok(()))
215    }
216    .await;
217    let res = match res {
218        Ok(Ok(())) => EncodedItem::Done,
219        Err(cause) => EncodedItem::Error(cause),
220        Ok(Err(err)) => return Err(err),
221    };
222    send.send(res).await
223}
224
225/// Encode ranges relevant to a query from a slice and outboard to a buffer.
226///
227/// This will compute the root hash, so it will have to traverse the entire tree.
228/// The `ranges` parameter just controls which parts of the data are written.
229///
230/// Except for writing to a buffer, this is the same as computing the subtree hash.
231/// The `min_level` parameter controls the minimum level that will be emitted as a leaf.
232/// Set this to 0 to disable chunk groups entirely.
233/// The `emit_data` parameter controls whether the data is written to the buffer.
234/// When setting this to false and setting query to `RangeSet::all()`, this can be used
235/// to write an outboard.
236///
237/// `res` will not contain the length prefix, so if you want a bao compatible format,
238/// you need to prepend it yourself.
239///
240/// This is used as a reference implementation in tests, but also to compute hashes
241/// below the chunk group size when creating responses for outboards with a chunk group
242/// size of >0.
243pub fn traverse_selected_rec(
244    start_chunk: ChunkNum,
245    data: Bytes,
246    is_root: bool,
247    query: &ChunkRangesRef,
248    min_level: u32,
249    emit_data: bool,
250    res: &mut Vec<EncodedItem>,
251) -> blake3::Hash {
252    traverse_selected_rec_impl(
253        start_chunk,
254        data,
255        is_root,
256        query,
257        min_level,
258        emit_data,
259        res,
260        HashMode::Standard,
261    )
262}
263
264/// Keyed version of [traverse_selected_rec], using BLAKE3 keyed hashing.
265#[allow(clippy::too_many_arguments)]
266pub fn keyed_traverse_selected_rec(
267    start_chunk: ChunkNum,
268    data: Bytes,
269    is_root: bool,
270    query: &ChunkRangesRef,
271    min_level: u32,
272    emit_data: bool,
273    res: &mut Vec<EncodedItem>,
274    key: &[u8; 32],
275) -> blake3::Hash {
276    traverse_selected_rec_impl(
277        start_chunk,
278        data,
279        is_root,
280        query,
281        min_level,
282        emit_data,
283        res,
284        HashMode::Keyed(*key),
285    )
286}
287
288#[allow(clippy::too_many_arguments)]
289fn traverse_selected_rec_impl(
290    start_chunk: ChunkNum,
291    data: Bytes,
292    is_root: bool,
293    query: &ChunkRangesRef,
294    min_level: u32,
295    emit_data: bool,
296    res: &mut Vec<EncodedItem>,
297    mode: HashMode,
298) -> blake3::Hash {
299    use blake3::CHUNK_LEN;
300    if data.len() <= CHUNK_LEN {
301        if emit_data && !query.is_empty() {
302            res.push(
303                Leaf {
304                    data: data.clone(),
305                    offset: start_chunk.to_bytes(),
306                }
307                .into(),
308            );
309        }
310        mode.hash_subtree(start_chunk.0, &data, is_root)
311    } else {
312        let chunks = data.len() / CHUNK_LEN + (data.len() % CHUNK_LEN != 0) as usize;
313        let chunks = chunks.next_power_of_two();
314        let level = chunks.trailing_zeros() - 1;
315        let mid = chunks / 2;
316        let mid_bytes = mid * CHUNK_LEN;
317        let mid_chunk = start_chunk + (mid as u64);
318        let (l_ranges, r_ranges) = split_inner(query, start_chunk, mid_chunk);
319        // for empty ranges, we don't want to emit anything.
320        // for full ranges where the level is below min_level, we want to emit
321        // just the data.
322        //
323        // todo: maybe call into blake3::hazmat::hash_subtree directly for this case? it would be faster.
324        let full = query.is_all();
325        let emit_parent = !query.is_empty() && (!full || level >= min_level);
326        let hash_offset = if emit_parent {
327            // make some room for the hash pair
328            let pair = Parent {
329                node: TreeNode(0),
330                pair: ([0; 32].into(), [0; 32].into()),
331            };
332            res.push(pair.into());
333            Some(res.len() - 1)
334        } else {
335            None
336        };
337        // recurse to the left and right to compute the hashes and emit data
338        let left = traverse_selected_rec_impl(
339            start_chunk,
340            data.slice(..mid_bytes),
341            false,
342            l_ranges,
343            min_level,
344            emit_data,
345            res,
346            mode,
347        );
348        let right = traverse_selected_rec_impl(
349            mid_chunk,
350            data.slice(mid_bytes..),
351            false,
352            r_ranges,
353            min_level,
354            emit_data,
355            res,
356            mode,
357        );
358        // backfill the hashes if needed
359        if let Some(o) = hash_offset {
360            // todo: figure out how to get the tree node from the start chunk!
361            let node = TreeNode(0);
362            res[o] = Parent {
363                node,
364                pair: (left, right),
365            }
366            .into();
367        }
368        mode.parent_cv(&left, &right, is_root)
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use crate::{
376        io::{
377            outboard::PreOrderMemOutboard,
378            sync::{encode_ranges_validated, keyed_encode_ranges_validated},
379        },
380        BlockSize, ChunkRanges,
381    };
382
383    fn flatten(items: Vec<EncodedItem>) -> Vec<u8> {
384        let mut res = Vec::new();
385        for item in items {
386            match item {
387                EncodedItem::Leaf(Leaf { data, .. }) => res.extend_from_slice(&data),
388                EncodedItem::Parent(Parent { pair: (l, r), .. }) => {
389                    res.extend_from_slice(l.as_bytes());
390                    res.extend_from_slice(r.as_bytes());
391                }
392                _ => {}
393            }
394        }
395        res
396    }
397
398    #[tokio::test]
399    async fn smoke() {
400        let data = [0u8; 100000];
401        let outboard = PreOrderMemOutboard::create(data, BlockSize::from_chunk_log(4));
402        let (mut tx, mut rx) = tokio::sync::mpsc::channel(10);
403        let mut encoded = Vec::new();
404        encode_ranges_validated(&data[..], &outboard, &ChunkRanges::empty(), &mut encoded).unwrap();
405        tokio::spawn(async move {
406            traverse_ranges_validated(&data[..], &outboard, &ChunkRanges::empty(), &mut tx)
407                .await
408                .unwrap();
409        });
410        let mut res = Vec::new();
411        while let Some(item) = rx.recv().await {
412            res.push(item);
413        }
414        println!("{res:?}");
415        let encoded2 = flatten(res);
416        assert_eq!(encoded, encoded2);
417    }
418
419    #[tokio::test]
420    async fn keyed_smoke() {
421        let data = [0u8; 100000];
422        let key = blake3::derive_key("bao-tree.test", b"mixed");
423        let outboard = PreOrderMemOutboard::create_keyed(data, BlockSize::from_chunk_log(4), &key);
424        let (mut tx, mut rx) = tokio::sync::mpsc::channel(10);
425        let mut encoded = Vec::new();
426        keyed_encode_ranges_validated(
427            &data[..],
428            &outboard,
429            &ChunkRanges::all(),
430            &mut encoded,
431            &key,
432        )
433        .unwrap();
434        tokio::spawn(async move {
435            keyed_traverse_ranges_validated(
436                &data[..],
437                &outboard,
438                &ChunkRanges::all(),
439                &mut tx,
440                &key,
441            )
442            .await
443            .unwrap();
444        });
445        let mut res = Vec::new();
446        while let Some(item) = rx.recv().await {
447            res.push(item);
448        }
449        let encoded2 = flatten(res);
450        assert_eq!(encoded, encoded2);
451    }
452}
453
454/// Trait identical to `ReadAt` but returning `Bytes` instead of reading into a buffer.
455///
456/// This forwards to the underlying `ReadAt` implementation except for `Bytes`, `&Bytes`, `&mut Bytes`.
457pub trait ReadBytesAt {
458    /// Version of `ReadAt::read_exact_at` that returns a `Bytes` instead of reading into a buffer.
459    fn read_bytes_at(&self, offset: u64, size: usize) -> std::io::Result<Bytes>;
460}
461
462mod impls {
463    use std::io;
464
465    use bytes::Bytes;
466
467    use super::ReadBytesAt;
468
469    // Macro for generic implementations (allocating with copy_from_slice)
470    macro_rules! impl_read_bytes_at_generic {
471    ($($t:ty),*) => {
472        $(
473            impl ReadBytesAt for $t {
474                fn read_bytes_at(&self, offset: u64, size: usize) -> io::Result<Bytes> {
475                    let mut buf = vec![0; size];
476                    ::positioned_io::ReadAt::read_exact_at(self, offset, &mut buf)?;
477                    Ok(buf.into())
478                }
479            }
480        )*
481    };
482}
483
484    // Macro for special implementations (non-allocating with slice)
485    macro_rules! impl_read_bytes_at_special {
486    ($($t:ty),*) => {
487        $(
488            impl ReadBytesAt for $t {
489                fn read_bytes_at(&self, offset: u64, size: usize) -> io::Result<Bytes> {
490                    let offset = offset as usize;
491                    if offset + size > self.len() {
492                        return Err(io::Error::new(
493                            io::ErrorKind::UnexpectedEof,
494                            "Read past end of buffer",
495                        ));
496                    }
497                    Ok(self.slice(offset..offset + size))
498                }
499            }
500        )*
501    };
502}
503
504    // Apply the macros
505    impl_read_bytes_at_generic!(&[u8], Vec<u8>);
506    impl_read_bytes_at_special!(Bytes, &Bytes, &mut Bytes);
507    #[cfg(feature = "fs")]
508    impl_read_bytes_at_generic!(std::fs::File);
509}