1use std::{
29 collections::BTreeMap,
30 future::Future,
31 path::{Path, PathBuf},
32 pin::Pin,
33 sync::{
34 Arc, Mutex, MutexGuard,
35 atomic::{AtomicU64, Ordering},
36 },
37 time::{SystemTime, UNIX_EPOCH},
38};
39
40use crate::error::{Error, Result};
41use crate::options::DurabilityMode;
42use crate::storage::{
43 StorageCapabilities, StorageFuture, StorageObjectDeleteBackend, StorageObjectId,
44 StorageObjectListBackend, StorageObjectListRequest, StorageObjectReadBackend,
45 StorageObjectWriteBackend, StorageReadBackend, StorageReadFuture, StorageReadObject,
46 ensure_whole_object_read_len,
47};
48
49#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
54pub type ObjectFuture<'op, T> = Pin<Box<dyn Future<Output = Result<T>> + 'op>>;
55
56#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
58pub type ObjectFuture<'op, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'op>>;
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub struct ETag(Arc<str>);
65
66impl ETag {
67 #[must_use]
69 pub fn new(tag: impl Into<Arc<str>>) -> Self {
70 Self(tag.into())
71 }
72
73 #[must_use]
75 pub fn as_str(&self) -> &str {
76 &self.0
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Precondition {
83 IfNoneMatch,
85 IfMatch(ETag),
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum PutIf {
96 Stored {
98 etag: ETag,
100 },
101 PreconditionFailed {
103 current: Option<ETag>,
105 },
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct ObjectMeta {
111 pub key: String,
113 pub size: u64,
115 pub etag: ETag,
117}
118
119pub trait ObjectClient: Send + Sync {
168 fn get<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>>;
170
171 fn get_range<'op>(&'op self, key: &str, offset: u64, len: u64) -> ObjectFuture<'op, Arc<[u8]>>;
174
175 fn put<'op>(&'op self, key: &str, bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag>;
177
178 fn delete<'op>(&'op self, key: &str) -> ObjectFuture<'op, ()>;
180
181 fn list<'op>(&'op self, prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>>;
183
184 fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>>;
187
188 fn put_if<'op>(
192 &'op self,
193 key: &str,
194 bytes: Arc<[u8]>,
195 precondition: Precondition,
196 ) -> ObjectFuture<'op, PutIf>;
197}
198
199static OBJECT_CLIENT_CONTRACT_PROBE_COUNTER: AtomicU64 = AtomicU64::new(0);
200
201pub async fn verify_object_client_contract(
240 client: Arc<dyn ObjectClient>,
241 prefix: impl Into<String>,
242) -> Result<()> {
243 let key = object_client_contract_probe_key(Path::new(&prefix.into()))?;
244 let result = verify_object_client_contract_at_key(&client, &key).await;
245 let cleanup = client.delete(&key).await;
246 match (result, cleanup) {
247 (Ok(()), Ok(())) => Ok(()),
248 (Err(error), _) | (Ok(()), Err(error)) => Err(error),
249 }
250}
251
252pub(crate) async fn verify_object_client_contract_for_open(
253 client: &Arc<dyn ObjectClient>,
254 db_path: &Path,
255 role: &str,
256) -> Result<()> {
257 let key = object_client_contract_probe_key_for_role(db_path, role)?;
258 let result = verify_object_client_contract_at_key(client, &key).await;
259 let cleanup = client.delete(&key).await;
260 match (result, cleanup) {
261 (Ok(()), Ok(())) => Ok(()),
262 (Err(error), _) | (Ok(()), Err(error)) => Err(error),
263 }
264}
265
266async fn verify_object_client_contract_at_key(
267 client: &Arc<dyn ObjectClient>,
268 key: &str,
269) -> Result<()> {
270 client.delete(key).await?;
271
272 let first = Arc::<[u8]>::from(b"trine-object-client-contract:first".as_slice());
273 let second = Arc::<[u8]>::from(b"trine-object-client-contract:second".as_slice());
274 let first_etag = client.put(key, Arc::clone(&first)).await?;
275 verify_object_client_observed_bytes(client, key, &first, &first_etag, "put").await?;
276
277 match client
278 .put_if(key, Arc::clone(&second), Precondition::IfNoneMatch)
279 .await?
280 {
281 PutIf::PreconditionFailed { current } if current.as_ref() == Some(&first_etag) => {}
282 PutIf::PreconditionFailed { current } => {
283 return Err(Error::Corruption {
284 message: format!(
285 "object client contract probe for {key} returned wrong IfNoneMatch ETag: {current:?}"
286 ),
287 });
288 }
289 PutIf::Stored { .. } => {
290 return Err(Error::Corruption {
291 message: format!(
292 "object client contract probe for {key} stored despite IfNoneMatch on an existing object"
293 ),
294 });
295 }
296 }
297
298 let mismatched = ETag::new("trine-object-client-contract-mismatch");
299 match client
300 .put_if(key, Arc::clone(&second), Precondition::IfMatch(mismatched))
301 .await?
302 {
303 PutIf::PreconditionFailed { .. } => {}
304 PutIf::Stored { .. } => {
305 return Err(Error::Corruption {
306 message: format!(
307 "object client contract probe for {key} stored despite a mismatched IfMatch ETag"
308 ),
309 });
310 }
311 }
312
313 let second_etag = match client
314 .put_if(
315 key,
316 Arc::clone(&second),
317 Precondition::IfMatch(first_etag.clone()),
318 )
319 .await?
320 {
321 PutIf::Stored { etag } => etag,
322 PutIf::PreconditionFailed { current } => {
323 return Err(Error::Corruption {
324 message: format!(
325 "object client contract probe for {key} rejected a matching IfMatch ETag: {current:?}"
326 ),
327 });
328 }
329 };
330 if second_etag == first_etag {
331 return Err(Error::Corruption {
332 message: format!(
333 "object client contract probe for {key} reused an ETag after overwriting bytes"
334 ),
335 });
336 }
337 verify_object_client_observed_bytes(client, key, &second, &second_etag, "put_if").await
338}
339
340async fn verify_object_client_observed_bytes(
341 client: &Arc<dyn ObjectClient>,
342 key: &str,
343 expected: &Arc<[u8]>,
344 expected_etag: &ETag,
345 operation: &str,
346) -> Result<()> {
347 let head = client.head(key).await?.ok_or_else(|| Error::Corruption {
348 message: format!("object client contract probe for {key} lost head after {operation}"),
349 })?;
350 if &head.etag != expected_etag || head.size != expected.len() as u64 {
351 return Err(Error::Corruption {
352 message: format!(
353 "object client contract probe for {key} observed stale head after {operation}"
354 ),
355 });
356 }
357 let bytes = client.get(key).await?.ok_or_else(|| Error::Corruption {
358 message: format!("object client contract probe for {key} lost bytes after {operation}"),
359 })?;
360 if bytes.as_ref() != expected.as_ref() {
361 return Err(Error::Corruption {
362 message: format!(
363 "object client contract probe for {key} observed stale bytes after {operation}"
364 ),
365 });
366 }
367 Ok(())
368}
369
370fn object_client_contract_probe_key(prefix: &Path) -> Result<String> {
371 object_client_contract_probe_key_for_role(prefix, "health")
372}
373
374fn object_client_contract_probe_key_for_role(db_path: &Path, role: &str) -> Result<String> {
375 let now = SystemTime::now()
376 .duration_since(UNIX_EPOCH)
377 .map_err(|error| Error::Corruption {
378 message: format!("system clock is before UNIX_EPOCH: {error}"),
379 })?;
380 let counter = OBJECT_CLIENT_CONTRACT_PROBE_COUNTER.fetch_add(1, Ordering::Relaxed);
381 Ok(db_path
382 .join(format!(
383 ".trine-object-client-contract-{role}-{}-{counter}",
384 now.as_nanos()
385 ))
386 .to_string_lossy()
387 .into_owned())
388}
389
390#[derive(Debug, Clone)]
392struct StoredObject {
393 bytes: Arc<[u8]>,
394 etag: ETag,
395}
396
397#[derive(Debug, Default)]
401pub struct InMemoryObjectStore {
402 objects: Mutex<BTreeMap<String, StoredObject>>,
403 next_etag: AtomicU64,
404}
405
406impl InMemoryObjectStore {
407 #[must_use]
409 pub fn new() -> Self {
410 Self::default()
411 }
412
413 fn mint_etag(&self) -> ETag {
414 let value = self.next_etag.fetch_add(1, Ordering::Relaxed);
415 ETag(Arc::from(format!("etag-{value}")))
416 }
417
418 fn lock(&self) -> Result<MutexGuard<'_, BTreeMap<String, StoredObject>>> {
419 self.objects.lock().map_err(|_| Error::Corruption {
420 message: "in-memory object store lock poisoned".to_owned(),
421 })
422 }
423
424 fn get_inner(&self, key: &str) -> Result<Option<Arc<[u8]>>> {
425 Ok(self
426 .lock()?
427 .get(key)
428 .map(|object| Arc::clone(&object.bytes)))
429 }
430
431 fn get_range_inner(&self, key: &str, offset: u64, len: u64) -> Result<Arc<[u8]>> {
432 let objects = self.lock()?;
433 let object = objects.get(key).ok_or_else(|| Error::Corruption {
434 message: format!("object {key} not found for range read"),
435 })?;
436 let offset = usize::try_from(offset)
437 .map_err(|_| Error::invalid_options("object range offset overflow"))?;
438 let len = usize::try_from(len)
439 .map_err(|_| Error::invalid_options("object range length overflow"))?;
440 let end = offset
441 .checked_add(len)
442 .ok_or_else(|| Error::invalid_options("object range end overflow"))?;
443 let slice = object
444 .bytes
445 .get(offset..end)
446 .ok_or_else(|| Error::Corruption {
447 message: format!("object {key} short read for range {offset}..{end}"),
448 })?;
449 Ok(Arc::from(slice))
450 }
451
452 fn put_inner(&self, key: &str, bytes: Arc<[u8]>) -> Result<ETag> {
453 let etag = self.mint_etag();
454 self.lock()?.insert(
455 key.to_owned(),
456 StoredObject {
457 bytes,
458 etag: etag.clone(),
459 },
460 );
461 Ok(etag)
462 }
463
464 fn delete_inner(&self, key: &str) -> Result<()> {
465 self.lock()?.remove(key);
466 Ok(())
467 }
468
469 fn list_inner(&self, prefix: &str) -> Result<Vec<ObjectMeta>> {
470 let objects = self.lock()?;
471 Ok(objects
472 .range(prefix.to_owned()..)
473 .take_while(|(key, _)| key.starts_with(prefix))
474 .map(|(key, object)| ObjectMeta {
475 key: key.clone(),
476 size: object.bytes.len() as u64,
477 etag: object.etag.clone(),
478 })
479 .collect())
480 }
481
482 fn head_inner(&self, key: &str) -> Result<Option<ObjectMeta>> {
483 Ok(self.lock()?.get(key).map(|object| ObjectMeta {
484 key: key.to_owned(),
485 size: object.bytes.len() as u64,
486 etag: object.etag.clone(),
487 }))
488 }
489
490 fn put_if_inner(
491 &self,
492 key: &str,
493 bytes: Arc<[u8]>,
494 precondition: &Precondition,
495 ) -> Result<PutIf> {
496 let mut objects = self.lock()?;
497 let current = objects.get(key).map(|object| object.etag.clone());
498 let allowed = match (precondition, ¤t) {
499 (Precondition::IfNoneMatch, None) => true,
500 (Precondition::IfMatch(expected), Some(actual)) => expected == actual,
501 (Precondition::IfNoneMatch, Some(_)) | (Precondition::IfMatch(_), None) => false,
502 };
503 if !allowed {
504 return Ok(PutIf::PreconditionFailed { current });
505 }
506 let etag = self.mint_etag();
507 objects.insert(
508 key.to_owned(),
509 StoredObject {
510 bytes,
511 etag: etag.clone(),
512 },
513 );
514 Ok(PutIf::Stored { etag })
515 }
516}
517
518impl ObjectClient for InMemoryObjectStore {
519 fn get<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>> {
520 let key = key.to_owned();
521 Box::pin(async move { self.get_inner(&key) })
522 }
523
524 fn get_range<'op>(&'op self, key: &str, offset: u64, len: u64) -> ObjectFuture<'op, Arc<[u8]>> {
525 let key = key.to_owned();
526 Box::pin(async move { self.get_range_inner(&key, offset, len) })
527 }
528
529 fn put<'op>(&'op self, key: &str, bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag> {
530 let key = key.to_owned();
531 Box::pin(async move { self.put_inner(&key, bytes) })
532 }
533
534 fn delete<'op>(&'op self, key: &str) -> ObjectFuture<'op, ()> {
535 let key = key.to_owned();
536 Box::pin(async move { self.delete_inner(&key) })
537 }
538
539 fn list<'op>(&'op self, prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>> {
540 let prefix = prefix.to_owned();
541 Box::pin(async move { self.list_inner(&prefix) })
542 }
543
544 fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>> {
545 let key = key.to_owned();
546 Box::pin(async move { self.head_inner(&key) })
547 }
548
549 fn put_if<'op>(
550 &'op self,
551 key: &str,
552 bytes: Arc<[u8]>,
553 precondition: Precondition,
554 ) -> ObjectFuture<'op, PutIf> {
555 let key = key.to_owned();
556 Box::pin(async move { self.put_if_inner(&key, bytes, &precondition) })
557 }
558}
559
560impl<C: ObjectClient + ?Sized> ObjectClient for Arc<C> {
564 fn get<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>> {
565 (**self).get(key)
566 }
567
568 fn get_range<'op>(&'op self, key: &str, offset: u64, len: u64) -> ObjectFuture<'op, Arc<[u8]>> {
569 (**self).get_range(key, offset, len)
570 }
571
572 fn put<'op>(&'op self, key: &str, bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag> {
573 (**self).put(key, bytes)
574 }
575
576 fn delete<'op>(&'op self, key: &str) -> ObjectFuture<'op, ()> {
577 (**self).delete(key)
578 }
579
580 fn list<'op>(&'op self, prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>> {
581 (**self).list(prefix)
582 }
583
584 fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>> {
585 (**self).head(key)
586 }
587
588 fn put_if<'op>(
589 &'op self,
590 key: &str,
591 bytes: Arc<[u8]>,
592 precondition: Precondition,
593 ) -> ObjectFuture<'op, PutIf> {
594 (**self).put_if(key, bytes, precondition)
595 }
596}
597
598#[derive(Clone)]
611pub(crate) struct ObjectStoreBackend {
612 client: Arc<dyn ObjectClient>,
613}
614
615impl std::fmt::Debug for ObjectStoreBackend {
616 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617 formatter
618 .debug_struct("ObjectStoreBackend")
619 .finish_non_exhaustive()
620 }
621}
622
623impl ObjectStoreBackend {
624 pub(crate) fn new(client: Arc<dyn ObjectClient>) -> Self {
625 Self { client }
626 }
627
628 pub(crate) fn client(&self) -> Arc<dyn ObjectClient> {
629 Arc::clone(&self.client)
630 }
631
632 fn object_key(object: &StorageObjectId) -> String {
633 object.path().to_string_lossy().into_owned()
634 }
635}
636
637#[derive(Debug, Clone)]
641pub(crate) struct ObjectStoreReadObject {
642 object: StorageObjectId,
643 bytes: Arc<[u8]>,
644}
645
646impl ObjectStoreReadObject {
647 fn read_exact_at_offset(&self, offset: usize, out: &mut [u8]) -> Result<()> {
648 let end = offset
649 .checked_add(out.len())
650 .ok_or_else(|| Error::invalid_options("object read offset overflow"))?;
651 let source = self
652 .bytes
653 .get(offset..end)
654 .ok_or_else(|| Error::Corruption {
655 message: format!("object {} short read", self.object.path().display()),
656 })?;
657 out.copy_from_slice(source);
658 Ok(())
659 }
660}
661
662impl StorageReadObject for ObjectStoreReadObject {
663 fn object(&self) -> &StorageObjectId {
664 &self.object
665 }
666
667 fn len(&self) -> StorageReadFuture<'_, u64> {
668 let len = self.bytes.len();
669 Box::pin(async move {
670 u64::try_from(len).map_err(|_| Error::invalid_options("object length overflow"))
671 })
672 }
673
674 fn read_exact_at<'op>(
675 &'op self,
676 offset: usize,
677 bytes: &'op mut [u8],
678 ) -> StorageReadFuture<'op, ()> {
679 Box::pin(async move { self.read_exact_at_offset(offset, bytes) })
680 }
681}
682
683impl StorageReadBackend for ObjectStoreBackend {
684 type ReadObject = ObjectStoreReadObject;
685
686 fn capabilities(&self) -> StorageCapabilities {
687 StorageCapabilities::object_store()
688 }
689
690 fn open_read(&self, object: StorageObjectId) -> StorageReadFuture<'_, Self::ReadObject> {
691 Box::pin(async move {
692 let key = Self::object_key(&object);
693 let meta = self
694 .client
695 .head(&key)
696 .await?
697 .ok_or_else(|| Error::Corruption {
698 message: format!("referenced object {key} cannot be opened"),
699 })?;
700 let bytes =
701 read_object_bytes_by_meta(self.client.as_ref(), &key, &object, &meta).await?;
702 Ok(ObjectStoreReadObject { object, bytes })
703 })
704 }
705}
706
707impl StorageObjectReadBackend for ObjectStoreBackend {
708 fn read_object_bytes(&self, object: StorageObjectId) -> StorageFuture<'_, Option<Arc<[u8]>>> {
709 Box::pin(async move {
710 let key = Self::object_key(&object);
711 let Some(meta) = self.client.head(&key).await? else {
712 return Ok(None);
713 };
714 read_object_bytes_by_meta(self.client.as_ref(), &key, &object, &meta)
715 .await
716 .map(Some)
717 })
718 }
719}
720
721impl StorageObjectWriteBackend for ObjectStoreBackend {
722 fn write_object(
723 &self,
724 object: StorageObjectId,
725 bytes: Arc<[u8]>,
726 _durability: DurabilityMode,
727 ) -> StorageFuture<'_, ()> {
728 Box::pin(async move {
731 self.client.put(&Self::object_key(&object), bytes).await?;
732 Ok(())
733 })
734 }
735}
736
737impl StorageObjectDeleteBackend for ObjectStoreBackend {
738 fn delete_object(&self, object: StorageObjectId) -> StorageFuture<'_, ()> {
739 Box::pin(async move { self.client.delete(&Self::object_key(&object)).await })
740 }
741}
742
743impl StorageObjectListBackend for ObjectStoreBackend {
744 fn list_objects(
745 &self,
746 request: StorageObjectListRequest,
747 ) -> StorageFuture<'_, Vec<StorageObjectId>> {
748 Box::pin(async move {
749 let root = request.root().to_path_buf();
750 let kind = request.kind();
751 let extension = request.file_extension();
752 let prefix = root.to_string_lossy().into_owned();
756 let mut objects: Vec<StorageObjectId> = self
757 .client
758 .list(&prefix)
759 .await?
760 .into_iter()
761 .map(|meta| PathBuf::from(meta.key))
762 .filter(|path| path.parent() == Some(root.as_path()))
763 .filter(|path| path_matches_extension(path, extension))
764 .map(|path| StorageObjectId::native_file(kind, path))
765 .collect();
766 objects.sort_unstable();
767 Ok(objects)
768 })
769 }
770}
771
772fn path_matches_extension(path: &Path, expected: Option<&str>) -> bool {
773 expected.is_none_or(|expected| {
774 path.extension()
775 .and_then(|extension| extension.to_str())
776 .is_some_and(|extension| extension.eq_ignore_ascii_case(expected))
777 })
778}
779
780fn ensure_object_meta_read_len(object: &StorageObjectId, meta: &ObjectMeta) -> Result<()> {
781 let len = usize::try_from(meta.size).map_err(|_| Error::Corruption {
782 message: format!("object {} length exceeds usize", object.path().display()),
783 })?;
784 ensure_whole_object_read_len(object, len)
785}
786
787async fn read_object_bytes_by_meta(
788 client: &dyn ObjectClient,
789 key: &str,
790 object: &StorageObjectId,
791 meta: &ObjectMeta,
792) -> Result<Arc<[u8]>> {
793 ensure_object_meta_read_len(object, meta)?;
794 let expected_len = usize::try_from(meta.size).map_err(|_| Error::Corruption {
795 message: format!("object {} length exceeds usize", object.path().display()),
796 })?;
797 if expected_len == 0 {
798 return Ok(Arc::from([]));
799 }
800 let bytes = client.get_range(key, 0, meta.size).await?;
801 if bytes.len() != expected_len {
802 return Err(Error::Corruption {
803 message: format!(
804 "object {} range read returned {} bytes for declared length {expected_len}",
805 object.path().display(),
806 bytes.len()
807 ),
808 });
809 }
810 ensure_whole_object_read_len(object, bytes.len())?;
811 Ok(bytes)
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817 use std::sync::atomic::{AtomicU64, Ordering};
818
819 fn bytes(data: &[u8]) -> Arc<[u8]> {
820 Arc::from(data)
821 }
822
823 fn block_on<T>(future: ObjectFuture<'_, T>) -> Result<T> {
826 use std::task::{Context, Poll, Wake, Waker};
827
828 struct NoopWaker;
829 impl Wake for NoopWaker {
830 fn wake(self: Arc<Self>) {}
831 }
832
833 let waker = Waker::from(Arc::new(NoopWaker));
834 let mut context = Context::from_waker(&waker);
835 let mut future = future;
836 match future.as_mut().poll(&mut context) {
837 Poll::Ready(result) => result,
838 Poll::Pending => panic!("in-memory object store future should be ready immediately"),
839 }
840 }
841
842 #[derive(Debug, Default)]
843 struct OversizedHeadClient {
844 get_calls: AtomicU64,
845 }
846
847 impl ObjectClient for OversizedHeadClient {
848 fn get<'op>(&'op self, _key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>> {
849 Box::pin(async move {
850 self.get_calls.fetch_add(1, Ordering::Relaxed);
851 Ok(Some(bytes(b"unreachable")))
852 })
853 }
854
855 fn get_range<'op>(
856 &'op self,
857 _key: &str,
858 _offset: u64,
859 _len: u64,
860 ) -> ObjectFuture<'op, Arc<[u8]>> {
861 Box::pin(async move { Err(Error::invalid_options("unexpected range read")) })
862 }
863
864 fn put<'op>(&'op self, _key: &str, _bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag> {
865 Box::pin(async move { Err(Error::invalid_options("unexpected put")) })
866 }
867
868 fn delete<'op>(&'op self, _key: &str) -> ObjectFuture<'op, ()> {
869 Box::pin(async move { Err(Error::invalid_options("unexpected delete")) })
870 }
871
872 fn list<'op>(&'op self, _prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>> {
873 Box::pin(async move { Err(Error::invalid_options("unexpected list")) })
874 }
875
876 fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>> {
877 let key = key.to_owned();
878 Box::pin(async move {
879 Ok(Some(ObjectMeta {
880 key,
881 size: u64::MAX,
882 etag: ETag::new("oversized"),
883 }))
884 })
885 }
886
887 fn put_if<'op>(
888 &'op self,
889 _key: &str,
890 _bytes: Arc<[u8]>,
891 _precondition: Precondition,
892 ) -> ObjectFuture<'op, PutIf> {
893 Box::pin(async move { Err(Error::invalid_options("unexpected put_if")) })
894 }
895 }
896
897 #[derive(Debug, Default)]
898 struct ShortRangeClient {
899 get_calls: AtomicU64,
900 }
901
902 impl ObjectClient for ShortRangeClient {
903 fn get<'op>(&'op self, _key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>> {
904 Box::pin(async move {
905 self.get_calls.fetch_add(1, Ordering::Relaxed);
906 Ok(Some(bytes(b"unreachable")))
907 })
908 }
909
910 fn get_range<'op>(
911 &'op self,
912 _key: &str,
913 _offset: u64,
914 _len: u64,
915 ) -> ObjectFuture<'op, Arc<[u8]>> {
916 Box::pin(async move { Ok(bytes(b"abc")) })
917 }
918
919 fn put<'op>(&'op self, _key: &str, _bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag> {
920 Box::pin(async move { Err(Error::invalid_options("unexpected put")) })
921 }
922
923 fn delete<'op>(&'op self, _key: &str) -> ObjectFuture<'op, ()> {
924 Box::pin(async move { Err(Error::invalid_options("unexpected delete")) })
925 }
926
927 fn list<'op>(&'op self, _prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>> {
928 Box::pin(async move { Err(Error::invalid_options("unexpected list")) })
929 }
930
931 fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>> {
932 let key = key.to_owned();
933 Box::pin(async move {
934 Ok(Some(ObjectMeta {
935 key,
936 size: 5,
937 etag: ETag::new("short-range"),
938 }))
939 })
940 }
941
942 fn put_if<'op>(
943 &'op self,
944 _key: &str,
945 _bytes: Arc<[u8]>,
946 _precondition: Precondition,
947 ) -> ObjectFuture<'op, PutIf> {
948 Box::pin(async move { Err(Error::invalid_options("unexpected put_if")) })
949 }
950 }
951
952 #[test]
953 fn put_then_get_roundtrips_and_overwrite_changes_etag() {
954 let store = InMemoryObjectStore::new();
955 let first = block_on(store.put("k", bytes(b"hello"))).unwrap();
956 assert_eq!(
957 block_on(store.get("k")).unwrap().as_deref(),
958 Some(b"hello".as_slice())
959 );
960 let second = block_on(store.put("k", bytes(b"world"))).unwrap();
961 assert_ne!(first, second, "overwrite mints a new ETag");
962 assert_eq!(
963 block_on(store.get("k")).unwrap().as_deref(),
964 Some(b"world".as_slice())
965 );
966 }
967
968 #[test]
969 fn get_absent_is_none_and_range_reads_a_window() {
970 let store = InMemoryObjectStore::new();
971 assert!(block_on(store.get("missing")).unwrap().is_none());
972 block_on(store.put("k", bytes(b"abcdef"))).unwrap();
973 assert_eq!(
974 block_on(store.get_range("k", 2, 3)).unwrap().as_ref(),
975 b"cde"
976 );
977 assert!(block_on(store.get_range("missing", 0, 1)).is_err());
979 assert!(block_on(store.get_range("k", 4, 10)).is_err());
980 }
981
982 #[test]
983 fn delete_is_idempotent() {
984 let store = InMemoryObjectStore::new();
985 block_on(store.put("k", bytes(b"x"))).unwrap();
986 block_on(store.delete("k")).unwrap();
987 assert!(block_on(store.get("k")).unwrap().is_none());
988 block_on(store.delete("k")).unwrap();
990 }
991
992 #[test]
993 fn list_returns_prefix_matches_in_key_order() {
994 let store = InMemoryObjectStore::new();
995 block_on(store.put("wal/2", bytes(b"b"))).unwrap();
996 block_on(store.put("wal/1", bytes(b"aa"))).unwrap();
997 block_on(store.put("table/9", bytes(b"c"))).unwrap();
998 let listed = block_on(store.list("wal/")).unwrap();
999 let keys: Vec<&str> = listed.iter().map(|meta| meta.key.as_str()).collect();
1000 assert_eq!(keys, ["wal/1", "wal/2"], "prefix-filtered, key-ordered");
1001 assert_eq!(listed[0].size, 2);
1002 assert_eq!(listed[1].size, 1);
1003 }
1004
1005 #[test]
1006 fn head_returns_metadata_without_bytes_and_none_when_absent() {
1007 let store = InMemoryObjectStore::new();
1008 assert!(block_on(store.head("k")).unwrap().is_none());
1009 let etag = block_on(store.put("k", bytes(b"hello"))).unwrap();
1010 let meta = block_on(store.head("k")).unwrap().expect("present");
1011 assert_eq!(meta.key, "k");
1012 assert_eq!(meta.size, 5);
1013 assert_eq!(meta.etag, etag);
1014 }
1015
1016 #[test]
1017 fn put_if_none_match_creates_only_when_absent() {
1018 let store = InMemoryObjectStore::new();
1019 let created = block_on(store.put_if("k", bytes(b"v1"), Precondition::IfNoneMatch)).unwrap();
1020 let etag = match created {
1021 PutIf::Stored { etag } => etag,
1022 PutIf::PreconditionFailed { .. } => panic!("create should succeed when absent"),
1023 };
1024 match block_on(store.put_if("k", bytes(b"v2"), Precondition::IfNoneMatch)).unwrap() {
1026 PutIf::PreconditionFailed { current } => assert_eq!(current, Some(etag)),
1027 PutIf::Stored { .. } => panic!("create should fail when present"),
1028 }
1029 assert_eq!(
1030 block_on(store.get("k")).unwrap().as_deref(),
1031 Some(b"v1".as_slice()),
1032 "refused create left the object unchanged"
1033 );
1034 }
1035
1036 #[test]
1037 fn put_if_match_is_a_compare_and_swap() {
1038 let store = InMemoryObjectStore::new();
1039 let v1 = block_on(store.put("k", bytes(b"v1"))).unwrap();
1040
1041 let v2 = match block_on(store.put_if("k", bytes(b"v2"), Precondition::IfMatch(v1.clone())))
1043 .unwrap()
1044 {
1045 PutIf::Stored { etag } => etag,
1046 PutIf::PreconditionFailed { .. } => panic!("CAS with current ETag should win"),
1047 };
1048 assert_ne!(v1, v2);
1049
1050 match block_on(store.put_if("k", bytes(b"v3"), Precondition::IfMatch(v1))).unwrap() {
1053 PutIf::PreconditionFailed { current } => assert_eq!(current, Some(v2)),
1054 PutIf::Stored { .. } => panic!("CAS with stale ETag should lose"),
1055 }
1056 assert_eq!(
1057 block_on(store.get("k")).unwrap().as_deref(),
1058 Some(b"v2".as_slice()),
1059 "the losing CAS left v2 in place"
1060 );
1061 }
1062
1063 #[test]
1064 fn put_if_match_on_absent_object_fails() {
1065 let store = InMemoryObjectStore::new();
1066 let phantom = ETag(Arc::from("etag-phantom"));
1067 match block_on(store.put_if("k", bytes(b"v"), Precondition::IfMatch(phantom))).unwrap() {
1068 PutIf::PreconditionFailed { current } => assert_eq!(current, None),
1069 PutIf::Stored { .. } => panic!("IfMatch cannot match a missing object"),
1070 }
1071 }
1072
1073 #[test]
1074 fn object_store_backend_round_trips_an_object() {
1075 use crate::storage::StorageObjectKind;
1076
1077 let backend = ObjectStoreBackend::new(Arc::new(InMemoryObjectStore::new()));
1078 let id = StorageObjectId::native_file(StorageObjectKind::Table, "/db/0001.trinet");
1079
1080 block_on(backend.write_object(id.clone(), bytes(b"hello world"), DurabilityMode::Flush))
1081 .unwrap();
1082
1083 assert_eq!(
1085 block_on(backend.read_object_bytes(id.clone()))
1086 .unwrap()
1087 .as_deref(),
1088 Some(b"hello world".as_slice())
1089 );
1090
1091 let object = block_on(backend.open_read(id.clone())).unwrap();
1093 assert_eq!(block_on(StorageReadObject::len(&object)).unwrap(), 11);
1094 let mut window = [0_u8; 5];
1095 block_on(StorageReadObject::read_exact_at(&object, 6, &mut window)).unwrap();
1096 assert_eq!(&window, b"world");
1097
1098 block_on(backend.delete_object(id.clone())).unwrap();
1100 assert!(block_on(backend.read_object_bytes(id)).unwrap().is_none());
1101 }
1102
1103 #[test]
1104 fn object_store_backend_rejects_oversized_head_before_get() {
1105 use crate::storage::StorageObjectKind;
1106
1107 let client = Arc::new(OversizedHeadClient::default());
1108 let backend = ObjectStoreBackend::new(client.clone());
1109 let id = StorageObjectId::native_file(StorageObjectKind::Table, "/db/huge.trinet");
1110
1111 let error =
1112 block_on(backend.read_object_bytes(id.clone())).expect_err("oversized head fails");
1113 assert!(error.to_string().contains("exceeds maximum"));
1114 assert_eq!(
1115 client.get_calls.load(Ordering::Relaxed),
1116 0,
1117 "oversized object should fail on HEAD before GET"
1118 );
1119
1120 let error = block_on(backend.open_read(id)).expect_err("oversized head fails");
1121 assert!(error.to_string().contains("exceeds maximum"));
1122 assert_eq!(
1123 client.get_calls.load(Ordering::Relaxed),
1124 0,
1125 "open_read should also fail on HEAD before GET"
1126 );
1127 }
1128
1129 #[test]
1130 fn object_store_backend_rejects_short_range_without_whole_get() {
1131 use crate::storage::StorageObjectKind;
1132
1133 let client = Arc::new(ShortRangeClient::default());
1134 let backend = ObjectStoreBackend::new(client.clone());
1135 let id = StorageObjectId::native_file(StorageObjectKind::Table, "/db/short.trinet");
1136
1137 let error = block_on(backend.read_object_bytes(id)).expect_err("short range fails");
1138 assert!(error.to_string().contains("declared length"));
1139 assert_eq!(
1140 client.get_calls.load(Ordering::Relaxed),
1141 0,
1142 "bounded range reads should not fall back to whole-object GET"
1143 );
1144 }
1145
1146 #[test]
1147 fn object_store_backend_lists_direct_children_by_extension() {
1148 use crate::storage::{StorageObjectKind, StorageObjectListRequest};
1149
1150 let backend = ObjectStoreBackend::new(Arc::new(InMemoryObjectStore::new()));
1151 let write = |key: &'static str| {
1152 block_on(backend.write_object(
1153 StorageObjectId::native_file(StorageObjectKind::Table, key),
1154 bytes(b"x"),
1155 DurabilityMode::Flush,
1156 ))
1157 .unwrap();
1158 };
1159 write("/db/0002.trinet");
1160 write("/db/0001.trinet");
1161 write("/db/MANIFEST"); write("/db/sub/9999.trinet"); let listed = block_on(
1165 backend.list_objects(
1166 StorageObjectListRequest::native_file(StorageObjectKind::Table, "/db")
1167 .with_file_extension("trinet"),
1168 ),
1169 )
1170 .unwrap();
1171 let paths: Vec<String> = listed
1172 .iter()
1173 .map(|id| id.path().to_string_lossy().into_owned())
1174 .collect();
1175 assert_eq!(
1176 paths,
1177 ["/db/0001.trinet", "/db/0002.trinet"],
1178 "only direct .trinet children, in key order"
1179 );
1180 }
1181}