commonware-runtime 0.0.62

Execute asynchronous tasks with a configurable scheduler.
Documentation
use std::num::NonZeroUsize;

/// A buffer for caching data written to the tip of a blob.
///
/// The buffer always represents data at the "tip" of the logical blob, starting at `offset` and
/// extending for `data.len()` bytes.
pub(super) struct Buffer {
    /// The data to be written to the blob.
    pub(super) data: Vec<u8>,

    /// The offset in the blob where the buffered data starts.
    ///
    /// This represents the logical position in the blob where `data[0]` would be written. The
    /// buffer is maintained at the "tip" to support efficient size calculation and appends.
    pub(super) offset: u64,

    /// The maximum size of the buffer.
    pub(super) capacity: usize,
}

impl Buffer {
    /// Creates a new buffer with the provided `size` and `capacity`.
    pub(super) fn new(size: u64, capacity: NonZeroUsize) -> Self {
        Self {
            data: Vec::with_capacity(capacity.get()),
            offset: size,
            capacity: capacity.get(),
        }
    }

    /// Returns the current logical size of the blob including any buffered data.
    pub(super) fn size(&self) -> u64 {
        self.offset + self.data.len() as u64
    }

    /// Returns true if the buffer is empty.
    pub(super) fn is_empty(&self) -> bool {
        self.data.is_empty()
    }

    /// Adjust the buffer to correspond to resizing the logical blob to size `len`.
    ///
    /// If the new size is greater than the current size, the existing buffer is returned (to be
    /// flushed to the underlying blob) and the buffer is reset to the empty state with an updated
    /// offset positioned at the end of the logical blob. (The "existing buffer" is what would have
    /// been returned by a call to [Self::take].)
    ///
    /// If the new size is less than the current size (but still greater than current offset), the
    /// buffer is truncated to the new size.
    ///
    /// If the new size is less than the current offset, the buffer is reset to the empty state with
    /// an updated offset positioned at the end of the logical blob.
    pub(super) fn resize(&mut self, len: u64) -> Option<(Vec<u8>, u64)> {
        // Handle case where the buffer is empty.
        if self.is_empty() {
            self.offset = len;
            return None;
        }

        // Handle case where there is some data in the buffer.
        if len >= self.size() {
            let previous = (
                std::mem::replace(&mut self.data, Vec::with_capacity(self.capacity)),
                self.offset,
            );
            self.offset = len;
            Some(previous)
        } else if len >= self.offset {
            self.data.truncate((len - self.offset) as usize);
            None
        } else {
            self.data.clear();
            self.offset = len;
            None
        }
    }

    /// Returns the buffered data and its blob offset, or returns `None` if the buffer is
    /// already empty.
    ///
    /// The buffer is reset to the empty state with an updated offset positioned at
    /// the end of the logical blob.
    pub(super) fn take(&mut self) -> Option<(Vec<u8>, u64)> {
        if self.is_empty() {
            return None;
        }
        let buf = std::mem::replace(&mut self.data, Vec::with_capacity(self.capacity));
        let offset = self.offset;
        self.offset += buf.len() as u64;
        Some((buf, offset))
    }

    /// Extract and return any data from the blob range `[offset,offset+buf.len)` that is contained
    /// in the buffer, returning the number of bytes that could not be extracted. (Any bytes
    /// that could not be extracted must reside at the beginning of the range.)
    ///
    /// # Panics
    ///
    /// Panics if the end offset of the requested data falls outside the range of the logical blob.
    pub(super) fn extract(&self, buf: &mut [u8], offset: u64) -> usize {
        let end_offset = offset
            .checked_add(buf.len() as u64)
            .expect("end_offset overflow");
        assert!(end_offset <= self.size());
        if end_offset <= self.offset {
            // Range does not overlap with the buffer.
            return buf.len();
        }

        let (start, remaining) = if offset < self.offset {
            // Some data is before the buffer.
            (0, (self.offset - offset) as usize)
        } else {
            // Can read entirely from the buffer.
            ((offset - self.offset) as usize, 0)
        };

        let end = start + buf.len() - remaining;
        assert!(end <= self.data.len());

        // Copy the requested buffered data into the appropriate part of the user-provided slice.
        buf[remaining..].copy_from_slice(&self.data[start..end]);

        remaining
    }

