Skip to main content

heddle_object_model/object/
tree_source.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Byte sources for streamable Tree reads.
3
4use std::{
5    fs::File,
6    io::{Read, Seek, SeekFrom},
7};
8
9use bytes::Bytes;
10
11use super::tree_stream::TreeStreamError;
12
13/// How a tree body may be trusted for content-address verification.
14///
15/// Sequential reads from offset 0 recompute the typed tree hash. Ranged
16/// resume skips the prefix and is allowed only after the caller has hashed
17/// these exact bytes. A hash-shaped path or an embedded tree id is not that
18/// proof. Object-store backends use [`Self::SequentialVerify`].
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum TreeBodyIntegrity {
21    /// Caller hashed these exact bytes; prefix skip is allowed.
22    VerifiedPlacement,
23    /// The reader must start at entry 0 and call `finish_and_verify`.
24    SequentialVerify,
25}
26
27/// Random-access source of an uncompressed canonical tree body.
28pub trait TreeByteSource {
29    fn read_exact_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), TreeStreamError>;
30    fn len(&self) -> u64;
31    fn integrity(&self) -> TreeBodyIntegrity;
32    fn bytes_read(&self) -> u64;
33
34    fn is_empty(&self) -> bool {
35        self.len() == 0
36    }
37}
38
39/// In-memory tree body, used by tests and stores that already hold the bytes.
40#[derive(Debug)]
41pub struct BytesTreeSource {
42    bytes: Bytes,
43    integrity: TreeBodyIntegrity,
44    bytes_read: u64,
45}
46
47impl BytesTreeSource {
48    pub fn verified_placement(bytes: impl Into<Bytes>) -> Self {
49        Self {
50            bytes: bytes.into(),
51            integrity: TreeBodyIntegrity::VerifiedPlacement,
52            bytes_read: 0,
53        }
54    }
55
56    pub fn sequential_verify(bytes: impl Into<Bytes>) -> Self {
57        Self {
58            bytes: bytes.into(),
59            integrity: TreeBodyIntegrity::SequentialVerify,
60            bytes_read: 0,
61        }
62    }
63}
64
65impl TreeByteSource for BytesTreeSource {
66    fn read_exact_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), TreeStreamError> {
67        let start =
68            usize::try_from(offset).map_err(|_| TreeStreamError::TruncatedFrame { offset })?;
69        let end = start
70            .checked_add(buf.len())
71            .ok_or(TreeStreamError::TruncatedFrame { offset })?;
72        let slice = self
73            .bytes
74            .get(start..end)
75            .ok_or(TreeStreamError::TruncatedFrame { offset })?;
76        buf.copy_from_slice(slice);
77        self.bytes_read += buf.len() as u64;
78        Ok(())
79    }
80
81    fn len(&self) -> u64 {
82        self.bytes.len() as u64
83    }
84
85    fn integrity(&self) -> TreeBodyIntegrity {
86        self.integrity
87    }
88
89    fn bytes_read(&self) -> u64 {
90        self.bytes_read
91    }
92}
93
94/// File-backed tree body. Sequential reads seek per frame; resume at
95/// ordinal > 0 is refused because a path is not a content-address proof.
96#[derive(Debug)]
97pub struct FileTreeSource {
98    file: File,
99    len: u64,
100    integrity: TreeBodyIntegrity,
101    bytes_read: u64,
102}
103
104impl FileTreeSource {
105    pub fn sequential_verify(file: File, len: u64) -> Self {
106        Self {
107            file,
108            len,
109            integrity: TreeBodyIntegrity::SequentialVerify,
110            bytes_read: 0,
111        }
112    }
113}
114
115impl TreeByteSource for FileTreeSource {
116    fn read_exact_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), TreeStreamError> {
117        if offset
118            .checked_add(buf.len() as u64)
119            .is_none_or(|end| end > self.len)
120        {
121            return Err(TreeStreamError::TruncatedFrame { offset });
122        }
123        self.file.seek(SeekFrom::Start(offset))?;
124        self.file.read_exact(buf)?;
125        self.bytes_read += buf.len() as u64;
126        Ok(())
127    }
128
129    fn len(&self) -> u64 {
130        self.len
131    }
132
133    fn integrity(&self) -> TreeBodyIntegrity {
134        self.integrity
135    }
136
137    fn bytes_read(&self) -> u64 {
138        self.bytes_read
139    }
140}
141
142/// Store-facing handle: either in-memory bytes or a seekable loose file.
143#[derive(Debug)]
144pub enum OpenedTreeBody {
145    Bytes(BytesTreeSource),
146    File(FileTreeSource),
147}
148
149impl TreeByteSource for OpenedTreeBody {
150    fn read_exact_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), TreeStreamError> {
151        match self {
152            Self::Bytes(source) => source.read_exact_at(offset, buf),
153            Self::File(source) => source.read_exact_at(offset, buf),
154        }
155    }
156
157    fn len(&self) -> u64 {
158        match self {
159            Self::Bytes(source) => source.len(),
160            Self::File(source) => source.len(),
161        }
162    }
163
164    fn integrity(&self) -> TreeBodyIntegrity {
165        match self {
166            Self::Bytes(source) => source.integrity(),
167            Self::File(source) => source.integrity(),
168        }
169    }
170
171    fn bytes_read(&self) -> u64 {
172        match self {
173            Self::Bytes(source) => source.bytes_read(),
174            Self::File(source) => source.bytes_read(),
175        }
176    }
177}