Skip to main content

commonware_storage/journal/segmented/
glob.rs

1//! Simple section-based blob storage for values.
2//!
3//! This module provides a minimal blob storage optimized for storing values where
4//! the size is tracked externally (in an index entry). Unlike the segmented variable
5//! journal, this format does not include a size prefix since the caller already
6//! knows the size.
7//!
8//! # Format
9//!
10//! Each entry is stored as:
11//!
12//! ```text
13//! +---+---+---+---+---+---+---+---+---+---+---+---+
14//! |     Compressed Data (variable)    |   CRC32   |
15//! +---+---+---+---+---+---+---+---+---+---+---+---+
16//! ```
17//!
18//! - **Compressed Data**: zstd compressed (if enabled) or raw codec output
19//! - **CRC32**: 4-byte checksum of the compressed data
20//!
21//! # Read Flow
22//!
23//! 1. Get `(offset, size)` from index entry
24//! 2. Read `size` bytes directly from blob at byte offset
25//! 3. Last 4 bytes are CRC32, verify it
26//! 4. Decompress remaining bytes if compression enabled
27//! 5. Decode value
28
29use super::manager::{Config as ManagerConfig, Manager, WriteFactory};
30use crate::{Context, journal::Error};
31use commonware_codec::{Codec, CodecShared, FixedSize};
32use commonware_cryptography::{Crc32, crc32};
33#[cfg(any(test, feature = "test-utils"))]
34use commonware_runtime::{Blob as _, ReadOptions, Storage, WriteOptions};
35use commonware_runtime::{BufMut, Error as RError, Handle};
36use std::{io::Cursor, num::NonZeroUsize};
37use zstd::{bulk::compress, decode_all};
38
39/// Physical overhead appended to every frame: the CRC32 of the frame's data.
40pub(crate) const CHECKSUM_SIZE: usize = crc32::Digest::SIZE;
41
42/// Configuration for blob storage.
43#[derive(Clone)]
44pub struct Config<C> {
45    /// The partition to use for storing blobs.
46    pub partition: String,
47
48    /// Optional compression level (using `zstd`) to apply to data before storing.
49    pub compression: Option<u8>,
50
51    /// The codec configuration to use for encoding and decoding items.
52    pub codec_config: C,
53
54    /// The size of the write buffer to use for each blob.
55    pub write_buffer: NonZeroUsize,
56}
57
58/// The glob's state, boxed so the public [Glob] handle stays pointer-sized.
59struct Inner<E: Context, V: Codec> {
60    manager: Manager<E, WriteFactory>,
61
62    /// Compression level (if enabled).
63    compression: Option<u8>,
64
65    /// Codec configuration.
66    codec_config: V::Cfg,
67}
68
69impl<E: Context, V: CodecShared> Inner<E, V> {
70    /// See [Glob::init].
71    async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
72        let manager_cfg = ManagerConfig {
73            partition: cfg.partition,
74            factory: WriteFactory {
75                capacity: cfg.write_buffer,
76                pool: context.storage_buffer_pool().clone(),
77            },
78        };
79        let manager = Manager::init(context, manager_cfg).await?;
80
81        Ok(Self {
82            manager,
83            compression: cfg.compression,
84            codec_config: cfg.codec_config,
85        })
86    }
87
88    /// See [Glob::append].
89    async fn append(&mut self, section: u64, value: &V) -> Result<(u64, u32), Error> {
90        // Encode and optionally compress, then append checksum
91        let buf = if let Some(level) = self.compression {
92            // Compressed: encode first, then compress, then append checksum
93            let encoded = value.encode();
94            let mut compressed =
95                compress(&encoded, level as i32).map_err(|_| Error::CompressionFailed)?;
96            let checksum = Crc32::checksum(&compressed);
97            compressed.put_u32(checksum);
98            compressed
99        } else {
100            // Uncompressed: pre-allocate exact size to avoid copying
101            let entry_size = value.encode_size() + CHECKSUM_SIZE;
102            let mut buf = Vec::with_capacity(entry_size);
103            value.write(&mut buf);
104            let checksum = Crc32::checksum(&buf);
105            buf.put_u32(checksum);
106            buf
107        };
108
109        // Write to blob
110        let entry_size = u32::try_from(buf.len()).map_err(|_| Error::ValueTooLarge)?;
111        let writer = self.manager.get_or_create(section).await?;
112        let offset = writer.size();
113        writer.write_at(offset, buf).await.map_err(Error::Runtime)?;
114
115        Ok((offset, entry_size))
116    }
117
118    /// See [Glob::get].
119    async fn get(&self, section: u64, offset: u64, size: u32) -> Result<V, Error> {
120        let writer = self
121            .manager
122            .get(section)?
123            .ok_or(Error::SectionOutOfRange(section))?;
124
125        // Read via buffered writer (handles read-through for buffered data)
126        let buf = writer.read_at(offset, size as usize).await?.coalesce();
127
128        // Entry format: [compressed_data] [crc32 (4 bytes)]
129        if buf.len() < CHECKSUM_SIZE {
130            return Err(Error::Runtime(RError::BlobInsufficientLength));
131        }
132
133        let data_len = buf.len() - CHECKSUM_SIZE;
134        let compressed_data = &buf.as_ref()[..data_len];
135        let stored_checksum = u32::from_be_bytes(
136            buf.as_ref()[data_len..]
137                .try_into()
138                .expect("checksum is 4 bytes"),
139        );
140
141        // Verify checksum
142        let checksum = Crc32::checksum(compressed_data);
143        if checksum != stored_checksum {
144            return Err(Error::ChecksumMismatch(stored_checksum, checksum));
145        }
146
147        // Decompress if needed and decode
148        let value = if self.compression.is_some() {
149            let decompressed =
150                decode_all(Cursor::new(compressed_data)).map_err(|_| Error::DecompressionFailed)?;
151            V::decode_cfg(decompressed.as_ref(), &self.codec_config).map_err(Error::Codec)?
152        } else {
153            V::decode_cfg(compressed_data, &self.codec_config).map_err(Error::Codec)?
154        };
155
156        Ok(value)
157    }
158
159    /// See [Glob::verify].
160    async fn verify(&self, section: u64, offset: u64, size: u32) -> Result<bool, Error> {
161        // A frame is at least its checksum trailer.
162        if (size as usize) < CHECKSUM_SIZE {
163            return Ok(false);
164        }
165        let Some(writer) = self.manager.get(section)? else {
166            return Ok(false);
167        };
168
169        let buf = match writer.read_at(offset, size as usize).await {
170            Ok(buf) => buf.coalesce(),
171            Err(RError::BlobInsufficientLength | RError::OffsetOverflow) => return Ok(false),
172            Err(err) => return Err(Error::Runtime(err)),
173        };
174        let data_len = buf.len() - CHECKSUM_SIZE;
175        let stored_checksum = u32::from_be_bytes(
176            buf.as_ref()[data_len..]
177                .try_into()
178                .expect("checksum is 4 bytes"),
179        );
180        Ok(Crc32::checksum(&buf.as_ref()[..data_len]) == stored_checksum)
181    }
182
183    /// See [Glob::inject].
184    #[cfg(test)]
185    async fn inject(&mut self, section: u64, offset: u64, buf: Vec<u8>) -> Result<(), Error> {
186        let writer = self.manager.get_or_create(section).await?;
187        writer.write_at(offset, buf).await.map_err(Error::Runtime)
188    }
189
190    /// See [Glob::sync].
191    async fn sync(&mut self, sections: impl crate::Sections) -> Result<(), Error> {
192        self.manager.sync(sections).await
193    }
194
195    /// See [Glob::start_sync].
196    async fn start_sync(&mut self, sections: impl crate::Sections) -> Result<Handle<()>, Error> {
197        self.manager.start_sync(sections).await
198    }
199
200    /// See [Glob::sync_all].
201    async fn sync_all(&mut self) -> Result<(), Error> {
202        self.manager.sync_all().await
203    }
204
205    /// See [Glob::size].
206    fn size(&self, section: u64) -> Result<u64, Error> {
207        self.manager.size(section)
208    }
209
210    /// See [Glob::rewind].
211    async fn rewind(&mut self, section: u64, size: u64) -> Result<(), Error> {
212        self.manager.rewind(section, size).await
213    }
214
215    /// See [Glob::rewind_section].
216    async fn rewind_section(&mut self, section: u64, size: u64) -> Result<(), Error> {
217        self.manager.rewind_section(section, size).await
218    }
219
220    /// See [Glob::prune].
221    async fn prune(&mut self, min: u64) -> Result<bool, Error> {
222        self.manager.prune(min).await
223    }
224
225    /// See [Glob::pruned].
226    const fn pruned(&self, section: u64) -> bool {
227        self.manager.pruned(section)
228    }
229
230    /// See [Glob::oldest_section].
231    fn oldest_section(&self) -> Option<u64> {
232        self.manager.oldest_section()
233    }
234
235    /// See [Glob::newest_section].
236    fn newest_section(&self) -> Option<u64> {
237        self.manager.newest_section()
238    }
239
240    /// See [Glob::sections].
241    fn sections(&self) -> impl Iterator<Item = u64> + '_ {
242        self.manager.sections()
243    }
244
245    /// See [Glob::remove_section].
246    async fn remove_section(&mut self, section: u64) -> Result<bool, Error> {
247        self.manager.remove_section(section).await
248    }
249
250    /// See [Glob::destroy].
251    async fn destroy(self) -> Result<(), Error> {
252        self.manager.destroy().await
253    }
254}
255
256/// Simple section-based blob storage for values.
257///
258/// Uses [`buffer::Write`](commonware_runtime::buffer::Write) for batching writes.
259/// Reads go directly to blobs without any caching (ideal for large values that
260/// shouldn't pollute a page cache).
261///
262/// Mutating functions consume the glob and return it only on success: an error (or a dropped
263/// future) destroys the handle. Mutations on pruned sections fail with
264/// [Error::AlreadyPrunedToSection] without mutating. Check [Glob::pruned] first to keep the
265/// handle.
266pub struct Glob<E: Context, V: Codec>(Box<Inner<E, V>>);
267
268impl<E: Context, V: CodecShared> std::fmt::Debug for Glob<E, V> {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        f.debug_struct("Glob")
271            .field("oldest_section", &self.oldest_section())
272            .field("newest_section", &self.newest_section())
273            .finish_non_exhaustive()
274    }
275}
276
277impl<E: Context, V: CodecShared> Glob<E, V> {
278    /// Initialize blob storage, opening existing section blobs.
279    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
280        Ok(Self(Box::new(Inner::init(context, cfg).await?)))
281    }
282
283    /// Append value to section.
284    ///
285    /// The returned offset is the byte offset where the entry was written.
286    /// The returned size is the total bytes written (compressed_data + crc32).
287    /// Both should be stored in the index entry for later retrieval.
288    pub async fn append(mut self, section: u64, value: &V) -> Result<(Self, u64, u32), Error> {
289        let (offset, size) = self.0.append(section, value).await?;
290        Ok((self, offset, size))
291    }
292
293    /// Read value at offset with known size (from index entry).
294    ///
295    /// The offset should be the byte offset returned by `append()`.
296    /// Reads directly from blob without any caching.
297    pub async fn get(&self, section: u64, offset: u64, size: u32) -> Result<V, Error> {
298        self.0.get(section, offset, size).await
299    }
300
301    /// Check whether the entry at `(offset, size)` in `section` has a valid trailing checksum.
302    ///
303    /// Returns `Ok(false)` if the frame is smaller than its checksum trailer, the section
304    /// does not exist, the range is not fully covered by the section, or the checksum does
305    /// not match. Other read failures are propagated.
306    pub(super) async fn verify(&self, section: u64, offset: u64, size: u32) -> Result<bool, Error> {
307        self.0.verify(section, offset, size).await
308    }
309
310    /// Inject arbitrary bytes at `offset` in `section`, bypassing entry framing.
311    #[cfg(test)]
312    pub(super) async fn inject(
313        &mut self,
314        section: u64,
315        offset: u64,
316        buf: Vec<u8>,
317    ) -> Result<(), Error> {
318        self.0.inject(section, offset, buf).await
319    }
320
321    /// Sync the given `sections` to disk (flushes write buffers).
322    pub async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
323        self.0.sync(sections).await?;
324        Ok(self)
325    }
326
327    /// Start syncing the given `sections` to disk.
328    ///
329    /// An error reported by the returned [Handle] is fatal to the glob: the caller
330    /// must stop using the returned glob.
331    pub async fn start_sync(
332        mut self,
333        sections: impl crate::Sections,
334    ) -> Result<(Self, Handle<()>), Error> {
335        let handle = self.0.start_sync(sections).await?;
336        Ok((self, handle))
337    }
338
339    /// Sync all sections to disk.
340    pub async fn sync_all(mut self) -> Result<Self, Error> {
341        self.0.sync_all().await?;
342        Ok(self)
343    }
344
345    /// Get the current size of a section (including buffered data).
346    pub fn size(&self, section: u64) -> Result<u64, Error> {
347        self.0.size(section)
348    }
349
350    /// Rewind to a specific section and size.
351    ///
352    /// Truncates the section to the given size and removes all sections after it.
353    pub async fn rewind(mut self, section: u64, size: u64) -> Result<Self, Error> {
354        self.0.rewind(section, size).await?;
355        Ok(self)
356    }
357
358    /// Rewind only the given section to a specific size.
359    ///
360    /// Unlike `rewind`, this does not affect other sections.
361    pub async fn rewind_section(mut self, section: u64, size: u64) -> Result<Self, Error> {
362        self.0.rewind_section(section, size).await?;
363        Ok(self)
364    }
365
366    /// Prune sections before min.
367    pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
368        let pruned = self.0.prune(min).await?;
369        Ok((self, pruned))
370    }
371
372    /// Returns true when `section` is below the prune floor.
373    ///
374    /// The floor only tracks prunes from the current execution and resets at init, so a
375    /// section pruned in a previous execution reports false.
376    pub fn pruned(&self, section: u64) -> bool {
377        self.0.pruned(section)
378    }
379
380    /// Returns the number of the oldest section.
381    pub fn oldest_section(&self) -> Option<u64> {
382        self.0.oldest_section()
383    }
384
385    /// Returns the number of the newest section.
386    pub fn newest_section(&self) -> Option<u64> {
387        self.0.newest_section()
388    }
389
390    /// Returns an iterator over all section numbers.
391    pub fn sections(&self) -> impl Iterator<Item = u64> + '_ {
392        self.0.sections()
393    }
394
395    /// Remove a specific section. Returns true if the section existed and was removed.
396    pub async fn remove_section(mut self, section: u64) -> Result<(Self, bool), Error> {
397        let removed = self.0.remove_section(section).await?;
398        Ok((self, removed))
399    }
400
401    /// Destroy all blobs.
402    pub async fn destroy(self) -> Result<(), Error> {
403        self.0.destroy().await
404    }
405}
406
407/// Flip one byte inside value frame `frame` of the blob at `name`, breaking that frame's CRC
408/// while leaving every other frame valid. Models a value torn by a crash after its index entry
409/// became durable. Addresses uncompressed fixed-size frames: `frame_size` is the encoded value
410/// size plus its CRC32.
411#[cfg(any(test, feature = "test-utils"))]
412pub async fn corrupt_frame(
413    storage: &impl Storage,
414    partition: &str,
415    name: &[u8],
416    frame: u64,
417    frame_size: u64,
418) {
419    let offset = frame * frame_size;
420    let (blob, size) = storage.open(partition, name).await.unwrap();
421    assert!(offset < size, "corruption target must be inside the blob");
422    let byte = blob
423        .read_at(offset, 1, ReadOptions::default())
424        .await
425        .unwrap()
426        .coalesce();
427    blob.write_at(offset, vec![byte.as_ref()[0] ^ 0xFF], WriteOptions::SYNC)
428        .await
429        .unwrap();
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use commonware_macros::test_traced;
436    use commonware_runtime::{Runner, Supervisor as _, deterministic};
437    use commonware_utils::NZUsize;
438
439    fn test_cfg() -> Config<()> {
440        Config {
441            partition: "test-partition".into(),
442            compression: None,
443            codec_config: (),
444            write_buffer: NZUsize!(1024),
445        }
446    }
447
448    #[test_traced]
449    fn test_glob_append_and_get() {
450        let executor = deterministic::Runner::default();
451        executor.start(|context| async move {
452            let glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
453                .await
454                .expect("Failed to init glob");
455
456            // Append a value
457            let value: i32 = 42;
458            let (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
459            assert_eq!(offset, 0);
460
461            // Get the value back
462            let retrieved = glob.get(1, offset, size).await.expect("Failed to get");
463            assert_eq!(retrieved, value);
464
465            // Sync and verify
466            let glob = glob.sync(1).await.expect("Failed to sync");
467            let retrieved = glob.get(1, offset, size).await.expect("Failed to get");
468            assert_eq!(retrieved, value);
469
470            glob.destroy().await.expect("Failed to destroy");
471        });
472    }
473
474    #[test_traced]
475    fn test_glob_multiple_values() {
476        let executor = deterministic::Runner::default();
477        executor.start(|context| async move {
478            let mut glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
479                .await
480                .expect("Failed to init glob");
481
482            // Append multiple values
483            let values: Vec<i32> = vec![1, 2, 3, 4, 5];
484            let mut locations = Vec::new();
485
486            for value in &values {
487                let offset;
488                let size;
489                (glob, offset, size) = glob.append(1, value).await.expect("Failed to append");
490                locations.push((offset, size));
491            }
492
493            // Get all values back
494            for (i, (offset, size)) in locations.iter().enumerate() {
495                let retrieved = glob.get(1, *offset, *size).await.expect("Failed to get");
496                assert_eq!(retrieved, values[i]);
497            }
498
499            glob.destroy().await.expect("Failed to destroy");
500        });
501    }
502
503    #[test_traced]
504    fn test_glob_with_compression() {
505        let executor = deterministic::Runner::default();
506        executor.start(|context| async move {
507            let cfg = Config {
508                partition: "test-partition".into(),
509                compression: Some(3), // zstd level 3
510                codec_config: (),
511                write_buffer: NZUsize!(1024),
512            };
513            let glob: Glob<_, [u8; 100]> = Glob::init(context.child("storage"), cfg)
514                .await
515                .expect("Failed to init glob");
516
517            // Append a value
518            let value: [u8; 100] = [0u8; 100]; // Compressible data
519            let (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
520
521            // Size should be smaller due to compression
522            assert!(size < 100 + 4);
523
524            // Get the value back
525            let retrieved = glob.get(1, offset, size).await.expect("Failed to get");
526            assert_eq!(retrieved, value);
527
528            glob.destroy().await.expect("Failed to destroy");
529        });
530    }
531
532    #[test_traced]
533    fn test_glob_prune() {
534        let executor = deterministic::Runner::default();
535        executor.start(|context| async move {
536            let mut glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
537                .await
538                .expect("Failed to init glob");
539
540            // Append to multiple sections
541            for section in 1..=5 {
542                (glob, _, _) = glob
543                    .append(section, &(section as i32))
544                    .await
545                    .expect("Failed to append");
546                glob = glob.sync(section).await.expect("Failed to sync");
547            }
548
549            // Prune sections < 3
550            let (glob, _) = glob.prune(3).await.expect("Failed to prune");
551
552            // The public accessor mirrors the guard
553            assert!(glob.pruned(1));
554            assert!(glob.pruned(2));
555            assert!(!glob.pruned(3));
556
557            // Sections 1 and 2 should be gone
558            assert!(glob.get(1, 0, 8).await.is_err());
559            assert!(glob.get(2, 0, 8).await.is_err());
560
561            // Sections 3-5 should still exist
562            assert!(glob.0.manager.blobs.contains_key(&3));
563            assert!(glob.0.manager.blobs.contains_key(&4));
564            assert!(glob.0.manager.blobs.contains_key(&5));
565
566            glob.destroy().await.expect("Failed to destroy");
567        });
568    }
569
570    #[test_traced]
571    fn test_glob_checksum_mismatch() {
572        let executor = deterministic::Runner::default();
573        executor.start(|context| async move {
574            let glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
575                .await
576                .expect("Failed to init glob");
577
578            // Append a value
579            let value: i32 = 42;
580            let (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
581            let mut glob = glob.sync(1).await.expect("Failed to sync");
582
583            // Corrupt the data by writing directly to the underlying blob
584            let writer = glob.0.manager.blobs.get_mut(&1).unwrap();
585            writer
586                .write_at(offset, vec![0xFF, 0xFF, 0xFF, 0xFF])
587                .await
588                .expect("Failed to corrupt");
589            writer.sync().await.expect("Failed to sync");
590
591            // Get should fail with checksum mismatch
592            let result = glob.get(1, offset, size).await;
593            assert!(matches!(result, Err(Error::ChecksumMismatch(_, _))));
594
595            glob.destroy().await.expect("Failed to destroy");
596        });
597    }
598
599    #[test_traced]
600    fn test_glob_rewind() {
601        let executor = deterministic::Runner::default();
602        executor.start(|context| async move {
603            let mut glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
604                .await
605                .expect("Failed to init glob");
606
607            // Append multiple values and track sizes
608            let values: Vec<i32> = vec![1, 2, 3, 4, 5];
609            let mut locations = Vec::new();
610
611            for value in &values {
612                let offset;
613                let size;
614                (glob, offset, size) = glob.append(1, value).await.expect("Failed to append");
615                locations.push((offset, size));
616            }
617            glob = glob.sync(1).await.expect("Failed to sync");
618
619            // Rewind to after the third value
620            let (third_offset, third_size) = locations[2];
621            let rewind_size = third_offset + u64::from(third_size);
622            let glob = glob
623                .rewind_section(1, rewind_size)
624                .await
625                .expect("Failed to rewind");
626
627            // First three values should still be readable
628            for (i, (offset, size)) in locations.iter().take(3).enumerate() {
629                let retrieved = glob.get(1, *offset, *size).await.expect("Failed to get");
630                assert_eq!(retrieved, values[i]);
631            }
632
633            // Fourth and fifth values should fail (reading past end of blob)
634            let (fourth_offset, fourth_size) = locations[3];
635            let result = glob.get(1, fourth_offset, fourth_size).await;
636            assert!(result.is_err());
637
638            glob.destroy().await.expect("Failed to destroy");
639        });
640    }
641
642    #[test_traced]
643    fn test_glob_persistence() {
644        let executor = deterministic::Runner::default();
645        executor.start(|context| async move {
646            let cfg = test_cfg();
647
648            // Create and populate glob
649            let glob: Glob<_, i32> = Glob::init(context.child("first"), cfg.clone())
650                .await
651                .expect("Failed to init glob");
652
653            let value: i32 = 42;
654            let (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
655            let glob = glob.sync(1).await.expect("Failed to sync");
656            drop(glob);
657
658            // Reopen and verify
659            let glob: Glob<_, i32> = Glob::init(context.child("second"), cfg)
660                .await
661                .expect("Failed to reinit glob");
662
663            let retrieved = glob.get(1, offset, size).await.expect("Failed to get");
664            assert_eq!(retrieved, value);
665
666            glob.destroy().await.expect("Failed to destroy");
667        });
668    }
669
670    #[test_traced]
671    fn test_glob_get_invalid_size() {
672        let executor = deterministic::Runner::default();
673        executor.start(|context| async move {
674            let glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
675                .await
676                .expect("Failed to init glob");
677
678            let (glob, offset, _size) = glob.append(1, &42).await.expect("Failed to append");
679            let glob = glob.sync(1).await.expect("Failed to sync");
680
681            // Size 0 - should fail
682            assert!(glob.get(1, offset, 0).await.is_err());
683
684            // Size < CRC_SIZE (1, 2, 3 bytes) - should fail with BlobInsufficientLength
685            for size in 1..4u32 {
686                let result = glob.get(1, offset, size).await;
687                assert!(matches!(
688                    result,
689                    Err(Error::Runtime(RError::BlobInsufficientLength))
690                ));
691            }
692
693            glob.destroy().await.expect("Failed to destroy");
694        });
695    }
696
697    #[test_traced]
698    fn test_glob_get_wrong_size() {
699        let executor = deterministic::Runner::default();
700        executor.start(|context| async move {
701            let glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
702                .await
703                .expect("Failed to init glob");
704
705            let (glob, offset, correct_size) = glob.append(1, &42).await.expect("Failed to append");
706            let glob = glob.sync(1).await.expect("Failed to sync");
707
708            // Size too small (but >= CRC_SIZE) - checksum mismatch
709            let result = glob.get(1, offset, correct_size - 1).await;
710            assert!(matches!(result, Err(Error::ChecksumMismatch(_, _))));
711
712            glob.destroy().await.expect("Failed to destroy");
713        });
714    }
715}