Skip to main content

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}