commonware_runtime/utils/buffer/read.rs
1use crate::{Blob, Error, IoBuf, IoBufMut, IoBufs, ReadOptions};
2use std::num::NonZeroUsize;
3
4/// A reader that buffers content from a [Blob] to optimize the performance
5/// of a full scan of contents.
6///
7/// # Allocation Semantics
8///
9/// - The internal read buffer is allocated lazily on the first refill.
10/// - Refills try to reclaim mutable ownership of that same backing allocation.
11/// - If backing is still shared (for example, previously returned slices are alive), a
12/// replacement is allocated and existing backing is left alive until all aliases drop.
13/// - [Self::read] returns zero-copy slices into refill buffers. Holding those slices may
14/// force allocation on subsequent refills.
15///
16/// # Example
17///
18/// ```
19/// use commonware_utils::NZUsize;
20/// use commonware_runtime::{
21/// Blob, Error, Runner, Storage, WriteOptions, buffer::Read, deterministic,
22/// };
23///
24/// let executor = deterministic::Runner::default();
25/// executor.start(|context| async move {
26/// // Open a blob and add some data (e.g., a journal file)
27/// let (blob, size) = context.open("my_partition", b"my_data").await.expect("unable to open blob");
28/// let data = b"Hello, world! This is a test.".to_vec();
29/// let size = data.len() as u64;
30/// blob.write_at(0, data, WriteOptions::default()).await.expect("unable to write data");
31///
32/// // Create a buffer
33/// let buffer = 64 * 1024;
34/// let mut reader = Read::new(blob, size, NZUsize!(buffer));
35///
36/// // Read data sequentially
37/// let header = reader.read(16).await.expect("unable to read data");
38/// println!("Read header: {:?}", header.coalesce().as_ref());
39///
40/// // Position is still at 16 (after header)
41/// assert_eq!(reader.position(), 16);
42/// });
43/// ```
44pub struct Read<B: Blob> {
45 /// The underlying blob to read from.
46 blob: B,
47 /// The buffer storing the data read from the blob.
48 buffer: IoBuf,
49 /// The current position in the blob from where the buffer was filled.
50 blob_position: u64,
51 /// The size of the blob.
52 blob_size: u64,
53 /// The current position within the buffer for reading.
54 buffer_position: usize,
55 /// The valid data length in the buffer.
56 buffer_valid_len: usize,
57 /// The maximum size of the buffer.
58 buffer_size: usize,
59}
60
61impl<B: Blob> Read<B> {
62 /// Creates a new `Read` that reads from the given blob with the specified buffer size.
63 pub fn new(blob: B, blob_size: u64, buffer_size: NonZeroUsize) -> Self {
64 Self {
65 blob,
66 // The first refill allocates the backing buffer.
67 buffer: IoBuf::default(),
68 blob_position: 0,
69 blob_size,
70 buffer_position: 0,
71 buffer_valid_len: 0,
72 buffer_size: buffer_size.get(),
73 }
74 }
75
76 /// Returns how many valid bytes are remaining in the buffer.
77 pub const fn buffer_remaining(&self) -> usize {
78 self.buffer_valid_len - self.buffer_position
79 }
80
81 /// Returns how many bytes remain in the blob from the current position.
82 pub const fn blob_remaining(&self) -> u64 {
83 self.blob_size
84 .saturating_sub(self.blob_position + self.buffer_position as u64)
85 }
86
87 /// Returns the number of bytes in the blob, as provided at construction.
88 pub const fn blob_size(&self) -> u64 {
89 self.blob_size
90 }
91
92 /// Refills the buffer from the blob starting at the current blob position.
93 /// Returns the number of bytes read or an error if the read failed.
94 async fn refill(&mut self) -> Result<usize, Error> {
95 // Update blob position to account for consumed bytes
96 self.blob_position += self.buffer_position as u64;
97 self.buffer_position = 0;
98 self.buffer_valid_len = 0;
99
100 // Calculate how many bytes remain in the blob
101 let blob_remaining = self.blob_size.saturating_sub(self.blob_position);
102 if blob_remaining == 0 {
103 return Err(Error::BlobInsufficientLength);
104 }
105
106 // Calculate how much to read (minimum of buffer size and remaining bytes)
107 let bytes_to_read = std::cmp::min(self.buffer_size as u64, blob_remaining) as usize;
108
109 // Reuse the existing allocation when it is uniquely owned and large enough. Otherwise
110 // allocate a replacement. Slices that readers still hold keep the old memory alive.
111 let current = std::mem::take(&mut self.buffer);
112 let buf = match current.try_into_mut() {
113 Ok(mut reusable) if reusable.capacity() >= bytes_to_read => {
114 reusable.clear();
115 reusable
116 }
117 _ => IoBufMut::with_capacity(bytes_to_read),
118 };
119 let read_result = self
120 .blob
121 .read_at_buf(
122 self.blob_position,
123 bytes_to_read,
124 buf,
125 ReadOptions::default(),
126 )
127 .await?;
128 self.buffer = read_result.coalesce().freeze();
129 self.buffer_valid_len = self.buffer.len();
130
131 Ok(self.buffer_valid_len)
132 }
133
134 /// Reads exactly `len` bytes and returns them as immutable bytes.
135 ///
136 /// Returned bytes are composed of zero-copy slices from the internal read buffer.
137 /// Holding returned slices can keep the current backing shared, which may require
138 /// allocation on later refills.
139 ///
140 /// Returns an error if not enough bytes are available.
141 pub async fn read(&mut self, len: usize) -> Result<IoBufs, Error> {
142 if len == 0 {
143 return Ok(IoBufs::default());
144 }
145
146 // Quick check against total remaining bytes at current position.
147 if self.blob_remaining() < len as u64 {
148 return Err(Error::BlobInsufficientLength);
149 }
150
151 // Read until we have enough bytes
152 let mut remaining = len;
153 let mut out = IoBufs::default();
154 while remaining > 0 {
155 // Check if we need to refill
156 if self.buffer_position >= self.buffer_valid_len {
157 self.refill().await?;
158 }
159
160 // Calculate how many bytes we can take from the buffer
161 let bytes_to_take = std::cmp::min(remaining, self.buffer_remaining());
162
163 // Append bytes from buffer to output
164 out.append(
165 self.buffer
166 .slice(self.buffer_position..(self.buffer_position + bytes_to_take)),
167 );
168
169 self.buffer_position += bytes_to_take;
170 remaining -= bytes_to_take;
171 }
172
173 Ok(out)
174 }
175
176 /// Returns the current absolute position in the blob.
177 pub const fn position(&self) -> u64 {
178 self.blob_position + self.buffer_position as u64
179 }
180
181 /// Repositions the buffer to read from the specified position in the blob.
182 pub const fn seek_to(&mut self, position: u64) -> Result<(), Error> {
183 // Check if the seek position is valid
184 if position > self.blob_size {
185 return Err(Error::BlobInsufficientLength);
186 }
187
188 // Check if the position is within the current buffer
189 let buffer_start = self.blob_position;
190 let buffer_end = self.blob_position + self.buffer_valid_len as u64;
191
192 if position >= buffer_start && position < buffer_end {
193 // Position is within the current buffer, adjust buffer_position
194 self.buffer_position = (position - self.blob_position) as usize;
195 } else {
196 // Position is outside the current buffer, reset buffer state
197 self.blob_position = position;
198 self.buffer_position = 0;
199 self.buffer_valid_len = 0;
200 }
201
202 Ok(())
203 }
204
205 /// Resizes the blob to the specified len and syncs the blob.
206 ///
207 /// This may be useful if reading some blob after unclean shutdown.
208 pub async fn resize(self, len: u64) -> Result<(), Error> {
209 self.blob.resize(len).await?;
210 self.blob.sync().await
211 }
212}