Skip to main content

nydus_storage/meta/
mod.rs

1// Copyright (C) 2021-2023 Alibaba Cloud. All rights reserved.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Generate, manage and access blob meta information for RAFS v6 data blobs.
6//!
7//! RAFS v6 filesystem includes three types of data:
8//! - fs meta: contain filesystem meta data including super block, inode table, dirent etc.
9//! - blob meta: contain digest and compression context for data chunks.
10//! - chunk data: contain chunked file data in compressed or uncompressed form.
11//!
12//! There are different ways to packing above three types of data into blobs:
13//! - meta blob/bootstrap: `fs meta`
14//! - native data blob: `chunk data` | `compression context table` | [`chunk digest table`] | [`table of context`]
15//! - native data blob with inlined fs meta: `chunk data` | `compression context table` | [`chunk digest table`] | `fs meta` | [`table of content`]
16//! - ZRan data blob: `compression context table` | [`chunk digest table`] | [`table of content`]
17//! - ZRan data blob with inlined fs meta: `compression context table` | [`chunk digest table`] | `fs meta` | [`table of content`]
18//!
19//! The blob compression context table contains following information:
20//! - chunk compression information table: to locate compressed/uncompressed chunks in the data blob
21//! - optional ZRan context table: to support randomly access/decompress gzip file
22//! - optional ZRan dictionary table: to support randomly access/decompress gzip file
23//!
24//! The blob compression context table is laid as below:
25//! | `chunk compression info table` | [`ZRan context table`] | [`ZRan dictionary table`]
26
27use std::any::Any;
28use std::borrow::Cow;
29use std::fs::OpenOptions;
30use std::io::Result;
31use std::mem::{size_of, ManuallyDrop};
32use std::ops::{Add, BitAnd, Not};
33use std::path::PathBuf;
34use std::sync::Arc;
35
36use nydus_utils::compress::zlib_random::ZranContext;
37use nydus_utils::crypt::decrypt_with_context;
38use nydus_utils::digest::{DigestData, RafsDigest};
39use nydus_utils::filemap::FileMapState;
40use nydus_utils::{compress, crypt};
41
42use crate::backend::BlobReader;
43use crate::device::v5::BlobV5ChunkInfo;
44use crate::device::{BlobChunkFlags, BlobChunkInfo, BlobFeatures, BlobInfo};
45use crate::meta::toc::{TocEntryList, TocLocation};
46use crate::utils::alloc_buf;
47use crate::{RAFS_MAX_CHUNKS_PER_BLOB, RAFS_MAX_CHUNK_SIZE};
48
49mod chunk_info_v1;
50pub use chunk_info_v1::BlobChunkInfoV1Ondisk;
51mod chunk_info_v2;
52pub use chunk_info_v2::BlobChunkInfoV2Ondisk;
53
54pub mod toc;
55
56mod zran;
57pub use zran::{ZranContextGenerator, ZranInflateContext};
58
59mod batch;
60pub use batch::{BatchContextGenerator, BatchInflateContext};
61
62const BLOB_CCT_MAGIC: u32 = 0xb10bb10bu32;
63const BLOB_CCT_HEADER_SIZE: u64 = 0x1000u64;
64const BLOB_CCT_CHUNK_SIZE_MASK: u64 = 0xff_ffff;
65
66const BLOB_CCT_V1_MAX_SIZE: u64 = RAFS_MAX_CHUNK_SIZE * 16;
67const BLOB_CCT_V2_MAX_SIZE: u64 = RAFS_MAX_CHUNK_SIZE * 24;
68//const BLOB_CCT_V1_RESERVED_SIZE: u64 = BLOB_METADATA_HEADER_SIZE - 44;
69const BLOB_CCT_V2_RESERVED_SIZE: u64 = BLOB_CCT_HEADER_SIZE - 64;
70
71/// File suffix for blob meta file.
72const BLOB_CCT_FILE_SUFFIX: &str = "blob.meta";
73/// File suffix for blob chunk digests.
74const BLOB_DIGEST_FILE_SUFFIX: &str = "blob.digest";
75/// File suffix for blob ToC.
76const BLOB_TOC_FILE_SUFFIX: &str = "blob.toc";
77
78/// On disk format for blob compression context table header.
79///
80/// Blob compression context table contains compression information for all chunks in the blob.
81/// The compression context table header will be written into the data blob in plaintext mode,
82/// and can be used as marker to locate the compression context table. All fields of compression
83/// context table header should be encoded in little-endian format.
84///
85/// The compression context table and header are arranged in the data blob as follows:
86///
87/// `chunk data`  |  `compression context table`  |  `[ZRan context table | ZRan dictionary]`  |  `compression context table header`
88#[repr(C)]
89#[derive(Clone, Copy, Debug)]
90pub struct BlobCompressionContextHeader {
91    /// Magic number to identify the header.
92    s_magic: u32,
93    /// Feature flags for the data blob.
94    s_features: u32,
95    /// Compression algorithm to process the compression context table.
96    s_ci_compressor: u32,
97    /// Number of entries in compression context table.
98    s_ci_entries: u32,
99    /// File offset to get the compression context table.
100    s_ci_offset: u64,
101    /// Size of compressed compression context table.
102    s_ci_compressed_size: u64,
103    /// Size of uncompressed compression context table.
104    s_ci_uncompressed_size: u64,
105    /// File offset to get the optional ZRan context data.
106    s_ci_zran_offset: u64,
107    /// Size of ZRan context data, including the ZRan context table and dictionary table.
108    s_ci_zran_size: u64,
109    /// Number of entries in the ZRan context table.
110    s_ci_zran_count: u32,
111
112    s_reserved: [u8; BLOB_CCT_V2_RESERVED_SIZE as usize],
113    /// Second magic number to identify the blob meta data header.
114    s_magic2: u32,
115}
116
117impl Default for BlobCompressionContextHeader {
118    fn default() -> Self {
119        BlobCompressionContextHeader {
120            s_magic: BLOB_CCT_MAGIC,
121            s_features: 0,
122            s_ci_compressor: compress::Algorithm::Lz4Block as u32,
123            s_ci_entries: 0,
124            s_ci_offset: 0,
125            s_ci_compressed_size: 0,
126            s_ci_uncompressed_size: 0,
127            s_ci_zran_offset: 0,
128            s_ci_zran_size: 0,
129            s_ci_zran_count: 0,
130            s_reserved: [0u8; BLOB_CCT_V2_RESERVED_SIZE as usize],
131            s_magic2: BLOB_CCT_MAGIC,
132        }
133    }
134}
135
136impl BlobCompressionContextHeader {
137    /// Check whether a blob feature is set or not.
138    pub fn has_feature(&self, feature: BlobFeatures) -> bool {
139        self.s_features & feature.bits() != 0
140    }
141
142    /// Get compression algorithm to process chunk compression information array.
143    pub fn ci_compressor(&self) -> compress::Algorithm {
144        if self.s_ci_compressor == compress::Algorithm::Lz4Block as u32 {
145            compress::Algorithm::Lz4Block
146        } else if self.s_ci_compressor == compress::Algorithm::GZip as u32 {
147            compress::Algorithm::GZip
148        } else if self.s_ci_compressor == compress::Algorithm::Zstd as u32 {
149            compress::Algorithm::Zstd
150        } else {
151            compress::Algorithm::None
152        }
153    }
154
155    /// Set compression algorithm to process chunk compression information array.
156    pub fn set_ci_compressor(&mut self, algo: compress::Algorithm) {
157        self.s_ci_compressor = algo as u32;
158    }
159
160    /// Get number of entries in chunk compression information array.
161    pub fn ci_entries(&self) -> u32 {
162        self.s_ci_entries
163    }
164
165    /// Set number of entries in chunk compression information array.
166    pub fn set_ci_entries(&mut self, entries: u32) {
167        self.s_ci_entries = entries;
168    }
169
170    /// Get offset of compressed chunk compression information array.
171    pub fn ci_compressed_offset(&self) -> u64 {
172        self.s_ci_offset
173    }
174
175    /// Set offset of compressed chunk compression information array.
176    pub fn set_ci_compressed_offset(&mut self, offset: u64) {
177        self.s_ci_offset = offset;
178    }
179
180    /// Get size of compressed chunk compression information array.
181    pub fn ci_compressed_size(&self) -> u64 {
182        self.s_ci_compressed_size
183    }
184
185    /// Set size of compressed chunk compression information array.
186    pub fn set_ci_compressed_size(&mut self, size: u64) {
187        self.s_ci_compressed_size = size;
188    }
189
190    /// Get size of uncompressed chunk compression information array.
191    pub fn ci_uncompressed_size(&self) -> u64 {
192        self.s_ci_uncompressed_size
193    }
194
195    /// Set size of uncompressed chunk compression information array.
196    pub fn set_ci_uncompressed_size(&mut self, size: u64) {
197        self.s_ci_uncompressed_size = size;
198    }
199
200    /// Get ZRan context information entry count.
201    pub fn ci_zran_count(&self) -> u32 {
202        self.s_ci_zran_count
203    }
204
205    /// Set ZRan context information entry count.
206    pub fn set_ci_zran_count(&mut self, count: u32) {
207        self.s_ci_zran_count = count;
208    }
209
210    /// Get offset of ZRan context information table.
211    pub fn ci_zran_offset(&self) -> u64 {
212        self.s_ci_zran_offset
213    }
214
215    /// Set offset of ZRan context information table.
216    pub fn set_ci_zran_offset(&mut self, offset: u64) {
217        self.s_ci_zran_offset = offset;
218    }
219
220    /// Get size of ZRan context information table and dictionary table.
221    pub fn ci_zran_size(&self) -> u64 {
222        self.s_ci_zran_size
223    }
224
225    /// Set size of ZRan context information table and dictionary table.
226    pub fn set_ci_zran_size(&mut self, size: u64) {
227        self.s_ci_zran_size = size;
228    }
229
230    /// Check whether uncompressed chunks are 4k aligned.
231    pub fn is_4k_aligned(&self) -> bool {
232        self.has_feature(BlobFeatures::ALIGNED)
233    }
234
235    /// Set flag indicating whether uncompressed chunks are aligned.
236    pub fn set_aligned(&mut self, aligned: bool) {
237        if aligned {
238            self.s_features |= BlobFeatures::ALIGNED.bits();
239        } else {
240            self.s_features &= !BlobFeatures::ALIGNED.bits();
241        }
242    }
243
244    /// Set flag indicating whether RAFS meta is inlined in the data blob.
245    pub fn set_inlined_fs_meta(&mut self, inlined: bool) {
246        if inlined {
247            self.s_features |= BlobFeatures::INLINED_FS_META.bits();
248        } else {
249            self.s_features &= !BlobFeatures::INLINED_FS_META.bits();
250        }
251    }
252
253    /// Set flag indicating whether chunk compression information format v2 is used or not.
254    pub fn set_chunk_info_v2(&mut self, enable: bool) {
255        if enable {
256            self.s_features |= BlobFeatures::CHUNK_INFO_V2.bits();
257        } else {
258            self.s_features &= !BlobFeatures::CHUNK_INFO_V2.bits();
259        }
260    }
261
262    /// Set flag indicating whether it's a ZRan blob or not.
263    pub fn set_ci_zran(&mut self, enable: bool) {
264        if enable {
265            self.s_features |= BlobFeatures::ZRAN.bits();
266        } else {
267            self.s_features &= !BlobFeatures::ZRAN.bits();
268        }
269    }
270
271    /// Set flag indicating whether blob.data and blob.meta are stored in separated blobs.
272    pub fn set_separate_blob(&mut self, enable: bool) {
273        if enable {
274            self.s_features |= BlobFeatures::SEPARATE.bits();
275        } else {
276            self.s_features &= !BlobFeatures::SEPARATE.bits();
277        }
278    }
279
280    /// Set flag indicating whether it's a blob for batch chunk or not.
281    pub fn set_ci_batch(&mut self, enable: bool) {
282        if enable {
283            self.s_features |= BlobFeatures::BATCH.bits();
284        } else {
285            self.s_features &= !BlobFeatures::BATCH.bits();
286        }
287    }
288
289    /// Set flag indicating whether chunk digest is inlined in the data blob or not.
290    pub fn set_inlined_chunk_digest(&mut self, enable: bool) {
291        if enable {
292            self.s_features |= BlobFeatures::INLINED_CHUNK_DIGEST.bits();
293        } else {
294            self.s_features &= !BlobFeatures::INLINED_CHUNK_DIGEST.bits();
295        }
296    }
297
298    /// Set flag indicating new blob format with tar headers.
299    pub fn set_has_tar_header(&mut self, enable: bool) {
300        if enable {
301            self.s_features |= BlobFeatures::HAS_TAR_HEADER.bits();
302        } else {
303            self.s_features &= !BlobFeatures::HAS_TAR_HEADER.bits();
304        }
305    }
306
307    /// Set flag indicating new blob format with toc headers.
308    pub fn set_has_toc(&mut self, enable: bool) {
309        if enable {
310            self.s_features |= BlobFeatures::HAS_TOC.bits();
311        } else {
312            self.s_features &= !BlobFeatures::HAS_TOC.bits();
313        }
314    }
315
316    /// Set flag indicating having inlined-meta capability.
317    pub fn set_cap_tar_toc(&mut self, enable: bool) {
318        if enable {
319            self.s_features |= BlobFeatures::CAP_TAR_TOC.bits();
320        } else {
321            self.s_features &= !BlobFeatures::CAP_TAR_TOC.bits();
322        }
323    }
324
325    /// Set flag indicating the blob is for RAFS filesystem in TARFS mode.
326    pub fn set_tarfs(&mut self, enable: bool) {
327        if enable {
328            self.s_features |= BlobFeatures::TARFS.bits();
329        } else {
330            self.s_features &= !BlobFeatures::TARFS.bits();
331        }
332    }
333
334    /// Set flag indicating the blob is encrypted.
335    pub fn set_encrypted(&mut self, enable: bool) {
336        if enable {
337            self.s_features |= BlobFeatures::ENCRYPTED.bits();
338        } else {
339            self.s_features &= !BlobFeatures::ENCRYPTED.bits();
340        }
341    }
342
343    /// Set flag indicating the blob is external.
344    pub fn set_external(&mut self, external: bool) {
345        if external {
346            self.s_features |= BlobFeatures::EXTERNAL.bits();
347        } else {
348            self.s_features &= !BlobFeatures::EXTERNAL.bits();
349        }
350    }
351
352    /// Get blob meta feature flags.
353    pub fn features(&self) -> u32 {
354        self.s_features
355    }
356
357    /// Convert the header as an `&[u8]`.
358    pub fn as_bytes(&self) -> &[u8] {
359        unsafe {
360            std::slice::from_raw_parts(
361                self as *const BlobCompressionContextHeader as *const u8,
362                size_of::<BlobCompressionContextHeader>(),
363            )
364        }
365    }
366
367    /// Set flag indicating whether it's a blob for batch chunk or not.
368    pub fn set_is_chunkdict_generated(&mut self, enable: bool) {
369        if enable {
370            self.s_features |= BlobFeatures::IS_CHUNKDICT_GENERATED.bits();
371        } else {
372            self.s_features &= !BlobFeatures::IS_CHUNKDICT_GENERATED.bits();
373        }
374    }
375}
376
377/// Struct to manage blob chunk compression information, a wrapper over [BlobCompressionContext].
378///
379/// A [BlobCompressionContextInfo] object is loaded from on disk [BlobCompressionContextHeader]
380/// object, and provides methods to query compression information about chunks in the blob.
381#[derive(Clone)]
382pub struct BlobCompressionContextInfo {
383    pub(crate) state: Arc<BlobCompressionContext>,
384}
385
386impl BlobCompressionContextInfo {
387    /// Create a new instance of [BlobCompressionContextInfo].
388    ///
389    /// If a blob compression context cache file is present and is valid, it will be reused.
390    /// Otherwise download compression context content from backend if `reader` is valid.
391    ///
392    /// The downloaded compression context table will be cached into a file named as
393    /// `[blob_id].blob.meta`. The cache file is readonly once created and may be accessed
394    /// concurrently by multiple clients.
395    pub fn new(
396        blob_path: &str,
397        blob_info: &BlobInfo,
398        reader: Option<&Arc<dyn BlobReader>>,
399        load_chunk_digest: bool,
400    ) -> Result<Self> {
401        assert_eq!(
402            size_of::<BlobCompressionContextHeader>() as u64,
403            BLOB_CCT_HEADER_SIZE
404        );
405        assert_eq!(size_of::<BlobChunkInfoV1Ondisk>(), 16);
406        assert_eq!(size_of::<BlobChunkInfoV2Ondisk>(), 24);
407        assert_eq!(size_of::<ZranInflateContext>(), 40);
408
409        let chunk_count = blob_info.chunk_count();
410        if chunk_count == 0 || chunk_count > RAFS_MAX_CHUNKS_PER_BLOB {
411            return Err(einval!("invalid chunk count in blob meta header"));
412        }
413
414        let uncompressed_size = blob_info.meta_ci_uncompressed_size() as usize;
415        let meta_path = format!("{}.{}", blob_path, BLOB_CCT_FILE_SUFFIX);
416        trace!(
417            "try to open blob meta file: path {:?} uncompressed_size {} chunk_count {}",
418            meta_path,
419            uncompressed_size,
420            chunk_count
421        );
422        let enable_write = reader.is_some();
423        let file = OpenOptions::new()
424            .read(true)
425            .write(enable_write)
426            .create(enable_write)
427            .open(&meta_path)
428            .map_err(|err| {
429                einval!(format!(
430                    "failed to open/create blob meta file {}: {}",
431                    meta_path, err
432                ))
433            })?;
434
435        let aligned_uncompressed_size = round_up_4k(uncompressed_size);
436        let expected_size = BLOB_CCT_HEADER_SIZE as usize + aligned_uncompressed_size;
437        let mut file_size = file.metadata()?.len();
438        if file_size == 0 && enable_write {
439            file.set_len(expected_size as u64)?;
440            file_size = expected_size as u64;
441        }
442        if file_size != expected_size as u64 {
443            return Err(einval!(format!(
444                "size of blob meta file '{}' doesn't match, expect {:x}, got {:x}",
445                meta_path, expected_size, file_size
446            )));
447        }
448
449        let mut filemap = FileMapState::new(file, 0, expected_size, enable_write)?;
450        let base = filemap.validate_range(0, expected_size)?;
451        let header =
452            filemap.get_mut::<BlobCompressionContextHeader>(aligned_uncompressed_size as usize)?;
453        if !Self::validate_header(blob_info, header)? {
454            if let Some(reader) = reader {
455                let buffer =
456                    unsafe { std::slice::from_raw_parts_mut(base as *mut u8, expected_size) };
457                Self::read_metadata(blob_info, reader, buffer)?;
458                if !Self::validate_header(blob_info, header)? {
459                    return Err(enoent!(format!("double check blob_info still invalid",)));
460                }
461                filemap.sync_data()?;
462            } else {
463                return Err(enoent!(format!(
464                    "blob meta header from file '{}' is invalid",
465                    meta_path
466                )));
467            }
468        }
469
470        let chunk_infos = BlobMetaChunkArray::from_file_map(&filemap, blob_info)?;
471        let chunk_infos = ManuallyDrop::new(chunk_infos);
472        let mut state = BlobCompressionContext {
473            blob_index: blob_info.blob_index(),
474            blob_features: blob_info.features().bits(),
475            compressed_size: blob_info.compressed_data_size(),
476            uncompressed_size: round_up_4k(blob_info.uncompressed_size()),
477            chunk_info_array: chunk_infos,
478            blob_meta_file_map: filemap,
479            ..Default::default()
480        };
481
482        if blob_info.has_feature(BlobFeatures::BATCH) {
483            let header = state
484                .blob_meta_file_map
485                .get_mut::<BlobCompressionContextHeader>(aligned_uncompressed_size as usize)?;
486            let inflate_offset = header.s_ci_zran_offset as usize;
487            let inflate_count = header.s_ci_zran_count as usize;
488            let batch_inflate_size = inflate_count * size_of::<BatchInflateContext>();
489            let ptr = state
490                .blob_meta_file_map
491                .validate_range(inflate_offset, batch_inflate_size)?;
492            let array = unsafe {
493                Vec::from_raw_parts(
494                    ptr as *mut u8 as *mut BatchInflateContext,
495                    inflate_count,
496                    inflate_count,
497                )
498            };
499            state.batch_info_array = ManuallyDrop::new(array);
500        } else if blob_info.has_feature(BlobFeatures::ZRAN) {
501            let header = state
502                .blob_meta_file_map
503                .get_mut::<BlobCompressionContextHeader>(aligned_uncompressed_size as usize)?;
504            let zran_offset = header.s_ci_zran_offset as usize;
505            let zran_count = header.s_ci_zran_count as usize;
506            let ci_zran_size = header.s_ci_zran_size as usize;
507            let zran_size = zran_count * size_of::<ZranInflateContext>();
508            let ptr = state
509                .blob_meta_file_map
510                .validate_range(zran_offset, zran_size)?;
511            let array = unsafe {
512                Vec::from_raw_parts(
513                    ptr as *mut u8 as *mut ZranInflateContext,
514                    zran_count,
515                    zran_count,
516                )
517            };
518            state.zran_info_array = ManuallyDrop::new(array);
519
520            let zran_dict_size = ci_zran_size - zran_size;
521            let ptr = state
522                .blob_meta_file_map
523                .validate_range(zran_offset + zran_size, zran_dict_size)?;
524            let array =
525                unsafe { Vec::from_raw_parts(ptr as *mut u8, zran_dict_size, zran_dict_size) };
526            state.zran_dict_table = ManuallyDrop::new(array);
527        }
528
529        if load_chunk_digest && blob_info.has_feature(BlobFeatures::INLINED_CHUNK_DIGEST) {
530            let digest_path = PathBuf::from(format!("{}.{}", blob_path, BLOB_DIGEST_FILE_SUFFIX));
531            if let Some(reader) = reader {
532                let toc_path = format!("{}.{}", blob_path, BLOB_TOC_FILE_SUFFIX);
533                let location = if blob_info.blob_toc_size() != 0 {
534                    let blob_size = reader
535                        .blob_size()
536                        .map_err(|_e| eio!("failed to get blob size"))?;
537                    let offset = blob_size - blob_info.blob_toc_size() as u64;
538                    let mut location = TocLocation::new(offset, blob_info.blob_toc_size() as u64);
539                    let digest = blob_info.blob_toc_digest();
540                    for c in digest {
541                        if *c != 0 {
542                            location.validate_digest = true;
543                            location.digest.data = *digest;
544                            break;
545                        }
546                    }
547                    location
548                } else {
549                    TocLocation::default()
550                };
551                let toc_list =
552                    TocEntryList::read_from_cache_file(toc_path, reader.as_ref(), &location)?;
553                toc_list.extract_from_blob(reader.clone(), None, Some(&digest_path))?;
554            }
555            if !digest_path.exists() {
556                return Err(eother!("failed to download chunk digest file from blob"));
557            }
558
559            let file = OpenOptions::new().read(true).open(&digest_path)?;
560            let md = file.metadata()?;
561            let size = 32 * blob_info.chunk_count() as usize;
562            if md.len() != size as u64 {
563                return Err(eother!(format!(
564                    "size of chunk digest file doesn't match, expect {}, got {}",
565                    size,
566                    md.len()
567                )));
568            }
569
570            let file_map = FileMapState::new(file, 0, size, false)?;
571            let ptr = file_map.validate_range(0, size)?;
572            let array = unsafe {
573                Vec::from_raw_parts(
574                    ptr as *mut u8 as *mut _,
575                    chunk_count as usize,
576                    chunk_count as usize,
577                )
578            };
579            state.chunk_digest_file_map = file_map;
580            state.chunk_digest_array = ManuallyDrop::new(array);
581        }
582
583        Ok(BlobCompressionContextInfo {
584            state: Arc::new(state),
585        })
586    }
587
588    /// Get data chunks covering uncompressed data range `[start, start + size)`.
589    ///
590    /// For 4k-aligned uncompressed data chunks, there may be padding areas between data chunks.
591    ///
592    /// The method returns error if any of following condition is true:
593    /// - range [start, start + size) is invalid.
594    /// - `start` is bigger than blob size.
595    /// - some portions of the range [start, start + size) is not covered by chunks.
596    /// - blob meta is invalid.
597    pub fn get_chunks_uncompressed(
598        &self,
599        start: u64,
600        size: u64,
601        batch_size: u64,
602    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
603        let end = start.checked_add(size).ok_or_else(|| {
604            einval!(format!(
605                "get_chunks_uncompressed: invalid start {}/size {}",
606                start, size
607            ))
608        })?;
609        if end > self.state.uncompressed_size {
610            return Err(einval!(format!(
611                "get_chunks_uncompressed: invalid end {}/uncompressed_size {}",
612                end, self.state.uncompressed_size
613            )));
614        }
615        let batch_end = if batch_size <= size {
616            end
617        } else {
618            std::cmp::min(
619                start.checked_add(batch_size).unwrap_or(end),
620                self.state.uncompressed_size,
621            )
622        };
623        let batch_size = if batch_size < size { size } else { batch_size };
624
625        self.state
626            .get_chunks_uncompressed(start, end, batch_end, batch_size)
627    }
628
629    /// Get data chunks covering compressed data range `[start, start + size)`.
630    ///
631    /// The method returns error if any of following condition is true:
632    /// - range [start, start + size) is invalid.
633    /// - `start` is bigger than blob size.
634    /// - some portions of the range [start, start + size) is not covered by chunks.
635    /// - blob meta is invalid.
636    pub fn get_chunks_compressed(
637        &self,
638        start: u64,
639        size: u64,
640        batch_size: u64,
641        prefetch: bool,
642    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
643        let end = start.checked_add(size).ok_or_else(|| {
644            einval!(einval!(format!(
645                "get_chunks_compressed: invalid start {}/size {}",
646                start, size
647            )))
648        })?;
649        if end > self.state.compressed_size {
650            return Err(einval!(format!(
651                "get_chunks_compressed: invalid end {}/compressed_size {}",
652                end, self.state.compressed_size
653            )));
654        }
655        let batch_end = if batch_size <= size {
656            end
657        } else {
658            std::cmp::min(
659                start.checked_add(batch_size).unwrap_or(end),
660                self.state.compressed_size,
661            )
662        };
663
664        self.state
665            .get_chunks_compressed(start, end, batch_end, batch_size, prefetch)
666    }
667
668    /// Amplify the request by appending more continuous chunks to the chunk array.
669    pub fn add_more_chunks(
670        &self,
671        chunks: &[Arc<dyn BlobChunkInfo>],
672        max_size: u64,
673    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
674        self.state.add_more_chunks(chunks, max_size)
675    }
676
677    /// Get number of chunks in the data blob.
678    pub fn get_chunk_count(&self) -> usize {
679        self.state.chunk_info_array.len()
680    }
681
682    /// Get index of chunk covering uncompressed `addr`.
683    pub fn get_chunk_index(&self, addr: u64) -> Result<usize> {
684        self.state.get_chunk_index(addr)
685    }
686
687    /// Get uncompressed offset of the chunk at `chunk_index`.
688    pub fn get_uncompressed_offset(&self, chunk_index: usize) -> u64 {
689        self.state.get_uncompressed_offset(chunk_index)
690    }
691
692    /// Get chunk digest for the chunk at `chunk_index`.
693    pub fn get_chunk_digest(&self, chunk_index: usize) -> Option<&[u8]> {
694        self.state.get_chunk_digest(chunk_index)
695    }
696
697    /// Get `BlobChunkInfo` object for the chunk at `chunk_index`.
698    pub fn get_chunk_info(&self, chunk_index: usize) -> Arc<dyn BlobChunkInfo> {
699        BlobMetaChunk::new(chunk_index, &self.state)
700    }
701
702    /// Get whether chunk at `chunk_index` is batch chunk.
703    /// Some chunks build in batch mode can also be non-batch chunks,
704    /// that they are too big to be put into a batch.
705    pub fn is_batch_chunk(&self, chunk_index: u32) -> bool {
706        self.state.is_batch_chunk(chunk_index as usize)
707    }
708
709    /// Get Batch index associated with the chunk at `chunk_index`.
710    pub fn get_batch_index(&self, chunk_index: u32) -> Result<u32> {
711        self.state.get_batch_index(chunk_index as usize)
712    }
713
714    /// Get uncompressed batch offset associated with the chunk at `chunk_index`.
715    pub fn get_uncompressed_offset_in_batch_buf(&self, chunk_index: u32) -> Result<u32> {
716        self.state
717            .get_uncompressed_offset_in_batch_buf(chunk_index as usize)
718    }
719
720    /// Get Batch context information at `batch_index`.
721    pub fn get_batch_context(&self, batch_index: u32) -> Result<&BatchInflateContext> {
722        self.state.get_batch_context(batch_index as usize)
723    }
724
725    /// Get compressed size associated with the chunk at `chunk_index`.
726    /// Capable of handling both batch and non-batch chunks.
727    pub fn get_compressed_size(&self, chunk_index: u32) -> Result<u32> {
728        self.state.get_compressed_size(chunk_index as usize)
729    }
730
731    /// Get ZRan index associated with the chunk at `chunk_index`.
732    pub fn get_zran_index(&self, chunk_index: u32) -> Result<u32> {
733        self.state.get_zran_index(chunk_index as usize)
734    }
735
736    /// Get ZRan offset associated with the chunk at `chunk_index`.
737    pub fn get_zran_offset(&self, chunk_index: u32) -> Result<u32> {
738        self.state.get_zran_offset(chunk_index as usize)
739    }
740
741    /// Get ZRan context information at `zran_index`.
742    pub fn get_zran_context(&self, zran_index: u32) -> Result<(ZranContext, &[u8])> {
743        self.state.get_zran_context(zran_index as usize)
744    }
745
746    fn read_metadata(
747        blob_info: &BlobInfo,
748        reader: &Arc<dyn BlobReader>,
749        buffer: &mut [u8],
750    ) -> Result<()> {
751        trace!(
752            "blob_info compressor {} ci_compressor {} ci_compressed_size {} ci_uncompressed_size {}",
753            blob_info.compressor(),
754            blob_info.meta_ci_compressor(),
755            blob_info.meta_ci_compressed_size(),
756            blob_info.meta_ci_uncompressed_size(),
757        );
758
759        let compressed_size = blob_info.meta_ci_compressed_size();
760        let uncompressed_size = blob_info.meta_ci_uncompressed_size();
761        let aligned_uncompressed_size = round_up_4k(uncompressed_size);
762        let expected_raw_size = (compressed_size + BLOB_CCT_HEADER_SIZE) as usize;
763        let mut raw_data = alloc_buf(expected_raw_size);
764
765        let read_size = (|| {
766            // The maximum retry times
767            let mut retry_count = 3;
768
769            loop {
770                match reader.read_all(&mut raw_data, blob_info.meta_ci_offset()) {
771                    Ok(size) => return Ok(size),
772                    Err(e) => {
773                        // Handle BackendError, retry a maximum of three times.
774                        if retry_count > 0 {
775                            warn!(
776                                "failed to read metadata for blob {} from backend, {}, retry read metadata",
777                                blob_info.blob_id(),
778                                e
779                            );
780                            retry_count -= 1;
781                            continue;
782                        }
783
784                        return Err(eio!(format!(
785                            "failed to read metadata for blob {} from backend, {}",
786                            blob_info.blob_id(),
787                            e
788                        )));
789                    }
790                }
791            }
792        })()?;
793
794        if read_size != expected_raw_size {
795            return Err(eio!(format!(
796                "failed to read metadata for blob {} from backend, compressor {}, got {} bytes, expect {} bytes",
797                blob_info.blob_id(),
798                blob_info.meta_ci_compressor(),
799                read_size,
800                expected_raw_size
801            )));
802        }
803
804        let decrypted = match decrypt_with_context(
805            &raw_data[0..compressed_size as usize],
806            &blob_info.cipher_object(),
807            &blob_info.cipher_context(),
808            blob_info.cipher() != crypt::Algorithm::None,
809        ){
810            Ok(data) => data,
811            Err(e) => return Err(eio!(format!(
812                "failed to decrypt metadata for blob {} from backend, cipher {}, encrypted data size {}, {}",
813                blob_info.blob_id(),
814                blob_info.cipher(),
815                compressed_size,
816                e
817            ))),
818        };
819        let header = match decrypt_with_context(
820            &raw_data[compressed_size as usize..expected_raw_size],
821            &blob_info.cipher_object(),
822            &blob_info.cipher_context(),
823            blob_info.cipher() != crypt::Algorithm::None,
824        ){
825            Ok(data) => data,
826            Err(e) => return Err(eio!(format!(
827                "failed to decrypt meta header for blob {} from backend, cipher {}, encrypted data size {}, {}",
828                blob_info.blob_id(),
829                blob_info.cipher(),
830                compressed_size,
831                e
832            ))),
833        };
834
835        let uncompressed = if blob_info.meta_ci_compressor() != compress::Algorithm::None {
836            // Lz4 does not support concurrent decompression of the same data into
837            // the same piece of memory. There will be multiple containers mmap the
838            // same file, causing the buffer to be shared between different
839            // processes. This will cause data errors due to race issues when
840            // decompressing with lz4. We solve this problem by creating a temporary
841            // memory to hold the decompressed data.
842            //
843            // Because this process will only be executed when the blob.meta file is
844            // created for the first time, which means that a machine will only
845            // execute the process once when the blob.meta is created for the first
846            // time, the memory consumption and performance impact are relatively
847            // small.
848            let mut uncompressed = vec![0u8; uncompressed_size as usize];
849            compress::decompress(
850                &decrypted,
851                &mut uncompressed,
852                blob_info.meta_ci_compressor(),
853            )
854            .map_err(|e| {
855                error!("failed to decompress blob meta data: {}", e);
856                e
857            })?;
858            Cow::Owned(uncompressed)
859        } else {
860            decrypted
861        };
862        buffer[0..uncompressed_size as usize].copy_from_slice(&uncompressed);
863        buffer[aligned_uncompressed_size as usize
864            ..(aligned_uncompressed_size + BLOB_CCT_HEADER_SIZE) as usize]
865            .copy_from_slice(&header);
866        Ok(())
867    }
868
869    fn validate_header(
870        blob_info: &BlobInfo,
871        header: &BlobCompressionContextHeader,
872    ) -> Result<bool> {
873        trace!("blob meta header magic {:x}/{:x}, entries {:x}/{:x}, features {:x}/{:x}, compressor {:x}/{:x}, ci_offset {:x}/{:x}, compressed_size {:x}/{:x}, uncompressed_size {:x}/{:x}",
874                u32::from_le(header.s_magic),
875                BLOB_CCT_MAGIC,
876                u32::from_le(header.s_ci_entries),
877                blob_info.chunk_count(),
878                u32::from_le(header.s_features),
879                blob_info.features().bits(),
880                u32::from_le(header.s_ci_compressor),
881                blob_info.meta_ci_compressor() as u32,
882                u64::from_le(header.s_ci_offset),
883                blob_info.meta_ci_offset(),
884                u64::from_le(header.s_ci_compressed_size),
885                blob_info.meta_ci_compressed_size(),
886                u64::from_le(header.s_ci_uncompressed_size),
887                blob_info.meta_ci_uncompressed_size());
888
889        if u32::from_le(header.s_magic) != BLOB_CCT_MAGIC
890            || u32::from_le(header.s_magic2) != BLOB_CCT_MAGIC
891            || (!blob_info.has_feature(BlobFeatures::IS_CHUNKDICT_GENERATED)
892                && u32::from_le(header.s_ci_entries) != blob_info.chunk_count())
893            || u32::from_le(header.s_ci_compressor) != blob_info.meta_ci_compressor() as u32
894            || u64::from_le(header.s_ci_offset) != blob_info.meta_ci_offset()
895            || u64::from_le(header.s_ci_compressed_size) != blob_info.meta_ci_compressed_size()
896            || u64::from_le(header.s_ci_uncompressed_size) != blob_info.meta_ci_uncompressed_size()
897        {
898            return Ok(false);
899        }
900
901        let chunk_count = blob_info.chunk_count();
902        if chunk_count == 0 || chunk_count > RAFS_MAX_CHUNKS_PER_BLOB {
903            return Err(einval!(format!(
904                "chunk count {:x} in blob meta header is invalid!",
905                chunk_count
906            )));
907        }
908
909        let info_size = u64::from_le(header.s_ci_uncompressed_size) as usize;
910        let aligned_info_size = round_up_4k(info_size);
911        if blob_info.has_feature(BlobFeatures::CHUNK_INFO_V2)
912            && (blob_info.has_feature(BlobFeatures::ZRAN)
913                || blob_info.has_feature(BlobFeatures::BATCH))
914        {
915            if info_size < (chunk_count as usize) * (size_of::<BlobChunkInfoV2Ondisk>()) {
916                return Err(einval!("uncompressed size in blob meta header is invalid!"));
917            }
918        } else if blob_info.has_feature(BlobFeatures::CHUNK_INFO_V2) {
919            if info_size != (chunk_count as usize) * (size_of::<BlobChunkInfoV2Ondisk>())
920                || (aligned_info_size as u64) > BLOB_CCT_V2_MAX_SIZE
921            {
922                return Err(einval!("uncompressed size in blob meta header is invalid!"));
923            }
924        } else if blob_info.has_feature(BlobFeatures::ZRAN)
925            || blob_info.has_feature(BlobFeatures::BATCH)
926        {
927            return Err(einval!("invalid feature flags in blob meta header!"));
928        } else if !blob_info.has_feature(BlobFeatures::IS_CHUNKDICT_GENERATED)
929            && (info_size != (chunk_count as usize) * (size_of::<BlobChunkInfoV1Ondisk>())
930                || (aligned_info_size as u64) > BLOB_CCT_V1_MAX_SIZE)
931        {
932            return Err(einval!("uncompressed size in blob meta header is invalid!"));
933        }
934
935        if blob_info.has_feature(BlobFeatures::ZRAN) {
936            let offset = header.s_ci_zran_offset;
937            if offset != (chunk_count as u64) * (size_of::<BlobChunkInfoV2Ondisk>() as u64) {
938                return Ok(false);
939            }
940            if offset + header.s_ci_zran_size > info_size as u64 {
941                return Ok(false);
942            }
943            let zran_count = header.s_ci_zran_count as u64;
944            let size = zran_count * size_of::<ZranInflateContext>() as u64;
945            if zran_count > chunk_count as u64 {
946                return Ok(false);
947            }
948            if size > header.s_ci_zran_size {
949                return Ok(false);
950            }
951        }
952
953        Ok(true)
954    }
955}
956
957/// Struct to maintain compression context information for all chunks in a blob.
958#[derive(Default)]
959pub struct BlobCompressionContext {
960    pub(crate) blob_index: u32,
961    pub(crate) blob_features: u32,
962    pub(crate) compressed_size: u64,
963    pub(crate) uncompressed_size: u64,
964    pub(crate) chunk_info_array: ManuallyDrop<BlobMetaChunkArray>,
965    pub(crate) chunk_digest_array: ManuallyDrop<Vec<DigestData>>,
966    pub(crate) batch_info_array: ManuallyDrop<Vec<BatchInflateContext>>,
967    pub(crate) zran_info_array: ManuallyDrop<Vec<ZranInflateContext>>,
968    pub(crate) zran_dict_table: ManuallyDrop<Vec<u8>>,
969    blob_meta_file_map: FileMapState,
970    chunk_digest_file_map: FileMapState,
971    chunk_digest_default: RafsDigest,
972}
973
974impl BlobCompressionContext {
975    fn get_chunks_uncompressed(
976        self: &Arc<BlobCompressionContext>,
977        start: u64,
978        end: u64,
979        batch_end: u64,
980        batch_size: u64,
981    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
982        self.chunk_info_array
983            .get_chunks_uncompressed(self, start, end, batch_end, batch_size)
984    }
985
986    fn get_chunks_compressed(
987        self: &Arc<BlobCompressionContext>,
988        start: u64,
989        end: u64,
990        batch_end: u64,
991        batch_size: u64,
992        prefetch: bool,
993    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
994        self.chunk_info_array
995            .get_chunks_compressed(self, start, end, batch_end, batch_size, prefetch)
996    }
997
998    fn add_more_chunks(
999        self: &Arc<BlobCompressionContext>,
1000        chunks: &[Arc<dyn BlobChunkInfo>],
1001        max_size: u64,
1002    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
1003        self.chunk_info_array
1004            .add_more_chunks(self, chunks, max_size)
1005    }
1006
1007    fn get_uncompressed_offset(&self, chunk_index: usize) -> u64 {
1008        self.chunk_info_array.uncompressed_offset(chunk_index)
1009    }
1010
1011    fn get_chunk_digest(&self, chunk_index: usize) -> Option<&[u8]> {
1012        if chunk_index < self.chunk_digest_array.len() {
1013            Some(&self.chunk_digest_array[chunk_index])
1014        } else {
1015            None
1016        }
1017    }
1018
1019    fn get_chunk_index(&self, addr: u64) -> Result<usize> {
1020        self.chunk_info_array
1021            .get_chunk_index_nocheck(self, addr, false)
1022    }
1023
1024    /// Get whether chunk at `chunk_index` is batch chunk.
1025    /// Some chunks build in batch mode can also be non-batch chunks,
1026    /// that they are too big to be put into a batch.
1027    fn is_batch_chunk(&self, chunk_index: usize) -> bool {
1028        self.chunk_info_array.is_batch(chunk_index)
1029    }
1030
1031    fn get_batch_index(&self, chunk_index: usize) -> Result<u32> {
1032        self.chunk_info_array.batch_index(chunk_index)
1033    }
1034
1035    fn get_uncompressed_offset_in_batch_buf(&self, chunk_index: usize) -> Result<u32> {
1036        self.chunk_info_array
1037            .uncompressed_offset_in_batch_buf(chunk_index)
1038    }
1039
1040    /// Get Batch context information for decoding.
1041    fn get_batch_context(&self, batch_index: usize) -> Result<&BatchInflateContext> {
1042        if batch_index < self.batch_info_array.len() {
1043            let ctx = &self.batch_info_array[batch_index];
1044            Ok(ctx)
1045        } else {
1046            Err(einval!(format!(
1047                "Invalid batch index, current: {}, max: {}",
1048                batch_index,
1049                self.batch_info_array.len()
1050            )))
1051        }
1052    }
1053
1054    /// Get compressed size associated with the chunk at `chunk_index`.
1055    /// Capable of handling both batch and non-batch chunks.
1056    pub fn get_compressed_size(&self, chunk_index: usize) -> Result<u32> {
1057        if self.is_batch_chunk(chunk_index) {
1058            let ctx = self
1059                .get_batch_context(self.get_batch_index(chunk_index)? as usize)
1060                .unwrap();
1061            Ok(ctx.compressed_size())
1062        } else {
1063            Ok(self.chunk_info_array.compressed_size(chunk_index))
1064        }
1065    }
1066
1067    fn get_zran_index(&self, chunk_index: usize) -> Result<u32> {
1068        self.chunk_info_array.zran_index(chunk_index)
1069    }
1070
1071    fn get_zran_offset(&self, chunk_index: usize) -> Result<u32> {
1072        self.chunk_info_array.zran_offset(chunk_index)
1073    }
1074
1075    /// Get ZRan context information for decoding.
1076    fn get_zran_context(&self, zran_index: usize) -> Result<(ZranContext, &[u8])> {
1077        if zran_index < self.zran_info_array.len() {
1078            let entry = &self.zran_info_array[zran_index];
1079            let dict_off = entry.dict_offset() as usize;
1080            let dict_size = entry.dict_size() as usize;
1081            if dict_off.checked_add(dict_size).is_none()
1082                || dict_off + dict_size > self.zran_dict_table.len()
1083            {
1084                return Err(einval!(format!(
1085                    "Invalid ZRan context, dict_off: {}, dict_size: {}, max: {}",
1086                    dict_off,
1087                    dict_size,
1088                    self.zran_dict_table.len()
1089                )));
1090            };
1091            let dict = &self.zran_dict_table[dict_off..dict_off + dict_size];
1092            let ctx = ZranContext::from(entry);
1093            Ok((ctx, dict))
1094        } else {
1095            Err(einval!(format!(
1096                "Invalid ZRan index, current: {}, max: {}",
1097                zran_index,
1098                self.zran_info_array.len()
1099            )))
1100        }
1101    }
1102
1103    pub(crate) fn is_separate(&self) -> bool {
1104        self.blob_features & BlobFeatures::SEPARATE.bits() != 0
1105    }
1106
1107    pub(crate) fn is_encrypted(&self) -> bool {
1108        self.blob_features & BlobFeatures::ENCRYPTED.bits() != 0
1109    }
1110}
1111
1112#[derive(Clone)]
1113/// A customized array to host chunk information table for a blob.
1114pub enum BlobMetaChunkArray {
1115    /// V1 chunk compression information array.
1116    V1(Vec<BlobChunkInfoV1Ondisk>),
1117    /// V2 chunk compression information array.
1118    V2(Vec<BlobChunkInfoV2Ondisk>),
1119}
1120
1121impl Default for BlobMetaChunkArray {
1122    fn default() -> Self {
1123        BlobMetaChunkArray::new_v2()
1124    }
1125}
1126
1127// Methods for RAFS filesystem builder.
1128impl BlobMetaChunkArray {
1129    /// Create a [BlobMetaChunkArray] with v1 chunk compression information format.
1130    pub fn new_v1() -> Self {
1131        BlobMetaChunkArray::V1(Vec::new())
1132    }
1133
1134    /// Create a [BlobMetaChunkArray] with v2 chunk compression information format.
1135    pub fn new_v2() -> Self {
1136        BlobMetaChunkArray::V2(Vec::new())
1137    }
1138
1139    /// Get number of entries in the chunk compression information array.
1140    pub fn len(&self) -> usize {
1141        match self {
1142            BlobMetaChunkArray::V1(v) => v.len(),
1143            BlobMetaChunkArray::V2(v) => v.len(),
1144        }
1145    }
1146
1147    /// Check whether the chunk compression information array is empty or not.
1148    pub fn is_empty(&self) -> bool {
1149        match self {
1150            BlobMetaChunkArray::V1(v) => v.is_empty(),
1151            BlobMetaChunkArray::V2(v) => v.is_empty(),
1152        }
1153    }
1154
1155    /// Convert the chunk compression information array as a u8 slice.
1156    pub fn as_byte_slice(&self) -> &[u8] {
1157        match self {
1158            BlobMetaChunkArray::V1(v) => unsafe {
1159                std::slice::from_raw_parts(
1160                    v.as_ptr() as *const u8,
1161                    v.len() * size_of::<BlobChunkInfoV1Ondisk>(),
1162                )
1163            },
1164            BlobMetaChunkArray::V2(v) => unsafe {
1165                std::slice::from_raw_parts(
1166                    v.as_ptr() as *const u8,
1167                    v.len() * size_of::<BlobChunkInfoV2Ondisk>(),
1168                )
1169            },
1170        }
1171    }
1172
1173    /// Add an entry of v1 chunk compression information into the array.
1174    pub fn add_v1(
1175        &mut self,
1176        compressed_offset: u64,
1177        compressed_size: u32,
1178        uncompressed_offset: u64,
1179        uncompressed_size: u32,
1180    ) {
1181        match self {
1182            BlobMetaChunkArray::V1(v) => {
1183                let mut meta = BlobChunkInfoV1Ondisk::default();
1184                meta.set_compressed_offset(compressed_offset);
1185                meta.set_compressed_size(compressed_size);
1186                meta.set_uncompressed_offset(uncompressed_offset);
1187                meta.set_uncompressed_size(uncompressed_size);
1188                v.push(meta);
1189            }
1190            BlobMetaChunkArray::V2(_v) => unimplemented!(),
1191        }
1192    }
1193
1194    /// Add an entry of v2 chunk compression information into the array.
1195    #[allow(clippy::too_many_arguments)]
1196    pub fn add_v2(
1197        &mut self,
1198        compressed_offset: u64,
1199        compressed_size: u32,
1200        uncompressed_offset: u64,
1201        uncompressed_size: u32,
1202        compressed: bool,
1203        encrypted: bool,
1204        has_crc32: bool,
1205        is_batch: bool,
1206        data: u64,
1207    ) {
1208        match self {
1209            BlobMetaChunkArray::V2(v) => {
1210                let mut meta = BlobChunkInfoV2Ondisk::default();
1211                meta.set_compressed_offset(compressed_offset);
1212                meta.set_compressed_size(compressed_size);
1213                meta.set_uncompressed_offset(uncompressed_offset);
1214                meta.set_uncompressed_size(uncompressed_size);
1215                meta.set_compressed(compressed);
1216                meta.set_encrypted(encrypted);
1217                meta.set_has_crc32(has_crc32);
1218                meta.set_batch(is_batch);
1219                meta.set_data(data);
1220                v.push(meta);
1221            }
1222            BlobMetaChunkArray::V1(_v) => unimplemented!(),
1223        }
1224    }
1225
1226    /// Add an entry of pre-built v2 chunk compression information into the array.
1227    pub fn add_v2_info(&mut self, chunk_info: BlobChunkInfoV2Ondisk) {
1228        match self {
1229            BlobMetaChunkArray::V2(v) => v.push(chunk_info),
1230            BlobMetaChunkArray::V1(_v) => unimplemented!(),
1231        }
1232    }
1233}
1234
1235impl BlobMetaChunkArray {
1236    fn from_file_map(filemap: &FileMapState, blob_info: &BlobInfo) -> Result<Self> {
1237        let chunk_count = blob_info.chunk_count();
1238        if blob_info.has_feature(BlobFeatures::CHUNK_INFO_V2) {
1239            let chunk_size = chunk_count as usize * size_of::<BlobChunkInfoV2Ondisk>();
1240            let base = filemap.validate_range(0, chunk_size)?;
1241            let v = unsafe {
1242                Vec::from_raw_parts(
1243                    base as *mut u8 as *mut BlobChunkInfoV2Ondisk,
1244                    chunk_count as usize,
1245                    chunk_count as usize,
1246                )
1247            };
1248            Ok(BlobMetaChunkArray::V2(v))
1249        } else {
1250            let chunk_size = chunk_count as usize * size_of::<BlobChunkInfoV1Ondisk>();
1251            let base = filemap.validate_range(0, chunk_size)?;
1252            let v = unsafe {
1253                Vec::from_raw_parts(
1254                    base as *mut u8 as *mut BlobChunkInfoV1Ondisk,
1255                    chunk_count as usize,
1256                    chunk_count as usize,
1257                )
1258            };
1259            Ok(BlobMetaChunkArray::V1(v))
1260        }
1261    }
1262
1263    fn get_chunk_index_nocheck(
1264        &self,
1265        state: &BlobCompressionContext,
1266        addr: u64,
1267        compressed: bool,
1268    ) -> Result<usize> {
1269        match self {
1270            BlobMetaChunkArray::V1(v) => {
1271                Self::_get_chunk_index_nocheck(state, v, addr, compressed, false)
1272            }
1273            BlobMetaChunkArray::V2(v) => {
1274                Self::_get_chunk_index_nocheck(state, v, addr, compressed, false)
1275            }
1276        }
1277    }
1278
1279    fn get_chunks_compressed(
1280        &self,
1281        state: &Arc<BlobCompressionContext>,
1282        start: u64,
1283        end: u64,
1284        batch_end: u64,
1285        batch_size: u64,
1286        prefetch: bool,
1287    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
1288        match self {
1289            BlobMetaChunkArray::V1(v) => {
1290                Self::_get_chunks_compressed(state, v, start, end, batch_end, batch_size, prefetch)
1291            }
1292            BlobMetaChunkArray::V2(v) => {
1293                Self::_get_chunks_compressed(state, v, start, end, batch_end, batch_size, prefetch)
1294            }
1295        }
1296    }
1297
1298    fn get_chunks_uncompressed(
1299        &self,
1300        state: &Arc<BlobCompressionContext>,
1301        start: u64,
1302        end: u64,
1303        batch_end: u64,
1304        batch_size: u64,
1305    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
1306        match self {
1307            BlobMetaChunkArray::V1(v) => {
1308                Self::_get_chunks_uncompressed(state, v, start, end, batch_end, batch_size)
1309            }
1310            BlobMetaChunkArray::V2(v) => {
1311                Self::_get_chunks_uncompressed(state, v, start, end, batch_end, batch_size)
1312            }
1313        }
1314    }
1315
1316    fn add_more_chunks(
1317        &self,
1318        state: &Arc<BlobCompressionContext>,
1319        chunks: &[Arc<dyn BlobChunkInfo>],
1320        max_size: u64,
1321    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
1322        match self {
1323            BlobMetaChunkArray::V1(v) => Self::_add_more_chunks(state, v, chunks, max_size),
1324            BlobMetaChunkArray::V2(v) => Self::_add_more_chunks(state, v, chunks, max_size),
1325        }
1326    }
1327
1328    fn compressed_offset(&self, index: usize) -> u64 {
1329        match self {
1330            BlobMetaChunkArray::V1(v) => v[index].compressed_offset(),
1331            BlobMetaChunkArray::V2(v) => v[index].compressed_offset(),
1332        }
1333    }
1334
1335    fn compressed_size(&self, index: usize) -> u32 {
1336        match self {
1337            BlobMetaChunkArray::V1(v) => v[index].compressed_size(),
1338            BlobMetaChunkArray::V2(v) => v[index].compressed_size(),
1339        }
1340    }
1341
1342    fn uncompressed_offset(&self, index: usize) -> u64 {
1343        match self {
1344            BlobMetaChunkArray::V1(v) => v[index].uncompressed_offset(),
1345            BlobMetaChunkArray::V2(v) => v[index].uncompressed_offset(),
1346        }
1347    }
1348
1349    fn uncompressed_size(&self, index: usize) -> u32 {
1350        match self {
1351            BlobMetaChunkArray::V1(v) => v[index].uncompressed_size(),
1352            BlobMetaChunkArray::V2(v) => v[index].uncompressed_size(),
1353        }
1354    }
1355
1356    fn is_batch(&self, index: usize) -> bool {
1357        match self {
1358            BlobMetaChunkArray::V1(v) => v[index].is_batch(),
1359            BlobMetaChunkArray::V2(v) => v[index].is_batch(),
1360        }
1361    }
1362
1363    fn batch_index(&self, index: usize) -> Result<u32> {
1364        match self {
1365            BlobMetaChunkArray::V1(v) => v[index].get_batch_index(),
1366            BlobMetaChunkArray::V2(v) => v[index].get_batch_index(),
1367        }
1368    }
1369
1370    fn uncompressed_offset_in_batch_buf(&self, index: usize) -> Result<u32> {
1371        match self {
1372            BlobMetaChunkArray::V1(v) => v[index].get_uncompressed_offset_in_batch_buf(),
1373            BlobMetaChunkArray::V2(v) => v[index].get_uncompressed_offset_in_batch_buf(),
1374        }
1375    }
1376
1377    fn zran_index(&self, index: usize) -> Result<u32> {
1378        match self {
1379            BlobMetaChunkArray::V1(v) => v[index].get_zran_index(),
1380            BlobMetaChunkArray::V2(v) => v[index].get_zran_index(),
1381        }
1382    }
1383
1384    fn zran_offset(&self, index: usize) -> Result<u32> {
1385        match self {
1386            BlobMetaChunkArray::V1(v) => v[index].get_zran_offset(),
1387            BlobMetaChunkArray::V2(v) => v[index].get_zran_offset(),
1388        }
1389    }
1390
1391    fn is_compressed(&self, index: usize) -> bool {
1392        match self {
1393            BlobMetaChunkArray::V1(v) => v[index].is_compressed(),
1394            BlobMetaChunkArray::V2(v) => v[index].is_compressed(),
1395        }
1396    }
1397
1398    fn is_encrypted(&self, index: usize) -> bool {
1399        match self {
1400            BlobMetaChunkArray::V1(v) => v[index].is_encrypted(),
1401            BlobMetaChunkArray::V2(v) => v[index].is_encrypted(),
1402        }
1403    }
1404
1405    fn has_crc32(&self, index: usize) -> bool {
1406        match self {
1407            BlobMetaChunkArray::V1(v) => v[index].has_crc32(),
1408            BlobMetaChunkArray::V2(v) => v[index].has_crc32(),
1409        }
1410    }
1411
1412    fn crc32(&self, index: usize) -> u32 {
1413        match self {
1414            BlobMetaChunkArray::V1(v) => v[index].crc32(),
1415            BlobMetaChunkArray::V2(v) => v[index].crc32(),
1416        }
1417    }
1418
1419    fn _get_chunk_index_nocheck<T: BlobMetaChunkInfo>(
1420        state: &BlobCompressionContext,
1421        chunks: &[T],
1422        addr: u64,
1423        compressed: bool,
1424        prefetch: bool,
1425    ) -> Result<usize> {
1426        let mut size = chunks.len();
1427        let mut left = 0;
1428        let mut right = size;
1429        let mut start = 0;
1430        let mut end = 0;
1431
1432        while left < right {
1433            let mid = left + size / 2;
1434            // SAFETY: the call is made safe by the following invariants:
1435            // - `mid >= 0`
1436            // - `mid < size`: `mid` is limited by `[left; right)` bound.
1437            let entry = &chunks[mid];
1438            if compressed {
1439                // Capable of handling both batch and non-batch chunks.
1440                let c_offset = entry.compressed_offset();
1441                let c_size = state.get_compressed_size(mid)?;
1442                (start, end) = (c_offset, c_offset + c_size as u64);
1443            } else {
1444                start = entry.uncompressed_offset();
1445                end = entry.uncompressed_end();
1446            };
1447
1448            if start > addr {
1449                right = mid;
1450            } else if end <= addr {
1451                left = mid + 1;
1452            } else {
1453                // Find the first chunk in the batch.
1454                if entry.is_batch() && entry.get_uncompressed_offset_in_batch_buf()? > 0 {
1455                    right = mid;
1456                } else {
1457                    return Ok(mid);
1458                }
1459            }
1460
1461            size = right - left;
1462        }
1463
1464        // Special handling prefetch for ZRan blobs because they may have holes.
1465        if prefetch {
1466            if right < chunks.len() {
1467                let entry = &chunks[right];
1468                if entry.compressed_offset() > addr {
1469                    return Ok(right);
1470                }
1471            }
1472            if left < chunks.len() {
1473                let entry = &chunks[left];
1474                if entry.compressed_offset() > addr {
1475                    return Ok(left);
1476                }
1477            }
1478        }
1479
1480        // if addr == self.chunks[last].compressed_offset, return einval with error msg.
1481        Err(einval!(format!(
1482            "failed to get chunk index, prefetch {}, left {}, right {}, start: {}, end: {}, addr: {}",
1483            prefetch, left, right, start, end, addr
1484        )))
1485    }
1486
1487    fn _get_chunks_uncompressed<T: BlobMetaChunkInfo>(
1488        state: &Arc<BlobCompressionContext>,
1489        chunk_info_array: &[T],
1490        start: u64,
1491        end: u64,
1492        batch_end: u64,
1493        batch_size: u64,
1494    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
1495        let mut vec = Vec::with_capacity(512);
1496        let mut index =
1497            Self::_get_chunk_index_nocheck(state, chunk_info_array, start, false, false)?;
1498        let entry = Self::get_chunk_entry(state, chunk_info_array, index)?;
1499        trace!(
1500            "get_chunks_uncompressed: entry {} {}",
1501            entry.uncompressed_offset(),
1502            entry.uncompressed_end()
1503        );
1504
1505        // Special handling of ZRan chunks
1506        if entry.is_zran() {
1507            let zran_index = entry.get_zran_index()?;
1508            let mut count = state.zran_info_array[zran_index as usize].out_size() as u64;
1509            let mut zran_last = zran_index;
1510            let mut zran_end = entry.aligned_uncompressed_end();
1511
1512            while index > 0 {
1513                let entry = Self::get_chunk_entry(state, chunk_info_array, index - 1)?;
1514                if !entry.is_zran() {
1515                    return Err(einval!(
1516                        "inconsistent ZRan and non-ZRan chunk compression information entries"
1517                    ));
1518                } else if entry.get_zran_index()? != zran_index {
1519                    // reach the header chunk associated with the same ZRan context.
1520                    break;
1521                } else {
1522                    index -= 1;
1523                }
1524            }
1525
1526            for entry in &chunk_info_array[index..] {
1527                entry.validate(state)?;
1528                if !entry.is_zran() {
1529                    return Err(einval!(
1530                        "inconsistent ZRan and non-ZRan chunk compression information entries"
1531                    ));
1532                }
1533                if entry.get_zran_index()? != zran_last {
1534                    let ctx = &state.zran_info_array[entry.get_zran_index()? as usize];
1535                    if count + ctx.out_size() as u64 >= batch_size
1536                        && entry.uncompressed_offset() >= end
1537                    {
1538                        return Ok(vec);
1539                    }
1540                    count += ctx.out_size() as u64;
1541                    zran_last = entry.get_zran_index()?;
1542                }
1543                zran_end = entry.aligned_uncompressed_end();
1544                vec.push(BlobMetaChunk::new(index, state));
1545                index += 1;
1546            }
1547
1548            if zran_end >= end {
1549                return Ok(vec);
1550            }
1551            return Err(einval!(format!(
1552                "entry not found index {} chunk_info_array.len {}, end 0x{:x}, range [0x{:x}-0x{:x}]",
1553                index,
1554                chunk_info_array.len(),
1555                vec.last().map(|v| v.uncompressed_end()).unwrap_or_default(),
1556                start,
1557                end,
1558            )));
1559        }
1560
1561        vec.push(BlobMetaChunk::new(index, state));
1562        let mut last_end = entry.aligned_uncompressed_end();
1563        if last_end >= batch_end {
1564            Ok(vec)
1565        } else {
1566            while index + 1 < chunk_info_array.len() {
1567                index += 1;
1568
1569                let entry = Self::get_chunk_entry(state, chunk_info_array, index)?;
1570                if entry.uncompressed_offset() != last_end {
1571                    return Err(einval!(format!(
1572                        "mismatch uncompressed {} size {} last_end {}",
1573                        entry.uncompressed_offset(),
1574                        entry.uncompressed_size(),
1575                        last_end
1576                    )));
1577                } else if last_end >= end && entry.aligned_uncompressed_end() >= batch_end {
1578                    // Avoid read amplify if next chunk is too big.
1579                    return Ok(vec);
1580                }
1581
1582                vec.push(BlobMetaChunk::new(index, state));
1583                last_end = entry.aligned_uncompressed_end();
1584                if last_end >= batch_end {
1585                    return Ok(vec);
1586                }
1587            }
1588
1589            if last_end >= end {
1590                Ok(vec)
1591            } else {
1592                Err(einval!(format!(
1593                    "entry not found index {} chunk_info_array.len {}, last_end 0x{:x}, end 0x{:x}, blob compressed size 0x{:x}",
1594                    index,
1595                    chunk_info_array.len(),
1596                    last_end,
1597                    end,
1598                    state.uncompressed_size,
1599                )))
1600            }
1601        }
1602    }
1603
1604    fn _get_chunks_compressed<T: BlobMetaChunkInfo>(
1605        state: &Arc<BlobCompressionContext>,
1606        chunk_info_array: &[T],
1607        start: u64,
1608        end: u64,
1609        batch_end: u64,
1610        batch_size: u64,
1611        prefetch: bool,
1612    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
1613        let mut vec = Vec::with_capacity(512);
1614        let mut index =
1615            Self::_get_chunk_index_nocheck(state, chunk_info_array, start, true, prefetch)?;
1616        let entry = Self::get_chunk_entry(state, chunk_info_array, index)?;
1617
1618        // Special handling of ZRan chunks
1619        if entry.is_zran() {
1620            let zran_index = entry.get_zran_index()?;
1621            let pos = state.zran_info_array[zran_index as usize].in_offset();
1622            let mut zran_last = zran_index;
1623
1624            while index > 0 {
1625                let entry = Self::get_chunk_entry(state, chunk_info_array, index - 1)?;
1626                if !entry.is_zran() {
1627                    return Err(einval!(
1628                        "inconsistent ZRan and non-ZRan chunk compression information entries"
1629                    ));
1630                } else if entry.get_zran_index()? != zran_index {
1631                    // reach the header chunk associated with the same ZRan context.
1632                    break;
1633                } else {
1634                    index -= 1;
1635                }
1636            }
1637
1638            for entry in &chunk_info_array[index..] {
1639                entry.validate(state)?;
1640                if !entry.is_zran() {
1641                    return Err(einval!(
1642                        "inconsistent ZRan and non-ZRan chunk compression information entries"
1643                    ));
1644                }
1645                if entry.get_zran_index()? != zran_last {
1646                    let ctx = &state.zran_info_array[entry.get_zran_index()? as usize];
1647                    if ctx.in_offset() + ctx.in_size() as u64 - pos > batch_size
1648                        && entry.compressed_offset() > end
1649                    {
1650                        return Ok(vec);
1651                    }
1652                    zran_last = entry.get_zran_index()?;
1653                }
1654                vec.push(BlobMetaChunk::new(index, state));
1655                index += 1;
1656            }
1657
1658            if let Some(c) = vec.last() {
1659                if c.uncompressed_end() >= end {
1660                    return Ok(vec);
1661                }
1662                // Special handling prefetch for ZRan blobs
1663                if prefetch && index >= chunk_info_array.len() {
1664                    return Ok(vec);
1665                }
1666            }
1667            return Err(einval!(format!(
1668                "entry not found index {} chunk_info_array.len {}",
1669                index,
1670                chunk_info_array.len(),
1671            )));
1672        }
1673
1674        vec.push(BlobMetaChunk::new(index, state));
1675        let mut last_end = entry.compressed_end();
1676        if last_end >= batch_end {
1677            Ok(vec)
1678        } else {
1679            while index + 1 < chunk_info_array.len() {
1680                index += 1;
1681
1682                let entry = Self::get_chunk_entry(state, chunk_info_array, index)?;
1683                // Avoid read amplify if next chunk is too big.
1684                if last_end >= end && entry.compressed_end() > batch_end {
1685                    return Ok(vec);
1686                }
1687
1688                vec.push(BlobMetaChunk::new(index, state));
1689                last_end = entry.compressed_end();
1690                if last_end >= batch_end {
1691                    return Ok(vec);
1692                }
1693            }
1694
1695            if last_end >= end || (prefetch && !vec.is_empty()) {
1696                Ok(vec)
1697            } else {
1698                Err(einval!(format!(
1699                    "entry not found index {} chunk_info_array.len {}, last_end 0x{:x}, end 0x{:x}, blob compressed size 0x{:x}",
1700                    index,
1701                    chunk_info_array.len(),
1702                    last_end,
1703                    end,
1704                    state.compressed_size,
1705                )))
1706            }
1707        }
1708    }
1709
1710    fn _add_more_chunks<T: BlobMetaChunkInfo>(
1711        state: &Arc<BlobCompressionContext>,
1712        chunk_info_array: &[T],
1713        chunks: &[Arc<dyn BlobChunkInfo>],
1714        max_size: u64,
1715    ) -> Result<Vec<Arc<dyn BlobChunkInfo>>> {
1716        let first_idx = chunks[0].id() as usize;
1717        let first_entry = Self::get_chunk_entry(state, chunk_info_array, first_idx)?;
1718        let last_idx = chunks[chunks.len() - 1].id() as usize;
1719        let last_entry = Self::get_chunk_entry(state, chunk_info_array, last_idx)?;
1720
1721        // The maximum size to be amplified in the current fetch request.
1722        let fetch_end = max_size + chunks[0].compressed_offset();
1723
1724        let mut vec = Vec::with_capacity(128);
1725
1726        // Special handling of ZRan chunks
1727        if first_entry.is_zran() {
1728            let first_zran_idx = first_entry.get_zran_index()?;
1729            let mut last_zran_idx = last_entry.get_zran_index()?;
1730            let mut index = first_idx;
1731            while index > 0 {
1732                let entry = Self::get_chunk_entry(state, chunk_info_array, index - 1)?;
1733                if !entry.is_zran() {
1734                    // All chunks should be ZRan chunks.
1735                    return Err(std::io::Error::other(
1736                        "invalid ZRan compression information data",
1737                    ));
1738                } else if entry.get_zran_index()? != first_zran_idx {
1739                    // reach the header chunk associated with the same ZRan context.
1740                    break;
1741                } else {
1742                    index -= 1;
1743                }
1744            }
1745
1746            for entry in &chunk_info_array[index..] {
1747                if entry.validate(state).is_err() || !entry.is_zran() {
1748                    return Err(std::io::Error::other(
1749                        "invalid ZRan compression information data",
1750                    ));
1751                } else if entry.get_zran_index()? > last_zran_idx {
1752                    if entry.compressed_end() + RAFS_MAX_CHUNK_SIZE <= fetch_end
1753                        && entry.get_zran_index()? == last_zran_idx + 1
1754                    {
1755                        vec.push(BlobMetaChunk::new(index, state));
1756                        last_zran_idx += 1;
1757                    } else {
1758                        return Ok(vec);
1759                    }
1760                } else {
1761                    vec.push(BlobMetaChunk::new(index, state));
1762                }
1763                index += 1;
1764            }
1765        } else {
1766            // Handling of Batch chunks and normal chunks
1767            let mut entry_idx = first_idx;
1768            let mut curr_batch_idx = u32::MAX;
1769
1770            // Search the first chunk of the current Batch.
1771            if first_entry.is_batch() {
1772                curr_batch_idx = first_entry.get_batch_index()?;
1773                while entry_idx > 0 {
1774                    let entry = Self::get_chunk_entry(state, chunk_info_array, entry_idx - 1)?;
1775                    if !entry.is_batch() || entry.get_batch_index()? != curr_batch_idx {
1776                        // Reach the previous non-batch or batch chunk.
1777                        break;
1778                    } else {
1779                        entry_idx -= 1;
1780                    }
1781                }
1782            }
1783
1784            // Iterate and add chunks.
1785            let mut idx_chunks = 0;
1786            for (idx, entry) in chunk_info_array.iter().enumerate().skip(entry_idx) {
1787                entry.validate(state)?;
1788
1789                // Add chunk if it is in the `chunks` array.
1790                if idx_chunks < chunks.len() && idx == chunks[idx_chunks].id() as usize {
1791                    vec.push(chunks[idx_chunks].clone());
1792                    idx_chunks += 1;
1793                    if entry.is_batch() {
1794                        curr_batch_idx = entry.get_batch_index()?;
1795                    }
1796                    continue;
1797                }
1798
1799                // If chunk is not in the `chunks` array, add it if in the current Batch,
1800                // or can be amplified.
1801                if entry.is_batch() {
1802                    if curr_batch_idx == entry.get_batch_index()? {
1803                        vec.push(BlobMetaChunk::new(idx, state));
1804                        continue;
1805                    }
1806
1807                    let batch_ctx = state.get_batch_context(entry.get_batch_index()? as usize)?;
1808                    if entry.compressed_offset() + batch_ctx.compressed_size() as u64 <= fetch_end {
1809                        vec.push(BlobMetaChunk::new(idx, state));
1810                        curr_batch_idx = entry.get_batch_index()?;
1811                    } else {
1812                        break;
1813                    }
1814                    continue;
1815                }
1816                if entry.compressed_end() <= fetch_end {
1817                    vec.push(BlobMetaChunk::new(idx, state));
1818                } else {
1819                    break;
1820                }
1821            }
1822        }
1823
1824        Ok(vec)
1825    }
1826
1827    fn get_chunk_entry<'a, T: BlobMetaChunkInfo>(
1828        state: &Arc<BlobCompressionContext>,
1829        chunk_info_array: &'a [T],
1830        index: usize,
1831    ) -> Result<&'a T> {
1832        assert!(index < chunk_info_array.len());
1833        let entry = &chunk_info_array[index];
1834        // If the chunk belongs to a chunkdict, skip the validation check.
1835        if state.blob_features & BlobFeatures::IS_CHUNKDICT_GENERATED.bits() == 0 {
1836            entry.validate(state)?;
1837        }
1838        Ok(entry)
1839    }
1840}
1841
1842/// An implementation of `trait BlobChunkInfo` based on blob meta information.
1843#[derive(Clone)]
1844pub struct BlobMetaChunk {
1845    chunk_index: usize,
1846    meta: Arc<BlobCompressionContext>,
1847}
1848
1849impl BlobMetaChunk {
1850    #[allow(clippy::new_ret_no_self)]
1851    pub(crate) fn new(
1852        chunk_index: usize,
1853        meta: &Arc<BlobCompressionContext>,
1854    ) -> Arc<dyn BlobChunkInfo> {
1855        assert!(chunk_index <= RAFS_MAX_CHUNKS_PER_BLOB as usize);
1856        Arc::new(BlobMetaChunk {
1857            chunk_index,
1858            meta: meta.clone(),
1859        }) as Arc<dyn BlobChunkInfo>
1860    }
1861}
1862
1863impl BlobChunkInfo for BlobMetaChunk {
1864    fn chunk_id(&self) -> &RafsDigest {
1865        if self.chunk_index < self.meta.chunk_digest_array.len() {
1866            let digest = &self.meta.chunk_digest_array[self.chunk_index];
1867            digest.into()
1868        } else {
1869            &self.meta.chunk_digest_default
1870        }
1871    }
1872
1873    fn id(&self) -> u32 {
1874        self.chunk_index as u32
1875    }
1876
1877    fn blob_index(&self) -> u32 {
1878        self.meta.blob_index
1879    }
1880
1881    fn compressed_offset(&self) -> u64 {
1882        self.meta
1883            .chunk_info_array
1884            .compressed_offset(self.chunk_index)
1885    }
1886
1887    fn compressed_size(&self) -> u32 {
1888        self.meta.chunk_info_array.compressed_size(self.chunk_index)
1889    }
1890
1891    fn uncompressed_offset(&self) -> u64 {
1892        self.meta
1893            .chunk_info_array
1894            .uncompressed_offset(self.chunk_index)
1895    }
1896
1897    fn uncompressed_size(&self) -> u32 {
1898        self.meta
1899            .chunk_info_array
1900            .uncompressed_size(self.chunk_index)
1901    }
1902
1903    fn is_batch(&self) -> bool {
1904        self.meta.chunk_info_array.is_batch(self.chunk_index)
1905    }
1906
1907    fn is_compressed(&self) -> bool {
1908        self.meta.chunk_info_array.is_compressed(self.chunk_index)
1909    }
1910
1911    fn is_encrypted(&self) -> bool {
1912        self.meta.chunk_info_array.is_encrypted(self.chunk_index)
1913    }
1914
1915    fn has_crc32(&self) -> bool {
1916        self.meta.chunk_info_array.has_crc32(self.chunk_index)
1917    }
1918
1919    fn crc32(&self) -> u32 {
1920        self.meta.chunk_info_array.crc32(self.chunk_index)
1921    }
1922
1923    fn as_any(&self) -> &dyn Any {
1924        self
1925    }
1926}
1927
1928impl BlobV5ChunkInfo for BlobMetaChunk {
1929    fn index(&self) -> u32 {
1930        self.chunk_index as u32
1931    }
1932
1933    fn file_offset(&self) -> u64 {
1934        // Not used for RAFS v6
1935        0
1936    }
1937
1938    fn flags(&self) -> BlobChunkFlags {
1939        let mut flags = BlobChunkFlags::empty();
1940        if self.is_compressed() {
1941            flags |= BlobChunkFlags::COMPRESSED;
1942        }
1943        flags
1944    }
1945
1946    fn as_base(&self) -> &dyn BlobChunkInfo {
1947        self
1948    }
1949}
1950
1951/// Trait to manage compression information about chunks based on blob meta.
1952pub trait BlobMetaChunkInfo {
1953    /// Get compressed offset of the chunk.
1954    fn compressed_offset(&self) -> u64;
1955
1956    /// Set compressed offset of the chunk.
1957    fn set_compressed_offset(&mut self, offset: u64);
1958
1959    /// Get compressed size of the chunk.
1960    fn compressed_size(&self) -> u32;
1961
1962    /// Set compressed size of the chunk.
1963    fn set_compressed_size(&mut self, size: u32);
1964
1965    /// Get end of compressed data of the chunk.
1966    fn compressed_end(&self) -> u64 {
1967        self.compressed_offset() + self.compressed_size() as u64
1968    }
1969
1970    /// Get uncompressed offset of the chunk.
1971    fn uncompressed_offset(&self) -> u64;
1972
1973    /// Set uncompressed offset of the chunk.
1974    fn set_uncompressed_offset(&mut self, offset: u64);
1975
1976    /// Get uncompressed end of the chunk.
1977    fn uncompressed_size(&self) -> u32;
1978
1979    /// Set uncompressed end of the chunk.
1980    fn set_uncompressed_size(&mut self, size: u32);
1981
1982    /// Get end of uncompressed data of the chunk.
1983    fn uncompressed_end(&self) -> u64 {
1984        self.uncompressed_offset() + self.uncompressed_size() as u64
1985    }
1986
1987    /// Get 4K-aligned end of uncompressed data of the chunk.
1988    fn aligned_uncompressed_end(&self) -> u64 {
1989        round_up_4k(self.uncompressed_end())
1990    }
1991
1992    /// Check whether chunk data is encrypted or not.
1993    fn is_encrypted(&self) -> bool;
1994
1995    /// Check whether chunk data has CRC or not.
1996    fn has_crc32(&self) -> bool;
1997
1998    /// Check whether the blob chunk is compressed or not.
1999    ///
2000    /// Assume the image builder guarantee that compress_size < uncompress_size if the chunk is
2001    /// compressed.
2002    fn is_compressed(&self) -> bool;
2003
2004    /// Check whether the chunk has associated Batch context data.
2005    fn is_batch(&self) -> bool;
2006
2007    /// Check whether the chunk has associated ZRan context data.
2008    fn is_zran(&self) -> bool;
2009
2010    /// Get index of the ZRan context data associated with the chunk.
2011    fn get_zran_index(&self) -> Result<u32>;
2012
2013    /// Get offset to get context data from the associated ZRan context.
2014    fn get_zran_offset(&self) -> Result<u32>;
2015
2016    /// Get index of the Batch context data associated with the chunk.
2017    fn get_batch_index(&self) -> Result<u32>;
2018
2019    /// Get offset of uncompressed chunk data inside the batch chunk.
2020    fn get_uncompressed_offset_in_batch_buf(&self) -> Result<u32>;
2021
2022    /// Get CRC32 of the chunk.
2023    fn crc32(&self) -> u32;
2024
2025    /// Get data associated with the entry. V2 only, V1 just returns zero.
2026    fn get_data(&self) -> u64;
2027
2028    /// Check whether the chunk compression information is valid or not.
2029    fn validate(&self, state: &BlobCompressionContext) -> Result<()>;
2030}
2031
2032/// Generate description string for blob meta features.
2033pub fn format_blob_features(features: BlobFeatures) -> String {
2034    let mut output = String::new();
2035    if features.contains(BlobFeatures::ALIGNED) {
2036        output += "aligned ";
2037    }
2038    if features.contains(BlobFeatures::BATCH) {
2039        output += "batch ";
2040    }
2041    if features.contains(BlobFeatures::CAP_TAR_TOC) {
2042        output += "cap_toc ";
2043    }
2044    if features.contains(BlobFeatures::INLINED_CHUNK_DIGEST) {
2045        output += "chunk-digest ";
2046    }
2047    if features.contains(BlobFeatures::CHUNK_INFO_V2) {
2048        output += "chunk-v2 ";
2049    }
2050    if features.contains(BlobFeatures::INLINED_FS_META) {
2051        output += "fs-meta ";
2052    }
2053    if features.contains(BlobFeatures::SEPARATE) {
2054        output += "separate ";
2055    }
2056    if features.contains(BlobFeatures::HAS_TAR_HEADER) {
2057        output += "tar-header ";
2058    }
2059    if features.contains(BlobFeatures::HAS_TOC) {
2060        output += "toc ";
2061    }
2062    if features.contains(BlobFeatures::ZRAN) {
2063        output += "zran ";
2064    }
2065    if features.contains(BlobFeatures::ENCRYPTED) {
2066        output += "encrypted ";
2067    }
2068    if features.contains(BlobFeatures::IS_CHUNKDICT_GENERATED) {
2069        output += "is-chunkdict-generated ";
2070    }
2071    output.trim_end().to_string()
2072}
2073
2074fn round_up_4k<T: Add<Output = T> + BitAnd<Output = T> + Not<Output = T> + From<u16>>(val: T) -> T {
2075    (val + T::from(0xfff)) & !T::from(0xfff)
2076}
2077
2078#[cfg(test)]
2079pub(crate) mod tests {
2080    use super::*;
2081    use crate::backend::{BackendResult, BlobReader};
2082    use crate::device::BlobFeatures;
2083    use crate::RAFS_DEFAULT_CHUNK_SIZE;
2084    use nix::sys::uio;
2085    use nydus_utils::digest::{self, DigestHasher};
2086    use nydus_utils::metrics::BackendMetrics;
2087    use std::fs::File;
2088    use std::os::unix::io::AsRawFd;
2089    use std::path::PathBuf;
2090
2091    pub(crate) struct DummyBlobReader {
2092        pub metrics: Arc<BackendMetrics>,
2093        pub file: File,
2094    }
2095
2096    impl BlobReader for DummyBlobReader {
2097        fn blob_size(&self) -> BackendResult<u64> {
2098            Ok(0)
2099        }
2100
2101        fn try_read(&self, buf: &mut [u8], offset: u64) -> BackendResult<usize> {
2102            let ret = uio::pread(self.file.as_raw_fd(), buf, offset as i64).unwrap();
2103            Ok(ret)
2104        }
2105
2106        fn metrics(&self) -> &BackendMetrics {
2107            &self.metrics
2108        }
2109    }
2110
2111    #[test]
2112    fn test_round_up_4k() {
2113        assert_eq!(round_up_4k(0), 0x0u32);
2114        assert_eq!(round_up_4k(1), 0x1000u32);
2115        assert_eq!(round_up_4k(0xfff), 0x1000u32);
2116        assert_eq!(round_up_4k(0x1000), 0x1000u32);
2117        assert_eq!(round_up_4k(0x1001), 0x2000u32);
2118        assert_eq!(round_up_4k(0x1fff), 0x2000u64);
2119    }
2120
2121    #[test]
2122    fn test_load_meta_ci_zran_add_more_chunks() {
2123        let root_dir = &std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
2124        let path = PathBuf::from(root_dir).join("../tests/texture/zran/233c72f2b6b698c07021c4da367cfe2dff4f049efbaa885ca0ff760ea297865a");
2125
2126        let features = BlobFeatures::ALIGNED
2127            | BlobFeatures::INLINED_FS_META
2128            | BlobFeatures::CHUNK_INFO_V2
2129            | BlobFeatures::ZRAN;
2130        let mut blob_info = BlobInfo::new(
2131            0,
2132            "233c72f2b6b698c07021c4da367cfe2dff4f049efbaa885ca0ff760ea297865a".to_string(),
2133            0x16c6000,
2134            9839040,
2135            RAFS_DEFAULT_CHUNK_SIZE as u32,
2136            0xa3,
2137            features,
2138        );
2139        blob_info.set_blob_meta_info(0, 0xa1290, 0xa1290, compress::Algorithm::None as u32);
2140        let meta =
2141            BlobCompressionContextInfo::new(&path.display().to_string(), &blob_info, None, false)
2142                .unwrap();
2143        assert_eq!(meta.state.chunk_info_array.len(), 0xa3);
2144        assert_eq!(meta.state.zran_info_array.len(), 0x15);
2145        assert_eq!(meta.state.zran_dict_table.len(), 0xa0348 - 0x15 * 40);
2146
2147        let chunks = vec![BlobMetaChunk::new(0, &meta.state)];
2148        let chunks = meta.add_more_chunks(chunks.as_slice(), 0x30000).unwrap();
2149        assert_eq!(chunks.len(), 67);
2150
2151        let chunks = vec![BlobMetaChunk::new(0, &meta.state)];
2152        let chunks = meta
2153            .add_more_chunks(chunks.as_slice(), RAFS_DEFAULT_CHUNK_SIZE)
2154            .unwrap();
2155        assert_eq!(chunks.len(), 67);
2156
2157        let chunks = vec![BlobMetaChunk::new(66, &meta.state)];
2158        let chunks = meta
2159            .add_more_chunks(chunks.as_slice(), RAFS_DEFAULT_CHUNK_SIZE)
2160            .unwrap();
2161        assert_eq!(chunks.len(), 67);
2162
2163        let chunks = vec![BlobMetaChunk::new(116, &meta.state)];
2164        let chunks = meta
2165            .add_more_chunks(chunks.as_slice(), RAFS_DEFAULT_CHUNK_SIZE)
2166            .unwrap();
2167        assert_eq!(chunks.len(), 1);
2168
2169        let chunks = vec![BlobMetaChunk::new(162, &meta.state)];
2170        let chunks = meta
2171            .add_more_chunks(chunks.as_slice(), RAFS_DEFAULT_CHUNK_SIZE)
2172            .unwrap();
2173        assert_eq!(chunks.len(), 12);
2174    }
2175
2176    #[test]
2177    fn test_load_meta_ci_zran_get_chunks_uncompressed() {
2178        let root_dir = &std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
2179        let path = PathBuf::from(root_dir).join("../tests/texture/zran/233c72f2b6b698c07021c4da367cfe2dff4f049efbaa885ca0ff760ea297865a");
2180
2181        let features = BlobFeatures::ALIGNED
2182            | BlobFeatures::INLINED_FS_META
2183            | BlobFeatures::CHUNK_INFO_V2
2184            | BlobFeatures::ZRAN;
2185        let mut blob_info = BlobInfo::new(
2186            0,
2187            "233c72f2b6b698c07021c4da367cfe2dff4f049efbaa885ca0ff760ea297865a".to_string(),
2188            0x16c6000,
2189            9839040,
2190            RAFS_DEFAULT_CHUNK_SIZE as u32,
2191            0xa3,
2192            features,
2193        );
2194        blob_info.set_blob_meta_info(0, 0xa1290, 0xa1290, compress::Algorithm::None as u32);
2195        let meta =
2196            BlobCompressionContextInfo::new(&path.display().to_string(), &blob_info, None, false)
2197                .unwrap();
2198        assert_eq!(meta.state.chunk_info_array.len(), 0xa3);
2199        assert_eq!(meta.state.zran_info_array.len(), 0x15);
2200        assert_eq!(meta.state.zran_dict_table.len(), 0xa0348 - 0x15 * 40);
2201
2202        let chunks = meta.get_chunks_uncompressed(0, 1, 0x30000).unwrap();
2203        assert_eq!(chunks.len(), 67);
2204
2205        let chunks = meta
2206            .get_chunks_uncompressed(0, 1, RAFS_DEFAULT_CHUNK_SIZE)
2207            .unwrap();
2208        assert_eq!(chunks.len(), 67);
2209
2210        let chunks = meta
2211            .get_chunks_uncompressed(0x112000, 0x10000, RAFS_DEFAULT_CHUNK_SIZE)
2212            .unwrap();
2213        assert_eq!(chunks.len(), 116);
2214
2215        let chunks = meta
2216            .get_chunks_uncompressed(0xf9b000, 0x100, RAFS_DEFAULT_CHUNK_SIZE)
2217            .unwrap();
2218        assert_eq!(chunks.len(), 12);
2219
2220        let chunks = meta
2221            .get_chunks_uncompressed(0xf9b000, 0x100, 4 * RAFS_DEFAULT_CHUNK_SIZE)
2222            .unwrap();
2223        assert_eq!(chunks.len(), 13);
2224
2225        let chunks = meta
2226            .get_chunks_uncompressed(0x16c5000, 0x100, 4 * RAFS_DEFAULT_CHUNK_SIZE)
2227            .unwrap();
2228        assert_eq!(chunks.len(), 12);
2229
2230        assert!(meta
2231            .get_chunks_uncompressed(0x2000000, 0x100, 4 * RAFS_DEFAULT_CHUNK_SIZE)
2232            .is_err());
2233    }
2234
2235    #[test]
2236    fn test_load_meta_ci_zran_get_chunks_compressed() {
2237        let root_dir = &std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
2238        let path = PathBuf::from(root_dir).join("../tests/texture/zran/233c72f2b6b698c07021c4da367cfe2dff4f049efbaa885ca0ff760ea297865a");
2239
2240        let features = BlobFeatures::ALIGNED
2241            | BlobFeatures::INLINED_FS_META
2242            | BlobFeatures::CHUNK_INFO_V2
2243            | BlobFeatures::ZRAN;
2244        let mut blob_info = BlobInfo::new(
2245            0,
2246            "233c72f2b6b698c07021c4da367cfe2dff4f049efbaa885ca0ff760ea297865a".to_string(),
2247            0x16c6000,
2248            9839040,
2249            RAFS_DEFAULT_CHUNK_SIZE as u32,
2250            0xa3,
2251            features,
2252        );
2253        blob_info.set_blob_meta_info(0, 0xa1290, 0xa1290, compress::Algorithm::None as u32);
2254        let meta =
2255            BlobCompressionContextInfo::new(&path.display().to_string(), &blob_info, None, false)
2256                .unwrap();
2257        assert_eq!(meta.state.chunk_info_array.len(), 0xa3);
2258        assert_eq!(meta.state.zran_info_array.len(), 0x15);
2259        assert_eq!(meta.state.zran_dict_table.len(), 0xa0348 - 0x15 * 40);
2260
2261        let chunks = meta.get_chunks_compressed(0xb8, 1, 0x30000, false).unwrap();
2262        assert_eq!(chunks.len(), 67);
2263
2264        let chunks = meta
2265            .get_chunks_compressed(0xb8, 1, RAFS_DEFAULT_CHUNK_SIZE, false)
2266            .unwrap();
2267        assert_eq!(chunks.len(), 116);
2268
2269        let chunks = meta
2270            .get_chunks_compressed(0xb8, 1, 2 * RAFS_DEFAULT_CHUNK_SIZE, false)
2271            .unwrap();
2272        assert_eq!(chunks.len(), 120);
2273
2274        let chunks = meta
2275            .get_chunks_compressed(0x5fd41e, 1, RAFS_DEFAULT_CHUNK_SIZE / 2, false)
2276            .unwrap();
2277        assert_eq!(chunks.len(), 3);
2278
2279        let chunks = meta
2280            .get_chunks_compressed(0x95d55d, 0x20, RAFS_DEFAULT_CHUNK_SIZE, false)
2281            .unwrap();
2282        assert_eq!(chunks.len(), 12);
2283
2284        assert!(meta
2285            .get_chunks_compressed(0x0, 0x1, RAFS_DEFAULT_CHUNK_SIZE, false)
2286            .is_err());
2287        assert!(meta
2288            .get_chunks_compressed(0x1000000, 0x1, RAFS_DEFAULT_CHUNK_SIZE, false)
2289            .is_err());
2290    }
2291
2292    #[test]
2293    fn test_blob_compression_context_header_getters_and_setters() {
2294        let mut header = BlobCompressionContextHeader::default();
2295
2296        assert_eq!(header.features(), 0);
2297        header.set_aligned(true);
2298        assert!(header.is_4k_aligned());
2299        header.set_aligned(false);
2300
2301        header.set_inlined_fs_meta(true);
2302        assert!(header.has_feature(BlobFeatures::INLINED_FS_META));
2303        header.set_inlined_fs_meta(false);
2304
2305        header.set_chunk_info_v2(true);
2306        assert!(header.has_feature(BlobFeatures::CHUNK_INFO_V2));
2307        header.set_chunk_info_v2(false);
2308
2309        header.set_ci_zran(true);
2310        assert!(header.has_feature(BlobFeatures::ZRAN));
2311        header.set_ci_zran(false);
2312
2313        header.set_separate_blob(true);
2314        assert!(header.has_feature(BlobFeatures::SEPARATE));
2315        header.set_separate_blob(false);
2316
2317        header.set_ci_batch(true);
2318        assert!(header.has_feature(BlobFeatures::BATCH));
2319        header.set_ci_batch(false);
2320
2321        header.set_inlined_chunk_digest(true);
2322        assert!(header.has_feature(BlobFeatures::INLINED_CHUNK_DIGEST));
2323        header.set_inlined_chunk_digest(false);
2324
2325        header.set_has_tar_header(true);
2326        assert!(header.has_feature(BlobFeatures::HAS_TAR_HEADER));
2327        header.set_has_tar_header(false);
2328
2329        header.set_has_toc(true);
2330        assert!(header.has_feature(BlobFeatures::HAS_TOC));
2331        header.set_has_toc(false);
2332
2333        header.set_cap_tar_toc(true);
2334        assert!(header.has_feature(BlobFeatures::CAP_TAR_TOC));
2335        header.set_cap_tar_toc(false);
2336
2337        header.set_tarfs(true);
2338        assert!(header.has_feature(BlobFeatures::TARFS));
2339        header.set_tarfs(false);
2340
2341        header.set_encrypted(true);
2342        assert!(header.has_feature(BlobFeatures::ENCRYPTED));
2343        header.set_encrypted(false);
2344
2345        assert_eq!(header.features(), 0);
2346
2347        assert_eq!(header.ci_compressor(), compress::Algorithm::Lz4Block);
2348        header.set_ci_compressor(compress::Algorithm::GZip);
2349        assert_eq!(header.ci_compressor(), compress::Algorithm::GZip);
2350        header.set_ci_compressor(compress::Algorithm::Zstd);
2351        assert_eq!(header.ci_compressor(), compress::Algorithm::Zstd);
2352
2353        let mut hasher = RafsDigest::hasher(digest::Algorithm::Sha256);
2354        hasher.digest_update(header.as_bytes());
2355        let hash: String = hasher.digest_finalize().into();
2356        assert_eq!(
2357            hash,
2358            String::from("f56a1129d3df9fc7d60b26dbf495a60bda3dfc265f4f37854e4a36b826b660fc")
2359        );
2360
2361        assert_eq!(header.ci_entries(), 0);
2362        header.set_ci_entries(1);
2363        assert_eq!(header.ci_entries(), 1);
2364
2365        assert_eq!(header.ci_compressed_offset(), 0);
2366        header.set_ci_compressed_offset(1);
2367        assert_eq!(header.ci_compressed_offset(), 1);
2368
2369        assert_eq!(header.ci_compressed_size(), 0);
2370        header.set_ci_compressed_size(1);
2371        assert_eq!(header.ci_compressed_size(), 1);
2372
2373        assert_eq!(header.ci_uncompressed_size(), 0);
2374        header.set_ci_uncompressed_size(1);
2375        assert_eq!(header.ci_uncompressed_size(), 1);
2376
2377        assert_eq!(header.ci_zran_count(), 0);
2378        header.set_ci_zran_count(1);
2379        assert_eq!(header.ci_zran_count(), 1);
2380
2381        assert_eq!(header.ci_zran_offset(), 0);
2382        header.set_ci_zran_offset(1);
2383        assert_eq!(header.ci_zran_offset(), 1);
2384
2385        assert_eq!(header.ci_zran_size(), 0);
2386        header.set_ci_zran_size(1);
2387        assert_eq!(header.ci_zran_size(), 1);
2388    }
2389
2390    #[test]
2391    fn test_format_blob_features() {
2392        let features = !BlobFeatures::default();
2393        let content = format_blob_features(features);
2394        assert!(content.contains("aligned"));
2395        assert!(content.contains("fs-meta"));
2396    }
2397
2398    #[test]
2399    fn test_add_more_chunks() {
2400        // Batch chunks: [chunk0, chunk1], chunk2, [chunk3, chunk4]
2401        let mut chunk0 = BlobChunkInfoV2Ondisk::default();
2402        chunk0.set_batch(true);
2403        chunk0.set_compressed(true);
2404        chunk0.set_batch_index(0);
2405        chunk0.set_uncompressed_offset_in_batch_buf(0);
2406        chunk0.set_uncompressed_offset(0);
2407        chunk0.set_uncompressed_size(0x2000);
2408        chunk0.set_compressed_offset(0);
2409
2410        let mut chunk1 = BlobChunkInfoV2Ondisk::default();
2411        chunk1.set_batch(true);
2412        chunk1.set_compressed(true);
2413        chunk1.set_batch_index(0);
2414        chunk1.set_uncompressed_offset_in_batch_buf(0x2000);
2415        chunk1.set_uncompressed_offset(0x2000);
2416        chunk1.set_uncompressed_size(0x1000);
2417        chunk1.set_compressed_offset(0);
2418
2419        let mut batch_ctx0 = BatchInflateContext::default();
2420        batch_ctx0.set_uncompressed_batch_size(0x3000);
2421        batch_ctx0.set_compressed_size(0x2000);
2422
2423        let mut chunk2 = BlobChunkInfoV2Ondisk::default();
2424        chunk2.set_batch(false);
2425        chunk2.set_compressed(true);
2426        chunk2.set_uncompressed_offset(0x3000);
2427        chunk2.set_compressed_offset(0x2000);
2428        chunk2.set_uncompressed_size(0x4000);
2429        chunk2.set_compressed_size(0x3000);
2430
2431        let mut chunk3 = BlobChunkInfoV2Ondisk::default();
2432        chunk3.set_batch(true);
2433        chunk3.set_compressed(true);
2434        chunk3.set_batch_index(1);
2435        chunk3.set_uncompressed_offset_in_batch_buf(0);
2436        chunk3.set_uncompressed_offset(0x7000);
2437        chunk3.set_uncompressed_size(0x2000);
2438        chunk3.set_compressed_offset(0x5000);
2439
2440        let mut chunk4 = BlobChunkInfoV2Ondisk::default();
2441        chunk4.set_batch(true);
2442        chunk4.set_compressed(true);
2443        chunk4.set_batch_index(1);
2444        chunk4.set_uncompressed_offset_in_batch_buf(0x2000);
2445        chunk4.set_uncompressed_offset(0x9000);
2446        chunk4.set_uncompressed_size(0x2000);
2447        chunk4.set_compressed_offset(0x5000);
2448
2449        let mut batch_ctx1 = BatchInflateContext::default();
2450        batch_ctx1.set_compressed_size(0x3000);
2451        batch_ctx1.set_uncompressed_batch_size(0x4000);
2452
2453        let chunk_info_array = vec![chunk0, chunk1, chunk2, chunk3, chunk4];
2454        let chunk_infos = BlobMetaChunkArray::V2(chunk_info_array);
2455        let chunk_infos = ManuallyDrop::new(chunk_infos);
2456
2457        let batch_ctx_array = vec![batch_ctx0, batch_ctx1];
2458        let batch_ctxes = ManuallyDrop::new(batch_ctx_array);
2459
2460        let state = BlobCompressionContext {
2461            chunk_info_array: chunk_infos,
2462            batch_info_array: batch_ctxes,
2463            compressed_size: 0x8000,
2464            uncompressed_size: 0xB000,
2465            blob_features: (BlobFeatures::BATCH
2466                | BlobFeatures::ALIGNED
2467                | BlobFeatures::INLINED_FS_META
2468                | BlobFeatures::CHUNK_INFO_V2)
2469                .bits(),
2470            ..Default::default()
2471        };
2472
2473        let state = Arc::new(state);
2474        let meta = BlobCompressionContextInfo { state };
2475
2476        // test read amplification
2477        let chunks = vec![BlobMetaChunk::new(0, &meta.state)];
2478        let chunks = meta
2479            .add_more_chunks(&chunks, RAFS_DEFAULT_CHUNK_SIZE)
2480            .unwrap();
2481        let chunk_ids: Vec<_> = chunks.iter().map(|c| c.id()).collect();
2482        assert_eq!(chunk_ids, vec![0, 1, 2, 3, 4]);
2483
2484        // test read the chunk in the middle of the batch chunk
2485        let chunks = vec![BlobMetaChunk::new(1, &meta.state)];
2486        let chunks = meta
2487            .add_more_chunks(&chunks, RAFS_DEFAULT_CHUNK_SIZE)
2488            .unwrap();
2489        let chunk_ids: Vec<_> = chunks.iter().map(|c| c.id()).collect();
2490        assert_eq!(chunk_ids, vec![0, 1, 2, 3, 4]);
2491
2492        // test no read amplification
2493        let chunks = vec![BlobMetaChunk::new(1, &meta.state)];
2494        let chunks = meta.add_more_chunks(&chunks, 0).unwrap();
2495        let chunk_ids: Vec<_> = chunks.iter().map(|c| c.id()).collect();
2496        assert_eq!(chunk_ids, vec![0, 1]);
2497
2498        // test read non-batch chunk
2499        let chunks = vec![BlobMetaChunk::new(2, &meta.state)];
2500        let chunks = meta.add_more_chunks(&chunks, 0).unwrap();
2501        let chunk_ids: Vec<_> = chunks.iter().map(|c| c.id()).collect();
2502        assert_eq!(chunk_ids, vec![2]);
2503
2504        // test small read amplification
2505        let chunks = vec![BlobMetaChunk::new(1, &meta.state)];
2506        let chunks = meta.add_more_chunks(&chunks, 0x6000).unwrap();
2507        let chunk_ids: Vec<_> = chunks.iter().map(|c| c.id()).collect();
2508        assert_eq!(chunk_ids, vec![0, 1, 2]);
2509    }
2510}