use commonware_macros::stability_scope;
stability_scope!(BETA, cfg(not(target_arch = "wasm32")) {
use crate::{BlobVersion, Error};
use std::{
fs::File,
io::{Read as _, Seek as _, SeekFrom},
ops::RangeInclusive,
path::Path,
};
pub(crate) fn sync(dir: &std::path::Path) -> std::io::Result<()> {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
use std::os::fd::AsRawFd;
let file = std::fs::File::open(dir)?;
if unsafe { libc::syncfs(file.as_raw_fd()) } == -1 {
return Err(std::io::Error::last_os_error());
}
tracing::debug!(
storage_directory = %dir.display(),
"made storage filesystem durable at startup (syncfs)"
);
Ok(())
} else {
unsafe { libc::sync() };
tracing::debug!(
storage_directory = %dir.display(),
"best-effort storage flush at startup (sync(); not a crash-durability guarantee)"
);
Ok(())
}
}
}
pub(crate) fn sync_dir(path: &Path) -> Result<(), Error> {
let dir = File::open(path).map_err(|e| {
Error::BlobOpenFailed(
path.to_string_lossy().to_string(),
"directory".to_string(),
e.into(),
)
})?;
dir.sync_all().map_err(|e| {
Error::BlobSyncFailed(
path.to_string_lossy().to_string(),
"directory".to_string(),
e.into(),
)
})
}
pub(crate) fn resolve_header(
file: &mut File,
raw_len: u64,
layouts: &RangeInclusive<Layout>,
versions: &RangeInclusive<BlobVersion>,
partition: &str,
name: &[u8],
) -> Result<Option<(u64, BlobVersion, u64)>, Error> {
let mut raw = vec![0u8; Header::resolve_len(raw_len)];
file.seek(SeekFrom::Start(0))
.map_err(|_| Error::ReadFailed)?;
file.read_exact(&mut raw).map_err(|_| Error::ReadFailed)?;
header::resolve(&raw, raw_len, layouts, versions, partition, name)
}
pub(crate) mod hold;
});
stability_scope!(ALPHA {
pub mod audited;
pub mod faulty;
pub mod memory;
});
stability_scope!(ALPHA, cfg(feature = "iouring-storage") {
pub mod iouring;
});
stability_scope!(BETA, cfg(all(not(target_arch = "wasm32"), not(feature = "iouring-storage"))) {
pub mod tokio;
});
stability_scope!(BETA {
pub mod metered;
mod header;
pub(crate) use crate::BlobLayout as Layout;
pub(crate) use header::Header;
pub fn validate_partition_name(partition: &str) -> Result<(), crate::Error> {
if partition.is_empty()
|| partition
.chars()
.any(|c| !(c.is_ascii_alphanumeric() || ['_', '-'].contains(&c)))
{
return Err(crate::Error::PartitionNameInvalid(partition.into()));
}
Ok(())
}
});
#[cfg(test)]
pub(crate) mod tests {
pub(crate) use super::header::tests::v0_blob_bytes;
use crate::{
Blob, BlobVersion, Buf, IoBuf, IoBufMut, IoBufs, IoBufsMut, ReadOptions, Storage,
WriteOptions,
};
use futures::FutureExt;
pub(crate) async fn run_storage_tests<S>(storage: S)
where
S: Storage + Send + Sync + 'static,
S::Blob: Send + Sync,
{
test_open_and_write(&storage).await;
test_remove(&storage).await;
test_read_after_remove_blob(&storage).await;
test_read_after_remove_partition(&storage).await;
test_recreate_after_remove(&storage).await;
test_read_after_remove_unsynced(&storage).await;
test_read_after_remove_handle_clones(&storage).await;
test_recreate_generations(&storage).await;
test_read_after_remove_partition_multi(&storage).await;
test_scan(&storage).await;
test_concurrent_access(&storage).await;
test_large_data(&storage).await;
test_overwrite_data(&storage).await;
test_read_beyond_bound(&storage).await;
test_write_at_large_offset(&storage).await;
test_write_at_sync(&storage).await;
test_start_sync(&storage).await;
test_append_data(&storage).await;
test_vectored_write_at(&storage).await;
test_vectored_write_at_large_offset(&storage).await;
test_sequential_read_write(&storage).await;
test_sequential_chunk_read_write(&storage).await;
test_read_empty_blob(&storage).await;
test_overlapping_writes(&storage).await;
test_resize_then_open(&storage).await;
test_partition_name_validation(&storage).await;
test_blob_version_mismatch(&storage).await;
test_aligned_layout(&storage).await;
test_read_zero_length(&storage).await;
test_read_at_buf_returns_same_buffer(&storage).await;
test_read_at_buf_insufficient_capacity(&storage).await;
test_read_at_buf_larger_capacity(&storage).await;
test_read_options(&storage).await;
}
async fn test_open_and_write<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, len) = storage.open("partition", b"test_blob").await.unwrap();
assert_eq!(len, 0);
blob.write_at(0, b"hello world", WriteOptions::default())
.await
.unwrap();
let read = blob.read_at(0, 11, ReadOptions::default()).await.unwrap();
assert_eq!(
read.coalesce(),
b"hello world",
"Blob content does not match expected value"
);
}
async fn test_remove<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
storage.open("partition", b"test_blob").await.unwrap();
storage
.remove("partition", Some(b"test_blob"))
.await
.unwrap();
let blobs = storage.scan("partition").await.unwrap();
assert!(blobs.is_empty(), "Blob was not removed as expected");
}
async fn test_read_after_remove_blob<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage.open("read_after_remove", b"by_name").await.unwrap();
let data: Vec<u8> = (0u8..=255).collect();
blob.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
storage
.remove("read_after_remove", Some(b"by_name"))
.await
.unwrap();
let blobs = storage.scan("read_after_remove").await.unwrap();
assert!(blobs.is_empty(), "Blob was not removed as expected");
let read = blob
.read_at(0, data.len(), ReadOptions::default())
.await
.unwrap();
assert_eq!(
read.coalesce().as_ref(),
data.as_slice(),
"open handle must remain readable after blob removal"
);
}
async fn test_read_after_remove_partition<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("read_after_remove_partition", b"victim")
.await
.unwrap();
let data: Vec<u8> = (0u8..=255).rev().collect();
blob.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
storage
.remove("read_after_remove_partition", None)
.await
.unwrap();
let read = blob
.read_at(0, data.len(), ReadOptions::default())
.await
.unwrap();
assert_eq!(
read.coalesce().as_ref(),
data.as_slice(),
"open handle must remain readable after partition removal"
);
}
async fn test_recreate_after_remove<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (old, _) = storage
.open("recreate_after_remove", b"name")
.await
.unwrap();
old.write_at(0, b"old contents", WriteOptions::default())
.await
.unwrap();
old.sync().await.unwrap();
storage
.remove("recreate_after_remove", Some(b"name"))
.await
.unwrap();
let (new, len) = storage
.open("recreate_after_remove", b"name")
.await
.unwrap();
assert_eq!(len, 0, "recreated blob must start empty");
new.write_at(0, b"new contents", WriteOptions::default())
.await
.unwrap();
new.sync().await.unwrap();
let old_read = old.read_at(0, 12, ReadOptions::default()).await.unwrap();
assert_eq!(
old_read.coalesce().as_ref(),
b"old contents",
"pre-removal handle must keep observing the removed blob"
);
let new_read = new.read_at(0, 12, ReadOptions::default()).await.unwrap();
assert_eq!(new_read.coalesce().as_ref(), b"new contents");
}
async fn test_read_after_remove_unsynced<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("read_after_remove_unsynced", b"name")
.await
.unwrap();
let data: Vec<u8> = (0u8..=255).cycle().take(64 * 1024).collect();
blob.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
let read = blob.read_at(0, 16, ReadOptions::default()).await.unwrap();
assert_eq!(read.coalesce().as_ref(), &data[..16]);
storage
.remove("read_after_remove_unsynced", Some(b"name"))
.await
.unwrap();
let read = blob
.read_at(0, data.len(), ReadOptions::default())
.await
.unwrap();
assert_eq!(
read.coalesce().as_ref(),
data.as_slice(),
"unsynced bytes must remain readable after removal"
);
let read = blob
.read_at(data.len() as u64 - 1, 1, ReadOptions::default())
.await
.unwrap();
assert_eq!(read.coalesce().as_ref(), &data[data.len() - 1..]);
}
async fn test_read_after_remove_handle_clones<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (first, _) = storage
.open("read_after_remove_clones", b"name")
.await
.unwrap();
let data: Vec<u8> = (0u8..=255).collect();
first
.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
first.sync().await.unwrap();
let second = first.clone();
let (independent, _) = storage
.open("read_after_remove_clones", b"name")
.await
.unwrap();
storage
.remove("read_after_remove_clones", Some(b"name"))
.await
.unwrap();
let third = first.clone();
drop(first);
for handle in [&second, &third, &independent] {
let read = handle
.read_at(0, data.len(), ReadOptions::default())
.await
.unwrap();
assert_eq!(read.coalesce().as_ref(), data.as_slice());
assert!(
handle
.read_at(data.len() as u64, 1, ReadOptions::default())
.await
.is_err(),
"out-of-bounds read must still fail after removal"
);
}
}
async fn test_recreate_generations<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let partition = "recreate_generations";
let mut handles = Vec::new();
for generation in 0u8..3 {
let (blob, len) = storage.open(partition, b"name").await.unwrap();
assert_eq!(len, 0, "each recreation must start empty");
let data = vec![generation; 32];
blob.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
storage.remove(partition, Some(b"name")).await.unwrap();
handles.push((blob, data));
}
for _ in 0..5 {
let (blob, _) = storage.open(partition, b"name").await.unwrap();
blob.write_at(0, vec![0xFF; 8], WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
drop(blob);
storage.remove(partition, Some(b"name")).await.unwrap();
}
for (blob, data) in &handles {
let read = blob
.read_at(0, data.len(), ReadOptions::default())
.await
.unwrap();
assert_eq!(
read.coalesce().as_ref(),
data.as_slice(),
"each handle must keep observing its own generation"
);
}
}
async fn test_read_after_remove_partition_multi<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let partition = "read_after_remove_partition_multi";
let (small_a, _) = storage.open(partition, b"a").await.unwrap();
small_a
.write_at(0, b"alpha", WriteOptions::default())
.await
.unwrap();
small_a.sync().await.unwrap();
let (small_b, _) = storage.open(partition, b"b").await.unwrap();
small_b
.write_at(0, b"bravo", WriteOptions::default())
.await
.unwrap();
const LARGE_LEN: usize = 1 << 20;
let (large, _) = storage.open(partition, b"large").await.unwrap();
let data: Vec<u8> = (0u8..=255).cycle().take(LARGE_LEN).collect();
large
.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
large.sync().await.unwrap();
storage.remove(partition, None).await.unwrap();
let read = small_a.read_at(0, 5, ReadOptions::default()).await.unwrap();
assert_eq!(read.coalesce().as_ref(), b"alpha");
let read = small_b.read_at(0, 5, ReadOptions::default()).await.unwrap();
assert_eq!(read.coalesce().as_ref(), b"bravo");
for (offset, len) in [(0usize, 4096), (123_457, 8192), (LARGE_LEN - 1, 1)] {
let read = large
.read_at(offset as u64, len, ReadOptions::default())
.await
.unwrap();
assert_eq!(
read.coalesce().as_ref(),
&data[offset..offset + len],
"offset={offset} len={len}"
);
}
let (fresh, len) = storage.open(partition, b"a").await.unwrap();
assert_eq!(len, 0, "recreated blob must start empty");
fresh
.write_at(0, b"fresh", WriteOptions::default())
.await
.unwrap();
fresh.sync().await.unwrap();
let read = small_a.read_at(0, 5, ReadOptions::default()).await.unwrap();
assert_eq!(
read.coalesce().as_ref(),
b"alpha",
"pre-removal handle must keep observing the removed partition's blob"
);
}
async fn test_scan<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
storage.open("partition", b"blob1").await.unwrap();
storage.open("partition", b"blob2").await.unwrap();
let blobs = storage.scan("partition").await.unwrap();
assert_eq!(
blobs.len(),
2,
"Scan did not return the expected number of blobs"
);
assert!(
blobs.contains(&b"blob1".to_vec()),
"Blob1 is missing from scan results"
);
assert!(
blobs.contains(&b"blob2".to_vec()),
"Blob2 is missing from scan results"
);
}
async fn test_concurrent_access<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage.open("partition", b"test_blob").await.unwrap();
blob.write_at(0, b"concurrent write", WriteOptions::default())
.await
.unwrap();
let write_task = tokio::spawn({
let blob = blob.clone();
async move {
blob.write_at(0, IoBuf::from(b"concurrent write"), WriteOptions::default())
.await
.unwrap();
}
});
let read_task = tokio::spawn({
let blob = blob.clone();
async move { blob.read_at(0, 16, ReadOptions::default()).await.unwrap() }
});
write_task.await.unwrap();
let buffer = read_task.await.unwrap();
assert_eq!(
buffer.coalesce(),
b"concurrent write",
"Concurrent access failed"
);
}
async fn test_large_data<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage.open("partition", b"large_blob").await.unwrap();
let large_data = vec![42u8; 10 * 1024 * 1024]; blob.write_at(0, large_data.clone(), WriteOptions::default())
.await
.unwrap();
let read = blob
.read_at(0, 10 * 1024 * 1024, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read, large_data.as_slice(), "Large data read/write failed");
}
async fn test_overwrite_data<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_overwrite_data", b"test_blob")
.await
.unwrap();
blob.write_at(0, b"initial data", WriteOptions::default())
.await
.unwrap();
blob.write_at(8, b"overwrite", WriteOptions::default())
.await
.unwrap();
let read = blob
.read_at(0, 17, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(
read, b"initial overwrite",
"Data was not overwritten correctly"
);
}
async fn test_read_beyond_bound<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_read_beyond_written_data", b"test_blob")
.await
.unwrap();
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
let result = blob.read_at(6, 10, ReadOptions::default()).await;
assert!(
result.is_err(),
"Reading beyond written data should return an error"
);
let buf = IoBufMut::with_capacity(10);
let result = blob.read_at_buf(6, 10, buf, ReadOptions::default()).await;
assert!(
result.is_err(),
"read_at_buf beyond written data should return an error"
);
}
async fn test_write_at_large_offset<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_write_at_large_offset", b"test_blob")
.await
.unwrap();
blob.write_at(10_000, b"offset data", WriteOptions::default())
.await
.unwrap();
let read = blob
.read_at(10_000, 11, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read, b"offset data", "Data at large offset is incorrect");
}
async fn test_write_at_sync<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_write_at_sync", b"test_blob")
.await
.unwrap();
blob.write_at(1024, Vec::<u8>::new(), WriteOptions::SYNC)
.await
.unwrap();
drop(blob);
let (blob, len) = storage
.open("test_write_at_sync", b"test_blob")
.await
.unwrap();
assert_eq!(len, 0);
blob.write_at(0, b"hello", WriteOptions::SYNC)
.await
.unwrap();
blob.write_at(
5,
vec![IoBuf::from(b" "), IoBuf::from(b"world")],
WriteOptions::SYNC,
)
.await
.unwrap();
drop(blob);
let (blob, len) = storage
.open("test_write_at_sync", b"test_blob")
.await
.unwrap();
assert_eq!(len, 11);
let read = blob
.read_at(0, 11, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), b"hello world");
}
async fn test_start_sync<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, len) = storage.open("test_start_sync", b"test_blob").await.unwrap();
assert_eq!(len, 0);
blob.write_at(0, b"hello world", WriteOptions::default())
.await
.unwrap();
blob.start_sync().await.await.unwrap();
drop(blob);
let (blob, len) = storage.open("test_start_sync", b"test_blob").await.unwrap();
assert_eq!(len, 11);
let read = blob
.read_at(0, 11, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), b"hello world");
}
async fn test_append_data<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_append_data", b"test_blob")
.await
.unwrap();
blob.write_at(0, b"first", WriteOptions::default())
.await
.unwrap();
blob.write_at(5, b"second", WriteOptions::default())
.await
.unwrap();
let read = blob
.read_at(0, 11, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read, b"firstsecond", "Appended data is incorrect");
}
async fn test_vectored_write_at<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let test = |partition, bufs: Vec<IoBuf>, options, context| async move {
let expected = IoBufs::from(bufs.clone()).coalesce();
let (blob, _) = storage.open(partition, b"test_blob").await.unwrap();
blob.write_at(0, bufs, options).await.unwrap();
let read = blob
.read_at(0, expected.len(), ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), expected.as_ref(), "{context}");
};
test(
"test_vectored_write_basic",
vec![
IoBuf::from(b"hello"),
IoBuf::from(b" "),
IoBuf::from(b"world"),
],
WriteOptions::default(),
"Vectored write content is incorrect",
)
.await;
test(
"test_vectored_write_empty_chunks",
vec![
IoBuf::default(),
IoBuf::from(b"abc"),
IoBuf::default(),
IoBuf::from(b"def"),
IoBuf::default(),
],
WriteOptions::default(),
"Vectored write with empties is incorrect",
)
.await;
let chunk_count = 1_025;
let mut bufs = Vec::with_capacity(chunk_count);
for i in 0..chunk_count {
bufs.push(IoBuf::from(vec![i as u8]));
}
test(
"test_vectored_write_many_chunks",
bufs.clone(),
WriteOptions::default(),
"Vectored write over batch size is incorrect",
)
.await;
test(
"test_vectored_sync_write_many_chunks",
bufs,
WriteOptions::SYNC,
"Synchronized vectored write over batch size is incorrect",
)
.await;
}
async fn test_vectored_write_at_large_offset<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_vectored_write_at_large_offset", b"test_blob")
.await
.unwrap();
let chunk_count = 128;
let mut bufs = Vec::with_capacity(chunk_count);
for i in 0..chunk_count {
bufs.push(IoBuf::from(vec![i as u8; i]));
}
let expected = IoBufs::from(bufs.clone()).coalesce();
blob.write_at(5_000, bufs, WriteOptions::default())
.await
.unwrap();
let read = blob
.read_at(5_000, expected.len(), ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(
read.as_ref(),
expected.as_ref(),
"Vectored write at offset content is incorrect"
);
let prefix = blob
.read_at(0, 5_000, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(prefix.as_ref(), [0u8; 5_000]);
}
async fn test_sequential_read_write<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage.open("partition", b"test_blob").await.unwrap();
blob.write_at(0, b"first", WriteOptions::default())
.await
.unwrap();
blob.write_at(10, b"second", WriteOptions::default())
.await
.unwrap();
let read = blob
.read_at(0, 5, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read, b"first", "Data at offset 0 is incorrect");
let read = blob
.read_at(10, 6, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read, b"second", "Data at offset 10 is incorrect");
}
async fn test_sequential_chunk_read_write<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_large_data_in_chunks", b"large_blob")
.await
.unwrap();
let chunk_size = 1024 * 1024; let num_chunks = 10;
let data = vec![7u8; chunk_size];
for i in 0..num_chunks {
blob.write_at(
(i * chunk_size) as u64,
data.clone(),
WriteOptions::default(),
)
.await
.unwrap();
}
for i in 0..num_chunks {
let read = blob
.read_at((i * chunk_size) as u64, chunk_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read, data.as_slice(), "Chunk {i} is incorrect");
}
}
async fn test_read_empty_blob<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_read_empty_blob", b"empty_blob")
.await
.unwrap();
let result = blob.read_at(0, 1, ReadOptions::default()).await;
assert!(
result.is_err(),
"Reading from an empty blob should return an error"
);
let buf = IoBufMut::with_capacity(1);
let result = blob.read_at_buf(0, 1, buf, ReadOptions::default()).await;
assert!(
result.is_err(),
"read_at_buf from an empty blob should return an error"
);
}
async fn test_overlapping_writes<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_overlapping_writes", b"test_blob")
.await
.unwrap();
blob.write_at(0, b"overlap", WriteOptions::default())
.await
.unwrap();
blob.write_at(4, b"map", WriteOptions::default())
.await
.unwrap();
let read = blob
.read_at(0, 7, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read, b"overmap", "Overlapping writes are incorrect");
}
async fn test_resize_then_open<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
{
let (blob, _) = storage
.open("test_resize_then_open", b"test_blob")
.await
.unwrap();
blob.write_at(0, b"hello world", WriteOptions::default())
.await
.unwrap();
blob.resize(5).await.unwrap();
blob.sync().await.unwrap();
}
let (blob, len) = storage
.open("test_resize_then_open", b"test_blob")
.await
.unwrap();
assert_eq!(len, 5, "Blob length after resize is incorrect");
let read = blob
.read_at(0, 5, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read, b"hello", "Resized data is incorrect");
}
async fn test_partition_name_validation<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
for valid in [
"partition",
"my_partition",
"my-partition",
"partition123",
"A1",
] {
assert!(
!matches!(
storage.open(valid, b"blob").await,
Err(crate::Error::PartitionNameInvalid(_))
),
"Valid partition name '{valid}' should be accepted by open"
);
assert!(
!matches!(
storage.remove(valid, None).await,
Err(crate::Error::PartitionNameInvalid(_))
),
"Valid partition name '{valid}' should be accepted by remove"
);
assert!(
!matches!(
storage.scan(valid).await,
Err(crate::Error::PartitionNameInvalid(_))
),
"Valid partition name '{valid}' should be accepted by scan"
);
}
for invalid in [
"my/partition",
"my.partition",
"my partition",
"../escape",
"",
] {
assert!(
matches!(
storage.open(invalid, b"blob").await,
Err(crate::Error::PartitionNameInvalid(_))
),
"Invalid partition name '{invalid}' should be rejected by open"
);
assert!(
matches!(
storage.remove(invalid, None).await,
Err(crate::Error::PartitionNameInvalid(_))
),
"Invalid partition name '{invalid}' should be rejected by remove"
);
assert!(
matches!(
storage.scan(invalid).await,
Err(crate::Error::PartitionNameInvalid(_))
),
"Invalid partition name '{invalid}' should be rejected by scan"
);
}
}
async fn test_blob_version_mismatch<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _, blob_version) = storage
.open_versioned(
"test_version_mismatch",
b"blob",
BlobVersion::new(1)..=BlobVersion::new(1),
)
.await
.unwrap();
assert_eq!(blob_version, BlobVersion::new(1));
blob.sync().await.unwrap();
drop(blob);
let (_, _, blob_version) = storage
.open_versioned(
"test_version_mismatch",
b"blob",
BlobVersion::new(0)..=BlobVersion::new(2),
)
.await
.unwrap();
assert_eq!(blob_version, BlobVersion::new(1));
let result = storage
.open_versioned(
"test_version_mismatch",
b"blob",
BlobVersion::new(2)..=BlobVersion::new(3),
)
.await;
assert!(
matches!(
result,
Err(crate::Error::BlobVersionMismatch { expected, found })
if expected == (BlobVersion::new(2)..=BlobVersion::new(3)) && found == BlobVersion::new(1)
),
"Expected BlobVersionMismatch error"
);
}
async fn test_aligned_layout<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, size, _) = storage
.open_versioned(
"test_aligned_layout",
b"blob",
BlobVersion::new(0)..=BlobVersion::new(0),
)
.await
.unwrap();
assert_eq!(size, 0);
blob.write_at(0, b"hello world".to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let read = blob
.read_at(0, 11, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), b"hello world");
drop(blob);
let (blob, size, _) = storage
.open_versioned(
"test_aligned_layout",
b"blob",
BlobVersion::new(0)..=BlobVersion::new(0),
)
.await
.unwrap();
assert_eq!(size, 11);
let read = blob
.read_at(6, 5, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), b"world");
blob.resize(5).await.unwrap();
blob.sync().await.unwrap();
drop(blob);
let (blob, size, _) = storage
.open_versioned(
"test_aligned_layout",
b"blob",
BlobVersion::new(0)..=BlobVersion::new(0),
)
.await
.unwrap();
assert_eq!(size, 5);
let read = blob
.read_at(0, 5, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), b"hello");
drop(blob);
}
async fn test_read_zero_length<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_read_at_zero_len", b"blob")
.await
.unwrap();
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
let output = blob.read_at(0, 0, ReadOptions::default()).await.unwrap();
assert_eq!(output.len(), 0);
let buf = IoBufMut::with_capacity(16);
let output = blob
.read_at_buf(0, 0, buf, ReadOptions::default())
.await
.unwrap();
assert_eq!(output.len(), 0);
}
async fn test_read_at_buf_returns_same_buffer<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_read_at_contract", b"blob")
.await
.unwrap();
blob.write_at(0, b"hello world", WriteOptions::default())
.await
.unwrap();
let input_buf = IoBufMut::zeroed(11);
let input_ptr = input_buf.as_ref().as_ptr();
let output = blob
.read_at_buf(0, 11, input_buf, ReadOptions::default())
.await
.unwrap();
assert!(
output.is_single(),
"Single input should return single output"
);
let output_ptr = output.chunk().as_ptr();
assert_eq!(
input_ptr, output_ptr,
"read_at must return the same buffer that was passed in"
);
assert_eq!(output.chunk(), b"hello world");
let buf1 = IoBufMut::zeroed(5);
let buf2 = IoBufMut::zeroed(6);
let ptr1 = buf1.as_ref().as_ptr();
let ptr2 = buf2.as_ref().as_ptr();
let input_bufs = IoBufsMut::from(vec![buf1, buf2]);
assert!(!input_bufs.is_single(), "Should be multi-chunk");
let mut output = blob
.read_at_buf(0, 11, input_bufs, ReadOptions::default())
.await
.unwrap();
assert!(
!output.is_single(),
"Multi-chunk input should return multi-chunk output"
);
assert_eq!(
output.chunk().as_ptr(),
ptr1,
"First chunk must be the same buffer"
);
assert_eq!(output.chunk(), b"hello");
output.advance(5);
assert_eq!(
output.chunk().as_ptr(),
ptr2,
"Second chunk must be the same buffer"
);
assert_eq!(output.chunk(), b" world");
output.advance(6);
assert_eq!(output.remaining(), 0);
let buf1 = IoBufMut::zeroed(2);
let buf2 = IoBufMut::zeroed(2);
let ptr1 = buf1.as_ref().as_ptr();
let input_bufs = IoBufsMut::from(vec![buf1, buf2]);
assert!(!input_bufs.is_single(), "Should be multi-chunk");
let output = blob
.read_at_buf(0, 2, input_bufs, ReadOptions::default())
.await
.unwrap();
assert!(
!output.is_single(),
"Multi-chunk input should remain multi-chunk when len only uses first chunk"
);
assert_eq!(
output.chunk().as_ptr(),
ptr1,
"First chunk must be the same buffer"
);
assert_eq!(output.chunk(), b"he");
}
async fn test_read_at_buf_insufficient_capacity<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_read_at_buf_capacity", b"blob")
.await
.unwrap();
blob.write_at(0, b"hello world", WriteOptions::default())
.await
.unwrap();
let buf = IoBufMut::with_capacity(5);
let result =
std::panic::AssertUnwindSafe(blob.read_at_buf(0, 11, buf, ReadOptions::default()))
.catch_unwind()
.await;
assert!(
result.is_err(),
"Expected panic for insufficient single buffer capacity"
);
let bufs = IoBufsMut::from(vec![IoBufMut::with_capacity(4), IoBufMut::with_capacity(4)]);
let result =
std::panic::AssertUnwindSafe(blob.read_at_buf(0, 11, bufs, ReadOptions::default()))
.catch_unwind()
.await;
assert!(
result.is_err(),
"Expected panic for insufficient multi-chunk buffer capacity"
);
}
async fn test_read_at_buf_larger_capacity<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage
.open("test_read_at_buf_large_cap", b"blob")
.await
.unwrap();
blob.write_at(0, b"hello world", WriteOptions::default())
.await
.unwrap();
let buf = IoBufMut::with_capacity(64);
assert_eq!(buf.len(), 0, "with_capacity should start at len 0");
let output = blob
.read_at_buf(0, 11, buf, ReadOptions::default())
.await
.unwrap();
assert_eq!(output.len(), 11);
assert_eq!(output.coalesce(), b"hello world");
let buf = IoBufMut::with_capacity(64);
let output = blob
.read_at_buf(0, 5, buf, ReadOptions::default())
.await
.unwrap();
assert_eq!(output.len(), 5);
assert_eq!(output.coalesce(), b"hello");
}
async fn test_read_options<S>(storage: &S)
where
S: Storage + Send + Sync,
S::Blob: Send + Sync,
{
let (blob, _) = storage.open("test_read_options", b"blob").await.unwrap();
blob.write_at(0, b"hello world", WriteOptions::default())
.await
.unwrap();
let default = blob
.read_at(0, 11, ReadOptions::default())
.await
.unwrap()
.coalesce();
let uncached = blob
.read_at(0, 11, ReadOptions::DONT_CACHE)
.await
.unwrap()
.coalesce();
assert_eq!(default.as_ref(), uncached.as_ref());
let default = blob.read_at(0, 0, ReadOptions::default()).await.unwrap();
let uncached = blob.read_at(0, 0, ReadOptions::DONT_CACHE).await.unwrap();
assert_eq!(default.len(), 0);
assert_eq!(uncached.len(), 0);
assert!(blob.read_at(0, 12, ReadOptions::default()).await.is_err());
assert!(blob.read_at(0, 12, ReadOptions::DONT_CACHE).await.is_err());
for options in [ReadOptions::default(), ReadOptions::DONT_CACHE] {
let bufs =
IoBufsMut::from(vec![IoBufMut::with_capacity(5), IoBufMut::with_capacity(6)]);
let output = blob.read_at_buf(0, 11, bufs, options).await.unwrap();
assert!(!output.is_single());
assert_eq!(output.coalesce(), b"hello world");
}
}
}