1use 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 crate::fxblob::atomic_vec::AtomicBitVec;
17use anyhow::{Context, Error, anyhow, bail, ensure};
18use delivery_blob::compression::{CompressionAlgorithm, CompressionInfo};
19use fidl_fuchsia_feedback::{Annotation, Attachment, CrashReport};
20use fidl_fuchsia_mem::Buffer as MemBuffer;
21use fuchsia_component_client::connect_to_protocol;
22use fuchsia_merkle::{Hash, MerkleVerifier, ReadSizedMerkleVerifier};
23use futures::{StreamExt, TryStreamExt, try_join};
24use fxfs::blob_metadata::{BlobFormat, BlobMetadata};
25use fxfs::errors::FxfsError;
26use fxfs::lock_keys;
27use fxfs::log::*;
28use fxfs::object_handle::ObjectHandle;
29use fxfs::object_store::transaction::LockKey;
30use fxfs::object_store::{AttributeId, DataObjectHandle, ObjectDescriptor, StoreObjectHandle};
31use fxfs::round::{round_down, round_up};
32use fxfs_macros::ToWeakNode;
33use mapping::Extent as MappingExtent;
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
40pub struct BlobExtents {
45 pub data: Vec<MappingExtent>,
46 pub merkle: Vec<MappingExtent>,
47}
48
49const PURGED: usize = 1 << (usize::BITS - 1);
53
54#[derive(ToWeakNode)]
56pub struct FxBlob {
57 handle: StoreObjectHandle<FxVolume>,
58 vmo: zx::Vmo,
59 open_count: AtomicUsize,
60 merkle_root: Hash,
61 merkle_verifier: ReadSizedMerkleVerifier,
62 compression_info: Option<CompressionInfo>,
63 uncompressed_size: u64, pager_packet_receiver_registration: Arc<PagerPacketReceiverRegistration<Self>>,
65 chunks_supplied: AtomicBitVec,
66}
67
68static_assertions::const_assert!(size_of::<FxBlob>() <= 192);
70
71impl FxBlob {
72 pub async fn new(
73 handle: StoreObjectHandle<FxVolume>,
74 merkle_root: Hash,
75 ) -> Result<Arc<Self>, Error> {
76 let stored_size =
77 handle.store().get_attribute_size(handle.object_id(), AttributeId::DATA).await?;
78 let metadata = BlobMetadata::read_from(&handle).await?;
79 let (uncompressed_size, compression_info) = match &metadata.format {
80 BlobFormat::Uncompressed => (stored_size, None),
81 BlobFormat::ChunkedZstd { uncompressed_size, chunk_size, compressed_offsets } => (
82 *uncompressed_size,
83 Some(CompressionInfo::new(
84 *chunk_size,
85 stored_size,
86 compressed_offsets,
87 CompressionAlgorithm::Zstd,
88 )?),
89 ),
90 BlobFormat::ChunkedLz4 { uncompressed_size, chunk_size, compressed_offsets } => (
91 *uncompressed_size,
92 Some(CompressionInfo::new(
93 *chunk_size,
94 stored_size,
95 compressed_offsets,
96 CompressionAlgorithm::Lz4,
97 )?),
98 ),
99 };
100 let merkle_verifier = metadata.into_merkle_verifier(merkle_root)?;
101
102 let min_chunk_size = min_chunk_size(&compression_info);
103 let merkle_verifier =
104 ReadSizedMerkleVerifier::new(merkle_verifier, min_chunk_size as usize)?;
105 let chunks_supplied = AtomicBitVec::new(uncompressed_size.div_ceil(min_chunk_size));
106
107 Ok(Arc::new_cyclic(|weak| {
108 let (vmo, pager_packet_receiver_registration) = handle
109 .owner()
110 .pager()
111 .create_vmo(weak.clone(), uncompressed_size, zx::VmoOptions::empty())
112 .unwrap();
113 set_vmo_name(&vmo, &merkle_root);
114 Self {
115 handle,
116 vmo,
117 open_count: AtomicUsize::new(0),
118 merkle_root,
119 merkle_verifier,
120 compression_info,
121 uncompressed_size,
122 pager_packet_receiver_registration: Arc::new(pager_packet_receiver_registration),
123 chunks_supplied,
124 }
125 }))
126 }
127
128 pub fn overwrite_me(
130 self: &Arc<Self>,
131 handle: DataObjectHandle<FxVolume>,
132 merkle_verifier: MerkleVerifier,
133 compression_info: Option<CompressionInfo>,
134 ) -> Arc<Self> {
135 let min_chunk_size = min_chunk_size(&compression_info);
136 let merkle_verifier =
137 ReadSizedMerkleVerifier::new(merkle_verifier, min_chunk_size as usize)
138 .expect("The chunk size should have been validated by the delivery blob parser");
139 let chunks_supplied = AtomicBitVec::new(self.uncompressed_size.div_ceil(min_chunk_size));
142 let vmo = self.vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
143
144 let new_blob = Arc::new(Self {
145 handle: handle.into_store_object_handle(),
146 vmo,
147 open_count: AtomicUsize::new(0),
148 merkle_root: self.merkle_root,
149 merkle_verifier,
150 compression_info,
151 uncompressed_size: self.uncompressed_size,
152 pager_packet_receiver_registration: self.pager_packet_receiver_registration.clone(),
153 chunks_supplied,
154 });
155
156 self.handle.owner().cache().remove(self.as_ref());
159
160 let receiver_lock =
163 self.pager_packet_receiver_registration.receiver().set_receiver(&new_blob);
164 if receiver_lock.is_strong() {
165 new_blob.open_count_add_one();
171 self.clone().open_count_sub_one();
172 }
173 new_blob
174 }
175
176 pub fn root(&self) -> Hash {
177 self.merkle_root
178 }
179
180 pub async fn get_mapping_extents(&self) -> Result<BlobExtents, Error> {
184 let tree = &self.handle.store().tree();
185 let layer_set = tree.layer_set();
186
187 let mut merger = layer_set.merger();
189 let stream = self.handle.extent_stream(&mut merger, AttributeId::DATA).await?;
190 let data = stream
192 .map(|res| {
193 let extent = res?;
194 MappingExtent::try_new(extent.logical_range(), Some(extent.device_range().start))
195 .map_err(|_| anyhow!(FxfsError::Inconsistent))
196 })
197 .try_collect()
198 .await?;
199
200 let mut merger = layer_set.merger();
202 let stream = self.handle.extent_stream(&mut merger, AttributeId::BLOB_METADATA).await?;
203 let merkle = stream
204 .map(|res| {
205 let extent = res?;
206 MappingExtent::try_new(extent.logical_range(), Some(extent.device_range().start))
207 .map_err(|_| anyhow!(FxfsError::Inconsistent))
208 })
209 .try_collect()
210 .await?;
211
212 Ok(BlobExtents { data, merkle })
213 }
214
215 fn record_page_fault_metric(&self, range: &Range<u64>) {
216 let chunk_size: u64 = min_chunk_size(&self.compression_info);
217
218 let first_chunk = range.start / chunk_size;
219 let last_chunk = range.end.div_ceil(chunk_size);
221
222 let supplied_count = self.chunks_supplied.test_and_set_range(first_chunk, last_chunk);
223
224 if supplied_count > 0 {
225 self.handle
226 .owner()
227 .blob_resupplied_count()
228 .increment(supplied_count, Ordering::Relaxed);
229 }
230 }
231
232 fn allocate_buffer(&self, size: u64) -> BufferFuture<'_> {
234 self.handle.store().device().allocate_buffer(size as usize)
235 }
236
237 fn allocate_trusted_buffer(&self, size: u64) -> BufferFuture<'_> {
240 self.handle.owner().blob_allocator().allocate_buffer(size as usize)
241 }
242
243 async fn read_blocks(&self, offset: u64, buf: MutableBufferRef<'_>) -> Result<(), Error> {
244 let fs = self.handle.store().filesystem();
245 let guard = fs
246 .lock_manager()
247 .read_lock(lock_keys![LockKey::object_attribute(
248 self.handle.store().store_object_id(),
249 self.handle.object_id(),
250 AttributeId::DATA,
251 )])
252 .await;
253 self.handle.read_unchecked(AttributeId::DATA, offset, buf, &guard).await
254 }
255}
256
257impl Drop for FxBlob {
258 fn drop(&mut self) {
259 let volume = self.handle.owner();
260 volume.cache().remove(self);
261 }
262}
263
264impl OpenedNode<FxBlob> {
265 pub fn create_child_vmo(&self) -> Result<zx::Vmo, Status> {
272 let blob = self.0.as_ref();
273 let child_vmo = blob.vmo.create_child(
274 zx::VmoChildOptions::REFERENCE | zx::VmoChildOptions::NO_WRITE,
275 0,
276 0,
277 )?;
278 if blob.handle.owner().pager().watch_for_zero_children(blob).map_err(map_to_status)? {
279 blob.open_count_add_one();
283 }
284 Ok(child_vmo)
285 }
286}
287
288impl FxNode for FxBlob {
289 fn object_id(&self) -> u64 {
290 self.handle.object_id()
291 }
292
293 fn parent(&self) -> Option<Arc<FxDirectory>> {
294 unreachable!(); }
296
297 fn set_parent(&self, _parent: Arc<FxDirectory>) {
298 }
300
301 fn open_count_add_one(&self) {
302 let old = self.open_count.fetch_add(1, Ordering::Relaxed);
303 assert!(old != PURGED && old != PURGED - 1);
304 }
305
306 fn open_count_sub_one(self: Arc<Self>) {
307 let old = self.open_count.fetch_sub(1, Ordering::Relaxed);
308 assert!(old & !PURGED > 0);
309 if old == PURGED + 1 {
310 let store = self.handle.store();
311 store
312 .filesystem()
313 .graveyard()
314 .queue_tombstone_object(store.store_object_id(), self.object_id());
315 }
316 }
317
318 fn object_descriptor(&self) -> ObjectDescriptor {
319 ObjectDescriptor::File
320 }
321
322 fn terminate(&self) {
323 self.pager_packet_receiver_registration.stop_watching_for_zero_children();
324 }
325
326 fn mark_to_be_purged(self: Arc<Self>) {
327 let old = self.open_count.fetch_or(PURGED, Ordering::Relaxed);
328 assert!(old & PURGED == 0);
329 if old == 0 {
330 let store = self.handle.store();
331 store
332 .filesystem()
333 .graveyard()
334 .queue_tombstone_object(store.store_object_id(), self.object_id());
335 }
336 }
337}
338
339impl PagerBacked for FxBlob {
340 fn try_keep_open(self: Arc<Self>) -> Result<OpenedNode<Self>, Arc<Self>> {
341 let mut old = self.open_count.load(Ordering::Relaxed);
342 loop {
343 if old & !PURGED == 0 {
344 return Err(self);
345 }
346 assert!(old & !PURGED < PURGED - 1);
347 match self.open_count.compare_exchange_weak(
348 old,
349 old + 1,
350 Ordering::Relaxed,
351 Ordering::Relaxed,
352 ) {
353 Ok(_) => return Ok(OpenedNode(self)),
354 Err(new_value) => old = new_value,
355 }
356 }
357 }
358
359 fn pager(&self) -> &crate::pager::Pager {
360 self.handle.owner().pager()
361 }
362
363 fn pager_packet_receiver_registration(&self) -> &PagerPacketReceiverRegistration<Self> {
364 &self.pager_packet_receiver_registration
365 }
366
367 fn vmo(&self) -> &zx::Vmo {
368 &self.vmo
369 }
370
371 fn page_in(self: Arc<Self>, range: PageInRange<Self>) {
372 let read_ahead_size = if let Some(compression_info) = &self.compression_info {
373 read_ahead_size_for_chunk_size(compression_info.chunk_size(), READ_AHEAD_SIZE)
374 } else {
375 READ_AHEAD_SIZE
376 };
377 default_page_in(self, range, read_ahead_size)
379 }
380
381 fn mark_dirty(self: Arc<Self>, _range: MarkDirtyRange<Self>) {
382 unreachable!();
383 }
384
385 fn on_zero_children(self: Arc<Self>) {
386 self.open_count_sub_one();
387 }
388
389 fn byte_size(&self) -> u64 {
390 self.uncompressed_size
391 }
392
393 async fn aligned_read(&self, range: Range<u64>) -> Result<Buffer<'_>, Error> {
394 ensure!(range.start < self.uncompressed_size, FxfsError::InvalidArgs);
397 self.record_page_fault_metric(&range);
398
399 let mut buffer = self.allocate_trusted_buffer(range.end - range.start).await;
400 let unaligned_bytes =
401 (std::cmp::min(range.end, self.uncompressed_size) - range.start) as usize;
402 match &self.compression_info {
403 None => self.read_blocks(range.start, buffer.as_mut()).await?,
404 Some(compression_info) => {
405 let compressed_offsets =
406 compression_info.compressed_range_for_uncompressed_range(&range)?;
407 let bs = self.handle.block_size();
408 let aligned = round_down(compressed_offsets.start, bs)
409 ..round_up(compressed_offsets.end, bs).unwrap();
410 let mut compressed_buf = self.allocate_buffer(aligned.end - aligned.start).await;
411
412 let mut decompression_errors = 0;
413 loop {
414 try_join!(self.read_blocks(aligned.start, compressed_buf.as_mut()), async {
415 buffer
416 .allocator()
417 .buffer_source()
418 .commit_range(buffer.range())
419 .map_err(|e| e.into())
420 })
421 .with_context(|| {
422 format!(
423 "Failed to read compressed range {:?}, len {}",
424 aligned,
425 compression_info.compressed_size()
426 )
427 })?;
428 let compressed_buf_range = (compressed_offsets.start - aligned.start) as usize
429 ..(compressed_offsets.end - aligned.start) as usize;
430
431 let decompression_result = {
432 fxfs_trace::duration!("blob-decompress", "len" => unaligned_bytes);
433 compression_info.decompress(
434 compressed_buf.as_ptr_slice().subslice(compressed_buf_range),
435 buffer.subslice_mut(0..unaligned_bytes).try_as_mut_slice().unwrap(),
436 range.start,
437 )
438 };
439 match decompression_result {
440 Ok(()) => break,
441 Err(error) => {
442 record_decompression_error_crash_report(
443 &compressed_buf.to_vec(),
444 &range,
445 &compressed_offsets,
446 &self.merkle_root,
447 )
448 .await;
449 decompression_errors += 1;
450 if decompression_errors == 2 {
451 bail!(
452 anyhow!(FxfsError::IntegrityError)
453 .context(format!("Decompression error: {error:?}"))
454 );
455 } else {
456 warn!(error:?; "Decompression error; retrying");
457 }
458 }
459 }
460 } if decompression_errors > 0 {
462 info!("Read succeeded on second attempt");
463 }
464 }
465 };
466 {
467 fxfs_trace::duration!("blob-verify", "len" => unaligned_bytes);
470 self.merkle_verifier.verify(
471 range.start as usize,
472 buffer.subslice(0..unaligned_bytes).try_as_slice().unwrap(),
473 )?;
474 }
475 buffer.subslice_mut(unaligned_bytes..buffer.len()).fill(0);
477 Ok(buffer)
478 }
479}
480
481fn set_vmo_name(vmo: &zx::Vmo, merkle_root: &Hash) {
482 let trimmed_merkle = &merkle_root.to_string()[0..BLOB_NAME_HASH_LENGTH];
483 let name = format!("{BLOB_NAME_PREFIX}{trimmed_merkle}");
484 let name = zx::Name::new(&name).unwrap();
485 vmo.set_name(&name).unwrap();
486}
487
488fn min_chunk_size(compression_info: &Option<CompressionInfo>) -> u64 {
489 if let Some(compression_info) = compression_info {
490 read_ahead_size_for_chunk_size(compression_info.chunk_size(), READ_AHEAD_SIZE)
491 } else {
492 READ_AHEAD_SIZE
493 }
494}
495
496fn read_ahead_size_for_chunk_size(chunk_size: u64, suggested_read_ahead_size: u64) -> u64 {
497 if chunk_size >= suggested_read_ahead_size {
498 chunk_size
499 } else {
500 round_down(suggested_read_ahead_size, chunk_size)
501 }
502}
503
504async fn record_decompression_error_crash_report(
505 compressed_buf: &[u8],
506 uncompressed_offsets: &Range<u64>,
507 compressed_offsets: &Range<u64>,
508 merkle_root: &Hash,
509) {
510 static DONE_ONCE: AtomicBool = AtomicBool::new(false);
511 if !DONE_ONCE.swap(true, Ordering::Relaxed) {
512 if let Ok(proxy) = connect_to_protocol::<fidl_fuchsia_feedback::CrashReporterMarker>() {
513 let size = compressed_buf.len() as u64;
514 let vmo = zx::Vmo::create(size).unwrap();
515 vmo.write(compressed_buf, 0).unwrap();
516 if let Err(e) = proxy
517 .file_report(CrashReport {
518 program_name: Some("fxfs".to_string()),
519 crash_signature: Some("fuchsia-fxfs-decompression_error".to_string()),
520 is_fatal: Some(false),
521 annotations: Some(vec![
522 Annotation {
523 key: "fxfs.range".to_string(),
524 value: format!("{:?}", uncompressed_offsets),
525 },
526 Annotation {
527 key: "fxfs.compressed_offsets".to_string(),
528 value: format!("{:?}", compressed_offsets),
529 },
530 Annotation {
531 key: "fxfs.merkle_root".to_string(),
532 value: format!("{}", merkle_root),
533 },
534 ]),
535 attachments: Some(vec![Attachment {
536 key: "fxfs_compressed_data".to_string(),
537 value: MemBuffer { vmo, size },
538 }]),
539 ..Default::default()
540 })
541 .await
542 {
543 error!(e:?; "Failed to file crash report");
544 } else {
545 warn!("Filed crash report for decompression error");
546 }
547 } else {
548 error!("Failed to connect to crash report service");
549 }
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::fuchsia::fxblob::testing::{BlobFixture, new_blob_fixture};
557 use crate::fuchsia::pager::PageInRange;
558 use crate::fxblob::testing::open_blob_fixture;
559 use assert_matches::assert_matches;
560 use delivery_blob::CompressionMode;
561 use delivery_blob::compression::{ChunkedArchiveOptions, CompressionAlgorithm};
562
563 use fuchsia_async::epoch::Epoch;
564 use fxfs_make_blob_image::FxBlobBuilder;
565 use storage_device::DeviceHolder;
566 use storage_device::fake_device::FakeDevice;
567
568 const BLOCK_SIZE: u64 = fuchsia_merkle::BLOCK_SIZE as u64;
569 const CHUNK_SIZE: usize = 32 * 1024;
570
571 #[fuchsia::test(threads = 10)]
572 async fn test_empty_blob() {
573 let fixture = new_blob_fixture().await;
574
575 let data = vec![];
576 let hash = fixture.write_blob(&data, CompressionMode::Never).await;
577 assert_eq!(fixture.read_blob(hash).await, data);
578
579 fixture.close().await;
580 }
581
582 #[fuchsia::test(threads = 10)]
583 async fn test_large_blob() {
584 let fixture = new_blob_fixture().await;
585
586 let data = vec![3; 3_000_000];
587 let hash = fixture.write_blob(&data, CompressionMode::Never).await;
588
589 assert_eq!(fixture.read_blob(hash).await, data);
590
591 fixture.close().await;
592 }
593
594 #[fuchsia::test(threads = 10)]
595 async fn test_get_multiple_mapping_extents() {
596 let fixture = new_blob_fixture().await;
597
598 {
599 let data = vec![0xff; 3_000_000];
600 let hash = fixture.write_blob(&data, CompressionMode::Never).await;
601 let name = format!("{}", hash);
602
603 let handle = fixture.get_blob_handle(&name).await;
604 let mut transaction =
605 handle.new_transaction().await.expect("failed to create transaction");
606 let mut buf = handle.allocate_buffer(8192).await;
607 buf.as_mut_ptr_slice().fill(0xaa);
608 handle
609 .txn_write(&mut transaction, 0, buf.as_ref())
610 .await
611 .expect("txn_write at offset 0 failed");
612 handle
613 .txn_write(&mut transaction, 16384, buf.as_ref())
614 .await
615 .expect("txn_write at offset 16384 failed");
616 transaction.commit().await.expect("failed to commit transaction");
617
618 let blob = fixture.get_blob(hash).await.expect("getting blob failed");
619 let extents = blob.get_mapping_extents().await.expect("get_mapping_extents failed");
620
621 assert!(!extents.merkle.is_empty(), "Expected at least one merkle extent");
622 assert!(
623 extents.data.len() > 1,
624 "Expected multiple data extents, got {}",
625 extents.data.len()
626 );
627
628 let overwrite_extent = extents
630 .data
631 .iter()
632 .find(|e| e.logical_range().start == 0)
633 .expect("extent at offset 0");
634 assert_eq!(
635 overwrite_extent.logical_range().end - overwrite_extent.logical_range().start,
636 8192
637 );
638 let overwrite_extent = extents
639 .data
640 .iter()
641 .find(|e| e.logical_range().start == 16384)
642 .expect("extent at offset 16384");
643 assert_eq!(
644 overwrite_extent.logical_range().end - overwrite_extent.logical_range().start,
645 8192
646 );
647 }
648
649 fixture.close().await;
650 }
651
652 #[fuchsia::test(threads = 10)]
653 async fn test_large_compressed_blob() {
654 let fixture = new_blob_fixture().await;
655
656 let data = vec![3; 3_000_000];
657 let hash = fixture.write_blob(&data, CompressionMode::Always).await;
658
659 assert_eq!(fixture.read_blob(hash).await, data);
660
661 fixture.close().await;
662 }
663
664 #[fuchsia::test(threads = 10)]
665 async fn test_non_page_aligned_blob() {
666 let fixture = new_blob_fixture().await;
667
668 let page_size = zx::system_get_page_size() as usize;
669 let data = vec![0xffu8; page_size - 1];
670 let hash = fixture.write_blob(&data, CompressionMode::Never).await;
671 assert_eq!(fixture.read_blob(hash).await, data);
672
673 {
674 let vmo = fixture.get_blob_vmo(hash).await;
675 let mut buf = vec![0x11u8; page_size];
676 vmo.read(&mut buf[..], 0).expect("vmo read failed");
677 assert_eq!(data, buf[..data.len()]);
678 assert_eq!(buf[data.len()], 0);
680 }
681
682 fixture.close().await;
683 }
684
685 #[fuchsia::test(threads = 10)]
686 async fn test_blob_invalid_contents() {
687 let fixture = new_blob_fixture().await;
688
689 let data = vec![0xffu8; (READ_AHEAD_SIZE + BLOCK_SIZE) as usize];
690 let hash = fixture.write_blob(&data, CompressionMode::Never).await;
691 let name = format!("{}", hash);
692
693 {
694 let handle = fixture.get_blob_handle(&name).await;
696 let mut transaction =
697 handle.new_transaction().await.expect("failed to create transaction");
698 let mut buf = handle.allocate_buffer(BLOCK_SIZE as usize).await;
699 buf.fill(0);
700 handle
701 .txn_write(&mut transaction, READ_AHEAD_SIZE, buf.as_ref())
702 .await
703 .expect("txn_write failed");
704 transaction.commit().await.expect("failed to commit transaction");
705 }
706
707 {
708 let blob_vmo = fixture.get_blob_vmo(hash).await;
709 let mut buf = vec![0; BLOCK_SIZE as usize];
710 assert_matches!(blob_vmo.read(&mut buf[..], 0), Ok(_));
711 assert_matches!(
712 blob_vmo.read(&mut buf[..], READ_AHEAD_SIZE),
713 Err(zx::Status::IO_DATA_INTEGRITY)
714 );
715 }
716
717 fixture.close().await;
718 }
719
720 #[fuchsia::test(threads = 10)]
721 async fn test_lz4_blob() {
722 let device = DeviceHolder::new(FakeDevice::new(16384, 512));
723 let blob_data = vec![0xAA; 68 * 1024];
724 let fxblob_builder = FxBlobBuilder::new(device).await.unwrap();
725 let blob = fxblob_builder
726 .generate_blob(blob_data.clone(), Some(CompressionAlgorithm::Lz4))
727 .unwrap();
728 let blob_hash = blob.hash();
729 fxblob_builder.install_blob(&blob).await.unwrap();
730 let device = fxblob_builder.finalize().await.unwrap().0;
731 device.reopen(false);
732 let fixture = open_blob_fixture(device).await;
733
734 assert_eq!(fixture.read_blob(blob_hash).await, blob_data);
735
736 fixture.close().await;
737 }
738
739 #[fuchsia::test(threads = 10)]
740 async fn test_blob_vmos_are_immutable() {
741 let fixture = new_blob_fixture().await;
742
743 let data = vec![0xffu8; 500];
744 let hash = fixture.write_blob(&data, CompressionMode::Never).await;
745 let blob_vmo = fixture.get_blob_vmo(hash).await;
746
747 assert_matches!(blob_vmo.set_size(20), Err(_));
749
750 assert_matches!(blob_vmo.write(b"overwrite", 0), Err(_));
752
753 assert_matches!(blob_vmo.set_stream_size(20), Err(_));
755
756 fixture.close().await;
757 }
758
759 const COMPRESSED_BLOB_CHUNK_SIZE: u64 = 32 * 1024;
760 const MAX_SMALL_OFFSET: u64 = u32::MAX as u64;
761 const ZSTD: CompressionAlgorithm = CompressionAlgorithm::Zstd;
762
763 #[fuchsia::test]
764 fn test_compression_info_offsets_must_start_with_zero() {
765 assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[], ZSTD).is_err());
766 assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[1], ZSTD).is_err());
767 assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0], ZSTD).is_ok());
768 }
769
770 #[fuchsia::test]
771 fn test_compression_info_offsets_must_be_sorted() {
772 assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0, 1, 2], ZSTD).is_ok());
773 assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0, 2, 1], ZSTD).is_err());
774 assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0, 1, 1], ZSTD).is_err());
775 }
776
777 #[fuchsia::test]
778 fn test_compression_info_compressed_range_for_uncompressed_range() {
779 fn check_compression_ranges(
780 offsets: &[u64],
781 compressed_size: u64,
782 expected_ranges: &[Range<u64>],
783 chunk_size: u64,
784 read_ahead_size: u64,
785 ) {
786 let compression_info =
787 CompressionInfo::new(chunk_size, compressed_size, offsets, ZSTD).unwrap();
788 for (i, expected_range) in expected_ranges.iter().enumerate() {
789 let i = i as u64;
790 let result = compression_info
791 .compressed_range_for_uncompressed_range(
792 &(i * read_ahead_size..(i + 1) * read_ahead_size),
793 )
794 .unwrap();
795 assert_eq!(&result, expected_range);
796 }
797 }
798 check_compression_ranges(
799 &[0, 10, 20, 30],
800 30,
801 &[0..10, 10..20, 20..30, 30..30],
802 COMPRESSED_BLOB_CHUNK_SIZE,
803 COMPRESSED_BLOB_CHUNK_SIZE,
804 );
805 check_compression_ranges(
806 &[0, 10, 20, 30],
807 30,
808 &[0..20, 20..30],
809 COMPRESSED_BLOB_CHUNK_SIZE,
810 COMPRESSED_BLOB_CHUNK_SIZE * 2,
811 );
812 check_compression_ranges(
813 &[0, 10, 20, 30],
814 30,
815 &[0..30],
816 COMPRESSED_BLOB_CHUNK_SIZE,
817 COMPRESSED_BLOB_CHUNK_SIZE * 4,
818 );
819 check_compression_ranges(
820 &[0, 10, 20, 30, MAX_SMALL_OFFSET + 10],
821 MAX_SMALL_OFFSET + 10,
822 &[0..MAX_SMALL_OFFSET + 10, MAX_SMALL_OFFSET + 10..MAX_SMALL_OFFSET + 10],
823 COMPRESSED_BLOB_CHUNK_SIZE,
824 COMPRESSED_BLOB_CHUNK_SIZE * 4,
825 );
826 check_compression_ranges(
827 &[
828 0,
829 10,
830 20,
831 30,
832 MAX_SMALL_OFFSET + 10,
833 MAX_SMALL_OFFSET + 20,
834 MAX_SMALL_OFFSET + 30,
835 MAX_SMALL_OFFSET + 40,
836 MAX_SMALL_OFFSET + 50,
837 ],
838 MAX_SMALL_OFFSET + 50,
839 &[
840 0..20,
841 20..MAX_SMALL_OFFSET + 10,
842 MAX_SMALL_OFFSET + 10..MAX_SMALL_OFFSET + 30,
843 MAX_SMALL_OFFSET + 30..MAX_SMALL_OFFSET + 50,
844 ],
845 COMPRESSED_BLOB_CHUNK_SIZE,
846 COMPRESSED_BLOB_CHUNK_SIZE * 2,
847 );
848 }
849
850 #[fuchsia::test]
851 fn test_compression_info_compressed_range_for_uncompressed_range_errors() {
852 let compression_info = CompressionInfo::new(
853 COMPRESSED_BLOB_CHUNK_SIZE,
854 MAX_SMALL_OFFSET + 50,
855 &[
856 0,
857 10,
858 20,
859 30,
860 MAX_SMALL_OFFSET + 10,
861 MAX_SMALL_OFFSET + 20,
862 MAX_SMALL_OFFSET + 30,
863 MAX_SMALL_OFFSET + 40,
864 MAX_SMALL_OFFSET + 50,
865 ],
866 ZSTD,
867 )
868 .unwrap();
869
870 assert!(
872 compression_info
873 .compressed_range_for_uncompressed_range(&(1..COMPRESSED_BLOB_CHUNK_SIZE),)
874 .is_err()
875 );
876
877 assert!(
879 compression_info
880 .compressed_range_for_uncompressed_range(
881 &(COMPRESSED_BLOB_CHUNK_SIZE * 9..COMPRESSED_BLOB_CHUNK_SIZE * 12),
882 )
883 .is_err()
884 );
885
886 assert!(
888 compression_info
889 .compressed_range_for_uncompressed_range(&(0..COMPRESSED_BLOB_CHUNK_SIZE + 1),)
890 .is_err()
891 );
892 assert!(
893 compression_info
894 .compressed_range_for_uncompressed_range(&(0..COMPRESSED_BLOB_CHUNK_SIZE - 1),)
895 .is_err()
896 );
897 assert!(
898 compression_info
899 .compressed_range_for_uncompressed_range(
900 &(COMPRESSED_BLOB_CHUNK_SIZE..COMPRESSED_BLOB_CHUNK_SIZE * 2 + 1),
901 )
902 .is_err()
903 );
904 assert!(
905 compression_info
906 .compressed_range_for_uncompressed_range(
907 &(COMPRESSED_BLOB_CHUNK_SIZE..COMPRESSED_BLOB_CHUNK_SIZE * 2 - 1),
908 )
909 .is_err()
910 );
911
912 assert!(
914 compression_info
915 .compressed_range_for_uncompressed_range(
916 &(COMPRESSED_BLOB_CHUNK_SIZE * 8..COMPRESSED_BLOB_CHUNK_SIZE * 8 + 4096),
917 )
918 .is_ok()
919 );
920 }
921
922 #[fuchsia::test]
923 fn test_read_ahead_size_for_chunk_size() {
924 assert_eq!(read_ahead_size_for_chunk_size(32 * 1024, 32 * 1024), 32 * 1024);
925 assert_eq!(read_ahead_size_for_chunk_size(48 * 1024, 32 * 1024), 48 * 1024);
926 assert_eq!(read_ahead_size_for_chunk_size(64 * 1024, 32 * 1024), 64 * 1024);
927
928 assert_eq!(read_ahead_size_for_chunk_size(32 * 1024, 64 * 1024), 64 * 1024);
929 assert_eq!(read_ahead_size_for_chunk_size(48 * 1024, 64 * 1024), 48 * 1024);
930 assert_eq!(read_ahead_size_for_chunk_size(64 * 1024, 64 * 1024), 64 * 1024);
931 assert_eq!(read_ahead_size_for_chunk_size(96 * 1024, 64 * 1024), 96 * 1024);
932
933 assert_eq!(read_ahead_size_for_chunk_size(32 * 1024, 128 * 1024), 128 * 1024);
934 assert_eq!(read_ahead_size_for_chunk_size(48 * 1024, 128 * 1024), 96 * 1024);
935 assert_eq!(read_ahead_size_for_chunk_size(64 * 1024, 128 * 1024), 128 * 1024);
936 assert_eq!(read_ahead_size_for_chunk_size(96 * 1024, 128 * 1024), 96 * 1024);
937 }
938
939 fn build_compression_info(size: usize) -> (CompressionInfo, Vec<u8>, Vec<u8>) {
940 let options =
941 ChunkedArchiveOptions::V3 { compression_algorithm: CompressionAlgorithm::Lz4 };
942 let mut compressor = options.compressor();
943 let mut uncompressed_data = Vec::with_capacity(size);
944 {
945 let mut run_length = 1;
946 let mut run_value: u8 = 0;
947 while uncompressed_data.len() < size {
948 uncompressed_data
949 .resize(std::cmp::min(uncompressed_data.len() + run_length, size), run_value);
950 run_length = (run_length + 1) % 19 + 1;
951 run_value = (run_value + 1) % 17;
952 }
953 }
954 let mut compressed_offsets = vec![0];
955 let mut compressed_data = vec![];
956 for chunk in uncompressed_data.chunks(CHUNK_SIZE) {
957 let mut compressed_chunk = compressor.compress(chunk, 0).unwrap();
958 compressed_data.append(&mut compressed_chunk);
959 compressed_offsets.push(compressed_data.len() as u64);
960 }
961 compressed_offsets.pop();
962 let compressed_size = compressed_data.len() as u64;
963 (
964 CompressionInfo::new(
965 CHUNK_SIZE as u64,
966 compressed_size,
967 &compressed_offsets,
968 CompressionAlgorithm::Lz4,
969 )
970 .unwrap(),
971 compressed_data,
972 uncompressed_data,
973 )
974 }
975
976 #[fuchsia::test]
977 fn test_compression_info_decompress_single_chunk() {
978 let (compression_info, compressed_data, uncompressed_data) =
979 build_compression_info(CHUNK_SIZE);
980 let mut decompressed_data = vec![0u8; CHUNK_SIZE + 1];
981
982 compression_info
983 .decompress(&compressed_data[..], &mut decompressed_data[0..CHUNK_SIZE], 0)
984 .expect("failed to decompress");
985 assert_eq!(uncompressed_data, decompressed_data[0..CHUNK_SIZE]);
986
987 compression_info
989 .decompress(&compressed_data[..], &mut decompressed_data[0..CHUNK_SIZE - 1], 0)
990 .expect_err("decompression should fail");
991
992 compression_info
994 .decompress(&compressed_data[..], &mut decompressed_data[0..CHUNK_SIZE - 1], 0)
995 .expect_err("decompression should fail");
996 }
997
998 #[fuchsia::test]
999 fn test_compression_info_decompress_multiple_chunks() {
1000 fn slice_for_chunks<'a>(
1001 compressed_data: &'a [u8],
1002 compression_info: &CompressionInfo,
1003 chunks: Range<u64>,
1004 ) -> &'a [u8] {
1005 let range = compression_info
1006 .compressed_range_for_uncompressed_range(
1007 &(chunks.start * CHUNK_SIZE as u64..chunks.end * CHUNK_SIZE as u64),
1008 )
1009 .unwrap();
1010 &compressed_data[range.start as usize..range.end as usize]
1011 }
1012
1013 const BLOB_SIZE: usize = CHUNK_SIZE * 4 + 4096;
1014 let (compression_info, compressed_data, uncompressed_data) =
1015 build_compression_info(BLOB_SIZE);
1016 let mut decompressed_data = vec![0u8; BLOB_SIZE];
1017
1018 compression_info
1020 .decompress(&compressed_data[..], &mut decompressed_data[..], 0)
1021 .expect("failed to decompress");
1022 assert_eq!(uncompressed_data, decompressed_data);
1023
1024 compression_info
1026 .decompress(
1027 slice_for_chunks(&compressed_data, &compression_info, 0..4),
1028 &mut decompressed_data[0..CHUNK_SIZE * 4],
1029 0,
1030 )
1031 .expect("failed to decompress");
1032 assert_eq!(&uncompressed_data[0..CHUNK_SIZE], &decompressed_data[0..CHUNK_SIZE]);
1033
1034 compression_info
1036 .decompress(
1037 slice_for_chunks(&compressed_data, &compression_info, 0..4),
1038 &mut decompressed_data[0..CHUNK_SIZE * 4 - 1],
1039 0,
1040 )
1041 .expect_err("decompression should fail");
1042
1043 compression_info
1045 .decompress(
1046 slice_for_chunks(&compressed_data, &compression_info, 0..4),
1047 &mut decompressed_data[0..CHUNK_SIZE * 4 + 1],
1048 0,
1049 )
1050 .expect_err("decompression should fail");
1051
1052 let partial_chunk = slice_for_chunks(&compressed_data, &compression_info, 4..5);
1054 compression_info
1055 .decompress(partial_chunk, &mut decompressed_data[0..4096], CHUNK_SIZE as u64 * 4)
1056 .expect("failed to decompress");
1057 assert_eq!(&uncompressed_data[CHUNK_SIZE * 4..], &decompressed_data[0..4096]);
1058
1059 compression_info
1061 .decompress(partial_chunk, &mut decompressed_data[0..4095], CHUNK_SIZE as u64 * 4)
1062 .expect_err("decompression should fail");
1063
1064 compression_info
1066 .decompress(partial_chunk, &mut decompressed_data[0..4097], CHUNK_SIZE as u64 * 4)
1067 .expect_err("decompression should fail");
1068 }
1069
1070 #[fuchsia::test(threads = 10)]
1071 async fn test_refault_metric() {
1072 let fixture = new_blob_fixture().await;
1073 {
1074 let volume = fixture.volume().volume().clone();
1075 const FILE_SIZE: u64 = READ_AHEAD_SIZE * 4 - 4096;
1076 let data = vec![0xffu8; FILE_SIZE as usize];
1077 let hash = fixture.write_blob(&data, CompressionMode::Never).await;
1078
1079 let blob = fixture.get_opened_blob(hash).await.unwrap();
1080 assert_eq!(blob.chunks_supplied.len(), 4);
1081 assert_eq!(&blob.chunks_supplied.get(), &[false, false, false, false]);
1083
1084 blob.vmo.read_to_vec::<u8>(4096, 4096).unwrap();
1085
1086 assert_eq!(&blob.chunks_supplied.get(), &[true, false, false, false]);
1087
1088 blob.vmo.read_to_vec::<u8>(READ_AHEAD_SIZE * 2 + 4096, READ_AHEAD_SIZE).unwrap();
1089 assert_eq!(&blob.chunks_supplied.get(), &[true, false, true, true]);
1090
1091 assert_eq!(volume.blob_resupplied_count().read(Ordering::SeqCst), 0);
1093
1094 blob.clone().page_in(PageInRange::new(
1099 FILE_SIZE - READ_AHEAD_SIZE..FILE_SIZE,
1100 blob.dup(),
1101 Epoch::global().guard(),
1102 ));
1103 Epoch::global().barrier().await;
1104
1105 assert_eq!(volume.blob_resupplied_count().read(Ordering::SeqCst), 2);
1106 }
1107
1108 fixture.close().await;
1109 }
1110}