Skip to main content

fxfs_platform_testing/fuchsia/fxblob/
blob.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! This module contains the [`FxBlob`] node type used to represent an immutable blob persisted to
6//! disk which can be read back.
7
8use crate::constants::*;
9use crate::fuchsia::directory::FxDirectory;
10use crate::fuchsia::errors::map_to_status;
11use crate::fuchsia::node::{FxNode, OpenedNode};
12use crate::fuchsia::pager::{
13    MarkDirtyRange, PageInRange, PagerBacked, PagerPacketReceiverRegistration, default_page_in,
14};
15use crate::fuchsia::volume::{FxVolume, READ_AHEAD_SIZE};
16use anyhow::{Context, Error, anyhow, bail, ensure};
17use delivery_blob::compression::{CompressionAlgorithm, CompressionInfo};
18use fidl_fuchsia_feedback::{Annotation, Attachment, CrashReport};
19use fidl_fuchsia_mem::Buffer as MemBuffer;
20use fuchsia_component_client::connect_to_protocol;
21use fuchsia_merkle::{Hash, MerkleVerifier, ReadSizedMerkleVerifier};
22use futures::{StreamExt, TryStreamExt, try_join};
23use fxfs::blob_metadata::{BlobFormat, BlobMetadata, FxfsBlobMetadataExt};
24use fxfs::errors::FxfsError;
25use fxfs::lock_keys;
26use fxfs::log::*;
27use fxfs::object_handle::ObjectHandle;
28use fxfs::object_store::transaction::LockKey;
29use fxfs::object_store::{AttributeId, DataObjectHandle, ObjectDescriptor, StoreObjectHandle};
30use fxfs::round::round_down;
31use fxfs_macros::ToWeakNode;
32use mapping::Extent as MappingExtent;
33use refaults_vmo::AtomicBitVec;
34use std::ops::Range;
35use std::sync::Arc;
36use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
37use storage_device::buffer::{Buffer, BufferFuture, MutableBufferRef};
38use zx::Status;
39
40/// The extent mappings for a blob's data and merkle attributes.
41///
42/// Each `MappingExtent` maps a logical byte range to a physical device offset.
43/// Extents are sorted by logical offset and non-overlapping.
44pub struct BlobExtents {
45    pub data: Vec<MappingExtent>,
46    pub merkle: Vec<MappingExtent>,
47}
48
49// When the top bit of the open count is set, it means the file has been deleted and when the count
50// drops to zero, it will be tombstoned.  Once it has dropped to zero, it cannot be opened again
51// (assertions will fire).
52const PURGED: usize = 1 << (usize::BITS - 1);
53
54const EMPTY_BLOB_HASH: Hash = Hash::from_array([
55    0x15, 0xec, 0x7b, 0xf0, 0xb5, 0x07, 0x32, 0xb4, 0x9f, 0x82, 0x28, 0xe0, 0x7d, 0x24, 0x36, 0x53,
56    0x38, 0xf9, 0xe3, 0xab, 0x99, 0x4b, 0x00, 0xaf, 0x08, 0xe5, 0xa3, 0xbf, 0xfe, 0x55, 0xfd, 0x8b,
57]);
58
59/// Represents an immutable blob stored on Fxfs with associated an merkle tree.
60#[derive(ToWeakNode)]
61pub struct FxBlob {
62    handle: StoreObjectHandle<FxVolume>,
63    vmo: zx::Vmo,
64    open_count: AtomicUsize,
65    merkle_root: Hash,
66    merkle_verifier: ReadSizedMerkleVerifier,
67    compression_info: Option<CompressionInfo>,
68    uncompressed_size: u64, // always set.
69    pager_packet_receiver_registration: Arc<PagerPacketReceiverRegistration<Self>>,
70    chunks_supplied: AtomicBitVec,
71}
72
73// Fuchsia can have many open blobs at once. The size of FxBlob is important.
74static_assertions::const_assert!(size_of::<FxBlob>() <= 192);
75
76impl FxBlob {
77    pub async fn new(
78        handle: StoreObjectHandle<FxVolume>,
79        merkle_root: Hash,
80    ) -> Result<Arc<Self>, Error> {
81        let stored_size =
82            handle.store().get_attribute_size(handle.object_id(), AttributeId::DATA).await?;
83        let metadata = BlobMetadata::read_from(&handle).await?;
84        let (uncompressed_size, compression_info) = match &metadata.format {
85            BlobFormat::Uncompressed => (stored_size, None),
86            BlobFormat::ChunkedZstd { uncompressed_size, chunk_size, compressed_offsets } => (
87                *uncompressed_size,
88                Some(CompressionInfo::new(
89                    *chunk_size,
90                    stored_size,
91                    compressed_offsets,
92                    CompressionAlgorithm::Zstd,
93                )?),
94            ),
95            BlobFormat::ChunkedLz4 { uncompressed_size, chunk_size, compressed_offsets } => (
96                *uncompressed_size,
97                Some(CompressionInfo::new(
98                    *chunk_size,
99                    stored_size,
100                    compressed_offsets,
101                    CompressionAlgorithm::Lz4,
102                )?),
103            ),
104        };
105
106        // Check the uncompressed size of the blob against the number of merkle leaves. This only
107        // ensures that the uncompressed size lands within the same merkle block as the true
108        // uncompressed size. To fully validate the uncompressed size requires reading in the last
109        // merkle block and verifying it.
110        let expect_merkle_hashes = if uncompressed_size <= fuchsia_merkle::BLOCK_SIZE as u64 {
111            0
112        } else {
113            uncompressed_size.div_ceil(fuchsia_merkle::BLOCK_SIZE as u64)
114        };
115        ensure!(
116            metadata.merkle_leaves.len() as u64 == expect_merkle_hashes,
117            FxfsError::IntegrityError
118        );
119        // Fully validate the empty blob and its size.
120        ensure!(
121            (uncompressed_size == 0) == (merkle_root == EMPTY_BLOB_HASH),
122            FxfsError::IntegrityError
123        );
124        let merkle_verifier = metadata.into_merkle_verifier(merkle_root)?;
125
126        let min_chunk_size = min_chunk_size(&compression_info);
127        let merkle_verifier =
128            ReadSizedMerkleVerifier::new(merkle_verifier, min_chunk_size as usize)?;
129        let chunks_supplied = AtomicBitVec::new(uncompressed_size.div_ceil(min_chunk_size));
130
131        Ok(Arc::new_cyclic(|weak| {
132            let (vmo, pager_packet_receiver_registration) = handle
133                .owner()
134                .pager()
135                .create_vmo(weak.clone(), uncompressed_size, zx::VmoOptions::empty())
136                .unwrap();
137            set_vmo_name(&vmo, &merkle_root);
138            Self {
139                handle,
140                vmo,
141                open_count: AtomicUsize::new(0),
142                merkle_root,
143                merkle_verifier,
144                compression_info,
145                uncompressed_size,
146                pager_packet_receiver_registration: Arc::new(pager_packet_receiver_registration),
147                chunks_supplied,
148            }
149        }))
150    }
151
152    /// Returns the new blob.
153    pub fn overwrite_me(
154        self: &Arc<Self>,
155        handle: DataObjectHandle<FxVolume>,
156        merkle_verifier: MerkleVerifier,
157        compression_info: Option<CompressionInfo>,
158    ) -> Arc<Self> {
159        let min_chunk_size = min_chunk_size(&compression_info);
160        let merkle_verifier =
161            ReadSizedMerkleVerifier::new(merkle_verifier, min_chunk_size as usize)
162                .expect("The chunk size should have been validated by the delivery blob parser");
163        // The chunk size may have changed between the old blob and the new blob. Preserving the
164        // chunks supplied bits isn't important.
165        let chunks_supplied = AtomicBitVec::new(self.uncompressed_size.div_ceil(min_chunk_size));
166        let vmo = self.vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
167
168        let new_blob = Arc::new(Self {
169            handle: handle.into_store_object_handle(),
170            vmo,
171            open_count: AtomicUsize::new(0),
172            merkle_root: self.merkle_root,
173            merkle_verifier,
174            compression_info,
175            uncompressed_size: self.uncompressed_size,
176            pager_packet_receiver_registration: self.pager_packet_receiver_registration.clone(),
177            chunks_supplied,
178        });
179
180        // We have tests that rely on the cache being purged and there are races where the
181        // `FxBlob::drop` isn't called early enough, which can make the test flaky.
182        self.handle.owner().cache().remove(self.as_ref());
183
184        // Lock must be held until the open counts is incremented to prevent concurrent handling of
185        // zero children signals.
186        let receiver_lock =
187            self.pager_packet_receiver_registration.receiver().set_receiver(&new_blob);
188        if receiver_lock.is_strong() {
189            // If there was a strong moved between them, then the counts exchange as well. It is
190            // only important that the increment happen under the lock as it may handle the next
191            // zero children signal, no new requests can now go to the old blob, and because
192            // existing requests hold an open count reference using `try_keep_open` for the duration
193            // of the request, we can immediately decrement the open count of the old blob.
194            new_blob.open_count_add_one();
195            self.clone().open_count_sub_one();
196        }
197        new_blob
198    }
199
200    pub fn root(&self) -> Hash {
201        self.merkle_root
202    }
203
204    /// Returns the extents mapping logical offsets to device offsets for both merkle and data
205    /// attributes. Each `MappingExtent` maps a logical byte range to a physical device offset.
206    /// Extents are sorted by logical offset and non-overlapping.
207    pub async fn get_mapping_extents(&self) -> Result<BlobExtents, Error> {
208        let tree = &self.handle.store().tree();
209        let layer_set = tree.layer_set();
210
211        // Query data extents
212        let mut merger = layer_set.merger();
213        let stream = self.handle.extent_stream(&mut merger, AttributeId::DATA).await?;
214        // TODO(https://fxbug.dev/535489428): Remove use of vec here.
215        let data = stream
216            .map(|res| {
217                let extent = res?;
218                MappingExtent::try_new(extent.logical_range(), Some(extent.device_range().start))
219                    .map_err(|_| anyhow!(FxfsError::Inconsistent))
220            })
221            .try_collect()
222            .await?;
223
224        // Query Merkle extents
225        let mut merger = layer_set.merger();
226        let stream = self.handle.extent_stream(&mut merger, AttributeId::BLOB_METADATA).await?;
227        let merkle = stream
228            .map(|res| {
229                let extent = res?;
230                MappingExtent::try_new(extent.logical_range(), Some(extent.device_range().start))
231                    .map_err(|_| anyhow!(FxfsError::Inconsistent))
232            })
233            .try_collect()
234            .await?;
235
236        Ok(BlobExtents { data, merkle })
237    }
238
239    /// Returns the stored (on-disk) byte size of the blob data attribute.
240    pub async fn stored_size(&self) -> Result<u64, Error> {
241        self.handle.store().get_attribute_size(self.handle.object_id(), AttributeId::DATA).await
242    }
243
244    fn record_page_fault_metric(&self, range: &Range<u64>) {
245        let chunk_size: u64 = min_chunk_size(&self.compression_info);
246
247        let first_chunk = range.start / chunk_size;
248        // The end of the range may not be chunk aligned if it's the last chunk.
249        let last_chunk = range.end.div_ceil(chunk_size);
250
251        let supplied_count = self.chunks_supplied.test_and_set_range(first_chunk, last_chunk);
252
253        if supplied_count > 0 {
254            // The counter is expressed in pages, and chunks are a multiple of the page size.  The
255            // last chunk of a blob may be partial, in which case this slightly overestimates.
256            let pages_per_chunk = chunk_size / zx::system_get_page_size() as u64;
257            self.handle
258                .owner()
259                .blob_resupplied_count()
260                .increment(supplied_count * pages_per_chunk, Ordering::Relaxed);
261        }
262    }
263
264    /// Allocates a device buffer for use with hardware/driver reads.
265    fn allocate_buffer(&self, size: u64) -> BufferFuture<'_> {
266        self.handle.store().device().allocate_buffer(size as usize)
267    }
268
269    /// Allocates a trusted buffer that no other processes or drivers can access,
270    /// suitable for zero-copy decompression and Merkle tree verification.
271    fn allocate_trusted_buffer(&self, size: u64) -> BufferFuture<'_> {
272        self.handle.owner().blob_allocator().allocate_buffer(size as usize)
273    }
274
275    async fn read_blocks(&self, offset: u64, buf: MutableBufferRef<'_>) -> Result<(), Error> {
276        let fs = self.handle.store().filesystem();
277        let guard = fs
278            .lock_manager()
279            .read_lock(lock_keys![LockKey::object_attribute(
280                self.handle.store().store_object_id(),
281                self.handle.object_id(),
282                AttributeId::DATA,
283            )])
284            .await;
285        self.handle.read_aligned_unchecked(AttributeId::DATA, offset, buf, &guard).await
286    }
287}
288
289impl Drop for FxBlob {
290    fn drop(&mut self) {
291        let volume = self.handle.owner();
292        volume.cache().remove(self);
293    }
294}
295
296impl OpenedNode<FxBlob> {
297    /// Creates a read-only child VMO for this blob backed by the pager. The blob cannot be purged
298    /// until all child VMOs have been destroyed.
299    ///
300    /// *WARNING*: We need to ensure the open count is non-zero before invoking this function, so
301    /// it is only implemented for [`OpenedNode<FxBlob>`]. This prevents the blob from being purged
302    /// before we get a chance to register it with the pager for [`zx::Signals::VMO_ZERO_CHILDREN`].
303    pub fn create_child_vmo(&self) -> Result<zx::Vmo, Status> {
304        let blob = self.0.as_ref();
305        let child_vmo = blob.vmo.create_child(
306            zx::VmoChildOptions::REFERENCE | zx::VmoChildOptions::NO_WRITE,
307            0,
308            0,
309        )?;
310        if blob.handle.owner().pager().watch_for_zero_children(blob).map_err(map_to_status)? {
311            // Take an open count so that we keep this object alive if it is otherwise closed. This
312            // is only valid since we know the current open count is non-zero, otherwise we might
313            // increment the open count after the blob has been purged.
314            blob.open_count_add_one();
315        }
316        Ok(child_vmo)
317    }
318}
319
320impl FxNode for FxBlob {
321    fn object_id(&self) -> u64 {
322        self.handle.object_id()
323    }
324
325    fn parent(&self) -> Option<Arc<FxDirectory>> {
326        unreachable!(); // Add a parent back-reference if needed.
327    }
328
329    fn set_parent(&self, _parent: Arc<FxDirectory>) {
330        // NOP
331    }
332
333    fn open_count_add_one(&self) {
334        let old = self.open_count.fetch_add(1, Ordering::Relaxed);
335        assert!(old != PURGED && old != PURGED - 1);
336    }
337
338    fn open_count_sub_one(self: Arc<Self>) {
339        let old = self.open_count.fetch_sub(1, Ordering::Relaxed);
340        assert!(old & !PURGED > 0);
341        if old == PURGED + 1 {
342            let store = self.handle.store();
343            store
344                .filesystem()
345                .graveyard()
346                .queue_tombstone_object(store.store_object_id(), self.object_id());
347        }
348    }
349
350    fn object_descriptor(&self) -> ObjectDescriptor {
351        ObjectDescriptor::File
352    }
353
354    fn terminate(&self) {
355        self.pager_packet_receiver_registration.stop_watching_for_zero_children();
356    }
357
358    fn mark_to_be_purged(self: Arc<Self>) {
359        let old = self.open_count.fetch_or(PURGED, Ordering::Relaxed);
360        assert!(old & PURGED == 0);
361        if old == 0 {
362            let store = self.handle.store();
363            store
364                .filesystem()
365                .graveyard()
366                .queue_tombstone_object(store.store_object_id(), self.object_id());
367        }
368    }
369}
370
371impl PagerBacked for FxBlob {
372    fn try_keep_open(self: Arc<Self>) -> Result<OpenedNode<Self>, Arc<Self>> {
373        let mut old = self.open_count.load(Ordering::Relaxed);
374        loop {
375            if old & !PURGED == 0 {
376                return Err(self);
377            }
378            assert!(old & !PURGED < PURGED - 1);
379            match self.open_count.compare_exchange_weak(
380                old,
381                old + 1,
382                Ordering::Relaxed,
383                Ordering::Relaxed,
384            ) {
385                Ok(_) => return Ok(OpenedNode(self)),
386                Err(new_value) => old = new_value,
387            }
388        }
389    }
390
391    fn pager(&self) -> &crate::pager::Pager {
392        self.handle.owner().pager()
393    }
394
395    fn pager_packet_receiver_registration(&self) -> &PagerPacketReceiverRegistration<Self> {
396        &self.pager_packet_receiver_registration
397    }
398
399    fn vmo(&self) -> &zx::Vmo {
400        &self.vmo
401    }
402
403    fn page_in(self: Arc<Self>, range: PageInRange<Self>) {
404        let read_ahead_size = if let Some(compression_info) = &self.compression_info {
405            read_ahead_size_for_chunk_size(compression_info.chunk_size(), READ_AHEAD_SIZE)
406        } else {
407            READ_AHEAD_SIZE
408        };
409        // Delegate to the generic page handling code.
410        default_page_in(self, range, read_ahead_size)
411    }
412
413    fn mark_dirty(self: Arc<Self>, _range: MarkDirtyRange<Self>) {
414        unreachable!();
415    }
416
417    fn on_zero_children(self: Arc<Self>) {
418        self.open_count_sub_one();
419    }
420
421    fn byte_size(&self) -> u64 {
422        self.uncompressed_size
423    }
424
425    async fn aligned_read(&self, range: Range<u64>) -> Result<Buffer<'_>, Error> {
426        // The vmo shouldn't have full pages beyond the end of the blob so we shouldn't be getting
427        // page faults for ranges beyond the end of the blob.
428        ensure!(range.start < self.uncompressed_size, FxfsError::InvalidArgs);
429        self.record_page_fault_metric(&range);
430
431        let mut buffer = match self.compression_info {
432            Some(_) => self.allocate_trusted_buffer(range.end - range.start).await,
433            None => self.allocate_buffer(range.end - range.start).await,
434        };
435        let unaligned_bytes =
436            (std::cmp::min(range.end, self.uncompressed_size) - range.start) as usize;
437        match &self.compression_info {
438            None => self.read_blocks(range.start, buffer.as_mut()).await?,
439            Some(compression_info) => {
440                let compressed_offsets =
441                    compression_info.compressed_range_for_uncompressed_range(&range)?;
442                let bs = self.handle.block_size();
443                let aligned = bs.align_range_outwards(&compressed_offsets).unwrap();
444                let mut compressed_buf = self.allocate_buffer(aligned.end - aligned.start).await;
445
446                let mut decompression_errors = 0;
447                loop {
448                    try_join!(self.read_blocks(aligned.start, compressed_buf.as_mut()), async {
449                        buffer
450                            .allocator()
451                            .buffer_source()
452                            .commit_range(buffer.range())
453                            .map_err(|e| e.into())
454                    })
455                    .with_context(|| {
456                        format!(
457                            "Failed to read compressed range {:?}, len {}",
458                            aligned,
459                            compression_info.compressed_size()
460                        )
461                    })?;
462                    let compressed_buf_range = (compressed_offsets.start - aligned.start) as usize
463                        ..(compressed_offsets.end - aligned.start) as usize;
464
465                    let decompression_result = {
466                        fxfs_trace::duration!("blob-decompress", "len" => unaligned_bytes);
467                        compression_info.decompress(
468                            compressed_buf.as_ptr_slice().subslice(compressed_buf_range),
469                            buffer.subslice_mut(0..unaligned_bytes).try_as_mut_slice().unwrap(),
470                            range.start,
471                        )
472                    };
473                    match decompression_result {
474                        Ok(()) => break,
475                        Err(error) => {
476                            record_decompression_error_crash_report(
477                                &compressed_buf.to_vec(),
478                                &range,
479                                &compressed_offsets,
480                                &self.merkle_root,
481                            )
482                            .await;
483                            decompression_errors += 1;
484                            if decompression_errors == 2 {
485                                bail!(
486                                    anyhow!(FxfsError::IntegrityError)
487                                        .context(format!("Decompression error: {error:?}"))
488                                );
489                            } else {
490                                warn!(error:?; "Decompression error; retrying");
491                            }
492                        }
493                    }
494                } // loop
495                if decompression_errors > 0 {
496                    info!("Read succeeded on second attempt");
497                }
498            }
499        };
500        // Zero the tail before verification.
501        buffer.subslice_mut(unaligned_bytes..buffer.len()).fill(0);
502        {
503            // TODO(https://fxbug.dev/42073035): This should be offloaded to the kernel at which
504            // point we can delete this.
505            fxfs_trace::duration!("blob-verify", "len" => unaligned_bytes);
506            self.merkle_verifier.verify_aligned(
507                range.start as usize,
508                buffer.as_ptr_slice().subslice(0..buffer.len()),
509                unaligned_bytes,
510            )?;
511        }
512        Ok(buffer)
513    }
514}
515
516fn set_vmo_name(vmo: &zx::Vmo, merkle_root: &Hash) {
517    let trimmed_merkle = &merkle_root.to_string()[0..BLOB_NAME_HASH_LENGTH];
518    let name = format!("{BLOB_NAME_PREFIX}{trimmed_merkle}");
519    let name = zx::Name::new(&name).unwrap();
520    vmo.set_name(&name).unwrap();
521}
522
523fn min_chunk_size(compression_info: &Option<CompressionInfo>) -> u64 {
524    if let Some(compression_info) = compression_info {
525        read_ahead_size_for_chunk_size(compression_info.chunk_size(), READ_AHEAD_SIZE)
526    } else {
527        READ_AHEAD_SIZE
528    }
529}
530
531fn read_ahead_size_for_chunk_size(chunk_size: u64, suggested_read_ahead_size: u64) -> u64 {
532    if chunk_size >= suggested_read_ahead_size {
533        chunk_size
534    } else {
535        round_down(suggested_read_ahead_size, chunk_size)
536    }
537}
538
539async fn record_decompression_error_crash_report(
540    compressed_buf: &[u8],
541    uncompressed_offsets: &Range<u64>,
542    compressed_offsets: &Range<u64>,
543    merkle_root: &Hash,
544) {
545    static DONE_ONCE: AtomicBool = AtomicBool::new(false);
546    if !DONE_ONCE.swap(true, Ordering::Relaxed) {
547        if let Ok(proxy) = connect_to_protocol::<fidl_fuchsia_feedback::CrashReporterMarker>() {
548            let size = compressed_buf.len() as u64;
549            let vmo = zx::Vmo::create(size).unwrap();
550            vmo.write(compressed_buf, 0).unwrap();
551            if let Err(e) = proxy
552                .file_report(CrashReport {
553                    program_name: Some("fxfs".to_string()),
554                    crash_signature: Some("fuchsia-fxfs-decompression_error".to_string()),
555                    is_fatal: Some(false),
556                    annotations: Some(vec![
557                        Annotation {
558                            key: "fxfs.range".to_string(),
559                            value: format!("{:?}", uncompressed_offsets),
560                        },
561                        Annotation {
562                            key: "fxfs.compressed_offsets".to_string(),
563                            value: format!("{:?}", compressed_offsets),
564                        },
565                        Annotation {
566                            key: "fxfs.merkle_root".to_string(),
567                            value: format!("{}", merkle_root),
568                        },
569                    ]),
570                    attachments: Some(vec![Attachment {
571                        key: "fxfs_compressed_data".to_string(),
572                        value: MemBuffer { vmo, size },
573                    }]),
574                    ..Default::default()
575                })
576                .await
577            {
578                error!(e:?; "Failed to file crash report");
579            } else {
580                warn!("Filed crash report for decompression error");
581            }
582        } else {
583            error!("Failed to connect to crash report service");
584        }
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591    use crate::fuchsia::fxblob::testing::{BlobFixture, new_blob_fixture};
592    use crate::fuchsia::pager::PageInRange;
593    use crate::fxblob::testing::open_blob_fixture;
594    use assert_matches::assert_matches;
595    use delivery_blob::CompressionMode;
596    use delivery_blob::compression::{ChunkedArchiveOptions, CompressionAlgorithm};
597
598    use fuchsia_async::epoch::Epoch;
599    use fxfs_make_blob_image::FxBlobBuilder;
600    use storage_device::DeviceHolder;
601    use storage_device::fake_device::FakeDevice;
602    use storage_units::page_size;
603
604    const BLOCK_SIZE: u64 = fuchsia_merkle::BLOCK_SIZE as u64;
605    const CHUNK_SIZE: usize = 32 * 1024;
606
607    #[fuchsia::test(threads = 10)]
608    async fn test_empty_blob() {
609        let fixture = new_blob_fixture().await;
610
611        let data = vec![];
612        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
613        assert_eq!(fixture.read_blob(hash).await, data);
614
615        fixture.close().await;
616    }
617
618    #[fuchsia::test(threads = 10)]
619    async fn test_large_blob() {
620        let fixture = new_blob_fixture().await;
621
622        let data = vec![3; 3_000_000];
623        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
624
625        assert_eq!(fixture.read_blob(hash).await, data);
626
627        fixture.close().await;
628    }
629
630    #[fuchsia::test(threads = 10)]
631    async fn test_get_multiple_mapping_extents() {
632        let fixture = new_blob_fixture().await;
633
634        {
635            let data = vec![0xff; 3_000_000];
636            let hash = fixture.write_blob(&data, CompressionMode::Never).await;
637            let name = format!("{}", hash);
638
639            let handle = fixture.get_blob_handle(&name).await;
640            let mut transaction =
641                handle.new_transaction().await.expect("failed to create transaction");
642            let mut buf = handle.allocate_buffer(8192).await;
643            buf.as_mut_ptr_slice().fill(0xaa);
644            handle
645                .txn_write(&mut transaction, 0, buf.as_ref())
646                .await
647                .expect("txn_write at offset 0 failed");
648            handle
649                .txn_write(&mut transaction, 16384, buf.as_ref())
650                .await
651                .expect("txn_write at offset 16384 failed");
652            transaction.commit().await.expect("failed to commit transaction");
653
654            let blob = fixture.get_blob(hash).await.expect("getting blob failed");
655            let extents = blob.get_mapping_extents().await.expect("get_mapping_extents failed");
656
657            assert!(!extents.merkle.is_empty(), "Expected at least one merkle extent");
658            assert!(
659                extents.data.len() > 1,
660                "Expected multiple data extents, got {}",
661                extents.data.len()
662            );
663
664            // The overwritten extents should each be exactly 8192 bytes.
665            let overwrite_extent = extents
666                .data
667                .iter()
668                .find(|e| e.logical_range().start == 0)
669                .expect("extent at offset 0");
670            assert_eq!(
671                overwrite_extent.logical_range().end - overwrite_extent.logical_range().start,
672                8192
673            );
674            let overwrite_extent = extents
675                .data
676                .iter()
677                .find(|e| e.logical_range().start == 16384)
678                .expect("extent at offset 16384");
679            assert_eq!(
680                overwrite_extent.logical_range().end - overwrite_extent.logical_range().start,
681                8192
682            );
683        }
684
685        fixture.close().await;
686    }
687
688    #[fuchsia::test(threads = 10)]
689    async fn test_large_compressed_blob() {
690        let fixture = new_blob_fixture().await;
691
692        let data = vec![3; 3_000_000];
693        let hash = fixture.write_blob(&data, CompressionMode::Always).await;
694
695        assert_eq!(fixture.read_blob(hash).await, data);
696
697        fixture.close().await;
698    }
699
700    #[fuchsia::test(threads = 10)]
701    async fn test_non_page_aligned_blob() {
702        let fixture = new_blob_fixture().await;
703
704        let page_size = page_size().get() as usize;
705        let data = vec![0xffu8; page_size - 1];
706        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
707        assert_eq!(fixture.read_blob(hash).await, data);
708
709        {
710            let vmo = fixture.get_blob_vmo(hash).await;
711            let mut buf = vec![0x11u8; page_size];
712            vmo.read(&mut buf[..], 0).expect("vmo read failed");
713            assert_eq!(data, buf[..data.len()]);
714            // Ensure the tail is zeroed
715            assert_eq!(buf[data.len()], 0);
716        }
717
718        fixture.close().await;
719    }
720
721    #[fuchsia::test(threads = 10)]
722    async fn test_blob_invalid_contents() {
723        let fixture = new_blob_fixture().await;
724
725        let data = vec![0xffu8; (READ_AHEAD_SIZE + BLOCK_SIZE) as usize];
726        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
727        let name = format!("{}", hash);
728
729        {
730            // Overwrite the second read-ahead window.  The first window should successfully verify.
731            let handle = fixture.get_blob_handle(&name).await;
732            let mut transaction =
733                handle.new_transaction().await.expect("failed to create transaction");
734            let mut buf = handle.allocate_buffer(BLOCK_SIZE as usize).await;
735            buf.fill(0);
736            handle
737                .txn_write(&mut transaction, READ_AHEAD_SIZE, buf.as_ref())
738                .await
739                .expect("txn_write failed");
740            transaction.commit().await.expect("failed to commit transaction");
741        }
742
743        {
744            let blob_vmo = fixture.get_blob_vmo(hash).await;
745            let mut buf = vec![0; BLOCK_SIZE as usize];
746            assert_matches!(blob_vmo.read(&mut buf[..], 0), Ok(_));
747            assert_matches!(
748                blob_vmo.read(&mut buf[..], READ_AHEAD_SIZE),
749                Err(zx::Status::IO_DATA_INTEGRITY)
750            );
751        }
752
753        fixture.close().await;
754    }
755
756    #[fuchsia::test(threads = 10)]
757    async fn test_blob_invalid_uncompressed_size() {
758        use fxfs::object_handle::WriteObjectHandle;
759
760        let fixture = new_blob_fixture().await;
761
762        // 1. A single-block blob (uncompressed_size <= BLOCK_SIZE) should have 0 merkle leaves in
763        // metadata. If metadata contains 1 leaf (even if it equals the root hash), opening fails.
764        {
765            let data = vec![0xaa; BLOCK_SIZE as usize];
766            let hash = fixture.write_blob(&data, CompressionMode::Never).await;
767            let handle = fixture.get_blob_handle(&hash.to_string()).await;
768            BlobMetadata { merkle_leaves: vec![hash.into()], format: BlobFormat::Uncompressed }
769                .write_to(&handle)
770                .await
771                .expect("write_to failed");
772            let err = fixture.get_blob(hash).await.err().expect("get_blob should fail");
773            assert_matches!(err.downcast_ref::<FxfsError>(), Some(FxfsError::IntegrityError));
774        }
775
776        // 2. A multi-block compressed blob (uncompressed_size > BLOCK_SIZE) with 0 merkle leaves in
777        // metadata must fail to open with IntegrityError.
778        {
779            let data = vec![0xbb; (BLOCK_SIZE * 2) as usize];
780            let hash = fixture.write_blob(&data, CompressionMode::Always).await;
781            let handle = fixture.get_blob_handle(&hash.to_string()).await;
782            let mut metadata = BlobMetadata::read_from(&handle).await.expect("read_from failed");
783            metadata.merkle_leaves.clear();
784            metadata.write_to(&handle).await.expect("write_to failed");
785            let err = fixture.get_blob(hash).await.err().expect("get_blob should fail");
786            assert_matches!(err.downcast_ref::<FxfsError>(), Some(FxfsError::IntegrityError));
787        }
788
789        // 3. A multi-block blob whose uncompressed size disagrees with the number of merkle leaves
790        // (even when merkle_leaves validly hashes to the merkle root) must fail with
791        // IntegrityError.
792        {
793            let data = vec![0xcc; (BLOCK_SIZE * 2) as usize];
794            let hash = fixture.write_blob(&data, CompressionMode::Never).await;
795            let handle = fixture.get_blob_handle(&hash.to_string()).await;
796            handle.truncate(BLOCK_SIZE * 3).await.expect("truncate failed");
797            let err = fixture.get_blob(hash).await.err().expect("get_blob should fail");
798            assert_matches!(err.downcast_ref::<FxfsError>(), Some(FxfsError::IntegrityError));
799        }
800
801        // 4. A non-empty single-block blob truncated to 0 bytes must fail to open with
802        // IntegrityError.
803        {
804            let data = vec![0xdd; 100];
805            let hash = fixture.write_blob(&data, CompressionMode::Never).await;
806            let handle = fixture.get_blob_handle(&hash.to_string()).await;
807            handle.truncate(0).await.expect("truncate failed");
808            let err = fixture.get_blob(hash).await.err().expect("get_blob should fail");
809            assert_matches!(err.downcast_ref::<FxfsError>(), Some(FxfsError::IntegrityError));
810        }
811
812        // 5. An empty blob whose uncompressed size is non-zero must fail to open with
813        // IntegrityError.
814        {
815            let hash = fixture.write_blob(&[], CompressionMode::Never).await;
816            let handle = fixture.get_blob_handle(&hash.to_string()).await;
817            handle.truncate(100).await.expect("truncate failed");
818            let err = fixture.get_blob(hash).await.err().expect("get_blob should fail");
819            assert_matches!(err.downcast_ref::<FxfsError>(), Some(FxfsError::IntegrityError));
820        }
821
822        fixture.close().await;
823    }
824
825    #[fuchsia::test(threads = 10)]
826    async fn test_lz4_blob() {
827        let device = DeviceHolder::new(FakeDevice::new(16384, 512));
828        let blob_data = vec![0xAA; 68 * 1024];
829        let fxblob_builder = FxBlobBuilder::new(device).await.unwrap();
830        let blob = fxblob_builder
831            .generate_blob(blob_data.clone(), Some(CompressionAlgorithm::Lz4))
832            .unwrap();
833        let blob_hash = blob.hash();
834        fxblob_builder.install_blob(&blob).await.unwrap();
835        let device = fxblob_builder.finalize().await.unwrap().0;
836        device.reopen(/*read_only=*/ false);
837        let fixture = open_blob_fixture(device).await;
838
839        assert_eq!(fixture.read_blob(blob_hash).await, blob_data);
840
841        fixture.close().await;
842    }
843
844    #[fuchsia::test(threads = 10)]
845    async fn test_blob_vmos_are_immutable() {
846        let fixture = new_blob_fixture().await;
847
848        let data = vec![0xffu8; 500];
849        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
850        let blob_vmo = fixture.get_blob_vmo(hash).await;
851
852        // The VMO shouldn't be resizable.
853        assert_matches!(blob_vmo.set_size(20), Err(_));
854
855        // The VMO shouldn't be writable.
856        assert_matches!(blob_vmo.write(b"overwrite", 0), Err(_));
857
858        // The VMO's content size shouldn't be modifiable.
859        assert_matches!(blob_vmo.set_stream_size(20), Err(_));
860
861        fixture.close().await;
862    }
863
864    const COMPRESSED_BLOB_CHUNK_SIZE: u64 = 32 * 1024;
865    const MAX_SMALL_OFFSET: u64 = u32::MAX as u64;
866    const ZSTD: CompressionAlgorithm = CompressionAlgorithm::Zstd;
867
868    #[fuchsia::test]
869    fn test_compression_info_offsets_must_start_with_zero() {
870        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[], ZSTD).is_err());
871        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[1], ZSTD).is_err());
872        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0], ZSTD).is_ok());
873    }
874
875    #[fuchsia::test]
876    fn test_compression_info_offsets_must_be_sorted() {
877        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0, 1, 2], ZSTD).is_ok());
878        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0, 2, 1], ZSTD).is_err());
879        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0, 1, 1], ZSTD).is_err());
880    }
881
882    #[fuchsia::test]
883    fn test_compression_info_compressed_range_for_uncompressed_range() {
884        fn check_compression_ranges(
885            offsets: &[u64],
886            compressed_size: u64,
887            expected_ranges: &[Range<u64>],
888            chunk_size: u64,
889            read_ahead_size: u64,
890        ) {
891            let compression_info =
892                CompressionInfo::new(chunk_size, compressed_size, offsets, ZSTD).unwrap();
893            for (i, expected_range) in expected_ranges.iter().enumerate() {
894                let i = i as u64;
895                let result = compression_info
896                    .compressed_range_for_uncompressed_range(
897                        &(i * read_ahead_size..(i + 1) * read_ahead_size),
898                    )
899                    .unwrap();
900                assert_eq!(&result, expected_range);
901            }
902        }
903        check_compression_ranges(
904            &[0, 10, 20, 30],
905            30,
906            &[0..10, 10..20, 20..30, 30..30],
907            COMPRESSED_BLOB_CHUNK_SIZE,
908            COMPRESSED_BLOB_CHUNK_SIZE,
909        );
910        check_compression_ranges(
911            &[0, 10, 20, 30],
912            30,
913            &[0..20, 20..30],
914            COMPRESSED_BLOB_CHUNK_SIZE,
915            COMPRESSED_BLOB_CHUNK_SIZE * 2,
916        );
917        check_compression_ranges(
918            &[0, 10, 20, 30],
919            30,
920            &[0..30],
921            COMPRESSED_BLOB_CHUNK_SIZE,
922            COMPRESSED_BLOB_CHUNK_SIZE * 4,
923        );
924        check_compression_ranges(
925            &[0, 10, 20, 30, MAX_SMALL_OFFSET + 10],
926            MAX_SMALL_OFFSET + 10,
927            &[0..MAX_SMALL_OFFSET + 10, MAX_SMALL_OFFSET + 10..MAX_SMALL_OFFSET + 10],
928            COMPRESSED_BLOB_CHUNK_SIZE,
929            COMPRESSED_BLOB_CHUNK_SIZE * 4,
930        );
931        check_compression_ranges(
932            &[
933                0,
934                10,
935                20,
936                30,
937                MAX_SMALL_OFFSET + 10,
938                MAX_SMALL_OFFSET + 20,
939                MAX_SMALL_OFFSET + 30,
940                MAX_SMALL_OFFSET + 40,
941                MAX_SMALL_OFFSET + 50,
942            ],
943            MAX_SMALL_OFFSET + 50,
944            &[
945                0..20,
946                20..MAX_SMALL_OFFSET + 10,
947                MAX_SMALL_OFFSET + 10..MAX_SMALL_OFFSET + 30,
948                MAX_SMALL_OFFSET + 30..MAX_SMALL_OFFSET + 50,
949            ],
950            COMPRESSED_BLOB_CHUNK_SIZE,
951            COMPRESSED_BLOB_CHUNK_SIZE * 2,
952        );
953    }
954
955    #[fuchsia::test]
956    fn test_compression_info_compressed_range_for_uncompressed_range_errors() {
957        let compression_info = CompressionInfo::new(
958            COMPRESSED_BLOB_CHUNK_SIZE,
959            MAX_SMALL_OFFSET + 50,
960            &[
961                0,
962                10,
963                20,
964                30,
965                MAX_SMALL_OFFSET + 10,
966                MAX_SMALL_OFFSET + 20,
967                MAX_SMALL_OFFSET + 30,
968                MAX_SMALL_OFFSET + 40,
969                MAX_SMALL_OFFSET + 50,
970            ],
971            ZSTD,
972        )
973        .unwrap();
974
975        // The start of reads must be chunk aligned.
976        assert!(
977            compression_info
978                .compressed_range_for_uncompressed_range(&(1..COMPRESSED_BLOB_CHUNK_SIZE),)
979                .is_err()
980        );
981
982        // Reading entirely past the last offset isn't allowed.
983        assert!(
984            compression_info
985                .compressed_range_for_uncompressed_range(
986                    &(COMPRESSED_BLOB_CHUNK_SIZE * 9..COMPRESSED_BLOB_CHUNK_SIZE * 12),
987                )
988                .is_err()
989        );
990
991        // Reading a different amount than the read-ahead size isn't allowed for middle offsets.
992        assert!(
993            compression_info
994                .compressed_range_for_uncompressed_range(&(0..COMPRESSED_BLOB_CHUNK_SIZE + 1),)
995                .is_err()
996        );
997        assert!(
998            compression_info
999                .compressed_range_for_uncompressed_range(&(0..COMPRESSED_BLOB_CHUNK_SIZE - 1),)
1000                .is_err()
1001        );
1002        assert!(
1003            compression_info
1004                .compressed_range_for_uncompressed_range(
1005                    &(COMPRESSED_BLOB_CHUNK_SIZE..COMPRESSED_BLOB_CHUNK_SIZE * 2 + 1),
1006                )
1007                .is_err()
1008        );
1009        assert!(
1010            compression_info
1011                .compressed_range_for_uncompressed_range(
1012                    &(COMPRESSED_BLOB_CHUNK_SIZE..COMPRESSED_BLOB_CHUNK_SIZE * 2 - 1),
1013                )
1014                .is_err()
1015        );
1016
1017        // Reading less than the read-ahead size for the last offset is allowed.
1018        assert!(
1019            compression_info
1020                .compressed_range_for_uncompressed_range(
1021                    &(COMPRESSED_BLOB_CHUNK_SIZE * 8..COMPRESSED_BLOB_CHUNK_SIZE * 8 + 4096),
1022                )
1023                .is_ok()
1024        );
1025    }
1026
1027    #[fuchsia::test]
1028    fn test_read_ahead_size_for_chunk_size() {
1029        assert_eq!(read_ahead_size_for_chunk_size(32 * 1024, 32 * 1024), 32 * 1024);
1030        assert_eq!(read_ahead_size_for_chunk_size(48 * 1024, 32 * 1024), 48 * 1024);
1031        assert_eq!(read_ahead_size_for_chunk_size(64 * 1024, 32 * 1024), 64 * 1024);
1032
1033        assert_eq!(read_ahead_size_for_chunk_size(32 * 1024, 64 * 1024), 64 * 1024);
1034        assert_eq!(read_ahead_size_for_chunk_size(48 * 1024, 64 * 1024), 48 * 1024);
1035        assert_eq!(read_ahead_size_for_chunk_size(64 * 1024, 64 * 1024), 64 * 1024);
1036        assert_eq!(read_ahead_size_for_chunk_size(96 * 1024, 64 * 1024), 96 * 1024);
1037
1038        assert_eq!(read_ahead_size_for_chunk_size(32 * 1024, 128 * 1024), 128 * 1024);
1039        assert_eq!(read_ahead_size_for_chunk_size(48 * 1024, 128 * 1024), 96 * 1024);
1040        assert_eq!(read_ahead_size_for_chunk_size(64 * 1024, 128 * 1024), 128 * 1024);
1041        assert_eq!(read_ahead_size_for_chunk_size(96 * 1024, 128 * 1024), 96 * 1024);
1042    }
1043
1044    fn build_compression_info(size: usize) -> (CompressionInfo, Vec<u8>, Vec<u8>) {
1045        let options =
1046            ChunkedArchiveOptions::V3 { compression_algorithm: CompressionAlgorithm::Lz4 };
1047        let mut compressor = options.compressor();
1048        let mut uncompressed_data = Vec::with_capacity(size);
1049        {
1050            let mut run_length = 1;
1051            let mut run_value: u8 = 0;
1052            while uncompressed_data.len() < size {
1053                uncompressed_data
1054                    .resize(std::cmp::min(uncompressed_data.len() + run_length, size), run_value);
1055                run_length = (run_length + 1) % 19 + 1;
1056                run_value = (run_value + 1) % 17;
1057            }
1058        }
1059        let mut compressed_offsets = vec![0];
1060        let mut compressed_data = vec![];
1061        for chunk in uncompressed_data.chunks(CHUNK_SIZE) {
1062            let mut compressed_chunk = compressor.compress(chunk, 0).unwrap();
1063            compressed_data.append(&mut compressed_chunk);
1064            compressed_offsets.push(compressed_data.len() as u64);
1065        }
1066        compressed_offsets.pop();
1067        let compressed_size = compressed_data.len() as u64;
1068        (
1069            CompressionInfo::new(
1070                CHUNK_SIZE as u64,
1071                compressed_size,
1072                &compressed_offsets,
1073                CompressionAlgorithm::Lz4,
1074            )
1075            .unwrap(),
1076            compressed_data,
1077            uncompressed_data,
1078        )
1079    }
1080
1081    #[fuchsia::test]
1082    fn test_compression_info_decompress_single_chunk() {
1083        let (compression_info, compressed_data, uncompressed_data) =
1084            build_compression_info(CHUNK_SIZE);
1085        let mut decompressed_data = vec![0u8; CHUNK_SIZE + 1];
1086
1087        compression_info
1088            .decompress(&compressed_data[..], &mut decompressed_data[0..CHUNK_SIZE], 0)
1089            .expect("failed to decompress");
1090        assert_eq!(uncompressed_data, decompressed_data[0..CHUNK_SIZE]);
1091
1092        // Too small of destination buffer.
1093        compression_info
1094            .decompress(&compressed_data[..], &mut decompressed_data[0..CHUNK_SIZE - 1], 0)
1095            .expect_err("decompression should fail");
1096
1097        // Too large of destination buffer.
1098        compression_info
1099            .decompress(&compressed_data[..], &mut decompressed_data[0..CHUNK_SIZE - 1], 0)
1100            .expect_err("decompression should fail");
1101    }
1102
1103    #[fuchsia::test]
1104    fn test_compression_info_decompress_multiple_chunks() {
1105        fn slice_for_chunks<'a>(
1106            compressed_data: &'a [u8],
1107            compression_info: &CompressionInfo,
1108            chunks: Range<u64>,
1109        ) -> &'a [u8] {
1110            let range = compression_info
1111                .compressed_range_for_uncompressed_range(
1112                    &(chunks.start * CHUNK_SIZE as u64..chunks.end * CHUNK_SIZE as u64),
1113                )
1114                .unwrap();
1115            &compressed_data[range.start as usize..range.end as usize]
1116        }
1117
1118        const BLOB_SIZE: usize = CHUNK_SIZE * 4 + 4096;
1119        let (compression_info, compressed_data, uncompressed_data) =
1120            build_compression_info(BLOB_SIZE);
1121        let mut decompressed_data = vec![0u8; BLOB_SIZE];
1122
1123        // Decompress the entire blob.
1124        compression_info
1125            .decompress(&compressed_data[..], &mut decompressed_data[..], 0)
1126            .expect("failed to decompress");
1127        assert_eq!(uncompressed_data, decompressed_data);
1128
1129        // Decompress just the whole chunks.
1130        compression_info
1131            .decompress(
1132                slice_for_chunks(&compressed_data, &compression_info, 0..4),
1133                &mut decompressed_data[0..CHUNK_SIZE * 4],
1134                0,
1135            )
1136            .expect("failed to decompress");
1137        assert_eq!(&uncompressed_data[0..CHUNK_SIZE], &decompressed_data[0..CHUNK_SIZE]);
1138
1139        // Too small of destination buffer for whole chunks.
1140        compression_info
1141            .decompress(
1142                slice_for_chunks(&compressed_data, &compression_info, 0..4),
1143                &mut decompressed_data[0..CHUNK_SIZE * 4 - 1],
1144                0,
1145            )
1146            .expect_err("decompression should fail");
1147
1148        // Too large of destination buffer for whole chunks.
1149        compression_info
1150            .decompress(
1151                slice_for_chunks(&compressed_data, &compression_info, 0..4),
1152                &mut decompressed_data[0..CHUNK_SIZE * 4 + 1],
1153                0,
1154            )
1155            .expect_err("decompression should fail");
1156
1157        // Decompress just the tail.
1158        let partial_chunk = slice_for_chunks(&compressed_data, &compression_info, 4..5);
1159        compression_info
1160            .decompress(partial_chunk, &mut decompressed_data[0..4096], CHUNK_SIZE as u64 * 4)
1161            .expect("failed to decompress");
1162        assert_eq!(&uncompressed_data[CHUNK_SIZE * 4..], &decompressed_data[0..4096]);
1163
1164        // Too small of destination buffer for the tail.
1165        compression_info
1166            .decompress(partial_chunk, &mut decompressed_data[0..4095], CHUNK_SIZE as u64 * 4)
1167            .expect_err("decompression should fail");
1168
1169        // Too large of destination buffer for the tail.
1170        compression_info
1171            .decompress(partial_chunk, &mut decompressed_data[0..4097], CHUNK_SIZE as u64 * 4)
1172            .expect_err("decompression should fail");
1173    }
1174
1175    #[fuchsia::test(threads = 10)]
1176    async fn test_refault_metric() {
1177        let fixture = new_blob_fixture().await;
1178        {
1179            let volume = fixture.volume().volume().clone();
1180            const FILE_SIZE: u64 = READ_AHEAD_SIZE * 4 - 4096;
1181            let data = vec![0xffu8; FILE_SIZE as usize];
1182            let hash = fixture.write_blob(&data, CompressionMode::Never).await;
1183
1184            let blob = fixture.get_opened_blob(hash).await.unwrap();
1185            assert_eq!(blob.chunks_supplied.len(), 4);
1186            // Nothing has been read yet.
1187            assert_eq!(&blob.chunks_supplied.get(), &[false, false, false, false]);
1188
1189            blob.vmo.read_to_vec::<u8>(4096, 4096).unwrap();
1190
1191            assert_eq!(&blob.chunks_supplied.get(), &[true, false, false, false]);
1192
1193            blob.vmo.read_to_vec::<u8>(READ_AHEAD_SIZE * 2 + 4096, READ_AHEAD_SIZE).unwrap();
1194            assert_eq!(&blob.chunks_supplied.get(), &[true, false, true, true]);
1195
1196            // We have loaded pages, but only once each.
1197            assert_eq!(volume.blob_resupplied_count().read(Ordering::SeqCst), 0);
1198
1199            // Re-read some pages.
1200
1201            // We can't evict pages from the VMO to get the kernel to resupply them but we can call
1202            // page_in directly and wait for the counters to change.
1203            blob.clone().page_in(PageInRange::new(
1204                FILE_SIZE - READ_AHEAD_SIZE..FILE_SIZE,
1205                blob.dup(),
1206                Epoch::global().guard(),
1207            ));
1208            Epoch::global().barrier().await;
1209
1210            // Two chunks were resupplied, and the counter is expressed in pages.
1211            let pages_per_chunk = READ_AHEAD_SIZE / zx::system_get_page_size() as u64;
1212            assert_eq!(volume.blob_resupplied_count().read(Ordering::SeqCst), 2 * pages_per_chunk);
1213        }
1214
1215        fixture.close().await;
1216    }
1217}