use std::io;
use bytes::Bytes;
use cyber_bao::ChunkRanges;
use tokio::io::AsyncReadExt;
use super::{bounded, hello_wire, root, EMPTY_HEX, HELLO_HEX};
use crate::{
api::{
blobs::{Bitfield, BlobStatus, Blobs},
Error, RequestError,
},
store::{mem::MemStore, readonly_mem::ReadonlyMemStore},
BlobFormat, Hash,
};
async fn complete(blobs: &Blobs) {
let hash = root(EMPTY_HEX);
assert_eq!(
blobs.status(hash).await.unwrap(),
BlobStatus::Complete { size: 0 }
);
assert!(blobs.get_bytes(hash).await.unwrap().is_empty());
let mut bytes = Vec::new();
assert_eq!(blobs.reader(hash).read_to_end(&mut bytes).await.unwrap(), 0);
assert!(bytes.is_empty());
assert_eq!(blobs.observe(hash).await.unwrap(), Bitfield::complete(0));
assert_eq!(
blobs.observe(hash).await_completion().await.unwrap(),
Bitfield::complete(0)
);
let wire = blobs
.export_bao(hash, ChunkRanges::all())
.bao_to_vec()
.await
.unwrap();
assert_eq!(wire.len(), 8);
assert_eq!(wire, [0; 8]);
}
#[tokio::test]
async fn memory_add_read_observe_export() {
bounded(async {
let store = MemStore::new();
let tag = store.add_bytes(Bytes::new()).temp_tag().await.unwrap();
assert_eq!(tag.hash(), root(EMPTY_HEX));
complete(&store).await;
drop(tag);
for format in [BlobFormat::Raw, BlobFormat::HashSeq] {
let tag = store
.add_bytes_with_opts((Bytes::new(), format))
.temp_tag()
.await
.unwrap();
assert_eq!(tag.hash(), root(EMPTY_HEX));
assert_eq!(tag.format(), format);
complete(&store).await;
drop(tag);
}
store.shutdown().await.unwrap();
})
.await;
}
#[tokio::test]
async fn readonly_computed_key_control() {
bounded(async {
let store = ReadonlyMemStore::new([b"".as_slice()]);
assert_eq!(store.list().hashes().await.unwrap(), [root(EMPTY_HEX)]);
complete(&store).await;
store.shutdown().await.unwrap();
})
.await;
}
#[cfg(feature = "fs-store")]
#[tokio::test]
async fn fs_add_formats_and_synthetic_reopen() {
use crate::store::fs::FsStore;
bounded(async {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("blobs.db");
let store = FsStore::load(&db).await.unwrap();
for format in [BlobFormat::Raw, BlobFormat::HashSeq] {
let tag = store
.add_bytes_with_opts((Bytes::new(), format))
.temp_tag()
.await
.unwrap();
assert_eq!(tag.hash(), root(EMPTY_HEX));
assert_eq!(tag.format(), format);
complete(&store).await;
drop(tag);
}
let exported = dir.path().join("empty.bin");
store.export(root(EMPTY_HEX), &exported).await.unwrap();
assert_eq!(std::fs::read(exported).unwrap(), b"");
store.shutdown().await.unwrap();
drop(store);
let store = FsStore::load(&db).await.unwrap();
complete(&store).await;
store.shutdown().await.unwrap();
})
.await;
}
#[cfg(feature = "fs-store")]
#[tokio::test]
async fn fs_synthetic_status_enumeration_and_nonempty_control() {
use crate::{api::blobs::DeleteOptions, store::fs::FsStore};
bounded(async {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("blobs.db");
let store = FsStore::load(&db).await.unwrap();
let empty = root(EMPTY_HEX);
let hello = root(HELLO_HEX);
let zero = Hash::from_bytes([0; 32]);
assert!(store.list().hashes().await.unwrap().is_empty());
complete(&store).await;
assert!(store.has(empty).await.unwrap());
for absent in [zero, hello] {
assert_eq!(store.status(absent).await.unwrap(), BlobStatus::NotFound);
assert!(!store.has(absent).await.unwrap());
}
let tag = store.add_bytes(Bytes::new()).temp_tag().await.unwrap();
assert_eq!(tag.hash(), empty);
assert!(store.list().hashes().await.unwrap().is_empty());
drop(tag);
let tag = store
.add_bytes(Bytes::from_static(b"hello"))
.temp_tag()
.await
.unwrap();
assert_eq!(tag.hash(), hello);
hello_complete(&store).await;
assert_eq!(store.list().hashes().await.unwrap(), [hello]);
drop(tag);
store.shutdown().await.unwrap();
drop(store);
let store = FsStore::load(&db).await.unwrap();
complete(&store).await;
assert!(store.has(empty).await.unwrap());
hello_complete(&store).await;
assert_eq!(store.list().hashes().await.unwrap(), [hello]);
store
.delete_with_opts(DeleteOptions {
hashes: vec![empty],
force: true,
})
.await
.unwrap();
complete(&store).await;
assert_eq!(store.list().hashes().await.unwrap(), [hello]);
assert_eq!(store.status(zero).await.unwrap(), BlobStatus::NotFound);
assert!(!store.has(zero).await.unwrap());
store.shutdown().await.unwrap();
})
.await;
}
#[cfg(feature = "fs-store")]
async fn hello_complete(blobs: &Blobs) {
let hello = root(HELLO_HEX);
assert_eq!(
blobs.status(hello).await.unwrap(),
BlobStatus::Complete { size: 5 }
);
assert!(blobs.has(hello).await.unwrap());
assert_eq!(blobs.get_bytes(hello).await.unwrap().as_ref(), b"hello");
assert_eq!(
blobs
.export_bao(hello, ChunkRanges::all())
.bao_to_vec()
.await
.unwrap(),
hello_wire()
);
}
#[tokio::test]
async fn local_canonical_zero_header_admission() {
bounded(async {
let store = MemStore::new();
let rest = store
.import_bao_reader(
root(EMPTY_HEX),
ChunkRanges::all(),
Bytes::from_static(&[0; 8]),
)
.await
.unwrap();
assert!(rest.is_empty()); store.shutdown().await.unwrap();
})
.await;
}
fn assert_io(error: RequestError, kind: io::ErrorKind, detail: Option<&str>) {
let RequestError::Inner {
source: Error::Io(error),
..
} = error
else {
panic!("expected wrapped input IO error: {error:?}");
};
assert_eq!(error.kind(), kind);
if let Some(detail) = detail {
assert_eq!(error.to_string(), detail);
}
}
#[tokio::test]
async fn local_zero_header_rejects_wrong_keys() {
bounded(async {
let store = MemStore::new();
for hash in [Hash::from_bytes([0; 32]), root(HELLO_HEX)] {
let error = store
.import_bao_reader(hash, ChunkRanges::all(), Bytes::from_static(&[0; 8]))
.await
.unwrap_err();
assert_io(error, io::ErrorKind::Other, Some("invalid size for hash"));
}
store.shutdown().await.unwrap();
})
.await;
}
#[tokio::test]
async fn local_short_header_is_unexpected_eof() {
bounded(async {
let store = MemStore::new();
let error = store
.import_bao_reader(
root(EMPTY_HEX),
ChunkRanges::all(),
Bytes::from_static(&[0; 7]),
)
.await
.unwrap_err();
assert_io(error, io::ErrorKind::UnexpectedEof, None);
store.shutdown().await.unwrap();
})
.await;
}
#[tokio::test]
async fn local_hello_runs_actual_decoder_and_import() {
bounded(async {
let store = MemStore::new();
let hash = root(HELLO_HEX);
let rest = store
.import_bao_reader(hash, ChunkRanges::all(), Bytes::from(hello_wire()))
.await
.unwrap();
assert!(rest.is_empty());
assert_eq!(
store.status(hash).await.unwrap(),
BlobStatus::Complete { size: 5 }
);
assert_eq!(store.get_bytes(hash).await.unwrap().as_ref(), b"hello");
assert_eq!(
store
.export_bao(hash, ChunkRanges::all())
.bao_to_vec()
.await
.unwrap(),
hello_wire()
);
store.shutdown().await.unwrap();
})
.await;
}
#[tokio::test]
async fn zero_key_has_no_empty_alias() {
bounded(async {
let store = MemStore::new();
let zero = Hash::from_bytes([0; 32]);
assert_eq!(store.status(zero).await.unwrap(), BlobStatus::NotFound);
let observation = store.observe(zero).await.unwrap();
assert_eq!(observation, Bitfield::empty());
assert!(!observation.is_complete());
store.shutdown().await.unwrap();
})
.await;
}