    /// Merges the provided `data` into the buffer at the provided blob `offset` if it falls
    /// entirely within the range `[buffer.offset,buffer.offset+capacity)`.
    ///
    /// The buffer will be expanded if necessary to accommodate the new data, and any gaps that
    /// may result are filled with zeros. Returns `true` if the merge was performed, otherwise the
    /// caller is responsible for continuing to manage the data.
    pub(super) fn merge(&mut self, data: &[u8], offset: u64) -> bool {
        let end_offset = offset
            .checked_add(data.len() as u64)
            .expect("end_offset overflow");
        let can_merge_into_buffer =
            offset >= self.offset && end_offset <= self.offset + self.capacity as u64;
        if !can_merge_into_buffer {
            return false;
        }
        let start = (offset - self.offset) as usize;
        let end = start + data.len();

        // Expand buffer if necessary (fills with zeros).
        if end > self.data.len() {
            self.data.resize(end, 0);
        }

        // Copy the provided data into the buffer.
        self.data[start..end].copy_from_slice(data.as_ref());

        true
    }

    /// Appends the provided `data` to the buffer, and returns `true` if the buffer is now above
    /// capacity. If above capacity, the caller is responsible for using `take` to bring it back
    /// under.
    pub(super) fn append(&mut self, data: &[u8]) -> bool {
        self.data.extend_from_slice(data);
        self.data.len() > self.capacity
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use commonware_utils::NZUsize;

    #[test]
    fn test_tip_append() {
        let mut buffer = Buffer::new(50, NZUsize!(100));
        assert_eq!(buffer.size(), 50);
        assert!(buffer.is_empty());
        assert_eq!(buffer.take(), None);

        // Add some data to the buffer.
        assert!(!buffer.append(&[1, 2, 3]));
        assert_eq!(buffer.size(), 53);
        assert!(!buffer.is_empty());

        // Confirm `take()` works as intended.
        assert_eq!(buffer.take(), Some((vec![1, 2, 3], 50)));
        assert_eq!(buffer.size(), 53);
        assert_eq!(buffer.take(), None);

        // Fill the buffer to capacity.
        let mut buf = vec![42; 100];
        assert!(!buffer.append(&buf));
        assert_eq!(buffer.size(), 153);

        // Add one more byte, which should push it over capacity. The byte should still be appended.
        assert!(buffer.append(&[43]));
        assert_eq!(buffer.size(), 154);
        buf.push(43);
        assert_eq!(buffer.take(), Some((buf, 53)));
    }

    #[test]
    fn test_tip_resize() {
        let mut buffer = Buffer::new(50, NZUsize!(100));
        buffer.append(&[1, 2, 3]);
        assert_eq!(buffer.size(), 53);

        // Resize the buffer to correspond to a blob resized to size 60. The returned buffer should
        // match exactly what we'd expect to be returned by `take` since 60 is greater than the
        // current size of 53.
        assert_eq!(buffer.resize(60), Some((vec![1, 2, 3], 50)));
        assert_eq!(buffer.size(), 60);
        assert_eq!(buffer.take(), None);

        buffer.append(&[4, 5, 6]);
        assert_eq!(buffer.size(), 63);

        // Resize the buffer down to size 61.
        assert_eq!(buffer.resize(61), None);
        assert_eq!(buffer.size(), 61);
        assert_eq!(buffer.take(), Some((vec![4], 60)));
        assert_eq!(buffer.size(), 61);

        buffer.append(&[7, 8, 9]);

        // Resize the buffer prior to the current offset of 61. This should simply reset the buffer
        // at the new size.
        assert_eq!(buffer.resize(59), None);
        assert_eq!(buffer.size(), 59);
        assert_eq!(buffer.take(), None);
        assert_eq!(buffer.size(), 59);
    }
}