diff --git a/Cargo.toml b/Cargo.toml index 72b6c1b8058..2dd17b30aeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["nydus", "nydus-backend", "nydus-config", "nydus-core", "nydus-error", "nydus-format", "nydus-storage", "nydus-telemetry"] [workspace.dependencies] -clap = { version = "4", features = ["derive", "env"] } +clap = { version = "4", features = ["derive", "env", "string"] } libc = "0.2" memmap2 = "0.9" serde = { version = "1", features = ["derive"] } diff --git a/docs/nydus.md b/docs/nydus.md index d59a8d675f5..e8eb473da4f 100644 --- a/docs/nydus.md +++ b/docs/nydus.md @@ -1062,7 +1062,7 @@ first logical external data block starts at offset 0 blob_meta then maps that logical byte offset to a compressed range in the full blob's data region. The block is mapped to its block_group by -`block_group_index = blkaddr >> block_group_block_bits`, and the block_group entry gives the +`block_group_index = blkaddr >> block_group_block_count_bits`, and the block_group entry gives the encoded `compressed_offset` (for example 0 for the first encoded block_group). ``` @@ -1113,8 +1113,8 @@ embedded blob meta region | block_groups_offset | | chunk_count | | block_group_count | -| chunk_block_bits (u8) | -| block_group_block_bits (u8 + pad) | +| chunk_block_count_bits (u8) | +| block_group_block_count_bits (u8 + pad) | | reserved tail (compat area) | +-------------------------------+ | chunk entries | @@ -1167,16 +1167,16 @@ Header details: chunk table. - `chunk_count` is the number of chunk entries. - `block_group_count` is the number of compressed block group entries. -- `chunk_block_bits` is log2 of the EROFS chunk size in 4 KiB blocks: - `chunk_size = 4096 << chunk_block_bits`, so the default 1 MiB chunk stores +- `chunk_block_count_bits` is log2 of the EROFS chunk size in 4 KiB blocks: + `chunk_size = 4096 << chunk_block_count_bits`, so the default 1 MiB chunk stores 8. Storing the exponent EROFS-style (the same quantity as `chunk_format & EROFS_CHUNK_FORMAT_BLKBITS_MASK`) makes non-power-of-two chunk sizes unrepresentable and feeds the shift-based offset math directly. -- `block_group_block_bits` is log2 of the per-block group block count, same - representation as `chunk_block_bits` (the default 4 MiB block group stores 10). - Every block group except the last is exactly `1 << block_group_block_bits` blocks, so +- `block_group_block_count_bits` is log2 of the per-block group block count, same + representation as `chunk_block_count_bits` (the default 4 MiB block group stores 10). + Every block group except the last is exactly `1 << block_group_block_count_bits` blocks, so the read path maps a block to its block group with - `block_group_index = block_id >> block_group_block_bits` in O(1). The two exponents + `block_group_index = block_id >> block_group_block_count_bits` in O(1). The two exponents are adjacent `u8`s at offset 48; the six bytes after them are reserved. - The header is one EROFS block (4096 bytes): the chunk table starts block aligned by construction, and everything between the last field and the end @@ -1206,7 +1206,7 @@ Block group details: - Block groups are formed by packing whole decoded blocks up to `--block-group-size` regardless of chunk boundaries, then compressing the batch as one unit. So - every block group but the last is exactly `1 << block_group_block_bits` blocks. + every block group but the last is exactly `1 << block_group_block_count_bits` blocks. - `uncompressed_block_offset` is the decoded cache 4 KiB block offset for the block group. Block groups are dense and contiguous in the decoded address space. - `compressed_offset` is the encoded payload's byte offset within the data @@ -1355,7 +1355,7 @@ ondemand blob — named by SHA256(full blob), one new nydus layer Every block group entry in the ondemand blob is a **redirect**: instead of describing this blob's own decoded address space, it names the source block group it is a copy of. Block group sizes follow the source block groups, so the uniform-size -invariant is relaxed and the O(1) `block >> block_group_block_bits` lookup is never +invariant is relaxed and the O(1) `block >> block_group_block_count_bits` lookup is never used on an ondemand blob: ```text @@ -1471,7 +1471,7 @@ When mounting with `--bootstrap + --blob-dir`: the cache directory. The cache verifies the blob meta header crc32c before mmaping the cached file and using its chunk entries. 7. Reads use logical uncompressed offsets from inode chunk indexes. The cache - layer maps an offset to its block group in O(1) with `block >> block_group_block_bits`, + layer maps an offset to its block group in O(1) with `block >> block_group_block_count_bits`, ensures every block group covering the requested range is fetched and decoded from the data region (validating block group CRC32C), and then reads the bytes straight out of the cache file. The cache file mirrors the dense decoded address space, @@ -1548,7 +1548,7 @@ Per-blob prefetch streams block groups into the cache: - The blob meta block groups are the compression/cache unit. Prefetch reads the data region in windows that accumulate consecutive block groups up to the default block group - uncompressed size (1 MiB), so each window decode covers one or more block groups. + uncompressed size (4 MiB), so each window decode covers one or more block groups. - For each window it issues a single contiguous backend range read, then decodes each contained block group (plain copy or zstd), validates length and CRC32C, writes the decoded bytes to the cache file at the block group's uncompressed offset, and diff --git a/nydus-backend/src/local.rs b/nydus-backend/src/local.rs index fe8dc31426e..ed481409a56 100644 --- a/nydus-backend/src/local.rs +++ b/nydus-backend/src/local.rs @@ -198,10 +198,7 @@ impl BlobBackend for Local { fn blob_metadata(&self, blob_id: &[u8; SHA256_DIGEST_SIZE]) -> io::Result { let source = self.resolved_source(blob_id)?; let data = self.read_blob_metadata_bytes(&source)?; - BlobMetadata::loader() - .blob_id(*blob_id) - .from_bytes(&data) - .map_err(io::Error::other) + BlobMetadata::from_bytes(&data, false).map_err(io::Error::other) } fn save_blob_metadata(&self, blob_id: &[u8; SHA256_DIGEST_SIZE], dst: &Path) -> io::Result<()> { @@ -260,16 +257,16 @@ fn probe_full_blob_source( mod tests { use super::*; use crate::ReadKind; - use nydus_format::blob::{BlobMetadataBlockGroup, BlobMetadataChunk}; + use nydus_format::blob::{BlobMetadataBlockGroup, BlobMetadataChunk, BlobMetadataCompressor}; use nydus_format::utils::sha256_bytes; use tempfile::tempdir; - fn blob_metadata(blob_id: [u8; SHA256_DIGEST_SIZE], payload: &[u8]) -> BlobMetadata { - BlobMetadata::from_parts( - blob_id, + fn blob_metadata(payload: &[u8]) -> BlobMetadata { + BlobMetadata::new( + BlobMetadataCompressor::None, 1, - vec![BlobMetadataBlockGroup::new(0, 1, 0, 4096, crc32c::crc32c(payload)).unwrap()], vec![BlobMetadataChunk::new(*blake3::hash(payload).as_bytes(), 0, 1).unwrap()], + vec![BlobMetadataBlockGroup::new(0, 1, 0, 4096, crc32c::crc32c(payload)).unwrap()], ) .unwrap() } @@ -280,13 +277,8 @@ mod tests { fn local_backend_reads_full_blob_file_and_sidecar_meta() { let dir = tempdir().unwrap(); let payload = vec![0xabu8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let full_blob_id = write_minimal_full_blob( - dir.path(), - &payload, - &blob_metadata(data_blob_id, &payload), - true, - ); + let full_blob_id = + write_minimal_full_blob(dir.path(), &payload, &blob_metadata(&payload), true); let backend = Local::new(dir.path().to_path_buf()); let blob_metadata = backend.blob_metadata(&full_blob_id).unwrap(); @@ -309,12 +301,8 @@ mod tests { let dir = tempdir().unwrap(); let payload = vec![0xcdu8; 4096]; let data_blob_id = sha256_bytes(&payload); - let full_blob_id = write_minimal_full_blob( - dir.path(), - &payload, - &blob_metadata(data_blob_id, &payload), - false, - ); + let full_blob_id = + write_minimal_full_blob(dir.path(), &payload, &blob_metadata(&payload), false); let backend = Local::new(dir.path().to_path_buf()); let blob_metadata = backend.blob_metadata(&full_blob_id).unwrap(); diff --git a/nydus-backend/src/registry/mod.rs b/nydus-backend/src/registry/mod.rs index 07e3e5b385a..af8ab9abaab 100644 --- a/nydus-backend/src/registry/mod.rs +++ b/nydus-backend/src/registry/mod.rs @@ -649,9 +649,7 @@ impl Registry { ReadContext::raw(ReadKind::OnDemand), )?; - BlobMetadata::loader() - .blob_id(*blob_id) - .from_bytes(&blob_metadata_bytes) + BlobMetadata::from_bytes(&blob_metadata_bytes, false) .map_err(|err| RegistryError::Io(io::Error::other(err))) } diff --git a/nydus-core/src/blob.rs b/nydus-core/src/blob.rs index 823c552472f..d0f69f82bab 100644 --- a/nydus-core/src/blob.rs +++ b/nydus-core/src/blob.rs @@ -130,7 +130,7 @@ impl Blobs { blocks: info.blocks, cache_size, cache_path, - is_redirect: cache.is_redirect_blob(), + is_redirect: cache.is_redirect(), }) }) .collect() diff --git a/nydus-core/src/reader/data.rs b/nydus-core/src/reader/data.rs index 293a949bd11..816b963b5b4 100644 --- a/nydus-core/src/reader/data.rs +++ b/nydus-core/src/reader/data.rs @@ -6,7 +6,7 @@ use nydus_format::erofs::{ EROFS_CHUNK_INDEX_SIZE, EROFS_INODE_CHUNK_BASED, EROFS_INODE_FLAT_INLINE, EROFS_INODE_FLAT_PLAIN, EROFS_NULL_ADDR, }; -use nydus_format::utils::round_up; +use nydus_format::utils::align_up_usize; use super::{ErofsReader, RawBlobInfo}; @@ -135,7 +135,8 @@ impl ErofsReader { let nchunks = inode.size().div_ceil(chunk_size) as usize; let inode_offset = self.nid_to_offset(nid); let header_size = inode.header_size() + inode.xattr_size(); - let index_offset = inode_offset + round_up(header_size, EROFS_CHUNK_INDEX_SIZE); + let index_offset = inode_offset + + align_up_usize(header_size, EROFS_CHUNK_INDEX_SIZE).expect("alignment overflowed"); let index_total = nchunks * EROFS_CHUNK_INDEX_SIZE; self.mmap_slice(index_offset, index_total) } diff --git a/nydus-core/src/reader/metadata.rs b/nydus-core/src/reader/metadata.rs index e4364768fa2..699026905fd 100644 --- a/nydus-core/src/reader/metadata.rs +++ b/nydus-core/src/reader/metadata.rs @@ -5,7 +5,7 @@ use nydus_format::erofs::{ EROFS_INODE_EXTENDED_SIZE, EROFS_INODE_FLAT_INLINE, EROFS_INODE_FLAT_PLAIN, EROFS_XATTR_ENTRY_HEADER_SIZE, EROFS_XATTR_IBODY_HEADER_SIZE, }; -use nydus_format::utils::round_up; +use nydus_format::utils::align_up_usize; use super::{ErofsReader, RawDirEntry}; @@ -288,7 +288,7 @@ impl ErofsReader { result.push((full_name, value)); // Advance to next entry (4-byte aligned) - pos = round_up(value_end, XATTR_ENTRY_ALIGN); + pos = align_up_usize(value_end, XATTR_ENTRY_ALIGN).expect("alignment overflowed"); } Ok(result) diff --git a/nydus-core/src/reader/mod.rs b/nydus-core/src/reader/mod.rs index cb9d70f7d36..6a101ba841f 100644 --- a/nydus-core/src/reader/mod.rs +++ b/nydus-core/src/reader/mod.rs @@ -276,8 +276,8 @@ impl ErofsReader { /// Return whether the blob identified by `blob_index` is an "ondemand" /// redirect blob (produced by `nydus optimize`). Opens the blob cache, /// which reads the local blob meta but performs no data prefetch. - pub fn is_redirect_blob(&self, blob_index: u16) -> io::Result { - self.blobs.is_redirect_blob(blob_index) + pub fn is_redirect(&self, blob_index: u16) -> io::Result { + self.blobs.is_redirect(blob_index) } /// Prefetch every block group of the blob identified by `blob_index`. An diff --git a/nydus-format/src/blob/algorithm.rs b/nydus-format/src/blob/algorithm.rs index cc0045bca33..4582d036a1a 100644 --- a/nydus-format/src/blob/algorithm.rs +++ b/nydus-format/src/blob/algorithm.rs @@ -6,6 +6,8 @@ use crate::blob::metadata::BlobMetadataFlags; use crate::error::{Error, Result}; use std::fmt; +/// The block group payload compressor a blob meta declares. `None` is the +/// absent-flag state: payloads are stored raw. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BlobMetadataCompressor { None, @@ -13,6 +15,7 @@ pub enum BlobMetadataCompressor { } impl BlobMetadataCompressor { + /// The flag bit encoding this compressor, empty for `None`. pub fn flag(self) -> BlobMetadataFlags { match self { Self::None => BlobMetadataFlags::empty(), @@ -21,6 +24,8 @@ impl BlobMetadataCompressor { } } +/// The lowercase algorithm name, as surfaced in the `build` and `check` +/// summaries. impl fmt::Display for BlobMetadataCompressor { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { @@ -42,12 +47,15 @@ impl From for BlobMetadataCompressor { } } +/// The chunk digest algorithm a blob meta declares, always explicit (see +/// the `TryFrom` below). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BlobMetadataDigester { Blake3, } impl BlobMetadataDigester { + /// The flag bit encoding this digester. pub fn flag(self) -> BlobMetadataFlags { match self { Self::Blake3 => BlobMetadataFlags::DIGESTER_BLAKE3, @@ -55,6 +63,8 @@ impl BlobMetadataDigester { } } +/// The lowercase algorithm name, as surfaced in the `build` and `check` +/// summaries. impl fmt::Display for BlobMetadataDigester { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { diff --git a/nydus-format/src/blob/metadata.rs b/nydus-format/src/blob/metadata.rs index 988a381d221..06354c98e4b 100644 --- a/nydus-format/src/blob/metadata.rs +++ b/nydus-format/src/blob/metadata.rs @@ -6,7 +6,6 @@ use crate::utils::le::{ read_u16_at, read_u32_at, read_u64_at, read_u8_at, write_u16_at, write_u32_at, write_u64_at, write_u8_at, }; -use crate::utils::SHA256_DIGEST_SIZE; use bitflags::bitflags; use crc32c::{crc32c, crc32c_append}; use memmap2::{Mmap, MmapOptions}; @@ -17,69 +16,71 @@ use std::ops::Range; use std::path::Path; /// On-disk magic: 8 raw ASCII bytes ("LPBLMETA" = LePton BLob META), written -/// as-is so a hexdump of the file starts with the readable string. Same style -/// and `magic + version + flags` header prefix as the blob footer +/// as-is so a hexdump of the file starts with the readable string. Same +/// style and `magic + version + flags` header prefix as the blob footer /// (`LPFOOTER`) and block_group_map (`LPGRPMAP`) sidecars. pub const NYDUS_BLOB_METADATA_MAGIC: [u8; 8] = *b"LPBLMETA"; /// On-disk format generation, informational only: readers do not gate on it. -/// Compatibility is governed EROFS-style by the magic (a new format family -/// gets a new magic) and the incompat half of `flags` (unknown incompat bits -/// reject the file). +/// Compatibility is governed EROFS-style by the magic and the incompat half +/// of `flags` (unknown incompat bits reject the file). pub const NYDUS_BLOB_METADATA_VERSION: u32 = 1; -/// The header's fixed on-disk size: one EROFS block. The chunk table starts -/// right after the header, so it is block aligned by construction, and the -/// unused tail of the header block is reserved for future compat fields -/// (writers zero it, readers ignore it; corruption is caught by the file -/// crc32c). +/// The header's fixed on-disk size: one EROFS block, keeping the chunk +/// table behind it block aligned by construction. The unused tail is +/// reserved for future compat fields (writers zero it, readers ignore it, +/// corruption is caught by the file crc32c). pub const NYDUS_BLOB_METADATA_HEADER_SIZE: usize = EROFS_BLOCK_SIZE as usize; -/// On-disk size of one chunk entry in the chunk table. +/// On-disk size of one chunk entry, pinned to [`BlobMetadataChunk`]'s Rust +/// layout by a const assert so mapped tables are readable in place. pub const NYDUS_BLOB_METADATA_CHUNK_ENTRY_SIZE: usize = 48; -/// On-disk size of one block group entry in the block group table. +/// On-disk size of one block group entry, pinned to +/// [`BlobMetadataBlockGroup`]'s Rust layout the same way. pub const NYDUS_BLOB_METADATA_BLOCK_GROUP_ENTRY_SIZE: usize = 40; /// Default chunk size: 1 MiB of the uncompressed address space per digest. -pub const NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE: u32 = 1024 * 1024; +pub const DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE: u32 = 1024 * 1024; /// The default chunk size in 4KiB blocks. -pub const NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT: u32 = - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE / EROFS_BLOCK_SIZE; +pub const DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT: u32 = + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE / EROFS_BLOCK_SIZE; -/// Default block group uncompressed size. Equal to the default chunk size, so -/// a default-geometry chunk always fits in one block group. -pub const NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE: u32 = - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE; +/// Default block group uncompressed size: the unit of compression and of a +/// single backend read, a multiple of the default chunk size so a +/// default-geometry chunk always fits in one block group. +pub const DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE: u32 = 4 * 1024 * 1024; /// The default block group size in 4KiB blocks. -pub const NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_BLOCK_COUNT: u32 = - NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE / EROFS_BLOCK_SIZE; +pub const DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_BLOCK_COUNT: u32 = + DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE / EROFS_BLOCK_SIZE; /// File-name suffix of a blob meta sidecar file (`.blob.meta`). pub const NYDUS_BLOB_METADATA_SUFFIX: &str = ".blob.meta"; -/// Largest allowed block-count exponent (`chunk_block_bits` / -/// `block_group_block_bits`): keeps the derived byte size (`4096 << bits`) -/// representable in a `u32` (2 GiB at most). -const NYDUS_BLOB_METADATA_MAX_BLOCK_BITS: u8 = 19; +/// Largest allowed block-count exponent (`chunk_block_count_bits` / +/// `block_group_block_count_bits`): keeps the derived byte size +/// (`4096 << bits`) within a `u32` (2 GiB at most). +const NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS: u8 = 19; /// Byte range of the crc32 field within the header. const NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD: Range = 16..20; +/// Chunk entries' reserved field, held to zero: entry-layout evolution is +/// signalled by an incompat flag bit, so writers zero it and readers reject +/// anything else. const NYDUS_BLOB_METADATA_CHUNK_RESERVED: u32 = 0; + +/// Block group entries' reserved tail, held to zero the same way. const NYDUS_BLOB_METADATA_BLOCK_GROUP_RESERVED: [u8; 6] = [0u8; 6]; bitflags! { - /// Feature bits, split EROFS-style (see [`crate::blob::flag`]): the - /// low 16 bits are **incompatible** features — a reader that does not - /// know a set bit cannot interpret the file and must reject it (like - /// `feature_incompat`). The high 16 bits are **compatible** features — - /// unknown bits are ignored so old readers keep working (like - /// `feature_compat`). Entry-layout evolution (wider chunk/block group - /// entries, new entry kinds) is expressed as a new incompat bit; header - /// growth uses the reserved tail plus a compat bit. + /// Feature bits, split EROFS-style (see [`crate::blob::flag`]): the low + /// 16 bits are incompatible features (unknown bits reject the file), the + /// high 16 bits are compatible features (unknown bits are ignored). + /// Entry-layout changes take a new incompat bit, header growth uses the + /// reserved tail plus a compat bit. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BlobMetadataFlags: u32 { const COMPRESSOR_ZSTD = 1 << 0; @@ -111,8 +112,11 @@ const NYDUS_BLOB_METADATA_SUPPORTED_INCOMPAT: u32 = BlobMetadataFlags::all().bit /// table's bytes /// 40 4 chunk_count /// 44 4 block_group_count -/// 48 1 chunk_block_bits log2 of 4KiB blocks per chunk -/// 49 1 block_group_block_bits log2 of 4KiB blocks per block group +/// 48 1 chunk_block_count_bits log2 of the per-chunk 4KiB +/// block count +/// 49 1 block_group_block_count_bits +/// log2 of the per-block group +/// 4KiB block count /// 50 6 reserved1 writers zero it, readers ignore it /// 56 4040 reserved writers zero it, readers ignore it /// ``` @@ -127,11 +131,15 @@ pub struct BlobMetadataHeader { block_groups_offset: u64, chunk_count: u32, block_group_count: u32, - chunk_block_bits: u8, - block_group_block_bits: u8, + chunk_block_count_bits: u8, + block_group_block_count_bits: u8, } impl BlobMetadataHeader { + /// Parse a header from exactly its `NYDUS_BLOB_METADATA_HEADER_SIZE` + /// bytes, verifying the intrinsic fields. The crc32 field seals the + /// whole serialized metadata, so the metadata read paths verify it, + /// not this parser. fn from_bytes(bytes: &[u8; NYDUS_BLOB_METADATA_HEADER_SIZE]) -> Result { let header = Self { magic: bytes[0..8].try_into().unwrap(), @@ -143,14 +151,18 @@ impl BlobMetadataHeader { block_groups_offset: read_u64_at(bytes, 32), chunk_count: read_u32_at(bytes, 40), block_group_count: read_u32_at(bytes, 44), - chunk_block_bits: read_u8_at(bytes, 48), - block_group_block_bits: read_u8_at(bytes, 49), + chunk_block_count_bits: read_u8_at(bytes, 48), + block_group_block_count_bits: read_u8_at(bytes, 49), }; header.validate()?; Ok(header) } + /// Serialize the header into its on-disk bytes. The reserved regions + /// are zeroed, so this is only the writer's view: raw bytes read from + /// disk may carry newer compat fields there that this type does not + /// model. fn to_bytes(self) -> [u8; NYDUS_BLOB_METADATA_HEADER_SIZE] { let mut data = [0u8; NYDUS_BLOB_METADATA_HEADER_SIZE]; data[0..8].copy_from_slice(&self.magic); @@ -162,27 +174,36 @@ impl BlobMetadataHeader { write_u64_at(&mut data, 32, self.block_groups_offset); write_u32_at(&mut data, 40, self.chunk_count); write_u32_at(&mut data, 44, self.block_group_count); - write_u8_at(&mut data, 48, self.chunk_block_bits); - write_u8_at(&mut data, 49, self.block_group_block_bits); + write_u8_at(&mut data, 48, self.chunk_block_count_bits); + write_u8_at(&mut data, 49, self.block_group_block_count_bits); data } + /// Validate the intrinsic field invariants, needing nothing beyond the + /// fields themselves. Run once per entry point: by [`Self::from_bytes`] + /// on the read side and by [`BlobMetadata::new`] on the write side. + /// + /// Deliberately not checked: `version` is informational (compatibility + /// is governed by the magic and the incompat flag bits), `reserved0` and + /// the reserved tail may carry a newer writer's compat fields (corruption + /// is caught by the crc32), and the entry counts are anchored against + /// the actual table bytes by [`BlobMetadata::validate_bytes`]. fn validate(&self) -> Result<()> { if self.magic != NYDUS_BLOB_METADATA_MAGIC { return Err(Error::InvalidImage("invalid blob meta magic".to_string())); } - if self.chunk_block_bits > NYDUS_BLOB_METADATA_MAX_BLOCK_BITS { + if self.chunk_block_count_bits > NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS { return Err(Error::InvalidImage(format!( - "blob meta chunk block bits too large: {}", - self.chunk_block_bits + "blob meta chunk block count bits too large: {}", + self.chunk_block_count_bits ))); } - if self.block_group_block_bits > NYDUS_BLOB_METADATA_MAX_BLOCK_BITS { + if self.block_group_block_count_bits > NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS { return Err(Error::InvalidImage(format!( - "blob meta block group block bits too large: {}", - self.block_group_block_bits + "blob meta block group block count bits too large: {}", + self.block_group_block_count_bits ))); } @@ -195,7 +216,7 @@ impl BlobMetadataHeader { let expected_block_groups_offset = self .chunks_offset - .checked_add(self.chunk_bytes()) + .checked_add(self.chunk_table_size()) .ok_or_else(|| Error::Overflow("blob meta block group offset overflow".to_string()))?; if self.block_groups_offset != expected_block_groups_offset { @@ -223,103 +244,92 @@ impl BlobMetadataHeader { Ok(()) } - fn set_counts_and_offsets(&mut self, chunk_count: u32, block_group_count: u32) -> Result<()> { - self.chunk_count = chunk_count; - self.block_group_count = block_group_count; - self.chunks_offset = NYDUS_BLOB_METADATA_HEADER_SIZE as u64; - self.block_groups_offset = self - .chunks_offset - .checked_add(chunk_count as u64 * size_of::() as u64) - .ok_or_else(|| Error::Overflow("blob meta block group offset overflow".to_string()))?; - Ok(()) - } - - fn set_chunk_block_count(&mut self, blocks: u32) -> Result<()> { - self.chunk_block_bits = block_count_to_bits(blocks, "chunk")?; - Ok(()) - } - - fn set_compressor(&mut self, compressor: BlobMetadataCompressor) { - let mut flags = self.flags(); - flags.remove(BlobMetadataFlags::COMPRESSOR_ZSTD); - flags.insert(compressor.flag()); - self.flags = flags.bits(); - } - + /// On-disk format generation, informational only: readers never gate + /// on it. pub fn version(&self) -> u32 { self.version } + /// The known feature bits as a typed view. Unknown compat bits are + /// dropped here (unknown incompat bits were already rejected at + /// validation). pub fn flags(&self) -> BlobMetadataFlags { BlobMetadataFlags::from_bits_truncate(self.flags) } + /// crc32c sealing the whole serialized metadata, exactly as stored on + /// disk. pub fn crc32(&self) -> u32 { self.crc32 } + /// The block group payload compressor, per the flags. pub fn compressor(&self) -> BlobMetadataCompressor { BlobMetadataCompressor::from(self.flags()) } + /// The chunk digest algorithm, per the flags (vetted at validation, so + /// the conversion cannot fail). pub fn digester(&self) -> BlobMetadataDigester { - BlobMetadataDigester::try_from(self.flags()).expect("validated blob meta digester") + BlobMetadataDigester::try_from(self.flags()).unwrap() } + /// Number of entries in the chunk table. pub fn chunk_count(&self) -> u32 { self.chunk_count } - pub fn block_group_count(&self) -> u32 { - self.block_group_count - } - - /// Number of 4 KiB blocks per chunk, derived from the stored exponent. + /// Uncompressed 4KiB blocks per chunk (`1 << chunk_block_count_bits`). pub fn chunk_block_count(&self) -> u32 { - 1u32 << self.chunk_block_bits + 1u32 << self.chunk_block_count_bits } + /// Uncompressed bytes per chunk. pub fn chunk_size(&self) -> u32 { - EROFS_BLOCK_SIZE << self.chunk_block_bits + EROFS_BLOCK_SIZE << self.chunk_block_count_bits } - /// log2 of the per-block group block count. - pub fn block_group_block_bits(&self) -> u8 { - self.block_group_block_bits + /// Byte offset of the chunk table, always right after the header. + pub fn chunks_offset(&self) -> u64 { + self.chunks_offset } - /// Number of uncompressed blocks per block group, derived from the - /// stored exponent. Every block group except the last is exactly this - /// many blocks, so the read path maps a block to its block group by - /// `block >> block_group_block_bits`. - pub fn block_group_block_count(&self) -> u32 { - 1u32 << self.block_group_block_bits + /// Byte size of the chunk table. + pub fn chunk_table_size(&self) -> u64 { + self.chunk_count as u64 * size_of::() as u64 } - pub fn chunks_offset(&self) -> u64 { - self.chunks_offset + /// Number of entries in the block group table. + pub fn block_group_count(&self) -> u32 { + self.block_group_count } - pub fn block_groups_offset(&self) -> u64 { - self.block_groups_offset + /// Uncompressed 4KiB blocks per block group + /// (`1 << block_group_block_count_bits`). + pub fn block_group_block_count(&self) -> u32 { + 1u32 << self.block_group_block_count_bits } - pub fn chunk_bytes(&self) -> u64 { - self.chunk_count as u64 * size_of::() as u64 + /// Byte offset of the block group table, right after the chunk table. + pub fn block_groups_offset(&self) -> u64 { + self.block_groups_offset } - pub fn block_group_bytes(&self) -> u64 { + /// Byte size of the block group table. + pub fn block_group_table_size(&self) -> u64 { self.block_group_count as u64 * size_of::() as u64 } - /// End offset of the entry region (header plus chunk and block group - /// tables), before padding to the block-aligned `metadata_size`. - pub fn entries_end(&self) -> u64 { - self.block_groups_offset + self.block_group_bytes() + /// Bytes the header and the tables actually use, before the tail + /// padding. + pub fn used_size(&self) -> u64 { + self.block_groups_offset + self.block_group_table_size() } - pub fn metadata_size(&self) -> u64 { - crate::utils::align_up(self.entries_end(), EROFS_BLOCK_SIZE as u64) + /// The full serialized size: [`Self::used_size`] aligned up to one + /// 4KiB block. + pub fn padded_size(&self) -> u64 { + crate::utils::align_up_u64(self.used_size(), EROFS_BLOCK_SIZE as u64) .expect("blob meta size overflowed") } } @@ -351,6 +361,8 @@ pub struct BlobMetadataChunk { reserved: u32, } +// Pins the Rust layout to the on-disk entry size: a drift would break the +// in-place mapped tables, so it fails the build instead. const _: () = assert!(size_of::() == NYDUS_BLOB_METADATA_CHUNK_ENTRY_SIZE); impl BlobMetadataChunk { @@ -398,7 +410,7 @@ impl BlobMetadataChunk { /// Validate the intrinsic field invariants. Run by every construction /// path ([`Self::new`], [`Self::from_bytes`]), so a chunk in hand is - /// always valid; mapped tables are validated entry by entry at load. + /// always valid. Mapped tables are validated entry by entry at load. fn validate(&self) -> Result<()> { if self.uncompressed_block_count == 0 { return Err(Error::InvalidImage( @@ -412,6 +424,12 @@ impl BlobMetadataChunk { )); } + self.uncompressed_block_offset + .checked_mul(EROFS_BLOCK_SIZE as u64) + .ok_or_else(|| { + Error::Overflow("blob meta chunk uncompressed byte offset overflow".to_string()) + })?; + self.uncompressed_offset() .checked_add(self.uncompressed_size()) .ok_or_else(|| Error::Overflow("blob meta chunk byte range overflow".to_string()))?; @@ -425,25 +443,32 @@ impl BlobMetadataChunk { Ok(()) } + /// Digest of the chunk's uncompressed bytes, algorithm per the header's + /// digester flag. pub fn digest(&self) -> &[u8; 32] { &self.digest } - /// Absolute block offset of this chunk within the dense uncompressed - /// address space. Chunks are independent of block groups, so this is a - /// plain block index into the blob, not a block group-relative offset. + /// Start of the chunk's span, in 4KiB blocks of the uncompressed + /// address space. pub fn uncompressed_block_offset(&self) -> u64 { self.uncompressed_block_offset } + /// Length of the chunk's span in 4KiB blocks, never zero. pub fn uncompressed_block_count(&self) -> u32 { self.uncompressed_block_count } + /// Start of the chunk's span in bytes (validation vetted the + /// conversion, so it cannot overflow). pub fn uncompressed_offset(&self) -> u64 { - self.uncompressed_block_offset * EROFS_BLOCK_SIZE as u64 + self.uncompressed_block_offset + .checked_mul(EROFS_BLOCK_SIZE as u64) + .expect("validated blob meta chunk byte offset") } + /// Length of the chunk's span in bytes. pub fn uncompressed_size(&self) -> u64 { self.uncompressed_block_count as u64 * EROFS_BLOCK_SIZE as u64 } @@ -454,6 +479,19 @@ impl BlobMetadataChunk { /// fill, and prefetch. Block group entries are packed back to back in the /// block group table right after the chunk table. /// +/// The two coordinate spaces the entry bridges: +/// +/// ```text +/// uncompressed address space: dense 4KiB blocks from 0, uniform span +/// ┌───────────┬───────────┬──────┐ +/// │ group 0 │ group 1 │ gr 2 │ (final group may be short) +/// └─────┬─────┴─────┬─────┴───┬──┘ +/// ▼ ▼ ▼ each group encoded on its own +/// ┌─────────┬──┬──────┐ +/// │ p0 │p1│ p2 │ compressed payloads: packed in +/// └─────────┴──┴──────┘ order, gaps allowed, byte-exact +/// ``` +/// /// The entry's 40 bytes (integers little-endian): /// /// ```text @@ -487,10 +525,12 @@ pub struct BlobMetadataBlockGroup { reserved: [u8; 6], } +// The same layout pin for block group entries. const _: () = assert!(size_of::() == NYDUS_BLOB_METADATA_BLOCK_GROUP_ENTRY_SIZE); impl BlobMetadataBlockGroup { + /// Creates a validated entry for a payload stored in this blob itself. pub fn new( uncompressed_block_offset: u64, uncompressed_block_count: u32, @@ -513,6 +553,9 @@ impl BlobMetadataBlockGroup { Ok(block_group) } + /// Creates a validated redirect entry: the payload lives in a block + /// group of another source blob, and the non-zero `source_blob_index` + /// is what marks the entry as a redirect. #[allow(clippy::too_many_arguments)] pub fn new_redirect( uncompressed_block_offset: u64, @@ -538,6 +581,8 @@ impl BlobMetadataBlockGroup { Ok(block_group) } + /// Parse a block group entry from exactly its 40 bytes, verifying the + /// intrinsic fields. pub fn from_bytes(bytes: &[u8; NYDUS_BLOB_METADATA_BLOCK_GROUP_ENTRY_SIZE]) -> Result { let block_group = Self { uncompressed_block_offset: read_u64_at(bytes, 0), @@ -554,6 +599,7 @@ impl BlobMetadataBlockGroup { Ok(block_group) } + /// Serialize the block group entry into its on-disk bytes. fn to_bytes(self) -> [u8; NYDUS_BLOB_METADATA_BLOCK_GROUP_ENTRY_SIZE] { let mut data = [0u8; NYDUS_BLOB_METADATA_BLOCK_GROUP_ENTRY_SIZE]; write_u64_at(&mut data, 0, self.uncompressed_block_offset); @@ -567,6 +613,9 @@ impl BlobMetadataBlockGroup { data } + /// Validate the intrinsic field invariants. Run by every construction + /// path, so a block group in hand is always valid. Cross-entry rules + /// (density, ordering) live in [`BlobMetadata::validate_block_groups`]. fn validate(&self) -> Result<()> { if self.uncompressed_block_count == 0 { return Err(Error::InvalidImage( @@ -618,6 +667,8 @@ impl BlobMetadataBlockGroup { Ok(()) } + /// The redirect variant of [`Self::validate`]: additionally requires + /// the non-zero `source_blob_index` that marks a redirect. fn validate_redirect(&self) -> Result<()> { if self.source_blob_index == 0 { return Err(Error::InvalidImage( @@ -668,11 +719,14 @@ impl BlobMetadataBlockGroup { Ok(()) } + /// Write the block group entry's on-disk bytes to `writer`. pub fn write_to(&self, writer: &mut dyn Write) -> Result<()> { writer.write_all(&self.to_bytes())?; Ok(()) } + /// A copy with the compressed offset shifted by `bias`, for payloads + /// embedded at an offset inside a full blob. pub fn checked_add_compressed_offset(&self, bias: u64) -> Result { let block_group = Self { compressed_offset: self.compressed_offset().checked_add(bias).ok_or_else(|| { @@ -685,49 +739,87 @@ impl BlobMetadataBlockGroup { Ok(block_group) } + /// Whether the payload lives in another source blob. pub fn is_redirect(&self) -> bool { self.source_blob_index != 0 } + /// The source blob holding the payload, zero when the payload is local. pub fn source_blob_index(&self) -> u16 { self.source_blob_index } + /// The block group within the source blob, redirect only. pub fn source_block_group_index(&self) -> u32 { self.source_block_group_index } + /// Start of the group's span, in 4KiB blocks of the uncompressed + /// address space. pub fn uncompressed_block_offset(&self) -> u64 { self.uncompressed_block_offset } + /// Length of the group's span in 4KiB blocks, never zero. pub fn uncompressed_block_count(&self) -> u32 { self.uncompressed_block_count } + /// Start of the group's span in bytes (validation vetted the + /// conversion, so it cannot overflow). pub fn uncompressed_offset(&self) -> u64 { self.uncompressed_block_offset .checked_mul(EROFS_BLOCK_SIZE as u64) .expect("validated blob meta block group byte offset") } + /// Length of the group's span in bytes. pub fn uncompressed_size(&self) -> u64 { self.uncompressed_block_count as u64 * EROFS_BLOCK_SIZE as u64 } + /// Byte offset of the encoded payload (payloads pack back to back, no + /// block alignment). pub fn compressed_offset(&self) -> u64 { self.compressed_offset } + /// Byte size of the encoded payload, never zero. pub fn compressed_size(&self) -> u32 { self.compressed_size } + /// crc32c of the group's uncompressed payload, checked after decode. pub fn crc32(&self) -> u32 { self.crc32 } + + /// True when any block group redirects to another source blob. + fn has_redirect(block_groups: &[Self]) -> bool { + block_groups.iter().any(Self::is_redirect) + } + + /// Derive the header's `block_group_block_count_bits` from the groups + /// themselves: the first group carries the uniform span (validated + /// later), a lone group rounds up to a power of two, and empty or + /// redirect tables fall back to the default geometry. + fn infer_block_count_bits(block_groups: &[Self]) -> Result { + let default_bits = DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_BLOCK_COUNT.ilog2() as u8; + if Self::has_redirect(block_groups) { + return Ok(default_bits); + } + + match block_groups { + [] => Ok(default_bits), + [only] => block_count_to_bits(only.uncompressed_block_count().next_power_of_two()), + [first, ..] => block_count_to_bits(first.uncompressed_block_count()), + } + } } +/// In-memory backing of the tables: owned vectors on the write side, a +/// shared file mapping read in place on the read side. +#[derive(Debug)] enum BlobMetadataStorage { Owned { chunks: Vec, @@ -748,230 +840,326 @@ enum BlobMetadataStorage { /// │ header │ chunk table │ block group table │ zero padding │ /// └────────┴─────────────┴───────────────────┴──────────────┘ /// 0 4096 ▲ EOF -/// the entries end here; the padding -/// runs to the 4KiB-aligned metadata_size +/// the entries end here, the padding +/// runs to the 4KiB-aligned padded_size /// ``` /// /// In memory the tables are either owned (the write side, built by -/// [`Self::from_parts`]) or a shared file mapping read in place -/// ([`Self::load`]), zero-copy thanks to the entries' pinned layout. +/// [`Self::new`]) or a shared file mapping read in place +/// ([`Self::from_path`]), zero-copy thanks to the entries' pinned layout. +#[derive(Debug)] pub struct BlobMetadata { header: BlobMetadataHeader, - blob_id: [u8; SHA256_DIGEST_SIZE], storage: BlobMetadataStorage, } impl BlobMetadata { - /// Creates validated, sealed metadata from owned tables, with the - /// uncompressed default; see [`Self::from_parts_with_options`]. - pub fn from_parts( - blob_id: [u8; SHA256_DIGEST_SIZE], - chunk_block_count: u32, - block_groups: Vec, - chunks: Vec, - ) -> Result { - Self::from_parts_with_options( - blob_id, - chunk_block_count, - BlobMetadataCompressor::None, - block_groups, - chunks, - ) - } - /// Creates validated, sealed metadata from owned tables: the header is /// derived from the tables and both are validated first, so constructed /// metadata is valid by definition, then the crc32 is computed over the /// final bytes. - pub fn from_parts_with_options( - blob_id: [u8; SHA256_DIGEST_SIZE], - chunk_block_count: u32, + pub fn new( compressor: BlobMetadataCompressor, - block_groups: Vec, + chunk_block_count: u32, chunks: Vec, + block_groups: Vec, ) -> Result { - // Zeros are placeholders: the setters below stamp the real layout - // and geometry before the header is sealed. - let mut header = BlobMetadataHeader { + let chunks_offset = NYDUS_BLOB_METADATA_HEADER_SIZE as u64; + let header = BlobMetadataHeader { magic: NYDUS_BLOB_METADATA_MAGIC, version: NYDUS_BLOB_METADATA_VERSION, - flags: BlobMetadataDigester::Blake3.flag().bits(), + flags: (BlobMetadataDigester::Blake3.flag() | compressor.flag()).bits(), crc32: 0, reserved0: 0, - chunks_offset: 0, - block_groups_offset: 0, - chunk_count: 0, - block_group_count: 0, - chunk_block_bits: 0, - block_group_block_bits: 0, + chunks_offset, + block_groups_offset: chunks_offset + .checked_add(chunks.len() as u64 * size_of::() as u64) + .ok_or_else(|| { + Error::Overflow("blob meta block group offset overflow".to_string()) + })?, + chunk_count: chunks.len() as u32, + block_group_count: block_groups.len() as u32, + chunk_block_count_bits: block_count_to_bits(chunk_block_count)?, + block_group_block_count_bits: BlobMetadataBlockGroup::infer_block_count_bits( + &block_groups, + )?, }; - header.set_chunk_block_count(chunk_block_count)?; - header.set_compressor(compressor); - header.set_counts_and_offsets(chunks.len() as u32, block_groups.len() as u32)?; - header.block_group_block_bits = infer_block_group_block_bits(&block_groups)?; - validate_tables(&block_groups, &chunks, header.block_group_block_count())?; + header.validate()?; + let mut blob_metadata = Self { header, - blob_id, storage: BlobMetadataStorage::Owned { chunks, block_groups, }, }; + blob_metadata.validate()?; blob_metadata.header.crc32 = blob_metadata.compute_crc32_from_parts(); Ok(blob_metadata) } - /// A copy of this metadata with `bias` added to every block group's - /// compressed byte offset: used when the encoded payload region moves as - /// a whole (e.g. embedding into a full blob). - pub fn checked_add_compressed_offset(&self, bias: u64) -> Result { - let mut block_groups = Vec::with_capacity(self.block_group_count()); - for block_group in self.block_groups() { - block_groups.push(block_group.checked_add_compressed_offset(bias)?); - } - Self::from_parts_with_options( - self.blob_id, - self.chunk_block_count(), - self.compressor(), - block_groups, - self.chunks().to_vec(), - ) - } - - /// Start configuring a blob meta read; finish with - /// [`load`](BlobMetadataLoader::load) or - /// [`from_bytes`](BlobMetadataLoader::from_bytes). - pub fn loader() -> BlobMetadataLoader { - BlobMetadataLoader::default() - } - - /// Read blob metadata from a file (mmap-backed), without verifying the - /// crc32; [`Self::loader`] holds the knobs. - pub fn load(path: &Path) -> Result { - Self::load_inner(path, false) - } - - fn from_bytes_inner( - data: &[u8], - blob_id: [u8; SHA256_DIGEST_SIZE], - check_crc32: bool, - ) -> Result { - if data.len() < NYDUS_BLOB_METADATA_HEADER_SIZE { + /// Read blob metadata from an in-memory byte slice, optionally verifying + /// the header crc32 over the full metadata. + pub fn from_bytes(bytes: &[u8], verify_crc32: bool) -> Result { + let Some((header_bytes, _)) = bytes.split_first_chunk::() + else { return Err(Error::InvalidImage("blob meta data too small".to_string())); - } - - let header = BlobMetadataHeader::from_bytes( - data[..NYDUS_BLOB_METADATA_HEADER_SIZE] - .try_into() - .expect("length checked"), - )?; - if data.len() as u64 != header.metadata_size() { - return Err(Error::InvalidImage(format!( - "blob meta data size mismatch: expected {}, got {}", - header.metadata_size(), - data.len() - ))); - } - validate_padding(data, &header)?; - if check_crc32 { - validate_blob_metadata_crc32(data, &header)?; - } + }; - let mut chunks = Vec::with_capacity(header.chunk_count() as usize); - for index in 0..header.chunk_count() as usize { - let start = header.chunks_offset() as usize + index * size_of::(); - chunks.push( - BlobMetadataChunk::from_bytes( - data[start..start + size_of::()] - .try_into() - .expect("length checked"), - ) - .with_context(|| format!("failed to read blob meta chunk {index}"))?, - ); - } + let header = BlobMetadataHeader::from_bytes(header_bytes)?; + Self::validate_bytes(bytes, &header, verify_crc32)?; + + let chunk_table = + &bytes[header.chunks_offset() as usize..header.block_groups_offset() as usize]; + let chunks = chunk_table + .chunks_exact(size_of::()) + .enumerate() + .map(|(index, entry)| { + BlobMetadataChunk::from_bytes(entry.try_into().unwrap()) + .with_context(|| format!("failed to read blob meta chunk {index}")) + }) + .collect::>>()?; + + let block_group_table = + &bytes[header.block_groups_offset() as usize..header.used_size() as usize]; + let block_groups = block_group_table + .chunks_exact(size_of::()) + .enumerate() + .map(|(index, entry)| { + BlobMetadataBlockGroup::from_bytes(entry.try_into().unwrap()) + .with_context(|| format!("failed to read blob meta block group {index}")) + }) + .collect::>>()?; - let mut block_groups = Vec::with_capacity(header.block_group_count() as usize); - for index in 0..header.block_group_count() as usize { - let start = - header.block_groups_offset() as usize + index * size_of::(); - block_groups.push( - BlobMetadataBlockGroup::from_bytes( - data[start..start + size_of::()] - .try_into() - .expect("length checked"), - ) - .with_context(|| format!("failed to read blob meta block group {index}"))?, - ); - } - validate_tables(&block_groups, &chunks, header.block_group_block_count())?; - Ok(Self { + let blob_metadata = Self { header, - blob_id, storage: BlobMetadataStorage::Owned { chunks, block_groups, }, - }) + }; + blob_metadata.validate()?; + Ok(blob_metadata) } - fn load_inner(path: &Path, check_crc32: bool) -> Result { + /// Read blob metadata from a file (mmap-backed), optionally verifying + /// the header crc32 over the full metadata. + pub fn from_path(path: &Path, verify_crc32: bool) -> Result { let file = File::open(path) .with_context(|| format!("failed to open blob meta: {}", path.display()))?; - let file_len = file.metadata()?.len(); - if file_len < NYDUS_BLOB_METADATA_HEADER_SIZE as u64 { - return Err(Error::InvalidImage("blob meta file too small".to_string())); - } let mmap = unsafe { MmapOptions::new().map(&file) } .with_context(|| format!("failed to mmap blob meta: {}", path.display()))?; - let header = BlobMetadataHeader::from_bytes( - mmap[..NYDUS_BLOB_METADATA_HEADER_SIZE] - .try_into() - .expect("length checked"), - )?; - if file_len != header.metadata_size() { + + let Some((header_bytes, _)) = mmap.split_first_chunk::() + else { + return Err(Error::InvalidImage("blob meta file too small".to_string())); + }; + + let header = BlobMetadataHeader::from_bytes(header_bytes)?; + Self::validate_bytes(&mmap, &header, verify_crc32)?; + + let blob_metadata = Self { + header, + storage: BlobMetadataStorage::Mapped(mmap), + }; + blob_metadata.validate()?; + Ok(blob_metadata) + } + + /// Validate the cross-entry table invariants. Run by every construction + /// path, so metadata in hand is always valid. + fn validate(&self) -> Result<()> { + self.validate_chunks()?; + self.validate_block_groups() + } + + /// Every chunk must be intrinsically valid and end within the blocks + /// the block groups cover. Runs before the density checks, so the bound + /// is just the last group's end, not yet a total. + fn validate_chunks(&self) -> Result<()> { + let uncompressed_block_end = self + .block_groups() + .last() + .map(|block_group| { + block_group.uncompressed_block_offset() + + block_group.uncompressed_block_count() as u64 + }) + .unwrap_or(0); + + for (index, chunk) in self.chunks().iter().enumerate() { + chunk + .validate() + .with_context(|| format!("invalid blob meta chunk {index}"))?; + + let chunk_block_end = chunk + .uncompressed_block_offset() + .checked_add(chunk.uncompressed_block_count() as u64) + .ok_or_else(|| { + Error::Overflow(format!("blob meta chunk {index} block range overflow")) + })?; + + if chunk_block_end > uncompressed_block_end { + return Err(Error::InvalidImage(format!( + "blob meta chunk {index} exceeds the blob block range: \ + ends at block {chunk_block_end}, blob ends at block {uncompressed_block_end}" + ))); + } + } + + Ok(()) + } + + /// The block groups must tile the uncompressed address space densely + /// from block 0 (making the last group's end the blob's total size), + /// keep the uniform span the header declares (the final group may be + /// short, redirect blobs are exempt), and keep their compressed ranges + /// ordered and non-overlapping (gaps allowed). + fn validate_block_groups(&self) -> Result<()> { + let block_groups = self.block_groups(); + let block_group_block_count = self.header.block_group_block_count(); + if block_group_block_count == 0 { + return Err(Error::InvalidImage( + "blob meta block group block count must be non-zero".to_string(), + )); + } + + let is_redirect = BlobMetadataBlockGroup::has_redirect(block_groups); + let mut next_uncompressed_block_offset = 0u64; + let mut next_compressed_offset = 0u64; + for (index, block_group) in block_groups.iter().enumerate() { + block_group + .validate() + .with_context(|| format!("invalid blob meta block group {index}"))?; + if block_group.uncompressed_block_offset() != next_uncompressed_block_offset { + return Err(Error::InvalidImage(format!( + "blob meta block groups must be dense: block group {index} starts at block {}, \ + expected block {next_uncompressed_block_offset}", + block_group.uncompressed_block_offset() + ))); + } + + if !is_redirect { + match ( + index + 1 == block_groups.len(), + block_group.uncompressed_block_count(), + ) { + (false, block_count) if block_count != block_group_block_count => { + return Err(Error::InvalidImage(format!( + "blob meta block group {index} must be exactly \ + {block_group_block_count} blocks, got {block_count}" + ))); + } + (true, block_count) if block_count > block_group_block_count => { + return Err(Error::InvalidImage(format!( + "blob meta final block group {index} exceeds \ + {block_group_block_count} blocks, got {block_count}" + ))); + } + _ => {} + } + } + + if block_group.compressed_offset() < next_compressed_offset { + return Err(Error::InvalidImage(format!( + "blob meta block group {index} overlaps the previous compressed range: \ + starts at byte {}, previous ends at byte {next_compressed_offset}", + block_group.compressed_offset() + ))); + } + + next_uncompressed_block_offset = block_group + .uncompressed_block_offset() + .checked_add(block_group.uncompressed_block_count() as u64) + .ok_or_else(|| { + Error::Overflow(format!( + "blob meta block group {index} uncompressed block range overflow" + )) + })?; + + next_compressed_offset = block_group + .compressed_offset() + .checked_add(block_group.compressed_size() as u64) + .ok_or_else(|| { + Error::Overflow(format!( + "blob meta block group {index} compressed range overflow" + )) + })?; + } + + Ok(()) + } + + /// Anchor a serialized buffer against its header: the buffer must be + /// exactly the declared padded size with a zeroed tail padding, and + /// with `verify_crc32` the stored seal must match the raw incoming + /// bytes — never a re-serialization, which would zero a newer writer's + /// compat fields and reject a valid image. + fn validate_bytes(bytes: &[u8], header: &BlobMetadataHeader, verify_crc32: bool) -> Result<()> { + if bytes.len() as u64 != header.padded_size() { return Err(Error::InvalidImage(format!( - "blob meta file size mismatch: expected {}, got {}", - header.metadata_size(), - file_len + "blob meta size mismatch: expected {}, got {}", + header.padded_size(), + bytes.len() ))); } - validate_padding(&mmap, &header)?; - if check_crc32 { - validate_blob_metadata_crc32(&mmap, &header)?; + + let padding = &bytes[header.used_size() as usize..]; + if padding.iter().any(|byte| *byte != 0) { + return Err(Error::InvalidImage( + "blob meta padding must be zero".to_string(), + )); } - validate_tables( - mapped_block_groups(&mmap, &header), - mapped_chunks(&mmap, &header), - header.block_group_block_count(), - )?; - Ok(Self { - header, - blob_id: [0u8; SHA256_DIGEST_SIZE], - storage: BlobMetadataStorage::Mapped(mmap), - }) + + if verify_crc32 { + let expected_crc32 = header.crc32(); + let actual_crc32 = Self::compute_crc32(bytes); + if expected_crc32 != actual_crc32 { + return Err(Error::InvalidImage(format!( + "blob meta crc32 mismatch: expected {expected_crc32:#010x}, \ + got {actual_crc32:#010x}" + ))); + } + } + + Ok(()) + } + + /// Rebuilt metadata with every compressed offset shifted by `bias`, for + /// compressed data embedded at `bias` inside a full blob (resealed via + /// [`Self::new`]). + pub fn checked_add_compressed_offset(&self, bias: u64) -> Result { + let mut block_groups = Vec::with_capacity(self.block_group_count()); + for block_group in self.block_groups() { + block_groups.push(block_group.checked_add_compressed_offset(bias)?); + } + + Self::new( + self.compressor(), + self.chunk_block_count(), + self.chunks().to_vec(), + block_groups, + ) } /// Write the serialized metadata (header, tables, zero padding) to - /// `writer`. + /// `writer`, resealing the crc32 over the emitted bytes: metadata + /// mapped from a newer writer re-serializes with the reserved compat + /// fields zeroed, so the stored seal may not match what is written. pub fn write_to(&self, writer: &mut dyn Write) -> Result<()> { - // Reseal on write rather than emitting the stored crc32: for - // metadata mapped from a newer writer, `to_bytes` zeroes the compat - // fields in the reserved header tail, so the emitted bytes differ - // from the stored ones and need their own seal. let mut header = self.header; header.crc32 = self.compute_crc32_from_parts(); + writer.write_all(&header.to_bytes())?; for chunk in self.chunks() { chunk.write_to(writer)?; } + for block_group in self.block_groups() { block_group.write_to(writer)?; } - let padding_size = self.padding_size(); - if padding_size > 0 { - writer.write_all(&vec![0u8; padding_size])?; - } + + let padding_size = (self.padded_size() - self.header.used_size()) as usize; + writer.write_all(&[0u8; EROFS_BLOCK_SIZE as usize][..padding_size])?; Ok(()) } @@ -982,69 +1170,89 @@ impl BlobMetadata { self.write_to(&mut file)?; file.flush() .with_context(|| format!("failed to flush blob meta: {}", path.display()))?; + Ok(()) } + /// The parsed header, exactly as stored on disk. pub fn header(&self) -> &BlobMetadataHeader { &self.header } - pub fn blob_id(&self) -> &[u8; SHA256_DIGEST_SIZE] { - &self.blob_id - } - + /// Number of entries in the chunk table. pub fn chunk_count(&self) -> usize { self.header.chunk_count() as usize } + /// Number of entries in the block group table. pub fn block_group_count(&self) -> usize { self.header.block_group_count() as usize } + /// Uncompressed 4KiB blocks per chunk. pub fn chunk_block_count(&self) -> u32 { self.header.chunk_block_count() } + /// Uncompressed bytes per chunk. pub fn chunk_size(&self) -> u32 { self.header.chunk_size() } + /// The block group payload compressor. pub fn compressor(&self) -> BlobMetadataCompressor { self.header.compressor() } + /// The chunk digest algorithm. pub fn digester(&self) -> BlobMetadataDigester { self.header.digester() } + /// The chunk table: the owned vector on the write side, the mapped file + /// region reinterpreted in place on the read side (sound because the + /// entry layout is pinned and the load path validated the table's + /// offset, alignment, and bounds). pub fn chunks(&self) -> &[BlobMetadataChunk] { match &self.storage { BlobMetadataStorage::Owned { chunks, .. } => chunks, - BlobMetadataStorage::Mapped(mmap) => mapped_chunks(mmap, &self.header), + BlobMetadataStorage::Mapped(mmap) => { + let offset = self.header.chunks_offset() as usize; + let count = self.header.chunk_count() as usize; + let bytes = &mmap[offset..offset + count * size_of::()]; + unsafe { std::slice::from_raw_parts(bytes.as_ptr().cast(), count) } + } } } + /// The block group table, backed the same two ways as [`Self::chunks`]. pub fn block_groups(&self) -> &[BlobMetadataBlockGroup] { match &self.storage { BlobMetadataStorage::Owned { block_groups, .. } => block_groups, - BlobMetadataStorage::Mapped(mmap) => mapped_block_groups(mmap, &self.header), + BlobMetadataStorage::Mapped(mmap) => { + let offset = self.header.block_groups_offset() as usize; + let count = self.header.block_group_count() as usize; + let bytes = &mmap[offset..offset + count * size_of::()]; + unsafe { std::slice::from_raw_parts(bytes.as_ptr().cast(), count) } + } } } - pub fn block_group_at(&self, index: usize) -> Option<&BlobMetadataBlockGroup> { + /// The block group at `index`, `None` past the table. + pub fn block_group(&self, index: usize) -> Option<&BlobMetadataBlockGroup> { self.block_groups().get(index) } - /// True when this blob is an "ondemand" redirect blob: its block groups - /// carry data belonging to other source blob devices. - pub fn is_redirect_blob(&self) -> bool { - self.block_groups() - .iter() - .any(BlobMetadataBlockGroup::is_redirect) + /// Whether any block group redirects to another source blob (an + /// ondemand redirect blob). + pub fn is_redirect(&self) -> bool { + BlobMetadataBlockGroup::has_redirect(self.block_groups()) } - /// Total number of uncompressed blocks in the dense address space. - pub fn total_blocks(&self) -> u64 { + /// Total uncompressed size of the blob in 4KiB blocks: block groups are + /// validated dense from block 0, so the last group's end offset is the + /// block count. + pub fn uncompressed_block_count(&self) -> u64 { self.block_groups() .last() .map(|block_group| { @@ -1054,323 +1262,96 @@ impl BlobMetadata { .unwrap_or(0) } - /// O(1) mapping from an uncompressed byte offset in the dense address - /// space to the index of the block group that contains it, or `None` - /// when the offset is past the end of the blob. Block groups are formed - /// by packing blocks up to the block group size independent of chunk - /// boundaries, so every block group except the last is exactly - /// `1 << block_group_block_bits` blocks and the block group index is a - /// single shift. - pub fn block_group_index_for_offset(&self, offset: u64) -> Option { - let block = offset / EROFS_BLOCK_SIZE as u64; - if block >= self.total_blocks() { + /// The block group covering `uncompressed_offset`, `None` past the end + /// of the blob: dense fixed-size groups make this a single shift, no + /// search. + pub fn block_group_index_from_uncompressed_offset( + &self, + uncompressed_offset: u64, + ) -> Option { + let block = uncompressed_offset / EROFS_BLOCK_SIZE as u64; + if block >= self.uncompressed_block_count() { return None; } - usize::try_from(block >> self.header.block_group_block_bits()).ok() + + usize::try_from(block >> self.header.block_group_block_count_bits).ok() + } + + /// Total uncompressed byte size of the blob: block groups are validated + /// dense from offset 0, so the last group's end offset is the size. + pub fn uncompressed_size(&self) -> u64 { + self.block_groups() + .last() + .map(|block_group| block_group.uncompressed_offset() + block_group.uncompressed_size()) + .unwrap_or(0) } - pub fn total_uncompressed_size(&self) -> u64 { - block_groups_total_uncompressed_size(self.block_groups()) + /// End of the last block group's compressed range: the compressed data + /// region's byte size when payloads pack from offset 0 without gaps + /// (the standalone layout), otherwise just an end coordinate (gaps and + /// bias shifts are legal on the compressed side). + pub fn compressed_end(&self) -> u64 { + self.block_groups() + .last() + .map(|block_group| { + block_group.compressed_offset() + block_group.compressed_size() as u64 + }) + .unwrap_or(0) } - pub fn total_compressed_size(&self) -> u64 { - block_groups_total_compressed_size(self.block_groups()) + /// The full serialized size, 4KiB aligned. + pub fn padded_size(&self) -> u64 { + self.header.padded_size() } - pub fn metadata_size(&self) -> u64 { - self.header.metadata_size() + /// crc32c over a serialized buffer with the header's crc32 field + /// treated as zero: what the read side verifies raw incoming bytes + /// against. + fn compute_crc32(bytes: &[u8]) -> u32 { + let (header, tail) = bytes.split_at(NYDUS_BLOB_METADATA_HEADER_SIZE); + let mut zeroed: [u8; NYDUS_BLOB_METADATA_HEADER_SIZE] = header.try_into().unwrap(); + zeroed[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD].fill(0); + crc32c_append(crc32c(&zeroed), tail) } - /// crc32c over the serialized metadata bytes with the crc32 field - /// treated as zero: the header (copied and zeroed) seeds the crc that - /// continues over the entries and padding. The reader verifies the raw - /// incoming bytes against it. - /// - /// # Panics - /// - /// Panics if `data` is shorter than the blob meta header. - fn compute_crc32(data: &[u8]) -> u32 { - let mut header: [u8; NYDUS_BLOB_METADATA_HEADER_SIZE] = data - [..NYDUS_BLOB_METADATA_HEADER_SIZE] - .try_into() - .expect("caller checked the header length"); - header[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD].fill(0); - crc32c_append(crc32c(&header), &data[NYDUS_BLOB_METADATA_HEADER_SIZE..]) - } - - /// The write-side counterpart of [`Self::compute_crc32`]: seal over the - /// serialized metadata with the crc field zeroed, streaming — the header - /// bytes seed the running crc32c that continues over the entries and - /// padding, so the metadata is never materialized as a whole. + /// The same seal computed from the in-memory parts exactly as + /// [`Self::write_to`] emits them (reserved regions zeroed): the write + /// side's view. fn compute_crc32_from_parts(&self) -> u32 { - let mut header = self.header.to_bytes(); - header[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD].fill(0); - let mut crc32 = crc32c(&header); + let mut zeroed = self.header.to_bytes(); + zeroed[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD].fill(0); + + let mut crc32 = crc32c(&zeroed); for chunk in self.chunks() { crc32 = crc32c_append(crc32, &chunk.to_bytes()); } + for block_group in self.block_groups() { crc32 = crc32c_append(crc32, &block_group.to_bytes()); } - const ZERO_BLOCK: [u8; EROFS_BLOCK_SIZE as usize] = [0u8; EROFS_BLOCK_SIZE as usize]; - let mut remaining = self.padding_size(); - while remaining > 0 { - let run = remaining.min(ZERO_BLOCK.len()); - crc32 = crc32c_append(crc32, &ZERO_BLOCK[..run]); - remaining -= run; - } - crc32 - } - - fn padding_size(&self) -> usize { - (self.metadata_size() - self.header.entries_end()) as usize - } -} - -/// Options for reading a [`BlobMetadata`], created via [`BlobMetadata::loader`]. -/// The two orthogonal knobs (CRC32 verification, attached blob id) replace -/// the previous per-combination constructors. -#[derive(Default, Clone, Copy)] -pub struct BlobMetadataLoader { - verify_crc32: bool, - blob_id: Option<[u8; SHA256_DIGEST_SIZE]>, -} - -impl BlobMetadataLoader { - /// Verify the header CRC32 over the full metadata during the read. - pub fn verify_crc32(mut self) -> Self { - self.verify_crc32 = true; - self - } - - /// Attach the owning blob id to the loaded metadata. - pub fn blob_id(mut self, blob_id: [u8; SHA256_DIGEST_SIZE]) -> Self { - self.blob_id = Some(blob_id); - self - } - - /// Read blob metadata from a file (mmap-backed). - pub fn load(self, path: &Path) -> Result { - let mut blob_metadata = BlobMetadata::load_inner(path, self.verify_crc32)?; - if let Some(blob_id) = self.blob_id { - blob_metadata.blob_id = blob_id; - } - Ok(blob_metadata) - } - /// Read blob metadata from an in-memory byte slice. - pub fn from_bytes(self, data: &[u8]) -> Result { - BlobMetadata::from_bytes_inner( - data, - self.blob_id.unwrap_or([0u8; SHA256_DIGEST_SIZE]), - self.verify_crc32, - ) + let padding_size = (self.padded_size() - self.header.used_size()) as usize; + crc32c_append(crc32, &[0u8; EROFS_BLOCK_SIZE as usize][..padding_size]) } } -fn block_count_to_bits(blocks: u32, what: &str) -> Result { - if blocks == 0 { - return Err(Error::InvalidImage(format!( - "blob meta {what} block count must be non-zero" - ))); - } +/// Encode a power-of-two 4KiB block count as the log2 stored in the +/// header's `*_block_count_bits` fields. +fn block_count_to_bits(blocks: u32) -> Result { if !blocks.is_power_of_two() { return Err(Error::InvalidImage(format!( - "blob meta {what} block count must be a power of two" + "blob meta block count must be a non-zero power of two: {blocks}" ))); } - let bits = blocks.trailing_zeros() as u8; - if bits > NYDUS_BLOB_METADATA_MAX_BLOCK_BITS { - return Err(Error::InvalidImage(format!( - "blob meta {what} block count too large: {blocks}" - ))); - } - Ok(bits) -} - -/// Infer the per-block group block-count exponent from the block group table. -/// -/// - A redirect (ondemand) blob copies block groups of arbitrary sizes from -/// its source blobs and never uses the block-to-block group mapping, so it -/// keeps the default exponent. -/// - A single-block group blob's only block group is also its (possibly -/// short) tail, so the exponent is the next power of two covering it: -/// every block then shifts to block group index 0. -/// - Otherwise the first block group is a full block group and must be a -/// power of two. -fn infer_block_group_block_bits(block_groups: &[BlobMetadataBlockGroup]) -> Result { - let default_bits = NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_BLOCK_COUNT.trailing_zeros() as u8; - if block_groups.is_empty() || block_groups.iter().any(BlobMetadataBlockGroup::is_redirect) { - return Ok(default_bits); - } - if block_groups.len() == 1 { - let covering = block_groups[0] - .uncompressed_block_count() - .next_power_of_two(); - return block_count_to_bits(covering, "block group"); - } - block_count_to_bits(block_groups[0].uncompressed_block_count(), "block group") -} - -fn validate_padding(data: &[u8], header: &BlobMetadataHeader) -> Result<()> { - let padding_start = header.entries_end() as usize; - if data[padding_start..].iter().any(|byte| *byte != 0) { - return Err(Error::InvalidImage( - "blob meta padding must be zero".to_string(), - )); - } - Ok(()) -} -fn validate_blob_metadata_crc32(data: &[u8], header: &BlobMetadataHeader) -> Result<()> { - let computed = BlobMetadata::compute_crc32(data); - if computed != header.crc32() { + let bits = blocks.ilog2() as u8; + if bits > NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS { return Err(Error::InvalidImage(format!( - "blob meta header crc32 mismatch: stored {:#010x}, computed {:#010x}", - header.crc32(), - computed + "blob meta block count too large: {blocks}" ))); } - Ok(()) -} - -fn validate_tables( - block_groups: &[BlobMetadataBlockGroup], - chunks: &[BlobMetadataChunk], - block_group_block_count: u32, -) -> Result<()> { - validate_block_groups(block_groups, block_group_block_count)?; - validate_chunks(block_groups, chunks) -} - -fn validate_block_groups( - block_groups: &[BlobMetadataBlockGroup], - block_group_block_count: u32, -) -> Result<()> { - if block_group_block_count == 0 { - return Err(Error::InvalidImage( - "blob meta block group block count must be non-zero".to_string(), - )); - } - // Redirect blobs copy block groups from arbitrary source blobs, so their - // block group sizes are inherently non-uniform and - // `block_group_index_for_offset` is never used on them. Only the - // dense-layout and compressed-overlap invariants apply. - let allow_nonuniform = block_groups.iter().any(BlobMetadataBlockGroup::is_redirect); - let mut previous_uncompressed_block_end = 0u64; - let mut previous_compressed_end = 0u64; - let last_index = block_groups.len().saturating_sub(1); - for (index, block_group) in block_groups.iter().enumerate() { - block_group - .validate() - .with_context(|| format!("invalid blob meta block group {index}"))?; - if block_group.uncompressed_block_offset() != previous_uncompressed_block_end { - return Err(Error::InvalidImage(format!( - "blob meta block groups must be dense at index {index}" - ))); - } - // Block groups pack whole blocks up to the block group size - // regardless of chunk boundaries, so every block group but the last - // holds exactly `block_group_block_count` blocks and the last holds - // at most that many. - if !allow_nonuniform { - if index < last_index { - if block_group.uncompressed_block_count() != block_group_block_count { - return Err(Error::InvalidImage(format!( - "blob meta block group {index} must be exactly {block_group_block_count} blocks, got {}", - block_group.uncompressed_block_count() - ))); - } - } else if block_group.uncompressed_block_count() > block_group_block_count { - return Err(Error::InvalidImage(format!( - "blob meta final block group {index} exceeds {block_group_block_count} blocks, got {}", - block_group.uncompressed_block_count() - ))); - } - } - // Encoded payloads are packed back-to-back in the data region, so - // each block group must start at or after the previous block group's - // byte end. No block alignment is required between compressed block - // groups. - if index > 0 && block_group.compressed_offset() < previous_compressed_end { - return Err(Error::InvalidImage(format!( - "blob meta block groups overlap compressed ranges at index {index}" - ))); - } - previous_uncompressed_block_end = block_group - .uncompressed_block_offset() - .checked_add(block_group.uncompressed_block_count() as u64) - .ok_or_else(|| { - Error::Overflow( - "blob meta block group uncompressed block range overflow".to_string(), - ) - })?; - previous_compressed_end = - block_group.compressed_offset() + block_group.compressed_size() as u64; - } - Ok(()) -} - -fn validate_chunks( - block_groups: &[BlobMetadataBlockGroup], - chunks: &[BlobMetadataChunk], -) -> Result<()> { - let total_blocks = block_groups - .last() - .map(|block_group| { - block_group.uncompressed_block_offset() + block_group.uncompressed_block_count() as u64 - }) - .unwrap_or(0); - for (index, chunk) in chunks.iter().enumerate() { - chunk - .validate() - .with_context(|| format!("invalid blob meta chunk {index}"))?; - // Chunks are independent of block groups; they only need to point at - // a valid block range inside the dense uncompressed address space. - let chunk_end = chunk - .uncompressed_block_offset() - .checked_add(chunk.uncompressed_block_count() as u64) - .ok_or_else(|| Error::Overflow("blob meta chunk block range overflow".to_string()))?; - if chunk_end > total_blocks { - return Err(Error::InvalidImage(format!( - "blob meta chunk {index} exceeds the blob block range" - ))); - } - } - Ok(()) -} - -fn block_groups_total_uncompressed_size(block_groups: &[BlobMetadataBlockGroup]) -> u64 { - block_groups - .last() - .map(|block_group| block_group.uncompressed_offset() + block_group.uncompressed_size()) - .unwrap_or(0) -} - -fn block_groups_total_compressed_size(block_groups: &[BlobMetadataBlockGroup]) -> u64 { - block_groups - .last() - .map(|block_group| block_group.compressed_offset() + block_group.compressed_size() as u64) - .unwrap_or(0) -} -fn mapped_chunks<'a>(data: &'a [u8], header: &BlobMetadataHeader) -> &'a [BlobMetadataChunk] { - let offset = header.chunks_offset() as usize; - let byte_len = header.chunk_count() as usize * size_of::(); - let bytes = &data[offset..offset + byte_len]; - let ptr = bytes.as_ptr().cast::(); - unsafe { std::slice::from_raw_parts(ptr, header.chunk_count() as usize) } -} - -fn mapped_block_groups<'a>( - data: &'a [u8], - header: &BlobMetadataHeader, -) -> &'a [BlobMetadataBlockGroup] { - let offset = header.block_groups_offset() as usize; - let byte_len = header.block_group_count() as usize * size_of::(); - let bytes = &data[offset..offset + byte_len]; - let ptr = bytes.as_ptr().cast::(); - unsafe { std::slice::from_raw_parts(ptr, header.block_group_count() as usize) } + Ok(bits) } #[cfg(test)] @@ -1382,16 +1363,20 @@ mod tests { *blake3::hash(bytes).as_bytes() } + fn chunk(payload: &[u8], block_offset: u64, block_count: u32) -> BlobMetadataChunk { + BlobMetadataChunk::new(digest(payload), block_offset, block_count).unwrap() + } + fn block_group( - uncompressed_block_offset: u64, - uncompressed_block_count: u32, + block_offset: u64, + block_count: u32, compressed_offset: u64, compressed_size: u32, payload: &[u8], ) -> BlobMetadataBlockGroup { BlobMetadataBlockGroup::new( - uncompressed_block_offset, - uncompressed_block_count, + block_offset, + block_count, compressed_offset, compressed_size, crc32c::crc32c(payload), @@ -1399,74 +1384,107 @@ mod tests { .unwrap() } - fn chunk( - payload: &[u8], - uncompressed_block_offset: u64, - uncompressed_block_count: u32, - ) -> BlobMetadataChunk { - BlobMetadataChunk::new( - digest(payload), - uncompressed_block_offset, - uncompressed_block_count, - ) - .unwrap() + fn build( + chunks: Vec, + block_groups: Vec, + ) -> Result { + BlobMetadata::new(BlobMetadataCompressor::None, 1, chunks, block_groups) } - /// The smallest interesting metadata: one single-block chunk in one - /// block group. - fn minimal_blob_metadata() -> BlobMetadata { + fn blob_metadata() -> BlobMetadata { let payload = vec![0x33; EROFS_BLOCK_SIZE as usize]; - BlobMetadata::from_parts( - [0x7bu8; SHA256_DIGEST_SIZE], + BlobMetadata::new( + BlobMetadataCompressor::None, 1, - vec![block_group(0, 1, 0, 4096, &payload)], vec![chunk(&payload, 0, 1)], + vec![block_group(0, 1, 0, EROFS_BLOCK_SIZE, &payload)], ) .unwrap() } fn sealed_metadata() -> Vec { let mut raw = Vec::new(); - minimal_blob_metadata().write_to(&mut raw).unwrap(); + blob_metadata().write_to(&mut raw).unwrap(); raw } #[test] - fn round_trips_through_mmap() { + fn accessors_expose_the_sealed_tables() { + let blob_metadata = blob_metadata(); + let header = blob_metadata.header(); + + assert_eq!(header.version(), NYDUS_BLOB_METADATA_VERSION); + assert_eq!(header.compressor(), BlobMetadataCompressor::None); + assert_eq!(header.digester(), BlobMetadataDigester::Blake3); + assert_eq!(header.chunks_offset(), 4096); + assert_eq!(header.chunk_table_size(), 48); + assert_eq!(header.block_groups_offset(), 4144); + assert_eq!(header.block_group_table_size(), 40); + assert_eq!(header.used_size(), 4184); + assert_eq!(header.padded_size(), 8192); + assert_eq!(header.chunk_block_count(), 1); + assert_eq!(header.chunk_size(), EROFS_BLOCK_SIZE); + assert_eq!(header.block_group_block_count(), 1); + assert_ne!(header.crc32(), 0); + + assert_eq!(blob_metadata.chunk_count(), 1); + assert_eq!(blob_metadata.block_group_count(), 1); + assert_eq!(blob_metadata.chunk_block_count(), 1); + assert_eq!(blob_metadata.chunk_size(), EROFS_BLOCK_SIZE); + assert_eq!(blob_metadata.compressor(), BlobMetadataCompressor::None); + assert_eq!(blob_metadata.digester(), BlobMetadataDigester::Blake3); + assert!(!blob_metadata.is_redirect()); + assert_eq!(blob_metadata.uncompressed_block_count(), 1); + assert_eq!(blob_metadata.uncompressed_size(), 4096); + assert_eq!(blob_metadata.compressed_end(), 4096); + assert_eq!(blob_metadata.padded_size(), 8192); + + let chunk = &blob_metadata.chunks()[0]; + assert_eq!(chunk.uncompressed_block_offset(), 0); + assert_eq!(chunk.uncompressed_block_count(), 1); + assert_eq!(chunk.uncompressed_offset(), 0); + assert_eq!(chunk.uncompressed_size(), 4096); + + let block_group = blob_metadata.block_group(0).unwrap(); + assert_eq!(block_group.uncompressed_block_offset(), 0); + assert_eq!(block_group.uncompressed_block_count(), 1); + assert_eq!(block_group.uncompressed_offset(), 0); + assert_eq!(block_group.uncompressed_size(), 4096); + assert_eq!(block_group.compressed_offset(), 0); + assert_eq!(block_group.compressed_size(), EROFS_BLOCK_SIZE); + assert!(blob_metadata.block_group(1).is_none()); + } + + #[test] + fn round_trips_through_a_sidecar_file() { let dir = tempdir().unwrap(); let path = dir.path().join("blob.meta"); - let blob_id = [0x5au8; SHA256_DIGEST_SIZE]; let payload_a = vec![0x11; EROFS_BLOCK_SIZE as usize]; let payload_b = vec![0x22; EROFS_BLOCK_SIZE as usize]; - let block_group_payload = [payload_a.as_slice(), payload_b.as_slice()].concat(); - let blob_metadata = BlobMetadata::from_parts( - blob_id, + let both = [payload_a.as_slice(), payload_b.as_slice()].concat(); + let blob_metadata = BlobMetadata::new( + BlobMetadataCompressor::None, 1, - vec![block_group(0, 2, 8192, 8192, &block_group_payload)], vec![chunk(&payload_a, 0, 1), chunk(&payload_b, 1, 1)], + vec![block_group(0, 2, 8192, 8192, &both)], ) .unwrap(); - blob_metadata.save(&path).unwrap(); - let loaded = BlobMetadata::load(&path).unwrap(); - - assert_eq!(loaded.header().chunk_count(), 2); - assert_eq!(loaded.header().block_group_count(), 1); - assert_eq!(loaded.header().version(), NYDUS_BLOB_METADATA_VERSION); - assert_eq!(loaded.header().chunk_bytes(), 96); - assert_eq!(loaded.header().block_group_bytes(), 40); - assert_eq!(loaded.header().entries_end(), 4096 + 96 + 40); - assert_eq!(loaded.header().metadata_size(), 8192); - assert_eq!(loaded.header().chunk_size(), EROFS_BLOCK_SIZE); + + let loaded = BlobMetadata::from_path(&path, false).unwrap(); + assert_eq!(loaded.chunk_count(), 2); + assert_eq!(loaded.block_group_count(), 1); assert_eq!(loaded.header().block_group_block_count(), 2); - assert_eq!(loaded.header().compressor(), BlobMetadataCompressor::None); - assert_eq!(loaded.header().digester(), BlobMetadataDigester::Blake3); - assert_ne!(loaded.header().crc32(), 0); - assert_eq!(loaded.block_groups()[0].compressed_offset(), 8192); assert_eq!(loaded.chunks()[1].digest(), &digest(&payload_b)); assert_eq!(loaded.chunks()[1].uncompressed_block_offset(), 1); - assert_eq!(loaded.block_group_index_for_offset(4096), Some(0)); - assert_eq!(loaded.total_uncompressed_size(), 8192); + assert_eq!(loaded.block_groups()[0].compressed_offset(), 8192); + assert_eq!( + loaded.block_group_index_from_uncompressed_offset(4096), + Some(0) + ); + assert_eq!(loaded.uncompressed_size(), 8192); + + BlobMetadata::from_path(&path, true).unwrap(); } #[test] @@ -1494,38 +1512,21 @@ mod tests { .unwrap(), ); - let loaded = BlobMetadata::loader().from_bytes(&raw).unwrap(); - + let loaded = BlobMetadata::from_bytes(&raw, false).unwrap(); assert_eq!(loaded.header().crc32(), corrupted_crc32); - let err = match BlobMetadata::loader().verify_crc32().from_bytes(&raw) { - Ok(_) => panic!("corrupted blob meta crc32 should be rejected"), - Err(err) => err, - }; + + let err = BlobMetadata::from_bytes(&raw, true).unwrap_err(); assert!(err.to_string().contains("crc32"), "{err}"); } #[test] - fn legacy_magics_reject() { - let dir = tempdir().unwrap(); - - // Legacy magics from earlier format generations must all be rejected: - // the old nydus compression-context magic and the v0 u32 "LPBM" magic - // (which serialized as "MBPL" on disk). - for (name, magic) in [ - ("nydus.blob.meta", 0xb10b_b10bu32), - ("v0.blob.meta", 0x4c50_424du32), - ] { - let path = dir.path().join(name); - let mut raw = vec![0u8; NYDUS_BLOB_METADATA_HEADER_SIZE]; - raw[..4].copy_from_slice(&magic.to_le_bytes()); - std::fs::write(&path, raw).unwrap(); + fn the_reserved_tail_is_ignored_but_fails_the_crc32_check() { + let mut raw = sealed_metadata(); + raw[NYDUS_BLOB_METADATA_HEADER_SIZE - 1] = 0xff; - let err = match BlobMetadata::load(&path) { - Ok(_) => panic!("{name}: legacy magic should be rejected"), - Err(err) => err, - }; - assert!(err.to_string().contains("magic"), "{name}: {err}"); - } + BlobMetadata::from_bytes(&raw, false).unwrap(); + let err = BlobMetadata::from_bytes(&raw, true).unwrap_err(); + assert!(err.to_string().contains("crc32"), "{err}"); } #[test] @@ -1562,9 +1563,7 @@ mod tests { let mut raw = sealed_metadata(); raw[offset..offset + 4].copy_from_slice(&value); - // The unchecked read applies only the compat rules; the crc32 - // seal is a separate, opt-in check. - let result = BlobMetadata::loader().from_bytes(&raw); + let result = BlobMetadata::from_bytes(&raw, false); match expected_err { None => { result.unwrap_or_else(|err| panic!("{case}: {err}")); @@ -1579,32 +1578,58 @@ mod tests { } } - // A future format generation is readable and preserved verbatim: - // version is informational. let mut future = sealed_metadata(); future[8..12].copy_from_slice(&(NYDUS_BLOB_METADATA_VERSION + 1).to_le_bytes()); - let loaded = BlobMetadata::loader().from_bytes(&future).unwrap(); + let loaded = BlobMetadata::from_bytes(&future, false).unwrap(); assert_eq!(loaded.header().version(), NYDUS_BLOB_METADATA_VERSION + 1); } #[test] - fn the_reserved_tail_is_ignored_but_fails_the_crc32_check() { - // Poke a byte inside the reserved header tail (between the last field - // and the end of the 4 KiB header block): a future writer may place - // compat fields there, so the unchecked read must ignore it — while - // the crc-checked read still flags it, since this file's crc was - // sealed over a zero tail. + fn legacy_magics_reject() { + let dir = tempdir().unwrap(); + + for (name, magic) in [ + ("nydus.blob.meta", 0xb10b_b10bu32), + ("v0.blob.meta", 0x4c50_424du32), + ] { + let path = dir.path().join(name); + let mut raw = vec![0u8; NYDUS_BLOB_METADATA_HEADER_SIZE]; + raw[..4].copy_from_slice(&magic.to_le_bytes()); + std::fs::write(&path, raw).unwrap(); + + let err = match BlobMetadata::from_path(&path, false) { + Ok(_) => panic!("{name}: legacy magic should be rejected"), + Err(err) => err, + }; + assert!(err.to_string().contains("magic"), "{name}: {err}"); + } + } + + #[test] + fn undersized_inputs_reject() { + let raw = sealed_metadata(); + + let err = BlobMetadata::from_bytes(&raw[..10], false).unwrap_err(); + assert!(err.to_string().contains("too small"), "{err}"); + + let err = BlobMetadata::from_bytes(&raw[..raw.len() - 1], false).unwrap_err(); + assert!(err.to_string().contains("size mismatch"), "{err}"); + + let dir = tempdir().unwrap(); + let path = dir.path().join("short.blob.meta"); + std::fs::write(&path, &raw[..10]).unwrap(); + let err = BlobMetadata::from_path(&path, false).unwrap_err(); + assert!(err.to_string().contains("too small"), "{err}"); + } + + #[test] + fn nonzero_tail_padding_rejects() { let mut raw = sealed_metadata(); - raw[NYDUS_BLOB_METADATA_HEADER_SIZE - 1] = 0xff; + let used_size = blob_metadata().header().used_size() as usize; + raw[used_size] = 0xff; - BlobMetadata::loader() - .from_bytes(&raw) - .expect("nonzero reserved tail must be ignored"); - let err = match BlobMetadata::loader().verify_crc32().from_bytes(&raw) { - Ok(_) => panic!("crc check should catch the unsealed tail change"), - Err(err) => err, - }; - assert!(err.to_string().contains("crc32"), "{err}"); + let err = BlobMetadata::from_bytes(&raw, false).unwrap_err(); + assert!(err.to_string().contains("padding must be zero"), "{err}"); } #[test] @@ -1640,9 +1665,9 @@ mod tests { let err = BlobMetadataChunk::new([0u8; 32], 0, 0).unwrap_err(); assert!(err.to_string().contains("must be non-zero"), "{err}"); - // Two invariants only raw bytes can violate — the constructors - // cannot express a source block group index without a source blob, - // nor a dirty reserved field. + let err = BlobMetadataChunk::new([0u8; 32], u64::MAX, 1).unwrap_err(); + assert!(err.to_string().contains("overflow"), "{err}"); + let valid = BlobMetadataBlockGroup::new(0, 1, 0, 4096, 0) .unwrap() .to_bytes(); @@ -1667,16 +1692,54 @@ mod tests { } #[test] - fn block_group_index_for_offset_maps_constant_sized_block_groups_by_division() { - // Block groups pack blocks up to the block group size, so every block - // group but the last holds exactly `block_group_block_count` blocks - // (2 here) and the index is a single division. Chunk boundaries are - // irrelevant to this mapping. + fn a_chunk_past_the_block_groups_rejects() { + let one = vec![0x11; EROFS_BLOCK_SIZE as usize]; + let err = build( + vec![chunk(&one, 1, 1)], + vec![block_group(0, 1, 0, EROFS_BLOCK_SIZE, &one)], + ) + .unwrap_err(); + + assert!( + err.to_string().contains("exceeds the blob block range"), + "{err}" + ); + } + + #[test] + fn block_groups_must_be_dense_from_block_zero() { + let one = vec![0x11; EROFS_BLOCK_SIZE as usize]; + let err = build( + vec![chunk(&one, 0, 1)], + vec![block_group(1, 1, 0, EROFS_BLOCK_SIZE, &one)], + ) + .unwrap_err(); + assert!(err.to_string().contains("dense"), "{err}"); + + let two = vec![0x22; 2 * EROFS_BLOCK_SIZE as usize]; + let err = build( + vec![chunk(&two, 0, 2)], + vec![ + block_group(0, 2, 0, 2 * EROFS_BLOCK_SIZE, &two), + block_group( + 3, + 2, + 2 * EROFS_BLOCK_SIZE as u64, + 2 * EROFS_BLOCK_SIZE, + &two, + ), + ], + ) + .unwrap_err(); + assert!(err.to_string().contains("dense"), "{err}"); + } + + #[test] + fn block_group_index_from_uncompressed_offset_maps_by_division() { let two = vec![0x11; 2 * EROFS_BLOCK_SIZE as usize]; let one = vec![0x22; EROFS_BLOCK_SIZE as usize]; - let blob_metadata = BlobMetadata::from_parts( - [0u8; SHA256_DIGEST_SIZE], - 1, + let blob_metadata = build( + vec![chunk(&two, 0, 2), chunk(&two, 2, 2), chunk(&one, 4, 1)], vec![ block_group(0, 2, 0, 2 * EROFS_BLOCK_SIZE, &two), block_group( @@ -1688,36 +1751,27 @@ mod tests { ), block_group(4, 1, 4 * EROFS_BLOCK_SIZE as u64, EROFS_BLOCK_SIZE, &one), ], - vec![chunk(&two, 0, 2), chunk(&two, 2, 2), chunk(&one, 4, 1)], ) .unwrap(); - assert_eq!(blob_metadata.header().block_group_block_count(), 2); + let block = EROFS_BLOCK_SIZE as u64; - assert_eq!(blob_metadata.block_group_index_for_offset(0), Some(0)); - assert_eq!( - blob_metadata.block_group_index_for_offset(2 * block - 1), - Some(0) - ); - assert_eq!( - blob_metadata.block_group_index_for_offset(2 * block), - Some(1) - ); - assert_eq!( - blob_metadata.block_group_index_for_offset(4 * block - 1), - Some(1) - ); - // The short final block group still maps by division. - assert_eq!( - blob_metadata.block_group_index_for_offset(4 * block), - Some(2) - ); - assert_eq!( - blob_metadata.block_group_index_for_offset(5 * block - 1), - Some(2) - ); - // Past the end of the blob. - assert_eq!(blob_metadata.block_group_index_for_offset(5 * block), None); + let cases = [ + (0, Some(0)), + (2 * block - 1, Some(0)), + (2 * block, Some(1)), + (4 * block - 1, Some(1)), + (4 * block, Some(2)), + (5 * block - 1, Some(2)), + (5 * block, None), + ]; + for (offset, expected) in cases { + assert_eq!( + blob_metadata.block_group_index_from_uncompressed_offset(offset), + expected, + "offset {offset}" + ); + } } #[test] @@ -1725,12 +1779,9 @@ mod tests { let two = vec![0x11; 2 * EROFS_BLOCK_SIZE as usize]; let three = vec![0x22; 3 * EROFS_BLOCK_SIZE as usize]; let one = vec![0x33; EROFS_BLOCK_SIZE as usize]; - // The first block group fixes the block group block count (2). The - // middle block group is a non-final block group of 3 blocks, which - // must be rejected. - let err = match BlobMetadata::from_parts( - [0u8; SHA256_DIGEST_SIZE], - 1, + + let err = build( + vec![chunk(&two, 0, 2), chunk(&three, 2, 3), chunk(&one, 5, 1)], vec![ block_group(0, 2, 0, 2 * EROFS_BLOCK_SIZE, &two), block_group( @@ -1742,61 +1793,62 @@ mod tests { ), block_group(5, 1, 5 * EROFS_BLOCK_SIZE as u64, EROFS_BLOCK_SIZE, &one), ], - vec![chunk(&two, 0, 2), chunk(&three, 2, 3), chunk(&one, 5, 1)], - ) { - Ok(_) => panic!("non-uniform block group sizes should be rejected"), - Err(err) => err, - }; - + ) + .unwrap_err(); assert!(err.to_string().contains("must be exactly"), "{err}"); + + let err = build( + vec![chunk(&two, 0, 2), chunk(&three, 2, 3)], + vec![ + block_group(0, 2, 0, 2 * EROFS_BLOCK_SIZE, &two), + block_group( + 2, + 3, + 2 * EROFS_BLOCK_SIZE as u64, + 3 * EROFS_BLOCK_SIZE, + &three, + ), + ], + ) + .unwrap_err(); + assert!(err.to_string().contains("exceeds"), "{err}"); } #[test] - fn single_block_group_blob_uses_covering_power_of_two_exponent() { - // A lone block group is also the (possibly short) tail, so its block - // count may be any value — 3 here. The header stores the covering - // exponent (4 blocks -> bits 2) so every block still shifts to block - // group index 0. + fn a_single_block_group_uses_a_covering_power_of_two_exponent() { let three = vec![0x44; 3 * EROFS_BLOCK_SIZE as usize]; - let blob_metadata = BlobMetadata::from_parts( - [0u8; SHA256_DIGEST_SIZE], - 1, - vec![block_group(0, 3, 0, 3 * EROFS_BLOCK_SIZE, &three)], + let blob_metadata = build( vec![chunk(&three, 0, 3)], + vec![block_group(0, 3, 0, 3 * EROFS_BLOCK_SIZE, &three)], ) .unwrap(); - assert_eq!(blob_metadata.header().block_group_block_bits(), 2); assert_eq!(blob_metadata.header().block_group_block_count(), 4); let block = EROFS_BLOCK_SIZE as u64; for index in 0..3u64 { assert_eq!( - blob_metadata.block_group_index_for_offset(index * block), + blob_metadata.block_group_index_from_uncompressed_offset(index * block), Some(0) ); } - assert_eq!(blob_metadata.block_group_index_for_offset(3 * block), None); + assert_eq!( + blob_metadata.block_group_index_from_uncompressed_offset(3 * block), + None + ); } #[test] - fn multi_block_group_blob_requires_power_of_two_full_block_groups() { - // With more than one block group the first is a full block group and - // defines the exponent, so a non-power-of-two size (3 blocks) cannot - // be encoded. + fn multi_block_group_blobs_require_power_of_two_full_block_groups() { let three = vec![0x55; 3 * EROFS_BLOCK_SIZE as usize]; let one = vec![0x66; EROFS_BLOCK_SIZE as usize]; - let err = match BlobMetadata::from_parts( - [0u8; SHA256_DIGEST_SIZE], - 1, + let err = build( + vec![chunk(&three, 0, 3), chunk(&one, 3, 1)], vec![ block_group(0, 3, 0, 3 * EROFS_BLOCK_SIZE, &three), block_group(3, 1, 3 * EROFS_BLOCK_SIZE as u64, EROFS_BLOCK_SIZE, &one), ], - vec![chunk(&three, 0, 3), chunk(&one, 3, 1)], - ) { - Ok(_) => panic!("non-power-of-two full block group should be rejected"), - Err(err) => err, - }; + ) + .unwrap_err(); assert!(err.to_string().contains("power of two"), "{err}"); } @@ -1804,47 +1856,48 @@ mod tests { #[test] fn packed_compressed_offsets_need_no_block_alignment() { let two = vec![0x11; 2 * EROFS_BLOCK_SIZE as usize]; - // Block group 1 starts exactly at block group 0's compressed byte end - // (5000), which is deliberately not block aligned: compressed block - // groups pack back-to-back. - let blob_metadata = BlobMetadata::from_parts( - [0u8; SHA256_DIGEST_SIZE], - 1, + let blob_metadata = build( + vec![chunk(&two, 0, 2), chunk(&two, 2, 2)], vec![ block_group(0, 2, 0, 5000, &two), block_group(2, 2, 5000, 3000, &two), ], - vec![chunk(&two, 0, 2), chunk(&two, 2, 2)], ) .unwrap(); assert_eq!(blob_metadata.block_groups()[1].compressed_offset(), 5000); - assert_eq!(blob_metadata.total_compressed_size(), 8000); + assert_eq!(blob_metadata.compressed_end(), 8000); } #[test] fn overlapping_compressed_ranges_reject() { let two = vec![0x22; 2 * EROFS_BLOCK_SIZE as usize]; - // Block group 1 starts before block group 0's compressed byte end - // (5000) -> overlap. - let err = match BlobMetadata::from_parts( - [0u8; SHA256_DIGEST_SIZE], - 1, + let err = build( + vec![chunk(&two, 0, 2), chunk(&two, 2, 2)], vec![ block_group(0, 2, 0, 5000, &two), block_group(2, 2, 4999, 3000, &two), ], - vec![chunk(&two, 0, 2), chunk(&two, 2, 2)], - ) { - Ok(_) => panic!("overlapping compressed ranges should be rejected"), - Err(err) => err, - }; + ) + .unwrap_err(); assert!(err.to_string().contains("overlap"), "{err}"); } #[test] - fn redirect_block_group_round_trips_and_reports_source() { + fn checked_add_compressed_offset_shifts_and_reseals() { + let shifted = blob_metadata().checked_add_compressed_offset(8192).unwrap(); + + assert_eq!(shifted.block_groups()[0].compressed_offset(), 8192); + assert_eq!(shifted.uncompressed_size(), 4096); + + let mut raw = Vec::new(); + shifted.write_to(&mut raw).unwrap(); + BlobMetadata::from_bytes(&raw, true).unwrap(); + } + + #[test] + fn redirect_block_groups_round_trip_and_report_their_source() { let payload = vec![0x44; 2 * EROFS_BLOCK_SIZE as usize]; let crc32 = crc32c::crc32c(&payload); let redirect = @@ -1863,7 +1916,6 @@ mod tests { redirect ); - // Normal block groups stay non-redirect after a round trip. let normal = block_group(0, 2, 0, 2 * EROFS_BLOCK_SIZE, &payload); assert!(!normal.is_redirect()); let loaded = BlobMetadataBlockGroup::from_bytes(&normal.to_bytes()).unwrap(); @@ -1918,25 +1970,22 @@ mod tests { .unwrap(), ]; - let blob_metadata = BlobMetadata::from_parts( - [0x9du8; SHA256_DIGEST_SIZE], - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, - block_groups.clone(), + let blob_metadata = BlobMetadata::new( + BlobMetadataCompressor::None, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, Vec::new(), + block_groups.clone(), ) .unwrap(); - assert!(blob_metadata.is_redirect_blob()); - // Redirect block groups are non-uniform and never use the - // block-to-block group mapping, so the header keeps the default - // exponent. + assert!(blob_metadata.is_redirect()); assert_eq!( - blob_metadata.header().block_group_block_bits(), - NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_BLOCK_COUNT.trailing_zeros() as u8 + blob_metadata.header().block_group_block_count(), + DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_BLOCK_COUNT ); blob_metadata.save(&path).unwrap(); - let loaded = BlobMetadata::load(&path).unwrap(); - assert!(loaded.is_redirect_blob()); + let loaded = BlobMetadata::from_path(&path, false).unwrap(); + assert!(loaded.is_redirect()); assert_eq!(loaded.block_groups(), block_groups.as_slice()); assert_eq!(loaded.block_groups()[1].source_blob_index(), 2); assert_eq!(loaded.block_groups()[2].source_block_group_index(), 9); diff --git a/nydus-format/src/blob/mod.rs b/nydus-format/src/blob/mod.rs index a51cf7f64a3..fbc699df484 100644 --- a/nydus-format/src/blob/mod.rs +++ b/nydus-format/src/blob/mod.rs @@ -6,7 +6,7 @@ use crate::erofs::bytes_to_blocks; use crate::error::{Context, Error, Result}; -use crate::utils::{align_up, write_zero_padding}; +use crate::utils::{align_up_u64, write_zeros}; use std::io::Write; pub mod algorithm; @@ -18,75 +18,54 @@ pub use footer::NYDUS_BLOB_FOOTER_ALIGNMENT; pub use footer::{BlobFooter, NYDUS_BLOB_FOOTER_SIZE}; pub use metadata::{ BlobMetadata, BlobMetadataBlockGroup, BlobMetadataChunk, - NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_BLOCK_COUNT, - NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE, NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, NYDUS_BLOB_METADATA_SUFFIX, + DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_BLOCK_COUNT, + DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE, DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, NYDUS_BLOB_METADATA_SUFFIX, }; -/// Append the trailing regions of the full-blob layout +/// Finish a full blob: append the trailing regions of the layout /// `[data][pad][bootstrap][pad][blob meta][footer]` to `writer`, which must -/// already hold the `data_size` bytes of blob data. An empty `bootstrap` -/// yields the ondemand layout (no bootstrap region, zero bootstrap blocks). -/// Returns the footer describing the assembled blob. -pub fn assemble_full_blob( +/// already hold the `compressed_data_size` bytes of blob data. An empty +/// `bootstrap` yields the ondemand layout (no bootstrap region, zero +/// bootstrap blocks). Returns the footer describing the finished blob. +pub fn finish_full_blob( writer: &mut dyn Write, - data_size: u64, + compressed_data_size: u64, bootstrap: &[u8], blob_metadata: &BlobMetadata, ) -> Result { - let bootstrap_size = u64::try_from(bootstrap.len()) - .map_err(|err| Error::Overflow(format!("bootstrap exceeds u64: {err}")))?; - let bootstrap_blocks = bytes_to_blocks(bootstrap_size, "bootstrap")?; - let bootstrap_offset = align_up(data_size, NYDUS_BLOB_FOOTER_ALIGNMENT) + let bootstrap_size = bootstrap.len() as u64; + let bootstrap_offset = align_up_u64(compressed_data_size, NYDUS_BLOB_FOOTER_ALIGNMENT) .ok_or_else(|| Error::Overflow("bootstrap offset overflow".to_string()))?; - let blob_metadata_offset = align_up( - bootstrap_offset - .checked_add(bootstrap_size) - .ok_or_else(|| Error::Overflow("blob meta offset overflow".to_string()))?, - NYDUS_BLOB_FOOTER_ALIGNMENT, - ) - .ok_or_else(|| Error::Overflow("blob meta offset overflow".to_string()))?; - let blob_metadata_size = blob_metadata.metadata_size(); - let blob_metadata_blocks = bytes_to_blocks(blob_metadata_size, "blob meta")?; + let blob_metadata_offset = bootstrap_offset + .checked_add(bootstrap_size) + .and_then(|bootstrap_end| align_up_u64(bootstrap_end, NYDUS_BLOB_FOOTER_ALIGNMENT)) + .ok_or_else(|| Error::Overflow("blob meta offset overflow".to_string()))?; - let mut blob_metadata_bytes = Vec::with_capacity( - usize::try_from(blob_metadata_size) - .map_err(|err| Error::Overflow(format!("blob meta size exceeds usize: {err}")))?, - ); + write_zeros(writer, bootstrap_offset - compressed_data_size)?; + writer + .write_all(bootstrap) + .context("failed to write blob bootstrap")?; + + write_zeros( + writer, + blob_metadata_offset - bootstrap_offset - bootstrap_size, + )?; blob_metadata - .write_to(&mut blob_metadata_bytes) - .context("failed to serialize blob meta")?; - if blob_metadata_bytes.len() as u64 != blob_metadata_size { - return Err(Error::InvalidImage(format!( - "serialized blob meta size mismatch: expected {}, got {}", - blob_metadata_size, - blob_metadata_bytes.len() - ))); - } + .write_to(writer) + .context("failed to write blob meta")?; let footer = BlobFooter::new( 0, - data_size, + compressed_data_size, bootstrap_offset, - bootstrap_blocks, - blob_metadata_offset, - blob_metadata_blocks, - )?; - - write_zero_padding(writer, data_size, bootstrap_offset)?; - writer - .write_all(bootstrap) - .context("failed to write blob bootstrap")?; - write_zero_padding( - writer, - bootstrap_offset + bootstrap_size, + bytes_to_blocks(bootstrap_size, "bootstrap")?, blob_metadata_offset, + bytes_to_blocks(blob_metadata.padded_size(), "blob meta")?, )?; - writer - .write_all(&blob_metadata_bytes) - .context("failed to write blob meta")?; footer .write_to(writer) .context("failed to write blob footer")?; + Ok(footer) } diff --git a/nydus-format/src/erofs/inode.rs b/nydus-format/src/erofs/inode.rs index 10aade77167..ba4a6fd5b57 100644 --- a/nydus-format/src/erofs/inode.rs +++ b/nydus-format/src/erofs/inode.rs @@ -1,8 +1,8 @@ use std::mem; use super::*; +use crate::utils::align_up_usize; use crate::utils::le::{read_u16, read_u32, read_u64, write_u16, write_u32, write_u64}; -use crate::utils::round_up; /// EROFS on-disk inode in compact format (32 bytes). #[repr(C, packed)] @@ -435,7 +435,7 @@ pub fn erofs_xattr_ibody_size(xattrs: &[XattrEntry]) -> usize { let mut size = EROFS_XATTR_IBODY_HEADER_SIZE; for entry in xattrs { let entry_size = EROFS_XATTR_ENTRY_HEADER_SIZE + entry.suffix.len() + entry.value.len(); - size += round_up(entry_size, 4); + size += align_up_usize(entry_size, 4).expect("alignment overflowed"); } size @@ -447,7 +447,7 @@ pub fn erofs_xattr_icount(xattr_ibody_size: usize) -> u16 { if xattr_ibody_size == 0 { 0 } else { - let aligned = round_up(xattr_ibody_size, 4); + let aligned = align_up_usize(xattr_ibody_size, 4).expect("alignment overflowed"); ((aligned - 8) / 4) as u16 } } diff --git a/nydus-format/src/utils/align.rs b/nydus-format/src/utils/align.rs index b0a21429b94..00a17156b79 100644 --- a/nydus-format/src/utils/align.rs +++ b/nydus-format/src/utils/align.rs @@ -3,32 +3,38 @@ /// `align` must be a power of two. Returns `None` when the rounded value /// would overflow `u64`. #[inline] -pub fn align_up(value: u64, align: u64) -> Option { +pub fn align_up_u64(value: u64, align: u64) -> Option { debug_assert!(align.is_power_of_two()); value.checked_add(align - 1).map(|v| v & !(align - 1)) } -/// Round `val` up to the next multiple of `align` (power of two). -/// Unchecked builder-path twin of [`align_up`]; panics on overflow. +/// The `usize` twin of [`align_up_u64`] for in-memory offsets and sizes. #[inline] -pub fn round_up(val: usize, align: usize) -> usize { - align_up(val as u64, align as u64).expect("size rounding overflowed") as usize +pub fn align_up_usize(value: usize, align: usize) -> Option { + debug_assert!(align.is_power_of_two()); + value.checked_add(align - 1).map(|v| v & !(align - 1)) } #[cfg(test)] mod tests { - use super::align_up; + use super::{align_up_u64, align_up_usize}; + + #[test] + fn align_up_u64_rounds_up_to_alignment() { + assert_eq!(align_up_u64(0, 8), Some(0)); + assert_eq!(align_up_u64(1, 8), Some(8)); + assert_eq!(align_up_u64(16, 8), Some(16)); + assert_eq!(align_up_u64(4097, 4096), Some(8192)); + } #[test] - fn align_up_rounds_up_to_alignment() { - assert_eq!(align_up(0, 8), Some(0)); - assert_eq!(align_up(1, 8), Some(8)); - assert_eq!(align_up(16, 8), Some(16)); - assert_eq!(align_up(4097, 4096), Some(8192)); + fn align_up_u64_detects_overflow() { + assert_eq!(align_up_u64(u64::MAX, 4096), None); } #[test] - fn align_up_detects_overflow() { - assert_eq!(align_up(u64::MAX, 4096), None); + fn align_up_usize_rounds_and_detects_overflow() { + assert_eq!(align_up_usize(4097, 4096), Some(8192)); + assert_eq!(align_up_usize(usize::MAX, 4096), None); } } diff --git a/nydus-format/src/utils/io.rs b/nydus-format/src/utils/io.rs index 445f9a7ea60..a5101a9ac7d 100644 --- a/nydus-format/src/utils/io.rs +++ b/nydus-format/src/utils/io.rs @@ -1,4 +1,4 @@ -use std::io::{self, Write}; +use std::io::{self, Read, Write}; use std::os::fd::RawFd; /// Read exactly `buf.len()` bytes from `fd` at `offset` without moving the @@ -34,19 +34,9 @@ pub fn pread_exact(fd: RawFd, buf: &mut [u8], offset: u64) -> io::Result<()> { Ok(()) } -/// Write `aligned - current` zero bytes to pad a region up to its aligned -/// end. Errors when `aligned < current`. -pub fn write_zero_padding(writer: &mut dyn Write, current: u64, aligned: u64) -> io::Result<()> { - if aligned < current { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "invalid blob region alignment", - )); - } - let padding = (aligned - current) as usize; - if padding > 0 { - writer.write_all(&vec![0u8; padding])?; - } +/// Write `count` zero bytes to `writer`. +pub fn write_zeros(writer: &mut dyn Write, count: u64) -> io::Result<()> { + io::copy(&mut io::repeat(0).take(count), writer)?; Ok(()) } diff --git a/nydus-format/src/utils/mod.rs b/nydus-format/src/utils/mod.rs index 886ee230950..8564ee96cdc 100644 --- a/nydus-format/src/utils/mod.rs +++ b/nydus-format/src/utils/mod.rs @@ -10,15 +10,15 @@ use std::path::Path; use crate::blob::{BlobMetadata, NYDUS_BLOB_METADATA_SUFFIX}; use crate::erofs::{ErofsSuperblock, EROFS_SUPER_OFFSET}; -pub use self::align::{align_up, round_up}; +pub use self::align::{align_up_u64, align_up_usize}; pub use self::digest::{ hex_string, parse_sha256_hex, sha256_bytes, sha256_file, sha256_file_range, SHA256_DIGEST_SIZE, }; -pub use self::io::{pread_exact, write_zero_padding}; +pub use self::io::{pread_exact, write_zeros}; /// Assemble a minimal full blob (`payload + trivial bootstrap + blob /// meta + footer`, production layout via -/// [`crate::blob::assemble_full_blob`]) into `dir`, named by its full +/// [`crate::blob::finish_full_blob`]) into `dir`, named by its full /// SHA256, optionally with a `.blob.meta` sidecar. Returns the full /// blob id. pub fn write_minimal_full_blob( @@ -35,7 +35,7 @@ pub fn write_minimal_full_blob( let mut full_blob = Vec::new(); full_blob.write_all(payload).unwrap(); - crate::blob::assemble_full_blob( + crate::blob::finish_full_blob( &mut full_blob, payload.len() as u64, &bootstrap, diff --git a/nydus-storage/src/cache/caches.rs b/nydus-storage/src/cache/caches.rs index b7aa730386c..fb84f6b2cca 100644 --- a/nydus-storage/src/cache/caches.rs +++ b/nydus-storage/src/cache/caches.rs @@ -134,8 +134,8 @@ impl BlobCaches { /// Return whether the blob identified by `blob_index` is an "ondemand" /// redirect blob (produced by `nydus optimize`). Opens the blob cache, /// which reads the local blob meta but performs no data prefetch. - pub fn is_redirect_blob(&self, blob_index: u16) -> io::Result { - Ok(self.cache(blob_index)?.is_redirect_blob()) + pub fn is_redirect(&self, blob_index: u16) -> io::Result { + Ok(self.cache(blob_index)?.is_redirect()) } /// Prefetch every block group of the blob identified by `blob_index`. An @@ -160,7 +160,7 @@ impl BlobCaches { // never delayed by the lock. Held (via the guard's file descriptor) // until this function returns. let _prefetch_lock = cache.prefetch_lock(); - if cache.is_redirect_blob() { + if cache.is_redirect() { // Time the ondemand (redirect) blob prefetch and report how many // source block groups it warmed vs skipped, so operators can tell // whether the streaming warmup outran the workload. diff --git a/nydus-storage/src/cache/local.rs b/nydus-storage/src/cache/local.rs index de3963b5b40..8ae7121d349 100644 --- a/nydus-storage/src/cache/local.rs +++ b/nydus-storage/src/cache/local.rs @@ -14,7 +14,7 @@ use crate::access_trace::TraceRecorder; use crate::block_group_map::BlockGroupMap; use nydus_backend::{BlobBackend, ReadContext, ReadKind}; use nydus_format::blob::{ - BlobMetadata, BlobMetadataBlockGroup, NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE, + BlobMetadata, BlobMetadataBlockGroup, DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE, NYDUS_BLOB_METADATA_SUFFIX, }; use nydus_format::utils::{hex_string, SHA256_DIGEST_SIZE}; @@ -154,7 +154,7 @@ impl LocalBlobCache { .create(true) .truncate(false) .open(&cache_data_path)?; - data_file.set_len(blob_metadata.total_uncompressed_size())?; + data_file.set_len(blob_metadata.uncompressed_size())?; drop(data_file); let block_group_map = BlockGroupMap::open( @@ -215,7 +215,7 @@ impl LocalBlobCache { .truncate(false) .open(&self.cache_data_path)?, ); - file.set_len(self.blob_metadata.total_uncompressed_size())?; + file.set_len(self.blob_metadata.uncompressed_size())?; nydus_telemetry::metrics::inc_cache_opened_files(); *cache_file = Some(file.clone()); Ok(file) @@ -337,7 +337,7 @@ impl LocalBlobCache { // Redirect (ondemand) blobs have a non-uniform block group layout, so the // O(1) division-based block group lookup below does not apply; they are // consumed exclusively through `stream_redirect`. - if self.blob_metadata.is_redirect_blob() { + if self.blob_metadata.is_redirect() { return Err(io::Error::new( io::ErrorKind::Unsupported, "redirect blob has no dense readable address space", @@ -361,7 +361,7 @@ impl LocalBlobCache { for block_group_index in first_block_group..=last_block_group { let block_group = *self .blob_metadata - .block_group_at(block_group_index) + .block_group(block_group_index) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, @@ -384,13 +384,13 @@ impl LocalBlobCache { ) -> io::Result> { let first = self .blob_metadata - .block_group_index_for_offset(offset) + .block_group_index_from_uncompressed_offset(offset) .ok_or_else(|| { io::Error::new(io::ErrorKind::NotFound, "blob meta block_group not found") })?; let last = self .blob_metadata - .block_group_index_for_offset(end - 1) + .block_group_index_from_uncompressed_offset(end - 1) .ok_or_else(|| { io::Error::new(io::ErrorKind::NotFound, "blob meta block_group not found") })?; @@ -492,7 +492,7 @@ impl BlobCache for LocalBlobCache { } // Fast path: another process (or an earlier run) already decoded every // block group; skip the batch planning and per-block group readiness scan. - if !self.blob_metadata.is_redirect_blob() && self.block_group_map.is_all_ready() { + if !self.blob_metadata.is_redirect() && self.block_group_map.is_all_ready() { return Ok(()); } @@ -509,7 +509,7 @@ impl BlobCache for LocalBlobCache { for batch in plan_prefetch_batches( block_groups, - NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE as u64, + DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE as u64, ) { super::check_prefetch_deadline(deadline)?; if batch @@ -554,7 +554,7 @@ impl BlobCache for LocalBlobCache { // historical writer crash between its bit and counter updates leaves // the counter short forever; the authoritative bitmap scan inside // latch_all_ready() latches the flag regardless. - if !self.blob_metadata.is_redirect_blob() { + if !self.blob_metadata.is_redirect() { self.block_group_map.latch_all_ready(); } @@ -595,7 +595,7 @@ impl BlobCache for LocalBlobCache { } fn ready_ranges(&self, offset: u64, len: u64) -> io::Result>> { - if len == 0 || self.blob_metadata.is_redirect_blob() { + if len == 0 || self.blob_metadata.is_redirect() { return Ok(Vec::new()); } let end = offset.checked_add(len).ok_or_else(|| { @@ -609,7 +609,7 @@ impl BlobCache for LocalBlobCache { .map(|block_groups| { let first_block_group = self .blob_metadata - .block_group_at(block_groups.start) + .block_group(block_groups.start) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, @@ -618,7 +618,7 @@ impl BlobCache for LocalBlobCache { })?; let last_block_group = self .blob_metadata - .block_group_at(block_groups.end - 1) + .block_group(block_groups.end - 1) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, @@ -633,8 +633,8 @@ impl BlobCache for LocalBlobCache { .collect() } - fn is_redirect_blob(&self) -> bool { - self.blob_metadata.is_redirect_blob() + fn is_redirect(&self) -> bool { + self.blob_metadata.is_redirect() } /// Acquire the per-blob cross-process prefetch lock, blocking (in 1s @@ -683,7 +683,7 @@ impl BlobCache for LocalBlobCache { // so we can stop waiting; the caller's prefetch then reduces to a // cheap all-ready scan. A redirect blob never marks its own map, // so keep waiting for the lock and rely on batch skipping. - if !self.blob_metadata.is_redirect_blob() && self.block_group_map.latch_all_ready() { + if !self.blob_metadata.is_redirect() && self.block_group_map.latch_all_ready() { return None; } if !contention_logged { @@ -706,7 +706,7 @@ impl BlobCache for LocalBlobCache { fn is_all_ready(&self) -> bool { // A redirect blob never marks its own block_group_map (its block groups fill other // blobs' caches), so the flag is meaningless there. - !self.blob_metadata.is_redirect_blob() && self.block_group_map.is_all_ready() + !self.blob_metadata.is_redirect() && self.block_group_map.is_all_ready() } fn for_each_redirect_block_group( @@ -812,7 +812,7 @@ impl BlobCache for LocalBlobCache { ) -> io::Result<()> { let block_group = self .blob_metadata - .block_group_at(block_group_index) + .block_group(block_group_index) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, @@ -854,21 +854,13 @@ fn load_or_fetch_blob_metadata( .suffix(".tmp") .tempfile_in(cache_dir)?; backend.save_blob_metadata(&blob_id, tmp.path())?; - if let Err(err) = BlobMetadata::loader() - .verify_crc32() - .blob_id(blob_id) - .load(tmp.path()) - { + if let Err(err) = BlobMetadata::from_path(tmp.path(), true) { return Err(io::Error::other(err)); } tmp.persist(blob_metadata_path).map_err(|err| err.error)?; } - BlobMetadata::loader() - .verify_crc32() - .blob_id(blob_id) - .load(blob_metadata_path) - .map_err(io::Error::other) + BlobMetadata::from_path(blob_metadata_path, true).map_err(io::Error::other) } /// Drop guard that ensures a leader always signals its flight and cleans up @@ -914,25 +906,21 @@ fn write_all_at(file: &File, offset: u64, buf: &[u8]) -> io::Result<()> { mod tests { use super::*; use nydus_backend::Local; - use nydus_format::blob::{BlobMetadataBlockGroup, BlobMetadataChunk}; + use nydus_format::blob::{BlobMetadataBlockGroup, BlobMetadataChunk, BlobMetadataCompressor}; use nydus_format::utils::sha256_bytes; use std::path::Path; use tempfile::tempdir; - fn blob_metadata(blob_id: [u8; SHA256_DIGEST_SIZE], payload: &[u8]) -> BlobMetadata { - blob_metadata_with_crc32(blob_id, payload, crc32c::crc32c(payload)) + fn blob_metadata(payload: &[u8]) -> BlobMetadata { + blob_metadata_with_crc32(payload, crc32c::crc32c(payload)) } - fn blob_metadata_with_crc32( - blob_id: [u8; SHA256_DIGEST_SIZE], - payload: &[u8], - crc32: u32, - ) -> BlobMetadata { - BlobMetadata::from_parts( - blob_id, + fn blob_metadata_with_crc32(payload: &[u8], crc32: u32) -> BlobMetadata { + BlobMetadata::new( + BlobMetadataCompressor::None, 1, - vec![BlobMetadataBlockGroup::new(0, 1, 0, 4096, crc32).unwrap()], vec![BlobMetadataChunk::new(*blake3::hash(payload).as_bytes(), 0, 1).unwrap()], + vec![BlobMetadataBlockGroup::new(0, 1, 0, 4096, crc32).unwrap()], ) .unwrap() } @@ -982,8 +970,7 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0xceu8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); @@ -1001,8 +988,7 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0x3du8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); @@ -1040,8 +1026,7 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0x2eu8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); @@ -1078,8 +1063,7 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0x5au8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); @@ -1109,8 +1093,7 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0x21u8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); @@ -1131,8 +1114,7 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0x77u8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); @@ -1154,8 +1136,7 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0x42u8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend = CountingBackend::new(backend_dir.path()); @@ -1206,14 +1187,14 @@ mod tests { // An ondemand (redirect) blob whose single block group redirects to source // blob 1 block group 0; its data region carries a copy of the source bytes. - let redirect_meta = BlobMetadata::from_parts( - sha256_bytes(&payload), + let redirect_meta = BlobMetadata::new( + BlobMetadataCompressor::None, 1, - vec![BlobMetadataBlockGroup::new_redirect(0, 1, 0, 4096, crc32, 1, 0).unwrap()], Vec::new(), + vec![BlobMetadataBlockGroup::new_redirect(0, 1, 0, 4096, crc32, 1, 0).unwrap()], ) .unwrap(); - assert!(redirect_meta.is_redirect_blob()); + assert!(redirect_meta.is_redirect()); let redirect_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &redirect_meta, true); @@ -1262,8 +1243,7 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0xbdu8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let blob_metadata_path = backend_dir .path() @@ -1292,12 +1272,7 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0xacu8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata_with_crc32( - data_blob_id, - &payload, - crc32c::crc32c(&payload).wrapping_add(1), - ); + let meta = blob_metadata_with_crc32(&payload, crc32c::crc32c(&payload).wrapping_add(1)); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); @@ -1317,7 +1292,7 @@ mod tests { let cache_dir = tempdir().unwrap(); let payload = vec![0x3du8; 4096]; let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, false); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); @@ -1351,13 +1326,12 @@ mod tests { let backend_dir = tempdir().unwrap(); let cache_dir = tempdir().unwrap(); let payload = vec![0x6eu8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); let cached = LocalBlobCache::open(full_blob_id, 1, cache_dir.path(), backend).unwrap(); - assert!(!cached.is_redirect_blob()); + assert!(!cached.is_redirect()); // Wrong length is rejected and the block group stays not-ready. let err = cached diff --git a/nydus-storage/src/cache/mod.rs b/nydus-storage/src/cache/mod.rs index aa9135c5550..237765997c8 100644 --- a/nydus-storage/src/cache/mod.rs +++ b/nydus-storage/src/cache/mod.rs @@ -82,7 +82,7 @@ pub trait BlobCache: Send + Sync { /// True when this blob is an "ondemand" redirect blob whose block groups carry /// data belonging to other source blob devices. - fn is_redirect_blob(&self) -> bool { + fn is_redirect(&self) -> bool { false } @@ -393,7 +393,7 @@ pub fn is_block_group_crc_mismatch(err: &io::Error) -> bool { #[cfg(test)] mod tests { use super::*; - use nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE; + use nydus_format::blob::DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE; use nydus_format::erofs::EROFS_BLOCK_SIZE; fn block_group( @@ -412,7 +412,7 @@ mod tests { #[test] fn plan_prefetch_batches_keeps_one_block_group_per_window_at_default_target() { - let blocks = NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE / EROFS_BLOCK_SIZE; + let blocks = DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE / EROFS_BLOCK_SIZE; let block_groups = vec![ block_group(0, blocks), block_group(blocks as u64, blocks), @@ -420,7 +420,7 @@ mod tests { ]; let batches = plan_prefetch_batches( &block_groups, - NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE as u64, + DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE as u64, ); assert_eq!(batches, vec![0..1, 1..2, 2..3]); } diff --git a/nydus-storage/src/cache/remote.rs b/nydus-storage/src/cache/remote.rs index 6cafa9d4ade..f73684a8768 100644 --- a/nydus-storage/src/cache/remote.rs +++ b/nydus-storage/src/cache/remote.rs @@ -49,7 +49,7 @@ impl BlobCache for RemoteBlobCache { } // Redirect (ondemand) blobs have a non-uniform block group layout and no // dense readable address space, exactly as in the local cache. - if self.blob_metadata.is_redirect_blob() { + if self.blob_metadata.is_redirect() { return Err(io::Error::new( io::ErrorKind::Unsupported, "redirect blob has no dense readable address space", @@ -61,13 +61,13 @@ impl BlobCache for RemoteBlobCache { })?; let first = self .blob_metadata - .block_group_index_for_offset(offset) + .block_group_index_from_uncompressed_offset(offset) .ok_or_else(|| { io::Error::new(io::ErrorKind::NotFound, "blob meta block group not found") })?; let last = self .blob_metadata - .block_group_index_for_offset(end - 1) + .block_group_index_from_uncompressed_offset(end - 1) .ok_or_else(|| { io::Error::new(io::ErrorKind::NotFound, "blob meta block group not found") })?; @@ -76,7 +76,7 @@ impl BlobCache for RemoteBlobCache { for block_group_index in first..=last { let block_group = *self .blob_metadata - .block_group_at(block_group_index) + .block_group(block_group_index) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, @@ -111,8 +111,8 @@ impl BlobCache for RemoteBlobCache { )) } - fn is_redirect_blob(&self) -> bool { - self.blob_metadata.is_redirect_blob() + fn is_redirect(&self) -> bool { + self.blob_metadata.is_redirect() } } @@ -120,16 +120,16 @@ impl BlobCache for RemoteBlobCache { mod tests { use super::*; use nydus_backend::Local; - use nydus_format::blob::{BlobMetadataBlockGroup, BlobMetadataChunk}; - use nydus_format::utils::{sha256_bytes, write_minimal_full_blob}; + use nydus_format::blob::{BlobMetadataBlockGroup, BlobMetadataChunk, BlobMetadataCompressor}; + use nydus_format::utils::write_minimal_full_blob; use tempfile::tempdir; - fn blob_metadata(blob_id: [u8; SHA256_DIGEST_SIZE], payload: &[u8]) -> BlobMetadata { - BlobMetadata::from_parts( - blob_id, + fn blob_metadata(payload: &[u8]) -> BlobMetadata { + BlobMetadata::new( + BlobMetadataCompressor::None, 1, - vec![BlobMetadataBlockGroup::new(0, 1, 0, 4096, crc32c::crc32c(payload)).unwrap()], vec![BlobMetadataChunk::new(*blake3::hash(payload).as_bytes(), 0, 1).unwrap()], + vec![BlobMetadataBlockGroup::new(0, 1, 0, 4096, crc32c::crc32c(payload)).unwrap()], ) .unwrap() } @@ -138,8 +138,7 @@ mod tests { fn remote_blob_cache_reads_without_touching_disk() { let backend_dir = tempdir().unwrap(); let payload = vec![0xabu8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); @@ -163,8 +162,7 @@ mod tests { fn remote_blob_cache_rejects_file_oriented_operations() { let backend_dir = tempdir().unwrap(); let payload = vec![0x11u8; 4096]; - let data_blob_id = sha256_bytes(&payload); - let meta = blob_metadata(data_blob_id, &payload); + let meta = blob_metadata(&payload); let full_blob_id = write_minimal_full_blob(backend_dir.path(), &payload, &meta, true); let backend: Arc = Arc::new(Local::new(backend_dir.path().to_path_buf())); diff --git a/nydus-storage/src/prefetch.rs b/nydus-storage/src/prefetch.rs index 7d9bc20680a..5eef1c753e0 100644 --- a/nydus-storage/src/prefetch.rs +++ b/nydus-storage/src/prefetch.rs @@ -86,7 +86,7 @@ impl BlobPrefetcher { // not spent pulling whole source blobs. for blob_index in self.priority { if self.scope != PrefetchScope::All { - match self.caches.is_redirect_blob(blob_index) { + match self.caches.is_redirect(blob_index) { Ok(true) => {} Ok(false) => continue, Err(err) => { diff --git a/nydus/src/bin/nydus/build.rs b/nydus/src/bin/nydus/build.rs index f9507d38452..826167fcb5f 100644 --- a/nydus/src/bin/nydus/build.rs +++ b/nydus/src/bin/nydus/build.rs @@ -3,7 +3,8 @@ use clap::{Parser, ValueEnum}; use nydus::build::{build_image, BuildImageOptions, Image}; use nydus::error::{Context, Error, Result}; use nydus_format::blob::{ - BlobFooter, BlobMetadata, BlobMetadataCompressor, NYDUS_BLOB_METADATA_SUFFIX, + BlobFooter, BlobMetadata, BlobMetadataCompressor, DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, NYDUS_BLOB_METADATA_SUFFIX, }; use nydus_format::erofs::EROFS_BLOB_ID_SIZE; use nydus_format::utils::hex_string; @@ -47,7 +48,10 @@ pub struct BuildCommand { #[arg( long, - default_value = "1MiB", + default_value = format!( + "{}MiB", + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE as u64 / bytesize::MIB + ), env = "NYDUS_BUILD_CHUNK_SIZE", help = "Specify the file chunk size (must be a power of two, >= 4KiB, and 4KiB-aligned). The value needs to be set with human readable format, for example: 4kib, 1mib" )] @@ -55,7 +59,10 @@ pub struct BuildCommand { #[arg( long, - default_value = "4MiB", + default_value = format!( + "{}MiB", + DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE as u64 / bytesize::MIB + ), env = "NYDUS_BUILD_BLOCK_GROUP_SIZE", help = "Specify the uncompressed size of each block group, the unit of compression and of a single backend read (must be a power of two, >= 1MiB, and >= the chunk size). The value needs to be set with human readable format, for example: 4mib, 16mib" )] @@ -403,8 +410,8 @@ fn print_blob_build_summary(summary: BlobBuildSummary<'_>) { block_group_count: summary.blob_metadata.block_group_count().to_string(), chunk_digester: summary.blob_metadata.digester().to_string(), chunk_compressor: summary.blob_metadata.compressor().to_string(), - blob_compressed_size: summary.blob_metadata.total_compressed_size().to_string(), - blob_uncompressed_size: summary.blob_metadata.total_uncompressed_size().to_string(), + blob_compressed_size: summary.blob_metadata.compressed_end().to_string(), + blob_uncompressed_size: summary.blob_metadata.uncompressed_size().to_string(), compressed_data_offset: summary.blob_footer.compressed_data_offset().to_string(), compressed_data_size: summary.blob_footer.compressed_data_size().to_string(), bootstrap_offset: summary.blob_footer.bootstrap_offset().to_string(), diff --git a/nydus/src/build/blob_chunk.rs b/nydus/src/build/blob_chunk.rs index 101fdeaaa62..e040037ad8c 100644 --- a/nydus/src/build/blob_chunk.rs +++ b/nydus/src/build/blob_chunk.rs @@ -2,10 +2,10 @@ use crc32c::crc32c; use nydus_error::{Context, Error, Result}; use nydus_format::blob::{ BlobMetadata, BlobMetadataBlockGroup, BlobMetadataChunk, BlobMetadataCompressor, - NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE, + DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE, }; use nydus_format::erofs::{ErofsChunkAddr, EROFS_BLOB_ID_SIZE, EROFS_BLOCK_SIZE, EROFS_NULL_ADDR}; -use nydus_format::utils::round_up; +use nydus_format::utils::align_up_usize; use sha2::{Digest, Sha256}; use std::fs::File; use std::io::{Read, Write}; @@ -52,7 +52,7 @@ impl BlobWriter { let file = File::create(path) .with_context(|| format!("failed to create blob device: {}", path.display()))?; - let block_group_size = file_chunk_size.max(NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE); + let block_group_size = file_chunk_size.max(DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_SIZE); Self::from_writer(file, file_chunk_size, block_group_size, compressor) } } @@ -79,10 +79,10 @@ impl BlobWriter { "blob writer block_group size must be at least the file chunk size".to_string(), )); } - // The blob meta header stores the block group size as a log2 exponent - // (`block_group_block_bits`), so it must be a power of two; being a power - // of two >= the (block-aligned) chunk size also makes it block - // aligned by construction. + // The blob meta header stores the block group's block count as a log2 + // exponent (`block_group_block_count_bits`), so it must be a power of + // two; being a power of two >= the (block-aligned) chunk size also + // makes it block aligned by construction. if !block_group_size.is_power_of_two() { return Err(Error::InvalidParameter( "blob writer block_group size must be a power of two".to_string(), @@ -130,31 +130,19 @@ impl BlobWriter { &self.blob_metadata_block_groups } - pub fn blob_metadata( - &self, - blob_id: [u8; EROFS_BLOB_ID_SIZE], - source_offset_bias: u64, - ) -> Result { - Ok(BlobMetadata::from_parts_with_options( - blob_id, - self.file_chunk_size / EROFS_BLOCK_SIZE, + pub fn blob_metadata(&self, source_offset_bias: u64) -> Result { + Ok(BlobMetadata::new( self.compressor, - self.blob_metadata_block_groups.clone(), + self.file_chunk_size / EROFS_BLOCK_SIZE, self.blob_metadata_chunks.clone(), + self.blob_metadata_block_groups.clone(), )? .checked_add_compressed_offset(source_offset_bias)?) } - pub fn write_blob_metadata( - &mut self, - path: &Path, - blob_id: [u8; EROFS_BLOB_ID_SIZE], - source_offset_bias: u64, - ) -> Result<()> { + pub fn write_blob_metadata(&mut self, path: &Path, source_offset_bias: u64) -> Result<()> { self.finish()?; - Ok(self - .blob_metadata(blob_id, source_offset_bias)? - .save(path)?) + Ok(self.blob_metadata(source_offset_bias)?.save(path)?) } pub fn finish(&mut self) -> Result<()> { @@ -207,7 +195,8 @@ impl BlobWriter { // full file chunk size. A full chunk is already block-aligned, while // a partial (tail) chunk keeps zero padding confined to its final // block so block groups pack dense real blocks instead of large zero runs. - let write_len = round_up(to_read, EROFS_BLOCK_SIZE as usize); + let write_len = + align_up_usize(to_read, EROFS_BLOCK_SIZE as usize).expect("alignment overflowed"); let blkaddr = self.append_chunk(&chunk_buf[..to_read], write_len)?; indexes.push(ErofsChunkAddr { @@ -323,7 +312,7 @@ pub(crate) fn compression_is_worthwhile(compressed_len: usize, uncompressed_len: #[cfg(test)] mod tests { use super::*; - use nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE; + use nydus_format::blob::DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE; use std::fs; use tempfile::tempdir; @@ -349,22 +338,29 @@ mod tests { let file_a = dir.path().join("a.bin"); let file_b = dir.path().join("b.bin"); - let mut content_a = vec![b'a'; NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as usize]; + let mut content_a = vec![b'a'; DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE as usize]; content_a.extend(vec![b'b'; EROFS_BLOCK_SIZE as usize]); fs::write(&file_a, &content_a).unwrap(); fs::write( &file_b, - vec![b'a'; NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as usize], + vec![b'a'; DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE as usize], ) .unwrap(); - let mut writer = - BlobWriter::new(&blob_path, NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE).unwrap(); + // Pin the block group size to the chunk size so the 513-block layout + // below packs across several block groups. + let mut writer = BlobWriter::from_writer( + File::create(&blob_path).unwrap(), + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, + BlobMetadataCompressor::None, + ) + .unwrap(); let indexes_a = writer .write_file_chunks(&file_a, content_a.len() as u64) .unwrap(); let indexes_b = writer - .write_file_chunks(&file_b, NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64) + .write_file_chunks(&file_b, DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE as u64) .unwrap(); writer.finish().unwrap(); @@ -396,30 +392,30 @@ mod tests { assert_eq!(block_groups[0].compressed_offset(), 0); assert_eq!( block_groups[0].compressed_size(), - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE ); assert_eq!(block_groups[1].uncompressed_block_offset(), 256); assert_eq!(block_groups[1].uncompressed_block_count(), 256); assert_eq!( block_groups[1].compressed_offset(), - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64 + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE as u64 ); assert_eq!( block_groups[1].compressed_size(), - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE ); assert_eq!(block_groups[2].uncompressed_block_offset(), 512); assert_eq!(block_groups[2].uncompressed_block_count(), 1); // Block groups pack back-to-back in the data region with no inter-block group padding. assert_eq!( block_groups[2].compressed_offset(), - 2 * NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64 + 2 * DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE as u64 ); assert_eq!(block_groups[2].compressed_size(), EROFS_BLOCK_SIZE); } #[test] - fn blob_writer_allows_small_file_chunks_with_one_megabyte_blob_metadata_block_groups() { + fn blob_writer_allows_small_file_chunks_with_default_size_blob_metadata_block_groups() { let dir = tempdir().unwrap(); let blob_path = dir.path().join("blob.data"); let input_path = dir.path().join("input.bin"); @@ -432,7 +428,7 @@ mod tests { .write_file_chunks(&input_path, content.len() as u64) .unwrap(); writer.finish().unwrap(); - let blob_metadata = writer.blob_metadata([0u8; EROFS_BLOB_ID_SIZE], 0).unwrap(); + let blob_metadata = writer.blob_metadata(0).unwrap(); assert_eq!(indexes.len(), 2); assert_eq!(indexes[0].blkaddr, 0); @@ -462,7 +458,7 @@ mod tests { .write_file_chunks(&input_path, content.len() as u64) .unwrap(); writer.finish().unwrap(); - let blob_metadata = writer.blob_metadata([0u8; EROFS_BLOB_ID_SIZE], 0).unwrap(); + let blob_metadata = writer.blob_metadata(0).unwrap(); // The all-zero chunk becomes a hole: a null chunk index with no blob // reference, no blob-meta chunk entry, and no bytes in the data region. @@ -511,12 +507,12 @@ mod tests { let dir = tempdir().unwrap(); let blob_path = dir.path().join("blob.data"); let input_path = dir.path().join("input.bin"); - let content = pseudo_random_bytes(NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as usize); + let content = pseudo_random_bytes(DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE as usize); fs::write(&input_path, &content).unwrap(); let mut writer = BlobWriter::new_with_compressor( &blob_path, - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, BlobMetadataCompressor::Zstd, ) .unwrap(); @@ -531,7 +527,7 @@ mod tests { assert_eq!(block_groups[0].uncompressed_block_count(), 256); assert_eq!( block_groups[0].uncompressed_size(), - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64 + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE as u64 ); assert_eq!( u64::from(block_groups[0].compressed_size()), @@ -546,26 +542,25 @@ mod tests { let blob_path = dir.path().join("blob.data"); let blob_metadata_path = dir.path().join("blob.blob.meta"); let input_path = dir.path().join("input.bin"); - let blob_id = [7u8; EROFS_BLOB_ID_SIZE]; fs::write(&input_path, vec![b'x'; 4096]).unwrap(); let mut writer = - BlobWriter::new(&blob_path, NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE).unwrap(); + BlobWriter::new(&blob_path, DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE).unwrap(); writer.write_file_chunks(&input_path, 4096).unwrap(); writer - .write_blob_metadata(&blob_metadata_path, blob_id, 8192) + .write_blob_metadata(&blob_metadata_path, 8192) .unwrap(); let raw = fs::read(&blob_metadata_path).unwrap(); // 4 KiB header block + one chunk + one block group, padded to a block. assert_eq!(raw.len(), 8192); - let blob_metadata = BlobMetadata::load(&blob_metadata_path).unwrap(); + let blob_metadata = BlobMetadata::from_path(&blob_metadata_path, false).unwrap(); assert_eq!(blob_metadata.header().chunk_count(), 1); assert_eq!(blob_metadata.header().block_group_count(), 1); - assert_eq!(blob_metadata.header().chunk_bytes(), 48); - assert_eq!(blob_metadata.header().block_group_bytes(), 40); - assert_eq!(blob_metadata.header().metadata_size(), 8192); + assert_eq!(blob_metadata.header().chunk_table_size(), 48); + assert_eq!(blob_metadata.header().block_group_table_size(), 40); + assert_eq!(blob_metadata.header().padded_size(), 8192); assert_eq!(blob_metadata.chunks()[0].uncompressed_block_offset(), 0); assert_eq!(blob_metadata.block_groups()[0].compressed_offset(), 8192); } diff --git a/nydus/src/build/bootstrap.rs b/nydus/src/build/bootstrap.rs index fba2850168c..4192f88ce9a 100644 --- a/nydus/src/build/bootstrap.rs +++ b/nydus/src/build/bootstrap.rs @@ -11,7 +11,7 @@ use nydus_format::erofs::{ ErofsDeviceSlot, EROFS_BLOCK_SIZE, EROFS_DEVICESLOT_SIZE, EROFS_FT_DIR, EROFS_SB_BASE_SIZE, EROFS_SUPER_OFFSET, }; -use nydus_format::utils::round_up; +use nydus_format::utils::align_up_usize; pub const FLATTENED_BLOB_ALIGNMENT: u64 = 0x8_0000; @@ -63,7 +63,8 @@ fn set_flattened_mapped_blkaddrs( "flattened blob alignment exceeds addressable size: {err}" )) })?; - let mapped_offset = round_up(next_offset_usize, alignment_usize) as u64; + let mapped_offset = align_up_usize(next_offset_usize, alignment_usize) + .expect("alignment overflowed") as u64; if mapped_offset % block_size != 0 { return Err(Error::InvalidImage( "flattened blob offset must be block aligned".to_string(), diff --git a/nydus/src/build/inode.rs b/nydus/src/build/inode.rs index 17208fc48de..1f5e685dc12 100644 --- a/nydus/src/build/inode.rs +++ b/nydus/src/build/inode.rs @@ -9,7 +9,7 @@ use nydus_format::erofs::{ EROFS_INODE_FLAT_INLINE, EROFS_INODE_FLAT_PLAIN, EROFS_XATTR_ENTRY_HEADER_SIZE, EROFS_XATTR_IBODY_HEADER_SIZE, EROFS_XATTR_INDEX_TRUSTED, NYDUS_XATTR_SUFFIX_PREFETCH_BLOBS, }; -use nydus_format::utils::round_up; +use nydus_format::utils::align_up_usize; use std::collections::{HashMap, HashSet}; use std::fs; use std::io::Write; @@ -131,7 +131,8 @@ pub(crate) fn erofs_inode_size(inode: &InodeInfo) -> usize { if chunk_index_entries.is_empty() { inode_isize + xattr_isize } else { - round_up(inode_isize + xattr_isize, EROFS_CHUNK_INDEX_SIZE) + align_up_usize(inode_isize + xattr_isize, EROFS_CHUNK_INDEX_SIZE) + .expect("alignment overflowed") + chunk_index_entries.len() * EROFS_CHUNK_INDEX_SIZE } } @@ -582,7 +583,8 @@ pub(crate) fn serialize_inode(inode: &InodeInfo, epoch: u64) -> Vec { } else { EROFS_INODE_COMPACT_SIZE }; - let extent_offset = round_up(base + xattr_size, EROFS_CHUNK_INDEX_SIZE); + let extent_offset = align_up_usize(base + xattr_size, EROFS_CHUNK_INDEX_SIZE) + .expect("alignment overflowed"); for (i, entry) in chunk_index_entries.iter().enumerate() { let index = ErofsChunkIndex::new(entry.blkaddr, entry.device_id); let off = extent_offset + i * EROFS_CHUNK_INDEX_SIZE; @@ -802,7 +804,7 @@ fn write_erofs_xattr_ibody(buf: &mut [u8], offset: usize, xattrs: &[XattrEntry]) ibody[value_start..][..value.len()].copy_from_slice(value); // Next entry begins at the next 4-byte boundary; padding is already zero. - entry_start = round_up(value_start + value.len(), 4); + entry_start = align_up_usize(value_start + value.len(), 4).expect("alignment overflowed"); } ibody_size diff --git a/nydus/src/build/layout.rs b/nydus/src/build/layout.rs index a4cbe7d929b..bda17f8a1f1 100644 --- a/nydus/src/build/layout.rs +++ b/nydus/src/build/layout.rs @@ -1,5 +1,5 @@ use nydus_format::erofs::{EROFS_BLOCK_SIZE, EROFS_SLOTSIZE}; -use nydus_format::utils::round_up; +use nydus_format::utils::align_up_usize; /// Metadata layout allocator. /// @@ -58,10 +58,10 @@ impl MetadataLayout { pub(crate) fn alloc_inode(&mut self, size: usize, has_inline: bool) -> (usize, u64) { let block = EROFS_BLOCK_SIZE as usize; if has_inline && self.cursor % block + size > block { - self.cursor = round_up(self.cursor, block); + self.cursor = align_up_usize(self.cursor, block).expect("alignment overflowed"); } - let aligned = round_up(size, EROFS_SLOTSIZE as usize); + let aligned = align_up_usize(size, EROFS_SLOTSIZE as usize).expect("alignment overflowed"); let offset = self.cursor; self.cursor += aligned; if self.buf.len() < self.cursor { @@ -74,7 +74,8 @@ impl MetadataLayout { /// Pad the metadata buffer to the next block boundary. pub(crate) fn pad_to_block(&mut self) -> usize { - let aligned = round_up(self.cursor, EROFS_BLOCK_SIZE as usize); + let aligned = + align_up_usize(self.cursor, EROFS_BLOCK_SIZE as usize).expect("alignment overflowed"); self.cursor = aligned; if self.buf.len() < self.cursor { self.buf.resize(self.cursor, 0); @@ -86,9 +87,11 @@ impl MetadataLayout { /// Allocate block-aligned space for directory data. /// Returns (offset_in_buf, start_block_address). pub(crate) fn alloc_dir_data(&mut self, size: usize) -> (usize, u64) { - self.cursor = round_up(self.cursor, EROFS_BLOCK_SIZE as usize); + self.cursor = + align_up_usize(self.cursor, EROFS_BLOCK_SIZE as usize).expect("alignment overflowed"); let offset = self.cursor; - let aligned_size = round_up(size, EROFS_BLOCK_SIZE as usize); + let aligned_size = + align_up_usize(size, EROFS_BLOCK_SIZE as usize).expect("alignment overflowed"); self.cursor += aligned_size; if self.buf.len() < self.cursor { self.buf.resize(self.cursor, 0); diff --git a/nydus/src/build/mod.rs b/nydus/src/build/mod.rs index cf8c39c179a..75b46e4c96c 100644 --- a/nydus/src/build/mod.rs +++ b/nydus/src/build/mod.rs @@ -101,9 +101,9 @@ impl BuildImageOptions { } // Validate the block group uncompressed size: a power of two (the - // blob meta header stores it as the log2 exponent `block_group_block_bits`), - // at least 1MiB, and at least the file chunk size so a chunk always - // fits in a block group. + // blob meta header stores its block count as the log2 exponent + // `block_group_block_count_bits`), at least 1MiB, and at least the + // file chunk size so a chunk always fits in a block group. if !block_group_size.is_power_of_two() || block_group_size < MIN_BLOCK_GROUP_SIZE { return Err(Error::InvalidParameter(format!( "block group size {block_group_size} must be a power of two and at least 1MiB" @@ -163,11 +163,11 @@ pub fn build_image(options: &BuildImageOptions, writer: impl Write) -> Result Result<(Vec, [u8; EROFS_BLOB_ID_SIZE], BlobFooter)> { let mut artifact = Vec::with_capacity( - usize::try_from(data.len() as u64 + blob_metadata.metadata_size()) + usize::try_from(data.len() as u64 + blob_metadata.padded_size()) .map_err(|err| Error::Overflow(format!("artifact exceeds usize: {err}")))? + NYDUS_BLOB_FOOTER_SIZE, ); artifact.extend_from_slice(data); - let footer = nydus_format::blob::assemble_full_blob( - &mut artifact, - data.len() as u64, - &[], - blob_metadata, - )?; + let footer = + nydus_format::blob::finish_full_blob(&mut artifact, data.len() as u64, &[], blob_metadata)?; let digest = sha256_bytes(&artifact); Ok((artifact, digest, footer)) diff --git a/nydus/src/check/mod.rs b/nydus/src/check/mod.rs index 49254a3d59d..9737c77f692 100644 --- a/nydus/src/check/mod.rs +++ b/nydus/src/check/mod.rs @@ -464,22 +464,22 @@ fn inspect_blob(path: &Path) -> Result> { } fn blob_metadata_summary_from_bytes(data: &[u8]) -> Result { - let blob_metadata = BlobMetadata::loader().from_bytes(data)?; + let blob_metadata = BlobMetadata::from_bytes(data, false)?; Ok(BlobMetadataSummary { chunk_count: blob_metadata.chunk_count(), block_group_count: blob_metadata.block_group_count(), chunk_size: blob_metadata.chunk_size(), digester: blob_metadata.digester(), compressor: blob_metadata.compressor(), - total_uncompressed_size: blob_metadata.total_uncompressed_size(), - total_compressed_size: blob_metadata.total_compressed_size(), + total_uncompressed_size: blob_metadata.uncompressed_size(), + total_compressed_size: blob_metadata.compressed_end(), }) } #[cfg(test)] mod tests { use super::*; - use nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT; + use nydus_format::blob::DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT; use std::fs; use tempfile::tempdir; @@ -552,9 +552,9 @@ mod tests { fn write_minimal_blob(path: &Path) -> ([u8; EROFS_BLOB_ID_SIZE], [u8; EROFS_BLOB_ID_SIZE]) { let data = [0x5au8; EROFS_BLOCK_SIZE as usize]; let data_digest = sha256_bytes(&data); - let blob_metadata = BlobMetadata::from_parts( - [0u8; EROFS_BLOB_ID_SIZE], - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, + let blob_metadata = BlobMetadata::new( + BlobMetadataCompressor::None, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, Vec::new(), Vec::new(), ) diff --git a/nydus/src/fanotify/core.rs b/nydus/src/fanotify/core.rs index 792163f96c5..f29434ecf92 100644 --- a/nydus/src/fanotify/core.rs +++ b/nydus/src/fanotify/core.rs @@ -377,7 +377,8 @@ pub(crate) fn align_fetch_range( let end = raw_end.min(cache_size); let aligned_off = offset & !(BLOCK_SIZE - 1); - let aligned_end = nydus_format::utils::align_up(end, BLOCK_SIZE).ok_or(RangeError::Overflow)?; + let aligned_end = + nydus_format::utils::align_up_u64(end, BLOCK_SIZE).ok_or(RangeError::Overflow)?; // `cache_size` is validated block-aligned at device enumeration, so rounding // `end` up never exceeds it; clamp as a safety net and verify the aligned // window stays inside the device. diff --git a/nydus/src/optimize/mod.rs b/nydus/src/optimize/mod.rs index 28532ba3636..bc6815b11c4 100644 --- a/nydus/src/optimize/mod.rs +++ b/nydus/src/optimize/mod.rs @@ -29,7 +29,7 @@ use nydus_core::ErofsReader; use nydus_error::{Context, Error, Result}; use nydus_format::blob::{ BlobFooter, BlobMetadata, BlobMetadataBlockGroup, BlobMetadataCompressor, - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, }; use nydus_format::erofs::EROFS_BLOB_ID_SIZE; use nydus_storage::access_trace::{TraceDocument, TraceEntry, TRACE_DOCUMENT_VERSION}; @@ -110,7 +110,7 @@ pub fn build_ondemand_blob( let block_group = *cache .blob_metadata() - .block_group_at(*block_group_index as usize) + .block_group(*block_group_index as usize) .ok_or_else(|| { Error::InvalidParameter(format!( "pattern references block group {block_group_index} out of range for blob {blob_index}" @@ -167,12 +167,11 @@ pub fn build_ondemand_blob( let mut data_digest = [0u8; EROFS_BLOB_ID_SIZE]; data_digest.copy_from_slice(&data_hasher.finalize()); - let blob_metadata = BlobMetadata::from_parts_with_options( - data_digest, - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, + let blob_metadata = BlobMetadata::new( BlobMetadataCompressor::Zstd, - ondemand_block_groups, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, Vec::new(), + ondemand_block_groups, ) .context("failed to assemble ondemand blob meta")?; diff --git a/nydus/src/ublk/core.rs b/nydus/src/ublk/core.rs index f73751a9f19..babb40b0903 100644 --- a/nydus/src/ublk/core.rs +++ b/nydus/src/ublk/core.rs @@ -16,7 +16,7 @@ use nydus_core::extent::MmapCache; use nydus_core::NydusCore; use nydus_error::{Context, Error, Result}; use nydus_format::erofs::EROFS_BLOCK_SIZE; -use nydus_format::utils::align_up; +use nydus_format::utils::align_up_u64; use tracing::warn; /// Logical block size exposed by the ublk device. Matching the EROFS block size @@ -42,7 +42,7 @@ impl UblkCore { let zero_fd = core.zero_fd(); // Round the device size up to a whole block: the kernel always reads in // block units, and the tail block of the last blob may be partial. - let device_size = align_up(core.flat_size(), UBLK_LOGICAL_BLOCK_SIZE) + let device_size = align_up_u64(core.flat_size(), UBLK_LOGICAL_BLOCK_SIZE) .ok_or_else(|| Error::Overflow("flattened device size overflow".to_string()))?; // Preparing a blob loads and validates its meta and sizes its cache // file. Doing it up front keeps the first block read (typically diff --git a/nydus/src/uffd/core.rs b/nydus/src/uffd/core.rs index eec44c14275..2bc30668852 100644 --- a/nydus/src/uffd/core.rs +++ b/nydus/src/uffd/core.rs @@ -7,7 +7,7 @@ use nydus_config::Config; use nydus_core::{Extent, NydusCore, ResolveMode}; use nydus_error::{Context, Error, Result}; use nydus_format::erofs::EROFS_BLOCK_SIZE; -use nydus_format::utils::align_up; +use nydus_format::utils::align_up_u64; use super::proto::{DeviceRange, FaultPolicy, VmaRegion}; @@ -64,7 +64,7 @@ pub struct UffdCore { impl UffdCore { pub fn new(bootstrap: &Path, config: Config) -> Result { let core = Arc::new(NydusCore::new(bootstrap, config)?); - let device_size = align_up(core.flat_size(), UFFD_TOTAL_SIZE_ALIGNMENT) + let device_size = align_up_u64(core.flat_size(), UFFD_TOTAL_SIZE_ALIGNMENT) .ok_or_else(|| Error::Overflow("alignment overflow".to_string()))?; Ok(Self { core, device_size }) diff --git a/nydus/tests/testsuite/erofs_reader.rs b/nydus/tests/testsuite/erofs_reader.rs index 7e00e2734d7..7d8349c81d5 100644 --- a/nydus/tests/testsuite/erofs_reader.rs +++ b/nydus/tests/testsuite/erofs_reader.rs @@ -141,9 +141,7 @@ fn reads_chunk_data_from_footer_based_full_blob() { &[0u8; 16], ) .expect("render embedded bootstrap"); - let blob_metadata = blob_writer - .blob_metadata(data_blob_id, 0) - .expect("blob meta"); + let blob_metadata = blob_writer.blob_metadata(0).expect("blob meta"); let data = fs::read(&data_path).expect("read data blob"); let full_blob_digest = diff --git a/nydus/tests/testsuite/fixture.rs b/nydus/tests/testsuite/fixture.rs index 956df13319d..d937cf2a6de 100644 --- a/nydus/tests/testsuite/fixture.rs +++ b/nydus/tests/testsuite/fixture.rs @@ -5,7 +5,7 @@ use std::io::Write; /// Assemble a footer-based full blob (`data | pad | bootstrap | pad | /// blob meta | footer`) in `blob_dir` via the production -/// `nydus_format::blob::assemble_full_blob`, rename it to its hex digest, and +/// `nydus_format::blob::finish_full_blob`, rename it to its hex digest, and /// return the digest. pub fn assemble_full_blob( blob_dir: &std::path::Path, @@ -16,7 +16,7 @@ pub fn assemble_full_blob( let full_blob_path = blob_dir.join("full.blob"); let mut full_blob = std::fs::File::create(&full_blob_path).expect("create full blob"); full_blob.write_all(data).expect("write data"); - nydus_format::blob::assemble_full_blob( + nydus_format::blob::finish_full_blob( &mut full_blob, data.len() as u64, bootstrap_bytes, diff --git a/nydus/tests/testsuite/nydus_core.rs b/nydus/tests/testsuite/nydus_core.rs index aa40521cfa8..7718944efeb 100644 --- a/nydus/tests/testsuite/nydus_core.rs +++ b/nydus/tests/testsuite/nydus_core.rs @@ -88,23 +88,26 @@ fn build_test_image_with_layout( let blob_dir = root.join("blobs"); fs::create_dir_all(&blob_dir).unwrap(); let staging = blob_dir.join("staging"); - let mut writer = BlobWriter::new_with_compressor( - &staging, - nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, + // Block group size pinned to 1 MiB (the chunk size) so the corpus above + // actually spans several block groups. + let mut writer = BlobWriter::from_writer( + fs::File::create(&staging).unwrap(), + nydus_format::blob::DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, + nydus_format::blob::DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, BlobMetadataCompressor::Zstd, ) .unwrap(); let mut inodes = build_tree( &corpus_dir, &mut writer, - nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, + nydus_format::blob::DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, &HashSet::new(), ) .unwrap(); writer.finish().unwrap(); let data_blob_id = writer.data_digest(); - let blob_metadata = writer.blob_metadata(data_blob_id, 0).unwrap(); + let blob_metadata = writer.blob_metadata(0).unwrap(); let blocks = writer.total_blocks(); set_root_prefetch_blobs_xattr(&mut inodes[0], &[1]).unwrap(); let embedded_device_slots = [ErofsDeviceSlot::with_blob_id(blocks, &data_blob_id)]; @@ -262,16 +265,18 @@ fn flattened_bootstrap_records_mapped_device_slots() { let blob_dir = dir.path().join("second-blobs"); fs::create_dir_all(&blob_dir).unwrap(); let staging = blob_dir.join("staging"); - let mut writer = BlobWriter::new_with_compressor( - &staging, - nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, + // Same pinned 1 MiB block group geometry as build_test_image_with_layout. + let mut writer = BlobWriter::from_writer( + fs::File::create(&staging).unwrap(), + nydus_format::blob::DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, + nydus_format::blob::DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, BlobMetadataCompressor::Zstd, ) .unwrap(); let mut inodes = build_tree( &corpus_dir, &mut writer, - nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, + nydus_format::blob::DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, &HashSet::new(), ) .unwrap();