diff --git a/.gitignore b/.gitignore index ea8c4bf..06aba01 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +Cargo.lock /target diff --git a/Cargo.toml b/Cargo.toml index 5b4d769..29f594d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,4 @@ name = "ufotofu_queues" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..9bd65c7 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ufotofu_queues-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +arbitrary = { version = "1", features = ["derive"] } +libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] } + +[dependencies.ufotofu_queues] +path = ".." + +[[bin]] +name = "fixed_bulk" +path = "fuzz_targets/fixed_bulk.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fixed_enqueue_dequeue" +path = "fuzz_targets/fixed_enqueue_dequeue.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/fixed_bulk.rs b/fuzz/fuzz_targets/fixed_bulk.rs new file mode 100644 index 0000000..3403350 --- /dev/null +++ b/fuzz/fuzz_targets/fixed_bulk.rs @@ -0,0 +1,85 @@ +#![no_main] + +use core::mem::MaybeUninit; +use core::slice; + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; + +use std::collections::VecDeque; + +use ufotofu_queues::fixed::Fixed; +use ufotofu_queues::Queue; + +#[derive(Debug, Arbitrary)] +enum Operation { + Enqueue(T), + Dequeue, + BulkEnqueue(Vec), + BulkDequeue(u8), +} + +fn maybe_uninit_slice_mut(slice: &mut [T]) -> &mut [MaybeUninit] { + let ptr = slice.as_mut_ptr().cast::>(); + unsafe { slice::from_raw_parts_mut(ptr, slice.len()) } +} + +fuzz_target!(|data: (Vec>, usize)| { + let operations = data.0; + let capacity = data.1; + + // Restrict capacity to between 1 and 2048 bytes (inclusive). + if capacity < 1 || capacity > 2048 { + return; + } + + let mut control = VecDeque::new(); + let mut test = Fixed::new(capacity); + + for operation in operations { + match operation { + Operation::Enqueue(item) => { + let control_result = if control.len() >= capacity { + Some(item) + } else { + control.push_back(item.clone()); + None + }; + let test_result = test.enqueue(item.clone()); + assert_eq!(test_result, control_result); + } + Operation::Dequeue => { + let control_result = control.pop_front(); + let test_result = test.dequeue(); + assert_eq!(test_result, control_result); + } + Operation::BulkEnqueue(items) => { + let amount = test.bulk_enqueue(&items); + for (count, item) in items.iter().enumerate() { + if count >= amount { + break; + } else { + control.push_back(item.clone()); + } + } + } + Operation::BulkDequeue(n) => { + let n = n as usize; + if n > 0 { + let mut control_buffer = vec![]; + let mut test_buffer = vec![]; + test_buffer.resize(n, 0_u8); + + let test_amount = test.bulk_dequeue(maybe_uninit_slice_mut(&mut test_buffer)); + for _ in 0..test_amount { + if let Some(item) = control.pop_front() { + control_buffer.push(item.clone()); + } + } + + assert_eq!(&test_buffer[..test_amount], &control_buffer[..test_amount]); + } + } + } + } +}); diff --git a/fuzz/fuzz_targets/fixed_enqueue_dequeue.rs b/fuzz/fuzz_targets/fixed_enqueue_dequeue.rs new file mode 100644 index 0000000..4a405ac --- /dev/null +++ b/fuzz/fuzz_targets/fixed_enqueue_dequeue.rs @@ -0,0 +1,47 @@ +#![no_main] +use std::collections::VecDeque; + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; + +use ufotofu_queues::fixed::Fixed; +use ufotofu_queues::Queue; + +#[derive(Debug, Arbitrary)] +enum Operation { + Enqueue(T), + Dequeue, +} + +fuzz_target!(|data: (Vec>, usize)| { + let operations = data.0; + let capacity = data.1; + + // Restrict capacity to between 1 and 2048 bytes (inclusive). + if capacity < 1 || capacity > 2048 { + return; + } + + let mut control = VecDeque::new(); + let mut test = Fixed::new(capacity); + + for operation in operations { + match operation { + Operation::Enqueue(item) => { + let control_result = if control.len() >= capacity { + Some(item) + } else { + control.push_back(item.clone()); + None + }; + let test_result = test.enqueue(item.clone()); + assert_eq!(test_result, control_result); + } + Operation::Dequeue => { + let control_result = control.pop_front(); + let test_result = test.dequeue(); + assert_eq!(test_result, control_result); + } + } + } +}); diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/src/fixed.rs b/src/fixed.rs new file mode 100644 index 0000000..5953bdf --- /dev/null +++ b/src/fixed.rs @@ -0,0 +1,257 @@ +extern crate alloc; + +use alloc::alloc::{Allocator, Global}; +use alloc::boxed::Box; + +use core::mem::MaybeUninit; + +use crate::Queue; + +/// A queue holding up to a certain number of items. The capacity is set upon +/// creation and remains fixed. Performs a single heap allocation on creation. +#[derive(Debug)] +pub struct Fixed { + /// Slice of memory. + data: Box<[MaybeUninit], A>, + /// Read index. + read: usize, + /// Amount of valid data. + amount: usize, +} + +impl Fixed { + pub fn new(capacity: usize) -> Self { + Fixed { + data: Box::new_uninit_slice(capacity), + read: 0, + amount: 0, + } + } +} + +impl Fixed { + pub fn new_in(capacity: usize, alloc: A) -> Self { + Fixed { + data: Box::new_uninit_slice_in(capacity, alloc), + read: 0, + amount: 0, + } + } +} + +impl Fixed { + fn is_data_contiguous(&self) -> bool { + self.read + self.amount < self.capacity() + } + + /// Return a readable slice from the queue. + fn readable_slice(&mut self) -> &[MaybeUninit] { + if self.is_data_contiguous() { + &self.data[self.read..self.write_to()] + } else { + &self.data[self.read..] + } + } + + /// Return a writeable slice from the queue. + fn writeable_slice(&mut self) -> &mut [MaybeUninit] { + let capacity = self.capacity(); + if self.is_data_contiguous() { + &mut self.data[self.read + self.amount..capacity] + } else { + &mut self.data[(self.read + self.amount) % capacity..self.read] + } + } + + pub fn capacity(&self) -> usize { + self.data.len() + } + + fn write_to(&self) -> usize { + (self.read + self.amount) % self.capacity() + } +} + +impl Queue for Fixed { + type Item = T; + + /// Return the amount of items in the queue. + fn amount(&self) -> usize { + self.amount + } + + /// Attempt to enqueue the next item. + /// + /// Will return the item if the queue is full at the time of calling. + fn enqueue(&mut self, item: T) -> Option { + if self.amount == self.capacity() { + Some(item) + } else { + self.data[self.write_to()].write(item); + self.amount += 1; + + None + } + } + + /// Expose a non-empty slice of memory for the client code to fill with items that should + /// be enqueued. + /// + /// Will return `None` if the queue is full at the time of calling. + fn enqueue_slots(&mut self) -> Option<&mut [MaybeUninit]> { + if self.amount == self.capacity() { + None + } else { + Some(self.writeable_slice()) + } + } + + /// Inform the queue that `amount` many items have been written to the first `amount` + /// indices of the `enqueue_slots` it has most recently exposed. + /// + /// #### Invariants + /// + /// Callers must have written into (at least) the `amount` many first `enqueue_slots` that + /// were most recently exposed. Failure to uphold this invariant may cause undefined behavior. + /// + /// #### Safety + /// + /// The queue will assume the first `amount` many `enqueue_slots` that were most recently + /// exposed to contain initialized memory after this call, even if the memory it exposed was + /// originally uninitialized. Violating the invariants will cause the queue to read undefined + /// memory, which triggers undefined behavior. + unsafe fn did_enqueue(&mut self, amount: usize) { + self.amount += amount; + } + + /// Attempt to dequeue the next item. + /// + /// Will return `None` if the queue is empty at the time of calling. + fn dequeue(&mut self) -> Option { + if self.amount == 0 { + None + } else { + let previous_read = self.read; + // Advance the read index by 1 or reset to 0 if at capacity. + self.read = (self.read + 1) % self.capacity(); + self.amount -= 1; + + Some(unsafe { self.data[previous_read].assume_init() }) + } + } + + /// Expose a non-empty slice of items to be dequeued. + /// + /// Will return `None` if the queue is empty at the time of calling. + fn dequeue_slots(&mut self) -> Option<&[T]> { + if self.amount == 0 { + None + } else { + Some(unsafe { MaybeUninit::slice_assume_init_ref(self.readable_slice()) }) + } + } + + /// Mark `amount` many items as having been dequeued. + /// + /// #### Invariants + /// + /// Callers must not mark items as dequeued that had not previously been exposed by + /// `dequeue_slots`. + fn did_dequeue(&mut self, amount: usize) { + self.read = (self.read + amount) % self.capacity(); + self.amount -= amount; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn enqueues_and_dequeues_with_correct_amount() { + let mut queue: Fixed = Fixed::new(4); + + assert_eq!(queue.enqueue(7), None); + assert_eq!(queue.enqueue(21), None); + assert_eq!(queue.enqueue(196), None); + assert_eq!(queue.amount(), 3); + + assert_eq!(queue.enqueue(233), None); + assert_eq!(queue.amount(), 4); + + // Queue should be first-in, first-out. + assert_eq!(queue.dequeue(), Some(7)); + assert_eq!(queue.amount(), 3); + } + + #[test] + fn bulk_enqueues_and_dequeues_with_correct_amount() { + let mut queue: Fixed = Fixed::new(4); + let mut buf: [MaybeUninit; 4] = MaybeUninit::uninit_array(); + + let enqueue_amount = queue.bulk_enqueue(b"ufo"); + let dequeue_amount = queue.bulk_dequeue(&mut buf); + + assert_eq!(enqueue_amount, dequeue_amount); + } + + #[test] + fn returns_item_on_enqueue_when_queue_is_full() { + let mut queue: Fixed = Fixed::new(1); + + assert_eq!(queue.enqueue(7), None); + + assert_eq!(queue.enqueue(0), Some(0)) + } + + #[test] + fn returns_none_on_dequeue_when_queue_is_empty() { + let mut queue: Fixed = Fixed::new(1); + + // Enqueue and then dequeue an item. + let _ = queue.enqueue(7); + let _ = queue.dequeue(); + + // The queue is now empty. + assert!(queue.dequeue().is_none()); + } + + #[test] + fn returnes_none_on_enqueue_slots_when_none_are_available() { + // Create a fixed queue that exposes four slots. + let mut queue: Fixed = Fixed::new(4); + + // Copy data to two of the available slots and call `did_enqueue`. + let data = b"tofu"; + let slots = queue.enqueue_slots().unwrap(); + MaybeUninit::copy_from_slice(&mut slots[0..2], &data[0..2]); + unsafe { + queue.did_enqueue(2); + } + + // Copy data to two of the available slots and call `did_enqueue`. + let slots = queue.enqueue_slots().unwrap(); + MaybeUninit::copy_from_slice(&mut slots[0..2], &data[0..2]); + unsafe { + queue.did_enqueue(2); + } + + // Make a third call to `enqueue_slots` after all available slots have been used. + assert!(queue.enqueue_slots().is_none()); + } + + #[test] + fn returns_none_on_dequeue_slots_when_none_are_available() { + // Create a fixed queue that exposes four slots. + let mut queue: Fixed = Fixed::new(4); + + let data = b"tofu"; + let _amount = queue.bulk_enqueue(data); + + let _slots = queue.dequeue_slots().unwrap(); + queue.did_dequeue(4); + + // Make a second call to `dequeue_slots` after all available slots have been used. + assert!(queue.dequeue_slots().is_none()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d12d9a..eb0ce7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,114 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +#![no_std] +#![feature(allocator_api)] +#![feature(maybe_uninit_slice)] +#![feature(maybe_uninit_uninit_array)] +#![feature(maybe_uninit_write_slice)] +#![feature(new_uninit)] + +pub mod fixed; + +use core::cmp::min; +use core::mem::MaybeUninit; + +pub trait Queue { + type Item: Copy; + + /// Return the amount of items in the queue. + fn amount(&self) -> usize; + + /// Attempt to enqueue the next item. + /// + /// Will return the item if the queue is full at the time of calling. + fn enqueue(&mut self, item: Self::Item) -> Option; + + /// Expose a non-empty slice of memory for the client code to fill with items that should + /// be enqueued. + /// + /// Will return `None` if the queue is full at the time of calling. + fn enqueue_slots(&mut self) -> Option<&mut [MaybeUninit]>; + + /// Inform the queue that `amount` many items have been written to the first `amount` + /// indices of the `enqueue_slots` it has most recently exposed. The semantics must be + /// equivalent to those of `enqueue` being called `amount` many times with exactly those + /// items. + /// + /// #### Invariants + /// + /// Callers must have written into (at least) the `amount` many first `enqueue_slots` that + /// were most recently exposed. Failure to uphold this invariant may cause undefined behavior. + /// + /// #### Safety + /// + /// Callers may assume the first `amount` many `enqueue_slots` that were most recently + /// exposed to contain initialized memory after this call, even if the memory it exposed was + /// originally uninitialized. Violating the invariants can cause the queue to read undefined + /// memory, which triggers undefined behavior. + unsafe fn did_enqueue(&mut self, amount: usize); + + /// Enqueue a non-zero number of items by reading them from a given buffer and returning how + /// many items were enqueued. + /// + /// Will return `0` if the queue is full at the time of calling. + /// + /// #### Implementation Notes + /// + /// The default implementation orchestrates `enqueue_slots` and `did_enqueue` in a + /// straightforward manner. Only provide your own implementation if you can do better + /// than that. + fn bulk_enqueue(&mut self, buffer: &[Self::Item]) -> usize { + match self.enqueue_slots() { + None => 0, + Some(slots) => { + let amount = min(slots.len(), buffer.len()); + MaybeUninit::copy_from_slice(&mut slots[..amount], &buffer[..amount]); + unsafe { + self.did_enqueue(amount); + } + + amount + } + } + } + + /// Attempt to dequeue the next item. + /// + /// Will return `None` if the queue is empty at the time of calling. + fn dequeue(&mut self) -> Option; + + /// Expose a non-empty slice of items to be dequeued (or an error). + /// The items in the slice must not have been emitted by `dequeue` before. + /// + /// Will return `None` if the queue is empty at the time of calling. + fn dequeue_slots(&mut self) -> Option<&[Self::Item]>; + + /// Mark `amount` many items as having been dequeued. Future calls to `dequeue` and to + /// `dequeue_slots` must act as if `dequeue` had been called `amount` many times. + /// + /// #### Invariants + /// + /// Callers must not mark items as dequeued that had not previously been exposed by `dequeue_slots`. + fn did_dequeue(&mut self, amount: usize); -#[cfg(test)] -mod tests { - use super::*; + /// Dequeue a non-zero number of items by writing them into a given buffer and returning how + /// many items were dequeued. + /// + /// Will return `0` if the queue is empty at the time of calling. + /// + /// #### Implementation Notes + /// + /// The default implementation orchestrates `dequeue_slots` and `did_dequeue` in a + /// straightforward manner. Only provide your own implementation if you can do better + /// than that. + fn bulk_dequeue(&mut self, buffer: &mut [MaybeUninit]) -> usize { + match self.dequeue_slots() { + None => 0, + Some(slots) => { + let amount = min(slots.len(), buffer.len()); + MaybeUninit::copy_from_slice(&mut buffer[..amount], &slots[..amount]); + self.did_dequeue(amount); - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + amount + } + } } }