From 2941640b5c68254e4eaef8768578bd03566c700d Mon Sep 17 00:00:00 2001 From: Gaius Date: Fri, 21 Aug 2026 10:25:24 +0000 Subject: [PATCH 1/7] feat(nydus-format): add shared incompat flags validation for blob formats Introduce a `blob::flag` module that centralises the EROFS-style incompatible/compatible flag split (low 16 bits incompat, high 16 bits compat) and a `validate_incompat_flags` helper that rejects records carrying unknown incompat bits with a descriptive error. This replaces ad-hoc per-format checks and ensures consistent forward-compatibility enforcement across blob meta and blob footer readers. Signed-off-by: Gaius --- nydus-format/src/blob/{validate.rs => flag.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename nydus-format/src/blob/{validate.rs => flag.rs} (100%) diff --git a/nydus-format/src/blob/validate.rs b/nydus-format/src/blob/flag.rs similarity index 100% rename from nydus-format/src/blob/validate.rs rename to nydus-format/src/blob/flag.rs From b5481f9175bd8a26a8ab66ac90ddb4bf5f60325c Mon Sep 17 00:00:00 2001 From: Gaius Date: Fri, 21 Aug 2026 10:30:21 +0000 Subject: [PATCH 2/7] refactor(blob-meta): rename fields, constants, and methods for consistency Rename `compressed_byte_offset` to `compressed_offset`, "record" to "entry" throughout the blob metadata spec and implementation, add a `NYDUS_` prefix to public constants, drop the `_byte_` infix from method names, introduce typed algorithm enums for blob meta compressor and digester, restructure header serialization to use byte-array methods, and update docs and tests accordingly. Signed-off-by: Gaius --- docs/nydus.md | 54 +- nydus-backend/src/local.rs | 7 +- nydus-core/src/reader/data.rs | 6 +- nydus-format/src/blob/algorithm.rs | 80 ++ nydus-format/src/blob/flag.rs | 1 - nydus-format/src/blob/footer.rs | 12 +- nydus-format/src/blob/metadata.rs | 1849 ++++++++++++++------------ nydus-format/src/blob/mod.rs | 28 +- nydus-format/src/utils/le.rs | 50 +- nydus-format/src/utils/mod.rs | 4 +- nydus-storage/src/block_group_map.rs | 2 +- nydus-storage/src/cache/local.rs | 45 +- nydus-storage/src/cache/mod.rs | 32 +- nydus-storage/src/cache/remote.rs | 8 +- nydus/src/bin/nydus/build.rs | 6 +- nydus/src/bin/nydus/check.rs | 8 +- nydus/src/bin/nydus/optimize.rs | 4 +- nydus/src/build/blob_chunk.rs | 63 +- nydus/src/check/mod.rs | 14 +- nydus/src/optimize/mod.rs | 8 +- nydus/tests/testsuite/nydus_core.rs | 8 +- 21 files changed, 1248 insertions(+), 1041 deletions(-) create mode 100644 nydus-format/src/blob/algorithm.rs diff --git a/docs/nydus.md b/docs/nydus.md index 66424e0e1cc..d59a8d675f5 100644 --- a/docs/nydus.md +++ b/docs/nydus.md @@ -194,7 +194,7 @@ Current implementation notes: standalone blob meta copy under `/.blob.meta`. - `--compressor zstd` attempts to compress each blob_meta block group as one unit. If the compressed bytes are larger than 70% of the uncompressed block group, - the block group is stored plain and its blob_meta block group record has + the block group is stored plain and its blob_meta block group entry has `compressed_size == uncompressed_block_count * 4096`. - `--compressor none` writes every block group plain. - `--exclude ` omits paths inside the source tree from the blob and the @@ -367,7 +367,7 @@ Current implementation notes: backend (with CRC validation on every path). - The ondemand artifact layout is `[block group data][blob meta][footer]` with `bootstrap_blocks = 0` (no embedded bootstrap) and an empty chunk table. Each - block group record is a redirect: it stores the source device id and source block group + block group entry is a redirect: it stores the source device id and source block group index, and its `crc32c` equals the source block group's decoded CRC. ### Check @@ -1062,8 +1062,8 @@ 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 record gives the -encoded `compressed_byte_offset` (for example 0 for the first encoded block_group). +`block_group_index = blkaddr >> block_group_block_bits`, and the block_group entry gives the +encoded `compressed_offset` (for example 0 for the first encoded block_group). ``` Blob identity is therefore attached to the device slot, not to the chunk index @@ -1090,7 +1090,7 @@ At the same time: Whenever build emits a full blob, it writes one blob meta region before the footer. Blob meta is the canonical catalog for the external data blob. A blob -meta chunk is a content-addressed record (BLAKE3 digest + absolute block range) +meta chunk is a content-addressed entry (BLAKE3 digest + absolute block range) used for inspection and future deduplication; chunks are independent of block groups. A blob meta block group is the compression unit and cache population unit. EROFS inode chunk indexes point into the logical uncompressed external-device address space; @@ -1117,7 +1117,7 @@ embedded blob meta region | block_group_block_bits (u8 + pad) | | reserved tail (compat area) | +-------------------------------+ -| chunk records | +| chunk entries | | 48 bytes each | | | | digest (BLAKE3) | @@ -1125,11 +1125,11 @@ embedded blob meta region | uncompressed_block_count | | reserved | +-------------------------------+ -| block_group records | +| block_group entries | | 40 bytes each | | | | uncompressed_block_offset | -| compressed_byte_offset | +| compressed_offset | | uncompressed_block_count | | compressed_size | | crc32c | @@ -1155,18 +1155,18 @@ Header details: bits are ignored (like `feature_compat`). `COMPRESSOR_ZSTD` (`1 << 0`) means zstd is the blob's default compressor; no compressor bit means stored plain. `DIGESTER_BLAKE3` (`1 << 1`) is mandatory for chunk digests. - Record-layout evolution (wider chunk/block group records, new record kinds) is + Entry-layout evolution (wider chunk/block group entries, new entry kinds) is expressed as a new incompat bit — the same way EROFS gates compact vs extended inodes — while header growth uses the reserved tail plus a compat bit. The `magic + version + flags` header prefix is shared with the block_block_group_map sidecar. - `crc32c` covers the full blob meta region with this field zeroed: the fixed - header, all chunk records, all block group records, and trailing zero padding. The cache layer + header, all chunk entries, all block group entries, and trailing zero padding. The cache layer verifies this crc32c before mmaping a cached blob meta file for chunk lookup. - `chunks_offset` is fixed at the header size. `block_groups_offset` follows the dense chunk table. -- `chunk_count` is the number of chunk records. -- `block_group_count` is the number of compressed block group records. +- `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 8. Storing the exponent EROFS-style (the same quantity as `chunk_format & @@ -1183,10 +1183,10 @@ Header details: of the header block is reserved for future compat fields — writers zero it, readers ignore it (so a compat extension does not break old readers), and corruption is caught by the region crc32c. Layout changes that old readers - cannot safely ignore (wider records, new tables, moved offsets) must use an + cannot safely ignore (wider entries, new tables, moved offsets) must use an incompat flag bit instead. The header intentionally does not store total compressed size or total uncompressed size — totals are computed - from the block group records — and the blob meta region is padded to a 4 KiB block + from the block group entries — and the blob meta region is padded to a 4 KiB block boundary. Chunk details: @@ -1209,14 +1209,14 @@ Block group details: every block group but the last is exactly `1 << block_group_block_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_byte_offset` is the encoded payload's byte offset within the data +- `compressed_offset` is the encoded payload's byte offset within the data region (not inside the whole full blob file). Encoded block groups are packed back-to-back with no inter-block group padding, so this is a plain byte position and is not block-aligned for compressed block groups. Runtime backends add the data-region base offset before issuing range reads. - `uncompressed_block_count` describes the decoded block group size in 4 KiB blocks. - `compressed_size` is the actual encoded byte length. The next block group starts at - exactly the previous block group's `compressed_byte_offset + compressed_size`. + exactly the previous block group's `compressed_offset + compressed_size`. - `crc32c` is computed over the decoded block group. If `compressed_size` equals `uncompressed_block_count * 4096`, runtime treats the block group as stored plain and skips decompression even when the header compressor is zstd. @@ -1226,11 +1226,11 @@ Block group details: `source_block_group_index`; phase-0 prefetch writes the decoded bytes into the source blob's cache instead of this blob's own cache. A blob containing any redirect block group is an "ondemand" blob: its block groups may be non-uniform in size - (the uniformity invariant is relaxed) and `block_group_index_for_byte_offset` is + (the uniformity invariant is relaxed) and `block_group_index_for_offset` is never used on it. The redirect block group's `crc32c` equals the source block group's decoded CRC so the fill is cross-checked before touching the source cache. -The writer does not bias `compressed_byte_offset` by the bootstrap size, and +The writer does not bias `compressed_offset` by the bootstrap size, and does not bias `uncompressed_block_offset`. Only the data region as a whole is padded to a 4 KiB boundary (so the embedded bootstrap that follows starts on a block); block groups themselves are not individually padded. @@ -1261,7 +1261,7 @@ chunk: A fully-zero chunk — a real filesystem hole reads back as zeros, and so does zero-filled data — is never stored: the builder emits the standard EROFS null chunk index (all 48 address bits set on disk) instead. Hole chunks occupy no -bytes in the data region, get no blob meta chunk record, and never touch the +bytes in the data region, get no blob meta chunk entry, and never touch the blob cache at runtime: the core read paths satisfy them with zeros directly, and native EROFS mounts decode the null address in-kernel the same way. @@ -1308,8 +1308,8 @@ encoded data region of the full blob: +---------------------------+------------------+--- | block_group 0 compressed bytes | block_group 1 bytes | ... +---------------------------+------------------+--- -^ compressed_byte_offset(g0) = 0 - ^ compressed_byte_offset(g1) +^ compressed_offset(g0) = 0 + ^ compressed_offset(g1) = offset(g0) + compressed_size(g0) ``` @@ -1317,7 +1317,7 @@ Hash and validation summary: - **BLAKE3 per chunk** (blob meta chunk table) — the deduplication key over the chunk's decoded, block-aligned bytes. -- **CRC32C per block group** (blob meta block group record) — validated after every fetch +- **CRC32C per block group** (blob meta block group entry) — validated after every fetch and decode, on both the on-demand and prefetch paths. - **SHA256 over the data region** — written into the bootstrap device slot as the blob id. @@ -1346,26 +1346,26 @@ ondemand blob — named by SHA256(full blob), one new nydus layer | blob meta | | header (crc32c) | | chunk table: empty | -| block_group table: redirect records | +| block_group table: redirect entries | +--------------------------------+ | footer (bootstrap_blocks = 0) | +--------------------------------+ ``` -Every block group record in the ondemand blob is a **redirect**: instead of +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 used on an ondemand blob: ```text -redirect block_group record (in the ondemand block_group table) +redirect block_group entry (in the ondemand block_group table) source_blob_index = 2 --+ source_block_group_index = 7 +--> names source blob 2, block_group 7; crc32c ------------------+ crc32c equals that block_group's decoded CRC - compressed_byte_offset --+ + compressed_offset -------+ compressed_size ---------+--> locates the encoded copy inside the ondemand data region ``` @@ -1469,7 +1469,7 @@ When mounting with `--bootstrap + --blob-dir`: 6. Before chunk lookup, check the cache directory for `.blob.meta`. If it is absent, download the standalone blob meta from the local backend into the cache directory. The cache verifies the blob meta header crc32c before - mmaping the cached file and using its chunk records. + 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`, ensures every block group covering the requested range is fetched and decoded from diff --git a/nydus-backend/src/local.rs b/nydus-backend/src/local.rs index d4f17ae1ca0..984b39e06ec 100644 --- a/nydus-backend/src/local.rs +++ b/nydus-backend/src/local.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock, RwLock}; use super::{BlobBackend, ReadContext}; -use nydus_format::blob::{BlobFooter, BlobMetadata, BLOB_METADATA_SUFFIX}; +use nydus_format::blob::{BlobFooter, BlobMetadata, NYDUS_BLOB_METADATA_SUFFIX}; use nydus_format::utils::{hex_string, sha256_file, sha256_file_range, SHA256_DIGEST_SIZE}; #[derive(Clone)] @@ -99,7 +99,10 @@ impl Local { ) })?; - let blob_metadata_name = format!("{}{BLOB_METADATA_SUFFIX}", file_name.to_string_lossy()); + let blob_metadata_name = format!( + "{}{NYDUS_BLOB_METADATA_SUFFIX}", + file_name.to_string_lossy() + ); Ok(self.root.join(blob_metadata_name)) } diff --git a/nydus-core/src/reader/data.rs b/nydus-core/src/reader/data.rs index 802e978121d..293a949bd11 100644 --- a/nydus-core/src/reader/data.rs +++ b/nydus-core/src/reader/data.rs @@ -13,13 +13,13 @@ use super::{ErofsReader, RawBlobInfo}; /// Resolve an absolute byte offset in the flattened device to the blob that /// backs it, returning `(blob_index, offset_within_blob)`. Returns `None` when /// the address is bootstrap-local (not in any blob's mapped range). -pub(crate) fn locate_flat_blob(blob_layout: &[RawBlobInfo], abs_byte: u64) -> Option<(u16, u64)> { +pub(crate) fn locate_flat_blob(blob_layout: &[RawBlobInfo], abs_offset: u64) -> Option<(u16, u64)> { let block_size = EROFS_BLOCK_SIZE as u64; for info in blob_layout { let start = info.mapped_blkaddr * block_size; let end = start + info.blocks * block_size; - if abs_byte >= start && abs_byte < end { - return Some((info.blob_index, abs_byte - start)); + if abs_offset >= start && abs_offset < end { + return Some((info.blob_index, abs_offset - start)); } } None diff --git a/nydus-format/src/blob/algorithm.rs b/nydus-format/src/blob/algorithm.rs new file mode 100644 index 00000000000..cc0045bca33 --- /dev/null +++ b/nydus-format/src/blob/algorithm.rs @@ -0,0 +1,80 @@ +//! The compression and digest algorithms a blob meta file declares in its +//! `flags` word: typed views over the algorithm flag bits, decoded per read +//! from `BlobMetadataFlags` and encoded back via `flag`. + +use crate::blob::metadata::BlobMetadataFlags; +use crate::error::{Error, Result}; +use std::fmt; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlobMetadataCompressor { + None, + Zstd, +} + +impl BlobMetadataCompressor { + pub fn flag(self) -> BlobMetadataFlags { + match self { + Self::None => BlobMetadataFlags::empty(), + Self::Zstd => BlobMetadataFlags::COMPRESSOR_ZSTD, + } + } +} + +impl fmt::Display for BlobMetadataCompressor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::None => "none", + Self::Zstd => "zstd", + }) + } +} + +/// Infallible: an absent compressor flag is the valid uncompressed state, +/// and `BlobMetadataFlags` can only hold defined bits. +impl From for BlobMetadataCompressor { + fn from(value: BlobMetadataFlags) -> Self { + if value.contains(BlobMetadataFlags::COMPRESSOR_ZSTD) { + Self::Zstd + } else { + Self::None + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlobMetadataDigester { + Blake3, +} + +impl BlobMetadataDigester { + pub fn flag(self) -> BlobMetadataFlags { + match self { + Self::Blake3 => BlobMetadataFlags::DIGESTER_BLAKE3, + } + } +} + +impl fmt::Display for BlobMetadataDigester { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Blake3 => "blake3", + }) + } +} + +/// Fallible: every blob meta must declare its digester, so an absent flag +/// is a corrupt file rather than a default. +impl TryFrom for BlobMetadataDigester { + type Error = Error; + + fn try_from(value: BlobMetadataFlags) -> Result { + if value.contains(BlobMetadataFlags::DIGESTER_BLAKE3) { + Ok(Self::Blake3) + } else { + Err(Error::InvalidImage( + "blob meta digester flag is missing".to_string(), + )) + } + } +} diff --git a/nydus-format/src/blob/flag.rs b/nydus-format/src/blob/flag.rs index f962de2d6b3..8cf4f25ae0e 100644 --- a/nydus-format/src/blob/flag.rs +++ b/nydus-format/src/blob/flag.rs @@ -11,7 +11,6 @@ use crate::error::{Error, Result}; pub const INCOMPAT_MASK: u32 = 0x0000_FFFF; /// Reject `flags` whose incompat half carries bits outside `supported`. -/// `what` names the format in the error message. pub fn validate_incompat_flags(flags: u32, supported: u32) -> Result<()> { let unknown_incompat = flags & INCOMPAT_MASK & !supported; if unknown_incompat != 0 { diff --git a/nydus-format/src/blob/footer.rs b/nydus-format/src/blob/footer.rs index 24b767bb80e..7b43c8e5ed8 100644 --- a/nydus-format/src/blob/footer.rs +++ b/nydus-format/src/blob/footer.rs @@ -1,4 +1,4 @@ -use crate::blob::validate::validate_incompat_flags; +use crate::blob::flag::validate_incompat_flags; use crate::erofs::{blocks_to_bytes, EROFS_BLOCK_SIZE}; use crate::error::{Context, Error, Result}; use crate::utils::le::{read_u32_at, read_u64_at, write_u32_at, write_u64_at}; @@ -27,7 +27,7 @@ pub const NYDUS_BLOB_FOOTER_SIZE: usize = 4096; /// this boundary (the EROFS block size). pub const NYDUS_BLOB_FOOTER_ALIGNMENT: u64 = EROFS_BLOCK_SIZE as u64; -/// `flags` is split EROFS-style (see [`crate::blob::validate`]): the low 16 +/// `flags` is split EROFS-style (see [`crate::blob::flag`]): the low 16 /// bits are incompatible features (unknown bits reject), the high 16 bits /// are compatible features (unknown bits are ignored). No bits are defined /// yet. @@ -111,7 +111,7 @@ impl BlobFooter { blob_metadata_blocks, }; - footer.validate_fields()?; + footer.validate()?; footer.validate_layout(footer.offset()?)?; footer.crc32 = Self::compute_crc32(&footer.to_bytes()); Ok(footer) @@ -127,7 +127,7 @@ impl BlobFooter { /// offsets as untrusted hints whose reads are bounds-checked downstream. pub fn from_bytes(bytes: &[u8; NYDUS_BLOB_FOOTER_SIZE]) -> Result { let footer = Self { - magic: bytes[0..8].try_into().expect("slice checked"), + magic: bytes[0..8].try_into().unwrap(), version: read_u32_at(bytes, 8), flags: read_u32_at(bytes, 12), crc32: read_u32_at(bytes, 16), @@ -139,7 +139,7 @@ impl BlobFooter { bootstrap_blocks: read_u32_at(bytes, 56), blob_metadata_blocks: read_u32_at(bytes, 60), }; - footer.validate_fields()?; + footer.validate()?; // Verify over the raw incoming bytes, never over `to_bytes()`: a // re-serialization emits only the fields this reader knows, zeroing a @@ -231,7 +231,7 @@ impl BlobFooter { /// the reserved tail may carry a newer writer's compat fields (corruption /// is caught by the crc32), and `bootstrap_blocks` may be zero (an /// ondemand redirect blob embeds no bootstrap image). - fn validate_fields(&self) -> Result<()> { + fn validate(&self) -> Result<()> { if self.magic != NYDUS_BLOB_FOOTER_MAGIC { return Err(Error::InvalidImage( "invalid nydus footer magic".to_string(), diff --git a/nydus-format/src/blob/metadata.rs b/nydus-format/src/blob/metadata.rs index 8da14832ed4..988a381d221 100644 --- a/nydus-format/src/blob/metadata.rs +++ b/nydus-format/src/blob/metadata.rs @@ -1,65 +1,85 @@ -use crate::blob::validate::validate_incompat_flags; +use crate::blob::algorithm::{BlobMetadataCompressor, BlobMetadataDigester}; +use crate::blob::flag::validate_incompat_flags; use crate::erofs::EROFS_BLOCK_SIZE; use crate::error::{Context, Error, Result}; -use crate::utils::le::{read_u16_from, read_u32_from, read_u64_from}; +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}; -use std::fmt; use std::fs::File; -use std::io::{Cursor, Read, Write}; +use std::io::Write; use std::mem::{align_of, size_of}; 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. -pub const BLOB_METADATA_MAGIC: [u8; 8] = *b"LPBLMETA"; +/// 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 +/// (`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 by the incompat half of `flags` (unknown incompat -/// bits reject the file). -pub const BLOB_METADATA_VERSION: u32 = 1; -/// Fixed header 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). -pub const BLOB_METADATA_HEADER_SIZE: u64 = EROFS_BLOCK_SIZE as u64; -pub const BLOB_METADATA_DEFAULT_CHUNK_SIZE: u32 = 1024 * 1024; -pub const BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT: u32 = - BLOB_METADATA_DEFAULT_CHUNK_SIZE / EROFS_BLOCK_SIZE; +/// gets a new 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). +pub const NYDUS_BLOB_METADATA_HEADER_SIZE: usize = EROFS_BLOCK_SIZE as usize; + +/// On-disk size of one chunk entry in the chunk table. +pub const NYDUS_BLOB_METADATA_CHUNK_ENTRY_SIZE: usize = 48; + +/// On-disk size of one block group entry in the block group table. +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; + +/// 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; + /// Default block group uncompressed size. Equal to the default chunk size, so /// a default-geometry chunk always fits in one block group. -pub const BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE: u32 = BLOB_METADATA_DEFAULT_CHUNK_SIZE; -pub const BLOB_METADATA_DEFAULT_BLOCK_GROUP_BLOCK_COUNT: u32 = - BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE / EROFS_BLOCK_SIZE; +pub const NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE: u32 = + NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE; + +/// 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; + /// File-name suffix of a blob meta sidecar file (`.blob.meta`). -pub const BLOB_METADATA_SUFFIX: &str = ".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 BLOB_METADATA_MAX_BLOCK_BITS: u8 = 19; +const NYDUS_BLOB_METADATA_MAX_BLOCK_BITS: u8 = 19; -/// Range of the crc32 field in the header, for zeroing it when computing -/// the crc32 over the serialized metadata. -const BLOB_METADATA_HEADER_CRC32_FIELD: Range = 16..20; +/// Byte range of the crc32 field within the header. +const NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD: Range = 16..20; -/// Bytes of the header actually carrying fields; the rest of the 4 KiB -/// header block is a reserved compat area (writer-zeroed, reader-ignored). -const BLOB_METADATA_HEADER_FIELD_BYTES: usize = 56; -const BLOB_METADATA_BLOCK_GROUP_RESERVED: [u8; 6] = [0u8; 6]; -const BLOB_METADATA_CHUNK_RESERVED: u32 = 0; +const NYDUS_BLOB_METADATA_CHUNK_RESERVED: u32 = 0; +const NYDUS_BLOB_METADATA_BLOCK_GROUP_RESERVED: [u8; 6] = [0u8; 6]; bitflags! { - /// Feature bits, split EROFS-style: 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`). Record-layout evolution (wider - /// chunk/block group records, new record 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 — 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. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BlobMetadataFlags: u32 { const COMPRESSOR_ZSTD = 1 << 0; @@ -67,135 +87,185 @@ bitflags! { } } -const BLOB_METADATA_COMPRESSOR_MASK: u32 = BlobMetadataFlags::COMPRESSOR_ZSTD.bits(); -const BLOB_METADATA_DIGESTER_MASK: u32 = BlobMetadataFlags::DIGESTER_BLAKE3.bits(); +/// Every defined flag bit sits in the incompat half, so the full set doubles +/// as the supported-incompat set (unknown incompat bits reject the file). +const NYDUS_BLOB_METADATA_SUPPORTED_INCOMPAT: u32 = BlobMetadataFlags::all().bits(); -#[repr(u32)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum BlobMetadataCompressor { - None = 0, - Zstd = 1, +/// The fixed-size header leading the serialized metadata: the geometry and +/// table map of the file, sealed with a crc32c over the whole file. +/// +/// The header's own 4096 bytes (integers little-endian): +/// +/// ```text +/// offset size field +/// 0 8 magic b"LPBLMETA" +/// 8 4 version informational, never gated on +/// 12 4 flags low 16 incompat / high 16 compat +/// 16 4 crc32 crc32c of the whole serialized +/// metadata with this field treated +/// as zero +/// 20 4 reserved0 future compat field slot +/// 24 8 chunks_offset always 4096: the chunk table +/// starts right after the header +/// 32 8 block_groups_offset chunks_offset plus the chunk +/// 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 +/// 50 6 reserved1 writers zero it, readers ignore it +/// 56 4040 reserved writers zero it, readers ignore it +/// ``` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BlobMetadataHeader { + magic: [u8; 8], + version: u32, + flags: u32, + crc32: u32, + reserved0: u32, + chunks_offset: u64, + block_groups_offset: u64, + chunk_count: u32, + block_group_count: u32, + chunk_block_bits: u8, + block_group_block_bits: u8, } -impl BlobMetadataCompressor { - pub fn flag(self) -> BlobMetadataFlags { - match self { - Self::None => BlobMetadataFlags::empty(), - Self::Zstd => BlobMetadataFlags::COMPRESSOR_ZSTD, - } +impl BlobMetadataHeader { + fn from_bytes(bytes: &[u8; NYDUS_BLOB_METADATA_HEADER_SIZE]) -> Result { + let header = Self { + magic: bytes[0..8].try_into().unwrap(), + version: read_u32_at(bytes, 8), + flags: read_u32_at(bytes, 12), + crc32: read_u32_at(bytes, 16), + reserved0: read_u32_at(bytes, 20), + chunks_offset: read_u64_at(bytes, 24), + 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), + }; + + header.validate()?; + Ok(header) } -} -impl fmt::Display for BlobMetadataCompressor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None => f.write_str("none"), - Self::Zstd => f.write_str("zstd"), - } + 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); + write_u32_at(&mut data, 8, self.version); + write_u32_at(&mut data, 12, self.flags); + write_u32_at(&mut data, 16, self.crc32); + write_u32_at(&mut data, 20, self.reserved0); + write_u64_at(&mut data, 24, self.chunks_offset); + 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); + data } -} -impl TryFrom for BlobMetadataCompressor { - type Error = crate::error::Error; + 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 { + return Err(Error::InvalidImage(format!( + "blob meta chunk block bits too large: {}", + self.chunk_block_bits + ))); + } + + if self.block_group_block_bits > NYDUS_BLOB_METADATA_MAX_BLOCK_BITS { + return Err(Error::InvalidImage(format!( + "blob meta block group block bits too large: {}", + self.block_group_block_bits + ))); + } - fn try_from(value: BlobMetadataFlags) -> Result { - match value.bits() & BLOB_METADATA_COMPRESSOR_MASK { - 0 => Ok(Self::None), - bits if bits == BlobMetadataFlags::COMPRESSOR_ZSTD.bits() => Ok(Self::Zstd), - bits => Err(crate::error::Error::Unsupported(format!( - "unsupported blob meta compressor flag set: {bits:#x}" - ))), + if self.chunks_offset != NYDUS_BLOB_METADATA_HEADER_SIZE as u64 { + return Err(Error::InvalidImage(format!( + "invalid blob meta chunks offset: {}", + self.chunks_offset + ))); } - } -} -#[repr(u32)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum BlobMetadataDigester { - Blake3 = 1, -} + let expected_block_groups_offset = self + .chunks_offset + .checked_add(self.chunk_bytes()) + .ok_or_else(|| Error::Overflow("blob meta block group offset overflow".to_string()))?; -impl BlobMetadataDigester { - pub fn flag(self) -> BlobMetadataFlags { - match self { - Self::Blake3 => BlobMetadataFlags::DIGESTER_BLAKE3, + if self.block_groups_offset != expected_block_groups_offset { + return Err(Error::InvalidImage(format!( + "invalid blob meta block groups offset: {}", + self.block_groups_offset + ))); } - } -} -impl fmt::Display for BlobMetadataDigester { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Blake3 => f.write_str("blake3"), + if self.chunks_offset % align_of::() as u64 != 0 { + return Err(Error::InvalidImage( + "blob meta chunks offset is not aligned".to_string(), + )); } - } -} -impl TryFrom for BlobMetadataDigester { - type Error = crate::error::Error; - - fn try_from(value: BlobMetadataFlags) -> Result { - match value.bits() & BLOB_METADATA_DIGESTER_MASK { - bits if bits == BlobMetadataFlags::DIGESTER_BLAKE3.bits() => Ok(Self::Blake3), - 0 => Err(crate::error::Error::InvalidImage( - "blob meta digester flag is missing".to_string(), - )), - bits => Err(crate::error::Error::Unsupported(format!( - "unsupported blob meta digester flag set: {bits:#x}" - ))), + if self.block_groups_offset % align_of::() as u64 != 0 { + return Err(Error::InvalidImage( + "blob meta block groups offset is not aligned".to_string(), + )); } + + let flags = BlobMetadataFlags::from_bits_truncate(self.flags); + BlobMetadataDigester::try_from(flags)?; + validate_incompat_flags(self.flags, NYDUS_BLOB_METADATA_SUPPORTED_INCOMPAT)?; + Ok(()) } -} -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct BlobMetadataHeader { - magic: [u8; 8], - version: u32, - flags: u32, - crc32: u32, - reserved0: u32, - chunks_offset: u64, - block_groups_offset: u64, - chunk_count: u32, - block_group_count: u32, - /// log2 of the chunk size in 4 KiB blocks, EROFS-style (the same quantity - /// as `chunk_format & EROFS_CHUNK_FORMAT_BLKBITS_MASK`, i.e. `chunkbits - - /// blkbits`). Storing the exponent makes non-power-of-two chunk sizes - /// unrepresentable and feeds shift-based offset math directly. - chunk_block_bits: u8, - /// log2 of the per-block group block count, same representation as - /// `chunk_block_bits`. The read path maps a block to its block group with - /// `block >> block_group_block_bits`. - block_group_block_bits: u8, -} + 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(()) + } -const _: () = assert!(size_of::() == BLOB_METADATA_HEADER_FIELD_BYTES); + fn set_chunk_block_count(&mut self, blocks: u32) -> Result<()> { + self.chunk_block_bits = block_count_to_bits(blocks, "chunk")?; + Ok(()) + } -impl Default for BlobMetadataHeader { - fn default() -> Self { - Self { - magic: BLOB_METADATA_MAGIC, - version: BLOB_METADATA_VERSION, - flags: BlobMetadataDigester::Blake3.flag().bits(), - crc32: 0, - reserved0: 0, - chunks_offset: BLOB_METADATA_HEADER_SIZE, - block_groups_offset: BLOB_METADATA_HEADER_SIZE, - chunk_count: 0, - block_group_count: 0, - chunk_block_bits: BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT.trailing_zeros() as u8, - block_group_block_bits: BLOB_METADATA_DEFAULT_BLOCK_GROUP_BLOCK_COUNT.trailing_zeros() - as u8, - } + 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(); } -} -impl BlobMetadataHeader { pub fn version(&self) -> u32 { self.version } + pub fn flags(&self) -> BlobMetadataFlags { + BlobMetadataFlags::from_bits_truncate(self.flags) + } + + pub fn crc32(&self) -> u32 { + self.crc32 + } + + pub fn compressor(&self) -> BlobMetadataCompressor { + BlobMetadataCompressor::from(self.flags()) + } + + pub fn digester(&self) -> BlobMetadataDigester { + BlobMetadataDigester::try_from(self.flags()).expect("validated blob meta digester") + } + pub fn chunk_count(&self) -> u32 { self.chunk_count } @@ -209,38 +279,23 @@ impl BlobMetadataHeader { 1u32 << self.chunk_block_bits } + pub fn chunk_size(&self) -> u32 { + EROFS_BLOCK_SIZE << self.chunk_block_bits + } + /// log2 of the per-block group block count. pub fn block_group_block_bits(&self) -> u8 { self.block_group_block_bits } - /// 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`. + /// 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 } - pub fn chunk_size(&self) -> u32 { - EROFS_BLOCK_SIZE << self.chunk_block_bits - } - - pub fn flags(&self) -> BlobMetadataFlags { - self.validated_flags().expect("validated blob meta flags") - } - - pub fn crc32(&self) -> u32 { - self.crc32 - } - - pub fn compressor(&self) -> BlobMetadataCompressor { - BlobMetadataCompressor::try_from(self.flags()).expect("validated blob meta compressor") - } - - pub fn digester(&self) -> BlobMetadataDigester { - BlobMetadataDigester::try_from(self.flags()).expect("validated blob meta digester") - } - pub fn chunks_offset(&self) -> u64 { self.chunks_offset } @@ -257,160 +312,173 @@ impl BlobMetadataHeader { self.block_group_count as u64 * size_of::() as u64 } - /// End offset of the record region (header plus chunk and block group tables), - /// before padding to the block-aligned `metadata_size`. - pub fn records_end(&self) -> 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() } pub fn metadata_size(&self) -> u64 { - crate::utils::align_up(self.records_end(), EROFS_BLOCK_SIZE as u64) + crate::utils::align_up(self.entries_end(), EROFS_BLOCK_SIZE as u64) .expect("blob meta size overflowed") } +} - 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 = BLOB_METADATA_HEADER_SIZE; - 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(()) +/// One chunk entry: the digest of a fixed-size span of the blob's dense +/// uncompressed address space. Chunk entries are packed back to back in the +/// chunk table right after the header. +/// +/// The entry's 48 bytes (integers little-endian): +/// +/// ```text +/// offset size field +/// 0 32 digest of the chunk's uncompressed +/// bytes, algorithm per the +/// digester flag +/// 32 8 uncompressed_block_offset 4KiB blocks +/// 40 4 uncompressed_block_count 4KiB blocks, never zero +/// 44 4 reserved must be zero +/// ``` +/// +/// The Rust layout is pinned to the on-disk layout (`repr(C)` plus the const +/// size assert) so a mapped chunk table is readable in place, zero-copy. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BlobMetadataChunk { + digest: [u8; 32], + uncompressed_block_offset: u64, + uncompressed_block_count: u32, + reserved: u32, +} + +const _: () = assert!(size_of::() == NYDUS_BLOB_METADATA_CHUNK_ENTRY_SIZE); + +impl BlobMetadataChunk { + /// Creates a validated chunk entry, so a constructed chunk is valid by + /// definition. + pub fn new( + digest: [u8; 32], + uncompressed_block_offset: u64, + uncompressed_block_count: u32, + ) -> Result { + let chunk = Self { + digest, + uncompressed_block_offset, + uncompressed_block_count, + reserved: NYDUS_BLOB_METADATA_CHUNK_RESERVED, + }; + + chunk.validate()?; + Ok(chunk) } - fn set_chunk_block_count(&mut self, blocks: u32) -> Result<()> { - self.chunk_block_bits = block_count_to_bits(blocks, "chunk")?; - Ok(()) + /// Parse a chunk entry from exactly its 48 bytes, verifying the + /// intrinsic fields. + pub fn from_bytes(bytes: &[u8; NYDUS_BLOB_METADATA_CHUNK_ENTRY_SIZE]) -> Result { + let chunk = Self { + digest: bytes[0..32].try_into().unwrap(), + uncompressed_block_offset: read_u64_at(bytes, 32), + uncompressed_block_count: read_u32_at(bytes, 40), + reserved: read_u32_at(bytes, 44), + }; + + chunk.validate()?; + Ok(chunk) } - 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(); + /// Serialize the chunk entry into its on-disk bytes. + fn to_bytes(self) -> [u8; NYDUS_BLOB_METADATA_CHUNK_ENTRY_SIZE] { + let mut data = [0u8; NYDUS_BLOB_METADATA_CHUNK_ENTRY_SIZE]; + data[0..32].copy_from_slice(&self.digest); + write_u64_at(&mut data, 32, self.uncompressed_block_offset); + write_u32_at(&mut data, 40, self.uncompressed_block_count); + write_u32_at(&mut data, 44, self.reserved); + data } + /// 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. fn validate(&self) -> Result<()> { - if self.magic != BLOB_METADATA_MAGIC { - return Err(Error::InvalidImage("invalid blob meta magic".to_string())); - } - // `version` is informational and deliberately not gated on: - // compatibility is carried by the magic and the incompat flag bits. - // `reserved0` is likewise not enforced to zero: it is a future - // compat-field slot, and corruption is caught by the file crc32c. - if self.chunk_block_bits > BLOB_METADATA_MAX_BLOCK_BITS { - return Err(Error::InvalidImage(format!( - "blob meta chunk block bits too large: {}", - self.chunk_block_bits - ))); - } - if self.block_group_block_bits > BLOB_METADATA_MAX_BLOCK_BITS { - return Err(Error::InvalidImage(format!( - "blob meta block group block bits too large: {}", - self.block_group_block_bits - ))); - } - self.validated_flags()?; - if self.chunks_offset != BLOB_METADATA_HEADER_SIZE { - return Err(Error::InvalidImage(format!( - "invalid blob meta chunks offset: {}", - self.chunks_offset - ))); - } - let expected_block_groups_offset = self - .chunks_offset - .checked_add(self.chunk_bytes()) - .ok_or_else(|| Error::Overflow("blob meta block group offset overflow".to_string()))?; - if self.block_groups_offset != expected_block_groups_offset { - return Err(Error::InvalidImage(format!( - "invalid blob meta block groups offset: {}", - self.block_groups_offset - ))); - } - if self.chunks_offset % align_of::() as u64 != 0 { + if self.uncompressed_block_count == 0 { return Err(Error::InvalidImage( - "blob meta chunks offset is not aligned".to_string(), + "blob meta chunk uncompressed block count must be non-zero".to_string(), )); } - if self.block_groups_offset % align_of::() as u64 != 0 { + + if self.reserved != NYDUS_BLOB_METADATA_CHUNK_RESERVED { return Err(Error::InvalidImage( - "blob meta block_groups offset is not aligned".to_string(), + "blob meta chunk reserved field must be zero".to_string(), )); } + + self.uncompressed_offset() + .checked_add(self.uncompressed_size()) + .ok_or_else(|| Error::Overflow("blob meta chunk byte range overflow".to_string()))?; + Ok(()) } - fn validated_flags(&self) -> Result { - // EROFS-style feature gating: unknown incompat (low-half) bits mean - // the file cannot be read correctly and must be rejected; unknown - // compat (high-half) bits are ignored. - validate_incompat_flags(self.flags, BlobMetadataFlags::all().bits())?; - let flags = BlobMetadataFlags::from_bits_truncate(self.flags); - BlobMetadataCompressor::try_from(flags)?; - BlobMetadataDigester::try_from(flags)?; - Ok(flags) + /// Write the chunk entry's on-disk bytes to `writer`. + pub fn write_to(&self, writer: &mut dyn Write) -> Result<()> { + writer.write_all(&self.to_bytes())?; + Ok(()) } - fn write_to_with_crc32(&self, writer: &mut dyn Write, crc32: u32) -> Result<()> { - writer.write_all(&self.to_bytes_with_crc32(crc32))?; - Ok(()) + pub fn digest(&self) -> &[u8; 32] { + &self.digest } - fn to_bytes_with_crc32(self, crc32: u32) -> [u8; BLOB_METADATA_HEADER_SIZE as usize] { - let mut data = [0u8; BLOB_METADATA_HEADER_SIZE as usize]; - data[0..8].copy_from_slice(&self.magic); - data[8..12].copy_from_slice(&self.version.to_le_bytes()); - data[12..16].copy_from_slice(&self.flags.to_le_bytes()); - data[BLOB_METADATA_HEADER_CRC32_FIELD].copy_from_slice(&crc32.to_le_bytes()); - data[20..24].copy_from_slice(&self.reserved0.to_le_bytes()); - data[24..32].copy_from_slice(&self.chunks_offset.to_le_bytes()); - data[32..40].copy_from_slice(&self.block_groups_offset.to_le_bytes()); - data[40..44].copy_from_slice(&self.chunk_count.to_le_bytes()); - data[44..48].copy_from_slice(&self.block_group_count.to_le_bytes()); - data[48] = self.chunk_block_bits; - data[49] = self.block_group_block_bits; - // data[50..56] stays zero: reserved after the two u8 exponents. - // data[56..4096] stays zero: reserved header tail. - data + /// 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. + pub fn uncompressed_block_offset(&self) -> u64 { + self.uncompressed_block_offset } - fn read_from(reader: &mut dyn Read) -> Result { - let header = Self { - magic: read_magic(reader)?, - version: read_u32_from(reader)?, - flags: read_u32_from(reader)?, - crc32: read_u32_from(reader)?, - reserved0: read_u32_from(reader)?, - chunks_offset: read_u64_from(reader)?, - block_groups_offset: read_u64_from(reader)?, - chunk_count: read_u32_from(reader)?, - block_group_count: read_u32_from(reader)?, - chunk_block_bits: read_u8(reader)?, - block_group_block_bits: { - let bits = read_u8(reader)?; - // Skip the 6 reserved bytes after the two u8 exponents. - let mut pad = [0u8; 6]; - reader.read_exact(&mut pad)?; - bits - }, - }; - // The rest of the header block is reserved for future compat fields. - // Writers zero it, but readers deliberately do not enforce that - // (EROFS-style): a newer writer may have placed compat fields here - // that this reader ignores. Corruption is caught by the file crc32c. - let mut tail = [0u8; BLOB_METADATA_HEADER_SIZE as usize - BLOB_METADATA_HEADER_FIELD_BYTES]; - reader.read_exact(&mut tail)?; - header.validate()?; - Ok(header) + pub fn uncompressed_block_count(&self) -> u32 { + self.uncompressed_block_count + } + + pub fn uncompressed_offset(&self) -> u64 { + self.uncompressed_block_offset * EROFS_BLOCK_SIZE as u64 + } + + pub fn uncompressed_size(&self) -> u64 { + self.uncompressed_block_count as u64 * EROFS_BLOCK_SIZE as u64 } } +/// One block group entry: how a span of the dense uncompressed address +/// space maps onto the blob's encoded payload — the unit of decode, cache +/// fill, and prefetch. Block group entries are packed back to back in the +/// block group table right after the chunk table. +/// +/// The entry's 40 bytes (integers little-endian): +/// +/// ```text +/// offset size field +/// 0 8 uncompressed_block_offset 4KiB blocks +/// 8 8 compressed_offset bytes: payloads pack back to +/// back, no block alignment +/// 16 4 uncompressed_block_count 4KiB blocks, never zero +/// 20 4 compressed_size bytes, never zero +/// 24 4 crc32 crc32c of the uncompressed +/// payload +/// 28 4 source_block_group_index redirect only, else zero +/// 32 2 source_blob_index non-zero marks a redirect +/// (see [`Self::new_redirect`]) +/// 34 6 reserved must be zero +/// ``` +/// +/// The Rust layout is pinned to the on-disk layout (`repr(C)` plus the const +/// size assert) so a mapped block group table is readable in place, +/// zero-copy. #[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BlobMetadataBlockGroup { uncompressed_block_offset: u64, - compressed_byte_offset: u64, + compressed_offset: u64, uncompressed_block_count: u32, compressed_size: u32, crc32: u32, @@ -419,245 +487,216 @@ pub struct BlobMetadataBlockGroup { reserved: [u8; 6], } -const _: () = assert!(size_of::() == 40); +const _: () = + assert!(size_of::() == NYDUS_BLOB_METADATA_BLOCK_GROUP_ENTRY_SIZE); impl BlobMetadataBlockGroup { pub fn new( uncompressed_block_offset: u64, uncompressed_block_count: u32, - compressed_byte_offset: u64, + compressed_offset: u64, compressed_size: u32, crc32: u32, ) -> Result { let block_group = Self { uncompressed_block_offset, - compressed_byte_offset, + compressed_offset, uncompressed_block_count, compressed_size, crc32, source_block_group_index: 0, source_blob_index: 0, - reserved: BLOB_METADATA_BLOCK_GROUP_RESERVED, + reserved: NYDUS_BLOB_METADATA_BLOCK_GROUP_RESERVED, }; + block_group.validate()?; Ok(block_group) } - /// A redirect block group carries data that belongs to another (source) blob. - /// At prefetch time the decoded bytes are written into the source blob's - /// cache instead of this blob's own cache. `source_blob_index` is the - /// 1-based blob index from the bootstrap device table and must be - /// non-zero; `crc32` must equal the source block group's crc32 so the redirect - /// can be cross-checked before filling the source cache. #[allow(clippy::too_many_arguments)] pub fn new_redirect( uncompressed_block_offset: u64, uncompressed_block_count: u32, - compressed_byte_offset: u64, + compressed_offset: u64, compressed_size: u32, crc32: u32, source_blob_index: u16, - source_block_group_index: u32, - ) -> Result { - if source_blob_index == 0 { - return Err(Error::InvalidImage( - "blob meta redirect block_group source blob index must be non-zero".to_string(), - )); - } - let block_group = Self { - uncompressed_block_offset, - compressed_byte_offset, - uncompressed_block_count, - compressed_size, - crc32, - source_block_group_index, - source_blob_index, - reserved: BLOB_METADATA_BLOCK_GROUP_RESERVED, - }; - block_group.validate()?; - Ok(block_group) - } - - pub fn is_redirect(&self) -> bool { - self.source_blob_index != 0 - } - - pub fn source_blob_index(&self) -> u16 { - self.source_blob_index - } - - pub fn source_block_group_index(&self) -> u32 { - self.source_block_group_index - } - - pub fn uncompressed_block_offset(&self) -> u64 { - self.uncompressed_block_offset - } - - pub fn uncompressed_block_count(&self) -> u32 { - self.uncompressed_block_count - } - - pub fn compressed_size(&self) -> u32 { - self.compressed_size - } - - pub fn crc32(&self) -> u32 { - self.crc32 - } - - pub fn uncompressed_byte_offset(&self) -> u64 { - self.uncompressed_block_offset - .checked_mul(EROFS_BLOCK_SIZE as u64) - .expect("validated blob meta block_group byte offset") - } - - pub fn uncompressed_byte_size(&self) -> u64 { - self.uncompressed_block_count as u64 * EROFS_BLOCK_SIZE as u64 - } - - pub fn uncompressed_byte_end(&self) -> u64 { - self.uncompressed_byte_offset() + self.uncompressed_byte_size() - } - - /// Byte offset of this block group's encoded payload within the blob data region. - /// Block groups are packed back-to-back, so this is a plain byte position and is - /// not block-aligned for compressed block groups. - pub fn compressed_byte_offset(&self) -> u64 { - self.compressed_byte_offset - } + source_block_group_index: u32, + ) -> Result { + let block_group = Self { + uncompressed_block_offset, + compressed_offset, + uncompressed_block_count, + compressed_size, + crc32, + source_block_group_index, + source_blob_index, + reserved: NYDUS_BLOB_METADATA_BLOCK_GROUP_RESERVED, + }; - pub fn compressed_byte_end(&self) -> u64 { - self.compressed_byte_offset + self.compressed_size as u64 + block_group.validate_redirect()?; + Ok(block_group) } - pub fn with_compressed_byte_offset_bias(&self, byte_bias: u64) -> Result { + pub fn from_bytes(bytes: &[u8; NYDUS_BLOB_METADATA_BLOCK_GROUP_ENTRY_SIZE]) -> Result { let block_group = Self { - compressed_byte_offset: self - .compressed_byte_offset() - .checked_add(byte_bias) - .ok_or_else(|| { - Error::Overflow("blob meta compressed byte offset overflow".to_string()) - })?, - ..*self + uncompressed_block_offset: read_u64_at(bytes, 0), + compressed_offset: read_u64_at(bytes, 8), + uncompressed_block_count: read_u32_at(bytes, 16), + compressed_size: read_u32_at(bytes, 20), + crc32: read_u32_at(bytes, 24), + source_block_group_index: read_u32_at(bytes, 28), + source_blob_index: read_u16_at(bytes, 32), + reserved: bytes[34..40].try_into().unwrap(), }; + block_group.validate()?; Ok(block_group) } - pub fn write_to(&self, writer: &mut dyn Write) -> Result<()> { - self.validate()?; - writer.write_all(&self.to_bytes())?; - Ok(()) - } - - fn to_bytes(self) -> [u8; 40] { - let mut data = [0u8; 40]; - data[0..8].copy_from_slice(&self.uncompressed_block_offset.to_le_bytes()); - data[8..16].copy_from_slice(&self.compressed_byte_offset.to_le_bytes()); - data[16..20].copy_from_slice(&self.uncompressed_block_count.to_le_bytes()); - data[20..24].copy_from_slice(&self.compressed_size.to_le_bytes()); - data[24..28].copy_from_slice(&self.crc32.to_le_bytes()); - data[28..32].copy_from_slice(&self.source_block_group_index.to_le_bytes()); - data[32..34].copy_from_slice(&self.source_blob_index.to_le_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); + write_u64_at(&mut data, 8, self.compressed_offset); + write_u32_at(&mut data, 16, self.uncompressed_block_count); + write_u32_at(&mut data, 20, self.compressed_size); + write_u32_at(&mut data, 24, self.crc32); + write_u32_at(&mut data, 28, self.source_block_group_index); + write_u16_at(&mut data, 32, self.source_blob_index); data[34..40].copy_from_slice(&self.reserved); data } - pub fn read_from(reader: &mut dyn Read) -> Result { - let block_group = Self { - uncompressed_block_offset: read_u64_from(reader)?, - compressed_byte_offset: read_u64_from(reader)?, - uncompressed_block_count: read_u32_from(reader)?, - compressed_size: read_u32_from(reader)?, - crc32: read_u32_from(reader)?, - source_block_group_index: read_u32_from(reader)?, - source_blob_index: read_u16_from(reader)?, - reserved: read_block_group_reserved(reader)?, - }; - block_group.validate()?; - Ok(block_group) - } - fn validate(&self) -> Result<()> { if self.uncompressed_block_count == 0 { return Err(Error::InvalidImage( - "blob meta block_group uncompressed block count must be non-zero".to_string(), + "blob meta block group uncompressed block count must be non-zero".to_string(), )); } + if self.compressed_size == 0 { return Err(Error::InvalidImage( - "blob meta block_group compressed size must be non-zero".to_string(), + "blob meta block group compressed size must be non-zero".to_string(), + )); + } + + if self.source_blob_index == 0 && self.source_block_group_index != 0 { + return Err(Error::InvalidImage( + "blob meta block group source block group index requires a source blob index" + .to_string(), + )); + } + + if self.reserved != NYDUS_BLOB_METADATA_BLOCK_GROUP_RESERVED { + return Err(Error::InvalidImage( + "blob meta block group reserved field must be zero".to_string(), )); } + self.uncompressed_block_offset .checked_mul(EROFS_BLOCK_SIZE as u64) .ok_or_else(|| { Error::Overflow( - "blob meta block_group uncompressed byte offset overflow".to_string(), + "blob meta block group uncompressed byte offset overflow".to_string(), ) })?; - self.uncompressed_byte_offset() - .checked_add(self.uncompressed_byte_size()) + + self.uncompressed_offset() + .checked_add(self.uncompressed_size()) .ok_or_else(|| { Error::Overflow( - "blob meta block_group uncompressed byte range overflow".to_string(), + "blob meta block group uncompressed byte range overflow".to_string(), ) })?; - self.compressed_byte_offset + + self.compressed_offset .checked_add(self.compressed_size as u64) .ok_or_else(|| { Error::Overflow("blob meta block group compressed byte range overflow".to_string()) })?; - if self.source_blob_index == 0 && self.source_block_group_index != 0 { + + Ok(()) + } + + fn validate_redirect(&self) -> Result<()> { + if self.source_blob_index == 0 { return Err(Error::InvalidImage( - "blob meta block_group source block_group index requires a source blob index" - .to_string(), + "blob meta redirect block group source blob index must be non-zero".to_string(), + )); + } + + if self.uncompressed_block_count == 0 { + return Err(Error::InvalidImage( + "blob meta block group uncompressed block count must be non-zero".to_string(), + )); + } + + if self.compressed_size == 0 { + return Err(Error::InvalidImage( + "blob meta block group compressed size must be non-zero".to_string(), )); } - if self.reserved != BLOB_METADATA_BLOCK_GROUP_RESERVED { + + if self.reserved != NYDUS_BLOB_METADATA_BLOCK_GROUP_RESERVED { return Err(Error::InvalidImage( - "blob meta block_group reserved field must be zero".to_string(), + "blob meta block group reserved field must be zero".to_string(), )); } + + self.uncompressed_block_offset + .checked_mul(EROFS_BLOCK_SIZE as u64) + .ok_or_else(|| { + Error::Overflow( + "blob meta block group uncompressed byte offset overflow".to_string(), + ) + })?; + + self.uncompressed_offset() + .checked_add(self.uncompressed_size()) + .ok_or_else(|| { + Error::Overflow( + "blob meta block group uncompressed byte range overflow".to_string(), + ) + })?; + + self.compressed_offset + .checked_add(self.compressed_size as u64) + .ok_or_else(|| { + Error::Overflow("blob meta block group compressed byte range overflow".to_string()) + })?; + Ok(()) } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct BlobMetadataChunk { - digest: [u8; 32], - uncompressed_block_offset: u64, - uncompressed_block_count: u32, - reserved: u32, -} -const _: () = assert!(size_of::() == 48); + pub fn write_to(&self, writer: &mut dyn Write) -> Result<()> { + writer.write_all(&self.to_bytes())?; + Ok(()) + } -impl BlobMetadataChunk { - pub fn new( - digest: [u8; 32], - uncompressed_block_offset: u64, - uncompressed_block_count: u32, - ) -> Result { - let chunk = Self { - digest, - uncompressed_block_offset, - uncompressed_block_count, - reserved: BLOB_METADATA_CHUNK_RESERVED, + 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(|| { + Error::Overflow("blob meta compressed byte offset overflow".to_string()) + })?, + ..*self }; - chunk.validate()?; - Ok(chunk) + + block_group.validate()?; + Ok(block_group) } - pub fn digest(&self) -> &[u8; 32] { - &self.digest + pub fn is_redirect(&self) -> bool { + self.source_blob_index != 0 + } + + pub fn source_blob_index(&self) -> u16 { + self.source_blob_index + } + + pub fn source_block_group_index(&self) -> u32 { + self.source_block_group_index } - /// 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. pub fn uncompressed_block_offset(&self) -> u64 { self.uncompressed_block_offset } @@ -666,55 +705,26 @@ impl BlobMetadataChunk { self.uncompressed_block_count } - pub fn uncompressed_byte_offset(&self) -> u64 { - self.uncompressed_block_offset * EROFS_BLOCK_SIZE as u64 + 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") } - pub fn uncompressed_byte_size(&self) -> u64 { + pub fn uncompressed_size(&self) -> u64 { self.uncompressed_block_count as u64 * EROFS_BLOCK_SIZE as u64 } - pub fn write_to(&self, writer: &mut dyn Write) -> Result<()> { - self.validate()?; - writer.write_all(&self.to_bytes())?; - Ok(()) - } - - fn to_bytes(self) -> [u8; 48] { - let mut data = [0u8; 48]; - data[0..32].copy_from_slice(&self.digest); - data[32..40].copy_from_slice(&self.uncompressed_block_offset.to_le_bytes()); - data[40..44].copy_from_slice(&self.uncompressed_block_count.to_le_bytes()); - data[44..48].copy_from_slice(&self.reserved.to_le_bytes()); - data + pub fn compressed_offset(&self) -> u64 { + self.compressed_offset } - pub fn read_from(reader: &mut dyn Read) -> Result { - let chunk = Self { - digest: read_digest(reader)?, - uncompressed_block_offset: read_u64_from(reader)?, - uncompressed_block_count: read_u32_from(reader)?, - reserved: read_u32_from(reader)?, - }; - chunk.validate()?; - Ok(chunk) + pub fn compressed_size(&self) -> u32 { + self.compressed_size } - fn validate(&self) -> Result<()> { - if self.uncompressed_block_count == 0 { - return Err(Error::InvalidImage( - "blob meta chunk uncompressed block count must be non-zero".to_string(), - )); - } - self.uncompressed_byte_offset() - .checked_add(self.uncompressed_byte_size()) - .ok_or_else(|| Error::Overflow("blob meta chunk byte range overflow".to_string()))?; - if self.reserved != BLOB_METADATA_CHUNK_RESERVED { - return Err(Error::InvalidImage( - "blob meta chunk reserved field must be zero".to_string(), - )); - } - Ok(()) + pub fn crc32(&self) -> u32 { + self.crc32 } } @@ -726,6 +736,25 @@ enum BlobMetadataStorage { Mapped(Mmap), } +/// A nydus blob's metadata: the chunk digest table and the block group table +/// describing how the blob's dense uncompressed address space maps onto its +/// encoded payload, sealed with a crc32c in the header. +/// +/// Serialized, it is the `.blob.meta` sidecar file — and, embedded verbatim, +/// the blob meta region of a full blob (see [`super::footer::BlobFooter`]): +/// +/// ```text +/// ┌────────┬─────────────┬───────────────────┬──────────────┐ +/// │ header │ chunk table │ block group table │ zero padding │ +/// └────────┴─────────────┴───────────────────┴──────────────┘ +/// 0 4096 ▲ EOF +/// the entries end here; the padding +/// runs to the 4KiB-aligned metadata_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. pub struct BlobMetadata { header: BlobMetadataHeader, blob_id: [u8; SHA256_DIGEST_SIZE], @@ -733,6 +762,8 @@ pub struct BlobMetadata { } 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, @@ -748,6 +779,10 @@ impl BlobMetadata { ) } + /// 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, @@ -755,7 +790,21 @@ impl BlobMetadata { block_groups: Vec, chunks: Vec, ) -> Result { - let mut header = BlobMetadataHeader::default(); + // Zeros are placeholders: the setters below stamp the real layout + // and geometry before the header is sealed. + let mut header = BlobMetadataHeader { + magic: NYDUS_BLOB_METADATA_MAGIC, + version: NYDUS_BLOB_METADATA_VERSION, + flags: BlobMetadataDigester::Blake3.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, + }; 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)?; @@ -763,28 +812,177 @@ impl BlobMetadata { validate_tables(&block_groups, &chunks, header.block_group_block_count())?; let mut blob_metadata = Self { header, - blob_id, - storage: BlobMetadataStorage::Owned { - chunks, - block_groups, - }, - }; - blob_metadata.header.crc32 = blob_metadata.compute_crc32_from_parts(); - Ok(blob_metadata) + blob_id, + storage: BlobMetadataStorage::Owned { + chunks, + block_groups, + }, + }; + 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 { + 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 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 { + header, + blob_id, + storage: BlobMetadataStorage::Owned { + chunks, + block_groups, + }, + }) + } + + fn load_inner(path: &Path, check_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() { + return Err(Error::InvalidImage(format!( + "blob meta file size mismatch: expected {}, got {}", + header.metadata_size(), + file_len + ))); + } + validate_padding(&mmap, &header)?; + if check_crc32 { + validate_blob_metadata_crc32(&mmap, &header)?; + } + 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), + }) } - pub fn with_compressed_offset_bias(&self, bias: u64) -> Result { - let mut block_groups = Vec::with_capacity(self.block_group_count()); + /// Write the serialized metadata (header, tables, zero padding) to + /// `writer`. + 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_groups.push(block_group.with_compressed_byte_offset_bias(bias)?); + block_group.write_to(writer)?; } - Self::from_parts_with_options( - self.blob_id, - self.chunk_block_count(), - self.compressor(), - block_groups, - self.chunks().to_vec(), - ) + let padding_size = self.padding_size(); + if padding_size > 0 { + writer.write_all(&vec![0u8; padding_size])?; + } + Ok(()) + } + + /// Write the serialized metadata to a new sidecar file at `path`. + pub fn save(&self, path: &Path) -> Result<()> { + let mut file = File::create(path) + .with_context(|| format!("failed to create blob meta: {}", path.display()))?; + self.write_to(&mut file)?; + file.flush() + .with_context(|| format!("failed to flush blob meta: {}", path.display()))?; + Ok(()) } pub fn header(&self) -> &BlobMetadataHeader { @@ -837,8 +1035,8 @@ impl BlobMetadata { 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. + /// 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() @@ -856,13 +1054,14 @@ 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 - /// compress 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 + /// 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_byte_offset(&self, offset: u64) -> Option { + pub fn block_group_index_for_offset(&self, offset: u64) -> Option { let block = offset / EROFS_BLOCK_SIZE as u64; if block >= self.total_blocks() { return None; @@ -884,26 +1083,29 @@ impl BlobMetadata { /// 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 records and padding. The reader verifies the raw + /// 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; BLOB_METADATA_HEADER_SIZE as usize] = data - [..BLOB_METADATA_HEADER_SIZE as usize] + let mut header: [u8; NYDUS_BLOB_METADATA_HEADER_SIZE] = data + [..NYDUS_BLOB_METADATA_HEADER_SIZE] .try_into() .expect("caller checked the header length"); - header[BLOB_METADATA_HEADER_CRC32_FIELD].fill(0); - crc32c_append(crc32c(&header), &data[BLOB_METADATA_HEADER_SIZE as usize..]) + 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. fn compute_crc32_from_parts(&self) -> u32 { - // Seal over the serialized metadata with the crc field zeroed; the - // header bytes seed the running crc32c that continues over the - // records and padding. - let mut crc32 = crc32c(&self.header.to_bytes_with_crc32(0)); + let mut header = self.header.to_bytes(); + header[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD].fill(0); + let mut crc32 = crc32c(&header); for chunk in self.chunks() { crc32 = crc32c_append(crc32, &chunk.to_bytes()); } @@ -921,127 +1123,7 @@ impl BlobMetadata { } fn padding_size(&self) -> usize { - (self.metadata_size() - self.header.records_end()) as usize - } - - pub fn write_to(&self, writer: &mut dyn Write) -> Result<()> { - self.header - .write_to_with_crc32(writer, self.compute_crc32_from_parts())?; - 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])?; - } - Ok(()) - } - - pub fn save(&self, path: &Path) -> Result<()> { - let mut file = File::create(path) - .with_context(|| format!("failed to create blob meta: {}", path.display()))?; - self.write_to(&mut file)?; - file.flush() - .with_context(|| format!("failed to flush blob meta: {}", path.display()))?; - Ok(()) - } - - /// Start configuring a blob meta read; finish with - /// [`load`](BlobMetadataLoader::load) or [`from_bytes`](BlobMetadataLoader::from_bytes). - pub fn loader() -> BlobMetadataLoader { - BlobMetadataLoader::default() - } - - fn from_bytes_inner( - data: &[u8], - blob_id: [u8; SHA256_DIGEST_SIZE], - check_crc32: bool, - ) -> Result { - if data.len() < BLOB_METADATA_HEADER_SIZE as usize { - return Err(Error::InvalidImage("blob meta data too small".to_string())); - } - - let mut cursor = Cursor::new(data); - let header = BlobMetadataHeader::read_from(&mut cursor)?; - 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); - cursor.set_position(header.chunks_offset()); - for index in 0..header.chunk_count() as usize { - chunks.push( - BlobMetadataChunk::read_from(&mut cursor) - .with_context(|| format!("failed to read blob meta chunk {index}"))?, - ); - } - - let mut block_groups = Vec::with_capacity(header.block_group_count() as usize); - cursor.set_position(header.block_groups_offset()); - for index in 0..header.block_group_count() as usize { - block_groups.push( - BlobMetadataBlockGroup::read_from(&mut cursor) - .with_context(|| format!("failed to read blob meta block_group {index}"))?, - ); - } - validate_tables(&block_groups, &chunks, header.block_group_block_count())?; - Ok(Self { - header, - blob_id, - storage: BlobMetadataStorage::Owned { - chunks, - block_groups, - }, - }) - } - - pub fn load(path: &Path) -> Result { - Self::load_inner(path, false) - } - - fn load_inner(path: &Path, check_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 < BLOB_METADATA_HEADER_SIZE { - 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 mut cursor = Cursor::new(&mmap[..BLOB_METADATA_HEADER_SIZE as usize]); - let header = BlobMetadataHeader::read_from(&mut cursor)?; - if file_len != header.metadata_size() { - return Err(Error::InvalidImage(format!( - "blob meta file size mismatch: expected {}, got {}", - header.metadata_size(), - file_len - ))); - } - validate_padding(&mmap, &header)?; - if check_crc32 { - validate_blob_metadata_crc32(&mmap, &header)?; - } - 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), - }) + (self.metadata_size() - self.header.entries_end()) as usize } } @@ -1098,7 +1180,7 @@ fn block_count_to_bits(blocks: u32, what: &str) -> Result { ))); } let bits = blocks.trailing_zeros() as u8; - if bits > BLOB_METADATA_MAX_BLOCK_BITS { + if bits > NYDUS_BLOB_METADATA_MAX_BLOCK_BITS { return Err(Error::InvalidImage(format!( "blob meta {what} block count too large: {blocks}" ))); @@ -1106,8 +1188,32 @@ fn block_count_to_bits(blocks: u32, what: &str) -> Result { 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.records_end() as usize; + 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(), @@ -1137,58 +1243,36 @@ fn validate_tables( validate_chunks(block_groups, chunks) } -/// 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 = 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_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(), + "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_byte_offset` is - // never used on them. Only the dense-layout and compressed-overlap - // invariants apply. + // 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_byte_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}"))?; + .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 compress 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. + // 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 { @@ -1204,10 +1288,11 @@ fn validate_block_groups( ))); } } - // 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_byte_offset() < previous_compressed_byte_end { + // 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}" ))); @@ -1217,10 +1302,11 @@ fn validate_block_groups( .checked_add(block_group.uncompressed_block_count() as u64) .ok_or_else(|| { Error::Overflow( - "blob meta block_group uncompressed block range overflow".to_string(), + "blob meta block group uncompressed block range overflow".to_string(), ) })?; - previous_compressed_byte_end = block_group.compressed_byte_end(); + previous_compressed_end = + block_group.compressed_offset() + block_group.compressed_size() as u64; } Ok(()) } @@ -1239,8 +1325,8 @@ fn validate_chunks( 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. + // 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) @@ -1257,14 +1343,14 @@ fn validate_chunks( fn block_groups_total_uncompressed_size(block_groups: &[BlobMetadataBlockGroup]) -> u64 { block_groups .last() - .map(BlobMetadataBlockGroup::uncompressed_byte_end) + .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(BlobMetadataBlockGroup::compressed_byte_end) + .map(|block_group| block_group.compressed_offset() + block_group.compressed_size() as u64) .unwrap_or(0) } @@ -1287,30 +1373,6 @@ fn mapped_block_groups<'a>( unsafe { std::slice::from_raw_parts(ptr, header.block_group_count() as usize) } } -fn read_u8(reader: &mut dyn Read) -> Result { - let mut buf = [0u8; 1]; - reader.read_exact(&mut buf)?; - Ok(buf[0]) -} - -fn read_magic(reader: &mut dyn Read) -> Result<[u8; 8]> { - let mut buf = [0u8; 8]; - reader.read_exact(&mut buf)?; - Ok(buf) -} - -fn read_block_group_reserved(reader: &mut dyn Read) -> Result<[u8; 6]> { - let mut buf = [0u8; 6]; - reader.read_exact(&mut buf)?; - Ok(buf) -} - -fn read_digest(reader: &mut dyn Read) -> Result<[u8; 32]> { - let mut digest = [0u8; 32]; - reader.read_exact(&mut digest)?; - Ok(digest) -} - #[cfg(test)] mod tests { use super::*; @@ -1323,14 +1385,14 @@ mod tests { fn block_group( uncompressed_block_offset: u64, uncompressed_block_count: u32, - compressed_byte_offset: u64, + compressed_offset: u64, compressed_size: u32, payload: &[u8], ) -> BlobMetadataBlockGroup { BlobMetadataBlockGroup::new( uncompressed_block_offset, uncompressed_block_count, - compressed_byte_offset, + compressed_offset, compressed_size, crc32c::crc32c(payload), ) @@ -1350,8 +1412,27 @@ mod tests { .unwrap() } + /// The smallest interesting metadata: one single-block chunk in one + /// block group. + fn minimal_blob_metadata() -> BlobMetadata { + let payload = vec![0x33; EROFS_BLOCK_SIZE as usize]; + BlobMetadata::from_parts( + [0x7bu8; SHA256_DIGEST_SIZE], + 1, + vec![block_group(0, 1, 0, 4096, &payload)], + vec![chunk(&payload, 0, 1)], + ) + .unwrap() + } + + fn sealed_metadata() -> Vec { + let mut raw = Vec::new(); + minimal_blob_metadata().write_to(&mut raw).unwrap(); + raw + } + #[test] - fn blob_metadata_round_trips_through_mmap() { + fn round_trips_through_mmap() { let dir = tempdir().unwrap(); let path = dir.path().join("blob.meta"); let blob_id = [0x5au8; SHA256_DIGEST_SIZE]; @@ -1371,58 +1452,47 @@ mod tests { assert_eq!(loaded.header().chunk_count(), 2); assert_eq!(loaded.header().block_group_count(), 1); - assert_eq!(loaded.header().version(), BLOB_METADATA_VERSION); + 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().records_end(), 4096 + 96 + 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); 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_byte_offset(), 8192); + 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_byte_offset(4096), Some(0)); + assert_eq!(loaded.block_group_index_for_offset(4096), Some(0)); assert_eq!(loaded.total_uncompressed_size(), 8192); } #[test] - fn blob_metadata_header_crc32_covers_full_metadata() { - let payload = vec![0x33; EROFS_BLOCK_SIZE as usize]; - let blob_metadata = BlobMetadata::from_parts( - [0x7bu8; SHA256_DIGEST_SIZE], - 1, - vec![block_group(0, 1, 0, 4096, &payload)], - vec![chunk(&payload, 0, 1)], - ) - .unwrap(); - let mut raw = Vec::new(); - blob_metadata.write_to(&mut raw).unwrap(); + fn the_header_crc32_seals_the_full_serialized_metadata() { + let raw = sealed_metadata(); - let stored_crc32 = - u32::from_le_bytes(raw[BLOB_METADATA_HEADER_CRC32_FIELD].try_into().unwrap()); - raw[BLOB_METADATA_HEADER_CRC32_FIELD].fill(0); + let stored_crc32 = u32::from_le_bytes( + raw[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD] + .try_into() + .unwrap(), + ); + let mut zeroed = raw.clone(); + zeroed[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD].fill(0); - assert_eq!(stored_crc32, crc32c::crc32c(&raw)); + assert_eq!(stored_crc32, crc32c::crc32c(&zeroed)); } #[test] - fn blob_metadata_read_keeps_but_checked_read_rejects_bad_header_crc32() { - let payload = vec![0x55; EROFS_BLOCK_SIZE as usize]; - let blob_metadata = BlobMetadata::from_parts( - [0x8cu8; SHA256_DIGEST_SIZE], - 1, - vec![block_group(0, 1, 0, 4096, &payload)], - vec![chunk(&payload, 0, 1)], - ) - .unwrap(); - let mut raw = Vec::new(); - blob_metadata.write_to(&mut raw).unwrap(); - raw[BLOB_METADATA_HEADER_CRC32_FIELD.start] ^= 0xff; - let corrupted_crc32 = - u32::from_le_bytes(raw[BLOB_METADATA_HEADER_CRC32_FIELD].try_into().unwrap()); + fn an_unchecked_read_keeps_a_bad_crc32_and_a_checked_read_rejects_it() { + let mut raw = sealed_metadata(); + raw[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD.start] ^= 0xff; + let corrupted_crc32 = u32::from_le_bytes( + raw[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD] + .try_into() + .unwrap(), + ); let loaded = BlobMetadata::loader().from_bytes(&raw).unwrap(); @@ -1431,11 +1501,11 @@ mod tests { Ok(_) => panic!("corrupted blob meta crc32 should be rejected"), Err(err) => err, }; - assert!(err.to_string().contains("crc32")); + assert!(err.to_string().contains("crc32"), "{err}"); } #[test] - fn blob_metadata_rejects_legacy_magics() { + fn legacy_magics_reject() { let dir = tempdir().unwrap(); // Legacy magics from earlier format generations must all be rejected: @@ -1446,7 +1516,7 @@ mod tests { ("v0.blob.meta", 0x4c50_424du32), ] { let path = dir.path().join(name); - let mut raw = vec![0u8; BLOB_METADATA_HEADER_SIZE as usize]; + 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(); @@ -1459,63 +1529,73 @@ mod tests { } #[test] - fn blob_metadata_version_is_informational_and_flags_split_compat_incompat() { - let payload = vec![0x66; EROFS_BLOCK_SIZE as usize]; - let blob_metadata = BlobMetadata::from_parts( - [0x1au8; SHA256_DIGEST_SIZE], - 1, - vec![block_group(0, 1, 0, 4096, &payload)], - vec![chunk(&payload, 0, 1)], - ) - .unwrap(); - let mut raw = Vec::new(); - blob_metadata.write_to(&mut raw).unwrap(); - - // A future format generation is readable: version is informational. - let mut future = raw.clone(); - future[8..12].copy_from_slice(&(BLOB_METADATA_VERSION + 1).to_le_bytes()); - let loaded = BlobMetadata::loader() - .from_bytes(&future) - .expect("future version must be readable"); - assert_eq!(loaded.header().version(), BLOB_METADATA_VERSION + 1); - - // An unknown compat (high-half) flag bit is ignored. - let mut compat = raw.clone(); - let flags = u32::from_le_bytes(compat[12..16].try_into().unwrap()) | (1 << 31); - compat[12..16].copy_from_slice(&flags.to_le_bytes()); - BlobMetadata::loader() - .from_bytes(&compat) - .expect("unknown compat flag must be ignored"); - - // An unknown incompat (low-half) flag bit rejects the file. - let mut incompat = raw; - let flags = u32::from_le_bytes(incompat[12..16].try_into().unwrap()) | (1 << 15); - incompat[12..16].copy_from_slice(&flags.to_le_bytes()); - let err = match BlobMetadata::loader().from_bytes(&incompat) { - Ok(_) => panic!("unknown incompat flag should be rejected"), - Err(err) => err, - }; - assert!(err.to_string().contains("incompat"), "{err}"); + fn header_mutations_follow_the_compat_rules() { + let base_flags = BlobMetadataDigester::Blake3.flag().bits(); + let cases: [(&str, usize, [u8; 4], Option<&str>); 4] = [ + ( + "future version is readable", + 8, + (NYDUS_BLOB_METADATA_VERSION + 1).to_le_bytes(), + None, + ), + ( + "unknown compat flag is ignored", + 12, + (base_flags | (1u32 << 31)).to_le_bytes(), + None, + ), + ( + "unknown incompat flag rejects", + 12, + (base_flags | (1u32 << 15)).to_le_bytes(), + Some("incompat"), + ), + ( + "nonzero reserved tail is readable", + NYDUS_BLOB_METADATA_HEADER_SIZE - 4, + [0, 0, 0, 0xff], + None, + ), + ]; + + for (case, offset, value, expected_err) in cases { + 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); + match expected_err { + None => { + result.unwrap_or_else(|err| panic!("{case}: {err}")); + } + Some(expected) => { + let err = match result { + Ok(_) => panic!("{case}: should be rejected"), + Err(err) => err, + }; + assert!(err.to_string().contains(expected), "{case}: {err}"); + } + } + } + + // 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(); + assert_eq!(loaded.header().version(), NYDUS_BLOB_METADATA_VERSION + 1); } #[test] - fn blob_metadata_ignores_reserved_tail_but_crc_check_catches_corruption() { - let payload = vec![0x77; EROFS_BLOCK_SIZE as usize]; - let blob_metadata = BlobMetadata::from_parts( - [0x2bu8; SHA256_DIGEST_SIZE], - 1, - vec![block_group(0, 1, 0, 4096, &payload)], - vec![chunk(&payload, 0, 1)], - ) - .unwrap(); - let mut raw = Vec::new(); - blob_metadata.write_to(&mut raw).unwrap(); + 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. - raw[BLOB_METADATA_HEADER_SIZE as usize - 1] = 0xff; + let mut raw = sealed_metadata(); + raw[NYDUS_BLOB_METADATA_HEADER_SIZE - 1] = 0xff; BlobMetadata::loader() .from_bytes(&raw) @@ -1528,10 +1608,70 @@ mod tests { } #[test] - fn block_group_index_for_byte_offset_maps_constant_sized_block_groups_by_division() { - // Block groups pack blocks up to the compress 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 invalid_entries_reject() { + let cases = [ + ( + "zero uncompressed block count", + BlobMetadataBlockGroup::new(0, 0, 0, 4096, 0), + "must be non-zero", + ), + ( + "zero compressed size", + BlobMetadataBlockGroup::new(0, 1, 0, 0, 0), + "must be non-zero", + ), + ( + "uncompressed byte offset overflow", + BlobMetadataBlockGroup::new(u64::MAX, 1, 0, 4096, 0), + "overflow", + ), + ( + "compressed byte range overflow", + BlobMetadataBlockGroup::new(0, 1, u64::MAX, 4096, 0), + "overflow", + ), + ]; + + for (case, result, expected) in cases { + let err = result.unwrap_err(); + assert!(err.to_string().contains(expected), "{case}: {err}"); + } + + 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 valid = BlobMetadataBlockGroup::new(0, 1, 0, 4096, 0) + .unwrap() + .to_bytes(); + + let mut orphan_source = valid; + write_u32_at(&mut orphan_source, 28, 7); + let err = BlobMetadataBlockGroup::from_bytes(&orphan_source).unwrap_err(); + assert!( + err.to_string().contains("requires a source blob index"), + "{err}" + ); + + let mut dirty_reserved = valid; + dirty_reserved[34] = 0xff; + let err = BlobMetadataBlockGroup::from_bytes(&dirty_reserved).unwrap_err(); + assert!(err.to_string().contains("reserved"), "{err}"); + + let mut dirty_chunk = chunk(&[0x11], 0, 1).to_bytes(); + write_u32_at(&mut dirty_chunk, 44, 1); + let err = BlobMetadataChunk::from_bytes(&dirty_chunk).unwrap_err(); + assert!(err.to_string().contains("reserved"), "{err}"); + } + + #[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. 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( @@ -1554,42 +1694,40 @@ mod tests { 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_byte_offset(0), Some(0)); + assert_eq!(blob_metadata.block_group_index_for_offset(0), Some(0)); assert_eq!( - blob_metadata.block_group_index_for_byte_offset(2 * block - 1), + blob_metadata.block_group_index_for_offset(2 * block - 1), Some(0) ); assert_eq!( - blob_metadata.block_group_index_for_byte_offset(2 * block), + blob_metadata.block_group_index_for_offset(2 * block), Some(1) ); assert_eq!( - blob_metadata.block_group_index_for_byte_offset(4 * block - 1), + 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_byte_offset(4 * block), + blob_metadata.block_group_index_for_offset(4 * block), Some(2) ); assert_eq!( - blob_metadata.block_group_index_for_byte_offset(5 * block - 1), + 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_byte_offset(5 * block), - None - ); + assert_eq!(blob_metadata.block_group_index_for_offset(5 * block), None); } #[test] - fn validate_block_groups_rejects_non_uniform_block_group_sizes() { + fn non_uniform_block_group_sizes_reject() { 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. + // 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, @@ -1606,18 +1744,19 @@ mod tests { ], vec![chunk(&two, 0, 2), chunk(&three, 2, 3), chunk(&one, 5, 1)], ) { - Ok(_) => panic!("non-uniform block_group sizes should be rejected"), + Ok(_) => panic!("non-uniform block group sizes should be rejected"), Err(err) => err, }; - assert!(err.to_string().contains("must be exactly")); + assert!(err.to_string().contains("must be exactly"), "{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. + // 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. let three = vec![0x44; 3 * EROFS_BLOCK_SIZE as usize]; let blob_metadata = BlobMetadata::from_parts( [0u8; SHA256_DIGEST_SIZE], @@ -1632,20 +1771,18 @@ mod tests { let block = EROFS_BLOCK_SIZE as u64; for index in 0..3u64 { assert_eq!( - blob_metadata.block_group_index_for_byte_offset(index * block), + blob_metadata.block_group_index_for_offset(index * block), Some(0) ); } - assert_eq!( - blob_metadata.block_group_index_for_byte_offset(3 * block), - None - ); + assert_eq!(blob_metadata.block_group_index_for_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. + // 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. 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( @@ -1657,7 +1794,7 @@ mod tests { ], vec![chunk(&three, 0, 3), chunk(&one, 3, 1)], ) { - Ok(_) => panic!("non-power-of-two full block_group should be rejected"), + Ok(_) => panic!("non-power-of-two full block group should be rejected"), Err(err) => err, }; @@ -1665,10 +1802,11 @@ mod tests { } #[test] - fn validate_block_groups_accepts_packed_non_block_aligned_compressed_offsets() { + 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. + // 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, @@ -1680,17 +1818,15 @@ mod tests { ) .unwrap(); - assert_eq!( - blob_metadata.block_groups()[1].compressed_byte_offset(), - 5000 - ); + assert_eq!(blob_metadata.block_groups()[1].compressed_offset(), 5000); assert_eq!(blob_metadata.total_compressed_size(), 8000); } #[test] - fn validate_block_groups_rejects_overlapping_compressed_ranges() { + 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. + // 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, @@ -1704,7 +1840,7 @@ mod tests { Err(err) => err, }; - assert!(err.to_string().contains("overlap")); + assert!(err.to_string().contains("overlap"), "{err}"); } #[test] @@ -1719,33 +1855,31 @@ mod tests { assert_eq!(redirect.source_blob_index(), 3); assert_eq!(redirect.source_block_group_index(), 7); - let mut raw = Vec::new(); - redirect.write_to(&mut raw).unwrap(); - assert_eq!(raw.len(), 40); - let loaded = BlobMetadataBlockGroup::read_from(&mut Cursor::new(&raw)).unwrap(); - assert_eq!(loaded, redirect); + let mut written = Vec::new(); + redirect.write_to(&mut written).unwrap(); + let bytes: [u8; 40] = written.as_slice().try_into().unwrap(); + assert_eq!( + BlobMetadataBlockGroup::from_bytes(&bytes).unwrap(), + 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 mut raw = Vec::new(); - normal.write_to(&mut raw).unwrap(); - let loaded = BlobMetadataBlockGroup::read_from(&mut Cursor::new(&raw)).unwrap(); + let loaded = BlobMetadataBlockGroup::from_bytes(&normal.to_bytes()).unwrap(); assert!(!loaded.is_redirect()); assert_eq!(loaded.source_block_group_index(), 0); } #[test] - fn redirect_block_group_rejects_zero_source_blob_index() { - let err = match BlobMetadataBlockGroup::new_redirect(0, 1, 0, EROFS_BLOCK_SIZE, 0, 0, 1) { - Ok(_) => panic!("zero source blob index should be rejected"), - Err(err) => err, - }; - assert!(err.to_string().contains("non-zero")); + fn a_redirect_with_a_zero_source_blob_index_rejects() { + let err = + BlobMetadataBlockGroup::new_redirect(0, 1, 0, EROFS_BLOCK_SIZE, 0, 0, 1).unwrap_err(); + assert!(err.to_string().contains("non-zero"), "{err}"); } #[test] - fn redirect_blob_metadata_allows_non_uniform_block_groups_and_round_trips() { + fn a_redirect_blob_allows_non_uniform_block_groups_and_round_trips() { let dir = tempdir().unwrap(); let path = dir.path().join("ondemand.blob.meta"); let two = vec![0x55; 2 * EROFS_BLOCK_SIZE as usize]; @@ -1786,17 +1920,18 @@ mod tests { let blob_metadata = BlobMetadata::from_parts( [0x9du8; SHA256_DIGEST_SIZE], - BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, + NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, block_groups.clone(), Vec::new(), ) .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. + // Redirect block groups are non-uniform and never use the + // block-to-block group mapping, so the header keeps the default + // exponent. assert_eq!( blob_metadata.header().block_group_block_bits(), - BLOB_METADATA_DEFAULT_BLOCK_GROUP_BLOCK_COUNT.trailing_zeros() as u8 + NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_BLOCK_COUNT.trailing_zeros() as u8 ); blob_metadata.save(&path).unwrap(); diff --git a/nydus-format/src/blob/mod.rs b/nydus-format/src/blob/mod.rs index 4c8745e8d1c..a51cf7f64a3 100644 --- a/nydus-format/src/blob/mod.rs +++ b/nydus-format/src/blob/mod.rs @@ -9,34 +9,20 @@ use crate::error::{Context, Error, Result}; use crate::utils::{align_up, write_zero_padding}; use std::io::Write; +pub mod algorithm; +pub mod flag; pub mod footer; pub mod metadata; -pub mod validate; +pub use algorithm::{BlobMetadataCompressor, BlobMetadataDigester}; pub use footer::NYDUS_BLOB_FOOTER_ALIGNMENT; pub use footer::{BlobFooter, NYDUS_BLOB_FOOTER_SIZE}; pub use metadata::{ - BlobMetadata, BlobMetadataBlockGroup, BlobMetadataChunk, BlobMetadataCompressor, - BlobMetadataDigester, BLOB_METADATA_DEFAULT_BLOCK_GROUP_BLOCK_COUNT, - BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE, BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, - BLOB_METADATA_DEFAULT_CHUNK_SIZE, BLOB_METADATA_SUFFIX, + 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, }; -/// The incompatible (reject-when-unknown) half of a format `flags` word. -pub const INCOMPAT_MASK: u32 = 0x0000_FFFF; - -/// Reject `flags` whose incompat half carries bits outside `supported`. -/// `what` names the format in the error message. -pub fn validate_incompat_flags(flags: u32, supported: u32) -> Result<()> { - let unknown_incompat = flags & INCOMPAT_MASK & !supported; - if unknown_incompat != 0 { - return Err(Error::Unsupported(format!( - "unsupported incompat flags {unknown_incompat:#x} (image is newer than this reader)" - ))); - } - - Ok(()) -} - /// Append the trailing regions of the full-blob layout /// `[data][pad][bootstrap][pad][blob meta][footer]` to `writer`, which must /// already hold the `data_size` bytes of blob data. An empty `bootstrap` diff --git a/nydus-format/src/utils/le.rs b/nydus-format/src/utils/le.rs index 30d4ec6c31d..6e679d1a24f 100644 --- a/nydus-format/src/utils/le.rs +++ b/nydus-format/src/utils/le.rs @@ -1,10 +1,7 @@ //! Little-endian integer helpers for the on-disk formats. //! -//! Three shapes for three call patterns: fixed-size byte-array struct fields -//! (`read_u32`), offset-addressed buffers (`read_u32_at`), and sequential -//! readers (`read_u32_from`). - -use std::io::{self, Read}; +//! Two shapes for two call patterns: fixed-size byte-array struct fields +//! (`read_u32`) and offset-addressed buffers (`read_u32_at`). /// Read a little-endian integer from a byte array. #[inline(always)] @@ -41,40 +38,41 @@ pub fn write_u64(b: &mut [u8; 8], v: u64) { /// /// Panics when `data` is too short — callers validate region sizes up front. #[inline] -pub fn read_u32_at(data: &[u8], offset: usize) -> u32 { - u32::from_le_bytes(data[offset..offset + 4].try_into().expect("slice checked")) +pub fn read_u8_at(data: &[u8], offset: usize) -> u8 { + data[offset] } #[inline] -pub fn read_u64_at(data: &[u8], offset: usize) -> u64 { - u64::from_le_bytes(data[offset..offset + 8].try_into().expect("slice checked")) +pub fn read_u16_at(data: &[u8], offset: usize) -> u16 { + u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap()) } #[inline] -pub fn write_u32_at(data: &mut [u8], offset: usize, value: u32) { - data[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +pub fn read_u32_at(data: &[u8], offset: usize) -> u32 { + u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) } #[inline] -pub fn write_u64_at(data: &mut [u8], offset: usize, value: u64) { - data[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); +pub fn read_u64_at(data: &[u8], offset: usize) -> u64 { + u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap()) } -/// Read a little-endian integer from a sequential reader. -pub fn read_u16_from(reader: &mut dyn Read) -> io::Result { - let mut buf = [0u8; 2]; - reader.read_exact(&mut buf)?; - Ok(u16::from_le_bytes(buf)) +#[inline] +pub fn write_u8_at(data: &mut [u8], offset: usize, value: u8) { + data[offset] = value; } -pub fn read_u32_from(reader: &mut dyn Read) -> io::Result { - let mut buf = [0u8; 4]; - reader.read_exact(&mut buf)?; - Ok(u32::from_le_bytes(buf)) +#[inline] +pub fn write_u16_at(data: &mut [u8], offset: usize, value: u16) { + data[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); } -pub fn read_u64_from(reader: &mut dyn Read) -> io::Result { - let mut buf = [0u8; 8]; - reader.read_exact(&mut buf)?; - Ok(u64::from_le_bytes(buf)) +#[inline] +pub fn write_u32_at(data: &mut [u8], offset: usize, value: u32) { + data[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +#[inline] +pub fn write_u64_at(data: &mut [u8], offset: usize, value: u64) { + data[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); } diff --git a/nydus-format/src/utils/mod.rs b/nydus-format/src/utils/mod.rs index bc2b0aecd28..886ee230950 100644 --- a/nydus-format/src/utils/mod.rs +++ b/nydus-format/src/utils/mod.rs @@ -7,7 +7,7 @@ use std::fs; use std::io::Write as _; use std::path::Path; -use crate::blob::{BlobMetadata, BLOB_METADATA_SUFFIX}; +use crate::blob::{BlobMetadata, NYDUS_BLOB_METADATA_SUFFIX}; use crate::erofs::{ErofsSuperblock, EROFS_SUPER_OFFSET}; pub use self::align::{align_up, round_up}; @@ -48,7 +48,7 @@ pub fn write_minimal_full_blob( if save_sidecar { blob_metadata .save(&dir.join(format!( - "{}{BLOB_METADATA_SUFFIX}", + "{}{NYDUS_BLOB_METADATA_SUFFIX}", hex_string(&full_blob_id) ))) .unwrap(); diff --git a/nydus-storage/src/block_group_map.rs b/nydus-storage/src/block_group_map.rs index 28aae11f452..66625eb3728 100644 --- a/nydus-storage/src/block_group_map.rs +++ b/nydus-storage/src/block_group_map.rs @@ -18,7 +18,7 @@ const BLOCK_GROUP_MAP_MAGIC: [u8; 8] = *b"LPGRPMAP"; /// bits (not format features), and unknown state bits are simply ignored. const BLOCK_GROUP_MAP_VERSION: u32 = 1; /// Fixed header size: one block-sized page, matching the blob meta header -/// (`BLOB_METADATA_HEADER_SIZE`) for a uniform sidecar format family. The bitmap +/// (`NYDUS_BLOB_METADATA_HEADER_SIZE`) for a uniform sidecar format family. The bitmap /// starts on a page boundary and the unused header tail is reserved for /// future fields. const BLOCK_GROUP_MAP_HEADER_SIZE: usize = nydus_format::erofs::EROFS_BLOCK_SIZE as usize; diff --git a/nydus-storage/src/cache/local.rs b/nydus-storage/src/cache/local.rs index 9971757328e..de3963b5b40 100644 --- a/nydus-storage/src/cache/local.rs +++ b/nydus-storage/src/cache/local.rs @@ -14,8 +14,8 @@ use crate::access_trace::TraceRecorder; use crate::block_group_map::BlockGroupMap; use nydus_backend::{BlobBackend, ReadContext, ReadKind}; use nydus_format::blob::{ - BlobMetadata, BlobMetadataBlockGroup, BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE, - BLOB_METADATA_SUFFIX, + BlobMetadata, BlobMetadataBlockGroup, NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE, + NYDUS_BLOB_METADATA_SUFFIX, }; use nydus_format::utils::{hex_string, SHA256_DIGEST_SIZE}; @@ -124,7 +124,8 @@ impl LocalBlobCache { let cache_key = backend.cache_key(&blob_id)?; let cache_key_hex = hex_string(&cache_key); - let blob_metadata_path = cache_dir.join(format!("{cache_key_hex}{BLOB_METADATA_SUFFIX}")); + let blob_metadata_path = + cache_dir.join(format!("{cache_key_hex}{NYDUS_BLOB_METADATA_SUFFIX}")); let blob_metadata = load_or_fetch_blob_metadata(blob_id, cache_dir, &blob_metadata_path, &backend)?; nydus_telemetry::metrics::track_blob_block_groups( @@ -317,7 +318,7 @@ impl LocalBlobCache { &mut buffers, ReadKind::OnDemand, )?; - write_all_at(cache_file, block_group.uncompressed_byte_offset(), decoded)?; + write_all_at(cache_file, block_group.uncompressed_offset(), decoded)?; self.block_group_map.set_ready(block_group_index)?; nydus_telemetry::metrics::inc_cache_ondemand_fill_block_group(); Ok(()) @@ -383,13 +384,13 @@ impl LocalBlobCache { ) -> io::Result> { let first = self .blob_metadata - .block_group_index_for_byte_offset(offset) + .block_group_index_for_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_byte_offset(end - 1) + .block_group_index_for_offset(end - 1) .ok_or_else(|| { io::Error::new(io::ErrorKind::NotFound, "blob meta block_group not found") })?; @@ -407,8 +408,10 @@ impl LocalBlobCache { batch: &Range, window: &mut Vec, ) -> io::Result { - let window_base = block_groups[batch.start].compressed_byte_offset(); - let window_end = block_groups[batch.end - 1].compressed_byte_end(); + let last_block_group = &block_groups[batch.end - 1]; + let window_base = block_groups[batch.start].compressed_offset(); + let window_end = + last_block_group.compressed_offset() + last_block_group.compressed_size() as u64; let window_len = usize::try_from(window_end - window_base).map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, @@ -416,9 +419,10 @@ impl LocalBlobCache { ) })?; window.resize(window_len, 0); - let uncompressed_offset = block_groups[batch.start].uncompressed_byte_offset(); - let uncompressed_size = - block_groups[batch.end - 1].uncompressed_byte_end() - uncompressed_offset; + let uncompressed_offset = block_groups[batch.start].uncompressed_offset(); + let uncompressed_size = last_block_group.uncompressed_offset() + + last_block_group.uncompressed_size() + - uncompressed_offset; let ctx = ReadContext::block_group(ReadKind::Prefetch, uncompressed_offset, uncompressed_size); self.backend @@ -503,9 +507,10 @@ impl BlobCache for LocalBlobCache { let mut decoded = Vec::new(); let mut window = Vec::new(); - for batch in - plan_prefetch_batches(block_groups, BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE as u64) - { + for batch in plan_prefetch_batches( + block_groups, + NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE as u64, + ) { super::check_prefetch_deadline(deadline)?; if batch .clone() @@ -534,7 +539,7 @@ impl BlobCache for LocalBlobCache { )?; write_all_at( cache_file.as_ref(), - block_group.uncompressed_byte_offset(), + block_group.uncompressed_offset(), &decoded, )?; self.block_group_map.set_ready(index)?; @@ -620,8 +625,10 @@ impl BlobCache for LocalBlobCache { "blob meta block_group not found", ) })?; - Ok(first_block_group.uncompressed_byte_offset().max(offset) - ..last_block_group.uncompressed_byte_end().min(end)) + Ok(first_block_group.uncompressed_offset().max(offset) + ..(last_block_group.uncompressed_offset() + + last_block_group.uncompressed_size()) + .min(end)) }) .collect() } @@ -729,7 +736,7 @@ impl BlobCache for LocalBlobCache { // work. let total_uncompressed: u64 = block_groups .iter() - .map(|block_group| block_group.uncompressed_byte_size()) + .map(|block_group| block_group.uncompressed_size()) .sum(); if threads <= 1 || total_uncompressed <= super::REDIRECT_PREFETCH_BATCH_SIZE { let mut window = Vec::new(); @@ -824,7 +831,7 @@ impl BlobCache for LocalBlobCache { let cache_file = self.cache_file()?; write_all_at( cache_file.as_ref(), - block_group.uncompressed_byte_offset(), + block_group.uncompressed_offset(), decoded, )?; self.block_group_map.set_ready(block_group_index)?; diff --git a/nydus-storage/src/cache/mod.rs b/nydus-storage/src/cache/mod.rs index 1d231a37141..aa9135c5550 100644 --- a/nydus-storage/src/cache/mod.rs +++ b/nydus-storage/src/cache/mod.rs @@ -182,9 +182,9 @@ pub fn plan_prefetch_batches( let mut start = 0usize; while start < block_groups.len() { let mut end = start + 1; - let mut accumulated = block_groups[start].uncompressed_byte_size(); + let mut accumulated = block_groups[start].uncompressed_size(); while end < block_groups.len() && accumulated < target_uncompressed { - accumulated = accumulated.saturating_add(block_groups[end].uncompressed_byte_size()); + accumulated = accumulated.saturating_add(block_groups[end].uncompressed_size()); end += 1; } batches.push(start..end); @@ -225,7 +225,7 @@ pub fn decode_block_group_from_window( decoded: &mut Vec, ) -> io::Result<()> { let relative_start = block_group - .compressed_byte_offset() + .compressed_offset() .checked_sub(window_base_offset) .and_then(|start| usize::try_from(start).ok()) .ok_or_else(|| { @@ -244,7 +244,7 @@ pub fn decode_block_group_from_window( ) })?; - let decoded_len = usize::try_from(block_group.uncompressed_byte_size()).map_err(|_| { + let decoded_len = usize::try_from(block_group.uncompressed_size()).map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, "blob meta block group uncompressed size exceeds usize", @@ -279,10 +279,10 @@ pub fn fetch_decode_validate_block_group_into<'a>( ) -> io::Result<&'a [u8]> { let ctx = ReadContext::block_group( kind, - block_group.uncompressed_byte_offset(), - block_group.uncompressed_byte_size(), + block_group.uncompressed_offset(), + block_group.uncompressed_size(), ); - let decoded_len = usize::try_from(block_group.uncompressed_byte_size()).map_err(|_| { + let decoded_len = usize::try_from(block_group.uncompressed_size()).map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, "blob meta block group uncompressed size exceeds usize", @@ -292,7 +292,7 @@ pub fn fetch_decode_validate_block_group_into<'a>( buffers.decoded.resize(decoded_len, 0); backend.read_range_into( blob_id, - block_group.compressed_byte_offset(), + block_group.compressed_offset(), &mut buffers.decoded, ctx, )?; @@ -305,7 +305,7 @@ pub fn fetch_decode_validate_block_group_into<'a>( .resize(block_group.compressed_size() as usize, 0); backend.read_range_into( blob_id, - block_group.compressed_byte_offset(), + block_group.compressed_offset(), &mut buffers.encoded, ctx, )?; @@ -340,14 +340,14 @@ fn is_stored_plain_block_group( block_group: &BlobMetadataBlockGroup, ) -> bool { blob_metadata.compressor() == BlobMetadataCompressor::None - || u64::from(block_group.compressed_size()) == block_group.uncompressed_byte_size() + || u64::from(block_group.compressed_size()) == block_group.uncompressed_size() } pub fn validate_decoded_block_group( block_group: &BlobMetadataBlockGroup, decoded: &[u8], ) -> io::Result<()> { - let expected = block_group.uncompressed_byte_size(); + let expected = block_group.uncompressed_size(); if decoded.len() as u64 != expected { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -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::BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE; + use nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE; use nydus_format::erofs::EROFS_BLOCK_SIZE; fn block_group( @@ -412,14 +412,16 @@ mod tests { #[test] fn plan_prefetch_batches_keeps_one_block_group_per_window_at_default_target() { - let blocks = BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE / EROFS_BLOCK_SIZE; + let blocks = NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE / EROFS_BLOCK_SIZE; let block_groups = vec![ block_group(0, blocks), block_group(blocks as u64, blocks), block_group(2 * blocks as u64, blocks), ]; - let batches = - plan_prefetch_batches(&block_groups, BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE as u64); + let batches = plan_prefetch_batches( + &block_groups, + NYDUS_BLOB_METADATA_DEFAULT_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 1f12deda6d7..6cafa9d4ade 100644 --- a/nydus-storage/src/cache/remote.rs +++ b/nydus-storage/src/cache/remote.rs @@ -61,13 +61,13 @@ impl BlobCache for RemoteBlobCache { })?; let first = self .blob_metadata - .block_group_index_for_byte_offset(offset) + .block_group_index_for_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_byte_offset(end - 1) + .block_group_index_for_offset(end - 1) .ok_or_else(|| { io::Error::new(io::ErrorKind::NotFound, "blob meta block group not found") })?; @@ -93,9 +93,9 @@ impl BlobCache for RemoteBlobCache { )?; // Copy the overlap between this block group's span and the request. - let block_group_start = block_group.uncompressed_byte_offset(); + let block_group_start = block_group.uncompressed_offset(); let copy_start = offset.max(block_group_start); - let copy_end = end.min(block_group.uncompressed_byte_end()); + let copy_end = end.min(block_group_start + block_group.uncompressed_size()); let source = &decoded[(copy_start - block_group_start) as usize..] [..(copy_end - copy_start) as usize]; let dst_start = (copy_start - offset) as usize; diff --git a/nydus/src/bin/nydus/build.rs b/nydus/src/bin/nydus/build.rs index 695d75812b0..f9507d38452 100644 --- a/nydus/src/bin/nydus/build.rs +++ b/nydus/src/bin/nydus/build.rs @@ -2,7 +2,9 @@ use bytesize::ByteSize; use clap::{Parser, ValueEnum}; use nydus::build::{build_image, BuildImageOptions, Image}; use nydus::error::{Context, Error, Result}; -use nydus_format::blob::{BlobFooter, BlobMetadata, BlobMetadataCompressor, BLOB_METADATA_SUFFIX}; +use nydus_format::blob::{ + BlobFooter, BlobMetadata, BlobMetadataCompressor, NYDUS_BLOB_METADATA_SUFFIX, +}; use nydus_format::erofs::EROFS_BLOB_ID_SIZE; use nydus_format::utils::hex_string; use nydus_telemetry::logging::init_command_tracing; @@ -222,7 +224,7 @@ impl BuildCommand { /// (`.blob.meta`) and returns its path. fn save_blob_metadata(image: &Image, full_blob_path: &Path) -> Result { let mut path = full_blob_path.to_path_buf().into_os_string(); - path.push(BLOB_METADATA_SUFFIX); + path.push(NYDUS_BLOB_METADATA_SUFFIX); let blob_metadata_path: PathBuf = path.into(); image.blob_metadata.save(&blob_metadata_path)?; diff --git a/nydus/src/bin/nydus/check.rs b/nydus/src/bin/nydus/check.rs index 05d96ca251f..73a40efb298 100644 --- a/nydus/src/bin/nydus/check.rs +++ b/nydus/src/bin/nydus/check.rs @@ -280,8 +280,8 @@ fn print_inline_across_blocks(stats: &ImageStats) { #[tabled(rename_all = "UPPERCASE")] struct InlineOverflowRow { nid: u64, - #[tabled(rename = "BLOCK OFFSET")] - block_offset: u64, + #[tabled(rename = "OFFSET IN BLOCK")] + offset_in_block: u64, #[tabled(rename = "HEADER SIZE")] header_size: u64, #[tabled(rename = "XATTR SIZE")] @@ -298,11 +298,11 @@ fn print_inline_across_blocks(stats: &ImageStats) { .take(MAX_LISTED) .map(|entry| InlineOverflowRow { nid: entry.nid, - block_offset: entry.block_offset, + offset_in_block: entry.offset_in_block, header_size: entry.header_size, xattr_size: entry.xattr_size, inline_size: entry.inline_size, - end: entry.block_offset + entry.header_size + entry.xattr_size + entry.inline_size, + end: entry.offset_in_block + entry.header_size + entry.xattr_size + entry.inline_size, }) .collect(); diff --git a/nydus/src/bin/nydus/optimize.rs b/nydus/src/bin/nydus/optimize.rs index 33321a2b373..37033abfe0d 100644 --- a/nydus/src/bin/nydus/optimize.rs +++ b/nydus/src/bin/nydus/optimize.rs @@ -5,7 +5,7 @@ use nydus::optimize::{ }; use nydus_backend::{build_backend, BlobBackend}; use nydus_config::Config; -use nydus_format::blob::BLOB_METADATA_SUFFIX; +use nydus_format::blob::NYDUS_BLOB_METADATA_SUFFIX; use nydus_format::erofs::EROFS_BLOCK_SIZE; use nydus_format::utils::hex_string; use nydus_telemetry::logging::init_command_tracing; @@ -170,7 +170,7 @@ impl OptimizeCommand { .with_context(|| format!("failed to write ondemand blob: {}", blob_path.display()))?; let blob_metadata_path = self .blob_dir - .join(format!("{digest_hex}{BLOB_METADATA_SUFFIX}")); + .join(format!("{digest_hex}{NYDUS_BLOB_METADATA_SUFFIX}")); ondemand.blob_metadata.save(&blob_metadata_path)?; fs::write(&self.bootstrap, &ondemand.bootstrap).with_context(|| { diff --git a/nydus/src/build/blob_chunk.rs b/nydus/src/build/blob_chunk.rs index dca3f47a832..101fdeaaa62 100644 --- a/nydus/src/build/blob_chunk.rs +++ b/nydus/src/build/blob_chunk.rs @@ -2,7 +2,7 @@ use crc32c::crc32c; use nydus_error::{Context, Error, Result}; use nydus_format::blob::{ BlobMetadata, BlobMetadataBlockGroup, BlobMetadataChunk, BlobMetadataCompressor, - BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE, + NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE, }; use nydus_format::erofs::{ErofsChunkAddr, EROFS_BLOB_ID_SIZE, EROFS_BLOCK_SIZE, EROFS_NULL_ADDR}; use nydus_format::utils::round_up; @@ -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(BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE); + let block_group_size = file_chunk_size.max(NYDUS_BLOB_METADATA_DEFAULT_BLOCK_GROUP_SIZE); Self::from_writer(file, file_chunk_size, block_group_size, compressor) } } @@ -142,7 +142,7 @@ impl BlobWriter { self.blob_metadata_block_groups.clone(), self.blob_metadata_chunks.clone(), )? - .with_compressed_offset_bias(source_offset_bias)?) + .checked_add_compressed_offset(source_offset_bias)?) } pub fn write_blob_metadata( @@ -323,7 +323,7 @@ pub(crate) fn compression_is_worthwhile(compressed_len: usize, uncompressed_len: #[cfg(test)] mod tests { use super::*; - use nydus_format::blob::BLOB_METADATA_DEFAULT_CHUNK_SIZE; + use nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE; use std::fs; use tempfile::tempdir; @@ -335,10 +335,9 @@ mod tests { assert_eq!(entry.uncompressed_block_offset(), 2); assert_eq!(entry.uncompressed_block_count(), 3); - assert_eq!(entry.uncompressed_byte_offset(), 0x2000); - assert_eq!(entry.uncompressed_byte_size(), 0x3000); - assert_eq!(entry.compressed_byte_offset(), 0x12345); - assert_eq!(entry.compressed_byte_end(), 0x12345 + 0x400); + assert_eq!(entry.uncompressed_offset(), 0x2000); + assert_eq!(entry.uncompressed_size(), 0x3000); + assert_eq!(entry.compressed_offset(), 0x12345); assert_eq!(entry.compressed_size(), 0x400); assert_eq!(entry.crc32(), crc32c(&payload)); } @@ -350,21 +349,22 @@ 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'; BLOB_METADATA_DEFAULT_CHUNK_SIZE as usize]; + let mut content_a = vec![b'a'; NYDUS_BLOB_METADATA_DEFAULT_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'; BLOB_METADATA_DEFAULT_CHUNK_SIZE as usize], + vec![b'a'; NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as usize], ) .unwrap(); - let mut writer = BlobWriter::new(&blob_path, BLOB_METADATA_DEFAULT_CHUNK_SIZE).unwrap(); + let mut writer = + BlobWriter::new(&blob_path, NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE).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, BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64) + .write_file_chunks(&file_b, NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64) .unwrap(); writer.finish().unwrap(); @@ -393,27 +393,27 @@ mod tests { // share block group 1, and the remainder spills into block group 2. assert_eq!(block_groups[0].uncompressed_block_offset(), 0); assert_eq!(block_groups[0].uncompressed_block_count(), 256); - assert_eq!(block_groups[0].compressed_byte_offset(), 0); + assert_eq!(block_groups[0].compressed_offset(), 0); assert_eq!( block_groups[0].compressed_size(), - BLOB_METADATA_DEFAULT_CHUNK_SIZE + NYDUS_BLOB_METADATA_DEFAULT_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_byte_offset(), - BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64 + block_groups[1].compressed_offset(), + NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64 ); assert_eq!( block_groups[1].compressed_size(), - BLOB_METADATA_DEFAULT_CHUNK_SIZE + NYDUS_BLOB_METADATA_DEFAULT_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_byte_offset(), - 2 * BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64 + block_groups[2].compressed_offset(), + 2 * NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64 ); assert_eq!(block_groups[2].compressed_size(), EROFS_BLOCK_SIZE); } @@ -441,12 +441,9 @@ mod tests { assert_eq!(blob_metadata.chunks().len(), 2); assert_eq!(blob_metadata.block_groups().len(), 1); assert_eq!(blob_metadata.chunks()[0].uncompressed_block_count(), 1); - assert_eq!(blob_metadata.chunks()[0].uncompressed_byte_size(), 4096); + assert_eq!(blob_metadata.chunks()[0].uncompressed_size(), 4096); assert_eq!(blob_metadata.chunks()[1].uncompressed_block_offset(), 1); - assert_eq!( - blob_metadata.block_groups()[0].uncompressed_byte_size(), - 8192 - ); + assert_eq!(blob_metadata.block_groups()[0].uncompressed_size(), 8192); } #[test] @@ -514,12 +511,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(BLOB_METADATA_DEFAULT_CHUNK_SIZE as usize); + let content = pseudo_random_bytes(NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as usize); fs::write(&input_path, &content).unwrap(); let mut writer = BlobWriter::new_with_compressor( &blob_path, - BLOB_METADATA_DEFAULT_CHUNK_SIZE, + NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, BlobMetadataCompressor::Zstd, ) .unwrap(); @@ -533,12 +530,12 @@ mod tests { assert_eq!(block_groups.len(), 1); assert_eq!(block_groups[0].uncompressed_block_count(), 256); assert_eq!( - block_groups[0].uncompressed_byte_size(), - BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64 + block_groups[0].uncompressed_size(), + NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE as u64 ); assert_eq!( u64::from(block_groups[0].compressed_size()), - block_groups[0].uncompressed_byte_size() + block_groups[0].uncompressed_size() ); assert_eq!(fs::read(&blob_path).unwrap(), content); } @@ -552,7 +549,8 @@ mod tests { let blob_id = [7u8; EROFS_BLOB_ID_SIZE]; fs::write(&input_path, vec![b'x'; 4096]).unwrap(); - let mut writer = BlobWriter::new(&blob_path, BLOB_METADATA_DEFAULT_CHUNK_SIZE).unwrap(); + let mut writer = + BlobWriter::new(&blob_path, NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE).unwrap(); writer.write_file_chunks(&input_path, 4096).unwrap(); writer .write_blob_metadata(&blob_metadata_path, blob_id, 8192) @@ -569,10 +567,7 @@ mod tests { assert_eq!(blob_metadata.header().block_group_bytes(), 40); assert_eq!(blob_metadata.header().metadata_size(), 8192); assert_eq!(blob_metadata.chunks()[0].uncompressed_block_offset(), 0); - assert_eq!( - blob_metadata.block_groups()[0].compressed_byte_offset(), - 8192 - ); + assert_eq!(blob_metadata.block_groups()[0].compressed_offset(), 8192); } fn pseudo_random_bytes(len: usize) -> Vec { diff --git a/nydus/src/check/mod.rs b/nydus/src/check/mod.rs index a28f43cecc8..49254a3d59d 100644 --- a/nydus/src/check/mod.rs +++ b/nydus/src/check/mod.rs @@ -72,7 +72,7 @@ pub struct ImageStats { /// the kernel rejects with `-EFSCORRUPTED` when the inode is read. pub struct InlineOverflow { pub nid: u64, - pub block_offset: u64, + pub offset_in_block: u64, pub header_size: u64, pub xattr_size: u64, pub inline_size: u64, @@ -241,13 +241,13 @@ fn check_inline_fit( file_size: u64, ) -> Option { let block = EROFS_BLOCK_SIZE as u64; - let block_offset = (nid * EROFS_SLOTSIZE as u64) % block; + let offset_in_block = (nid * EROFS_SLOTSIZE as u64) % block; // Full blocks live in the data area; only the remainder is packed inline. let inline_size = file_size % block; - if block_offset + header_size + xattr_size + inline_size > block { + if offset_in_block + header_size + xattr_size + inline_size > block { Some(InlineOverflow { nid, - block_offset, + offset_in_block, header_size, xattr_size, inline_size, @@ -479,7 +479,7 @@ fn blob_metadata_summary_from_bytes(data: &[u8]) -> Result #[cfg(test)] mod tests { use super::*; - use nydus_format::blob::BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT; + use nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT; use std::fs; use tempfile::tempdir; @@ -488,7 +488,7 @@ mod tests { // nid 2557 sits 4000 bytes into its block; a 65-byte symlink target // behind a 32-byte header ends one byte past the block. let overflow = check_inline_fit(2557, 32, 0, 65).expect("should overflow"); - assert_eq!(overflow.block_offset, 4000); + assert_eq!(overflow.offset_in_block, 4000); assert_eq!(overflow.inline_size, 65); // 64 bytes exactly fills the block tail. @@ -554,7 +554,7 @@ mod tests { let data_digest = sha256_bytes(&data); let blob_metadata = BlobMetadata::from_parts( [0u8; EROFS_BLOB_ID_SIZE], - BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, + NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, Vec::new(), Vec::new(), ) diff --git a/nydus/src/optimize/mod.rs b/nydus/src/optimize/mod.rs index fdde515aa46..28532ba3636 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, - BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, + NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, }; use nydus_format::erofs::EROFS_BLOB_ID_SIZE; use nydus_storage::access_trace::{TraceDocument, TraceEntry, TRACE_DOCUMENT_VERSION}; @@ -122,14 +122,14 @@ pub fn build_ondemand_blob( ))); } - let decoded_len = usize::try_from(block_group.uncompressed_byte_size()).map_err(|err| { + let decoded_len = usize::try_from(block_group.uncompressed_size()).map_err(|err| { Error::Overflow(format!( "block group uncompressed size exceeds usize: {err}" )) })?; decoded.resize(decoded_len, 0); cache - .read_at(block_group.uncompressed_byte_offset(), &mut decoded) + .read_at(block_group.uncompressed_offset(), &mut decoded) .with_context(|| { format!("failed to read block group {block_group_index} of blob {blob_index}") })?; @@ -169,7 +169,7 @@ pub fn build_ondemand_blob( let blob_metadata = BlobMetadata::from_parts_with_options( data_digest, - BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, + NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, BlobMetadataCompressor::Zstd, ondemand_block_groups, Vec::new(), diff --git a/nydus/tests/testsuite/nydus_core.rs b/nydus/tests/testsuite/nydus_core.rs index 0cef3d5cd1c..aa40521cfa8 100644 --- a/nydus/tests/testsuite/nydus_core.rs +++ b/nydus/tests/testsuite/nydus_core.rs @@ -90,14 +90,14 @@ fn build_test_image_with_layout( let staging = blob_dir.join("staging"); let mut writer = BlobWriter::new_with_compressor( &staging, - nydus_format::blob::BLOB_METADATA_DEFAULT_CHUNK_SIZE, + nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, BlobMetadataCompressor::Zstd, ) .unwrap(); let mut inodes = build_tree( &corpus_dir, &mut writer, - nydus_format::blob::BLOB_METADATA_DEFAULT_CHUNK_SIZE, + nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, &HashSet::new(), ) .unwrap(); @@ -264,14 +264,14 @@ fn flattened_bootstrap_records_mapped_device_slots() { let staging = blob_dir.join("staging"); let mut writer = BlobWriter::new_with_compressor( &staging, - nydus_format::blob::BLOB_METADATA_DEFAULT_CHUNK_SIZE, + nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, BlobMetadataCompressor::Zstd, ) .unwrap(); let mut inodes = build_tree( &corpus_dir, &mut writer, - nydus_format::blob::BLOB_METADATA_DEFAULT_CHUNK_SIZE, + nydus_format::blob::NYDUS_BLOB_METADATA_DEFAULT_CHUNK_SIZE, &HashSet::new(), ) .unwrap(); From 2a8c926f8eb17777bf79076847960bc5ccce4cff Mon Sep 17 00:00:00 2001 From: Gaius Date: Mon, 24 Aug 2026 10:11:56 +0000 Subject: [PATCH 3/7] refactor(blob-metadata): rename constants to DEFAULT_ prefix and expand block group size to 4 MiB Rename blob metadata constants to use the `DEFAULT_` prefix for clarity, rename `block_group_block_bits` fields to `block_group_block_count_bits`, expand the default block group size to 4 MiB, move validation logic into methods, and add the clap `string` feature. Signed-off-by: Gaius --- Cargo.toml | 2 +- docs/nydus.md | 26 +- nydus-backend/src/local.rs | 7 +- nydus-format/src/blob/metadata.rs | 783 +++++++++++++--------------- nydus-format/src/blob/mod.rs | 8 +- nydus-storage/src/cache/local.rs | 16 +- nydus-storage/src/cache/mod.rs | 6 +- nydus-storage/src/cache/remote.rs | 7 +- nydus/src/bin/nydus/build.rs | 13 +- nydus/src/build/blob_chunk.rs | 61 ++- nydus/src/build/mod.rs | 8 +- nydus/src/check/mod.rs | 7 +- nydus/src/optimize/mod.rs | 8 +- nydus/tests/testsuite/nydus_core.rs | 21 +- 14 files changed, 468 insertions(+), 505 deletions(-) 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 984b39e06ec..92cb0bdebe0 100644 --- a/nydus-backend/src/local.rs +++ b/nydus-backend/src/local.rs @@ -260,16 +260,17 @@ 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( + BlobMetadata::new( blob_id, + 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() } diff --git a/nydus-format/src/blob/metadata.rs b/nydus-format/src/blob/metadata.rs index 988a381d221..f656bc62cc5 100644 --- a/nydus-format/src/blob/metadata.rs +++ b/nydus-format/src/blob/metadata.rs @@ -42,28 +42,28 @@ pub const NYDUS_BLOB_METADATA_CHUNK_ENTRY_SIZE: usize = 48; 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`) +/// Largest allowed block-count exponent (`chunk_block_count_bits` / +/// `block_group_block_count_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; +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; @@ -111,8 +111,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,8 +130,8 @@ 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 { @@ -143,8 +146,8 @@ 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()?; @@ -162,8 +165,8 @@ 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 } @@ -172,17 +175,17 @@ impl BlobMetadataHeader { 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 +198,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,29 +226,6 @@ 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(); - } - pub fn version(&self) -> u32 { self.version } @@ -263,63 +243,51 @@ impl BlobMetadataHeader { } pub fn digester(&self) -> BlobMetadataDigester { - BlobMetadataDigester::try_from(self.flags()).expect("validated blob meta digester") + BlobMetadataDigester::try_from(self.flags()).unwrap() } 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. pub fn chunk_block_count(&self) -> u32 { - 1u32 << self.chunk_block_bits + 1u32 << self.chunk_block_count_bits } 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 + 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 + 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 + pub fn block_group_count(&self) -> u32 { + self.block_group_count } - pub fn block_groups_offset(&self) -> u64 { - self.block_groups_offset + 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 + pub fn block_groups_offset(&self) -> u64 { + self.block_groups_offset } - pub fn block_group_bytes(&self) -> u64 { + 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() + 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) + pub fn padded_size(&self) -> u64 { + crate::utils::align_up(self.used_size(), EROFS_BLOCK_SIZE as u64) .expect("blob meta size overflowed") } } @@ -412,6 +380,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()))?; @@ -429,9 +403,6 @@ impl BlobMetadataChunk { &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. pub fn uncompressed_block_offset(&self) -> u64 { self.uncompressed_block_offset } @@ -441,7 +412,9 @@ impl BlobMetadataChunk { } 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") } pub fn uncompressed_size(&self) -> u64 { @@ -726,6 +699,50 @@ impl BlobMetadataBlockGroup { 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) + } + + 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()), + } + } + + fn compute_uncompressed_block_end(block_groups: &[Self]) -> u64 { + block_groups + .last() + .map(|block_group| { + block_group.uncompressed_block_offset() + + block_group.uncompressed_block_count() as u64 + }) + .unwrap_or(0) + } + + fn compute_uncompressed_end(block_groups: &[Self]) -> u64 { + block_groups + .last() + .map(|block_group| block_group.uncompressed_offset() + block_group.uncompressed_size()) + .unwrap_or(0) + } + + fn compute_compressed_end(block_groups: &[Self]) -> u64 { + block_groups + .last() + .map(|block_group| { + block_group.compressed_offset() + block_group.compressed_size() as u64 + }) + .unwrap_or(0) + } } enum BlobMetadataStorage { @@ -749,11 +766,11 @@ enum BlobMetadataStorage { /// └────────┴─────────────┴───────────────────┴──────────────┘ /// 0 4096 ▲ EOF /// the entries end here; the padding -/// runs to the 4KiB-aligned metadata_size +/// 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::new`]) or a shared file mapping read in place /// ([`Self::load`]), zero-copy thanks to the entries' pinned layout. pub struct BlobMetadata { header: BlobMetadataHeader, @@ -762,54 +779,39 @@ pub struct BlobMetadata { } 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( + pub fn new( blob_id: [u8; SHA256_DIGEST_SIZE], - chunk_block_count: u32, 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, @@ -818,41 +820,12 @@ impl BlobMetadata { 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( + fn from_bytes( data: &[u8], blob_id: [u8; SHA256_DIGEST_SIZE], check_crc32: bool, @@ -866,10 +839,10 @@ impl BlobMetadata { .try_into() .expect("length checked"), )?; - if data.len() as u64 != header.metadata_size() { + if data.len() as u64 != header.padded_size() { return Err(Error::InvalidImage(format!( "blob meta data size mismatch: expected {}, got {}", - header.metadata_size(), + header.padded_size(), data.len() ))); } @@ -878,41 +851,47 @@ impl BlobMetadata { 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( + let chunks = (0..header.chunk_count() as usize) + .map(|index| { + let start = + header.chunks_offset() as usize + index * size_of::(); BlobMetadataChunk::from_bytes( data[start..start + size_of::()] .try_into() .expect("length checked"), ) - .with_context(|| format!("failed to read blob meta chunk {index}"))?, - ); - } + .with_context(|| format!("failed to read blob meta chunk {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( + let block_groups = (0..header.block_group_count() as usize) + .map(|index| { + let start = header.block_groups_offset() as usize + + index * size_of::(); 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 { + .with_context(|| format!("failed to read blob meta block group {index}")) + }) + .collect::>>()?; + let blob_metadata = Self { header, blob_id, storage: BlobMetadataStorage::Owned { chunks, block_groups, }, - }) + }; + blob_metadata.validate()?; + Ok(blob_metadata) + } + + /// 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 load_inner(path: &Path, check_crc32: bool) -> Result { @@ -929,10 +908,10 @@ impl BlobMetadata { .try_into() .expect("length checked"), )?; - if file_len != header.metadata_size() { + if file_len != header.padded_size() { return Err(Error::InvalidImage(format!( "blob meta file size mismatch: expected {}, got {}", - header.metadata_size(), + header.padded_size(), file_len ))); } @@ -940,16 +919,141 @@ impl BlobMetadata { if check_crc32 { validate_blob_metadata_crc32(&mmap, &header)?; } - validate_tables( - mapped_block_groups(&mmap, &header), - mapped_chunks(&mmap, &header), - header.block_group_block_count(), - )?; - Ok(Self { + let blob_metadata = Self { header, blob_id: [0u8; SHA256_DIGEST_SIZE], storage: BlobMetadataStorage::Mapped(mmap), - }) + }; + blob_metadata.validate()?; + Ok(blob_metadata) + } + + fn validate(&self) -> Result<()> { + self.validate_chunks()?; + self.validate_block_groups() + } + + 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_blob = 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_blob { + 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(()) + } + + fn validate_chunks(&self) -> Result<()> { + let uncompressed_block_end = + BlobMetadataBlockGroup::compute_uncompressed_block_end(self.block_groups()); + 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(()) + } + + 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.blob_id, + self.compressor(), + self.chunk_block_count(), + self.chunks().to_vec(), + block_groups, + ) + } + + /// Start configuring a blob meta read; finish with + /// [`load`](BlobMetadataLoader::load) or + /// [`from_bytes`](BlobMetadataLoader::from_bytes). + pub fn loader() -> BlobMetadataLoader { + BlobMetadataLoader::default() } /// Write the serialized metadata (header, tables, zero padding) to @@ -968,10 +1072,10 @@ impl BlobMetadata { 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])?; - } + // The tail padding is sub-block by construction (`padded_size` is + // `used_size` aligned up to one block), so one zero block covers it. + let padding_size = (self.padded_size() - self.header.used_size()) as usize; + writer.write_all(&[0u8; EROFS_BLOCK_SIZE as usize][..padding_size])?; Ok(()) } @@ -1038,20 +1142,12 @@ impl BlobMetadata { /// 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) + BlobMetadataBlockGroup::has_redirect(self.block_groups()) } /// Total number of uncompressed blocks in the dense address space. pub fn total_blocks(&self) -> u64 { - self.block_groups() - .last() - .map(|block_group| { - block_group.uncompressed_block_offset() - + block_group.uncompressed_block_count() as u64 - }) - .unwrap_or(0) + BlobMetadataBlockGroup::compute_uncompressed_block_end(self.block_groups()) } /// O(1) mapping from an uncompressed byte offset in the dense address @@ -1059,71 +1155,50 @@ impl BlobMetadata { /// 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 + /// `1 << block_group_block_count_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() { 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() } pub fn total_uncompressed_size(&self) -> u64 { - block_groups_total_uncompressed_size(self.block_groups()) + BlobMetadataBlockGroup::compute_uncompressed_end(self.block_groups()) } pub fn total_compressed_size(&self) -> u64 { - block_groups_total_compressed_size(self.block_groups()) - } - - pub fn metadata_size(&self) -> u64 { - self.header.metadata_size() - } - - /// 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. + BlobMetadataBlockGroup::compute_compressed_end(self.block_groups()) + } + + pub fn padded_size(&self) -> u64 { + self.header.padded_size() + } + + 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) + } + 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 + let padding_size = (self.padded_size() - self.header.used_size()) as usize; + crc32c_append(crc32, &[0u8; EROFS_BLOCK_SIZE as usize][..padding_size]) } } @@ -1160,7 +1235,7 @@ impl BlobMetadataLoader { /// Read blob metadata from an in-memory byte slice. pub fn from_bytes(self, data: &[u8]) -> Result { - BlobMetadata::from_bytes_inner( + BlobMetadata::from_bytes( data, self.blob_id.unwrap_or([0u8; SHA256_DIGEST_SIZE]), self.verify_crc32, @@ -1168,52 +1243,25 @@ impl BlobMetadataLoader { } } -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" - ))); - } +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 { + + let bits = blocks.ilog2() as u8; + if bits > NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS { return Err(Error::InvalidImage(format!( - "blob meta {what} block count too large: {blocks}" + "blob meta 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") + Ok(bits) } fn validate_padding(data: &[u8], header: &BlobMetadataHeader) -> Result<()> { - let padding_start = header.entries_end() as usize; + let padding_start = header.used_size() as usize; if data[padding_start..].iter().any(|byte| *byte != 0) { return Err(Error::InvalidImage( "blob meta padding must be zero".to_string(), @@ -1234,126 +1282,6 @@ fn validate_blob_metadata_crc32(data: &[u8], header: &BlobMetadataHeader) -> Res 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::(); @@ -1416,11 +1344,12 @@ mod tests { /// block group. fn minimal_blob_metadata() -> BlobMetadata { let payload = vec![0x33; EROFS_BLOCK_SIZE as usize]; - BlobMetadata::from_parts( + BlobMetadata::new( [0x7bu8; SHA256_DIGEST_SIZE], + BlobMetadataCompressor::None, 1, - vec![block_group(0, 1, 0, 4096, &payload)], vec![chunk(&payload, 0, 1)], + vec![block_group(0, 1, 0, 4096, &payload)], ) .unwrap() } @@ -1439,11 +1368,12 @@ mod tests { 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( + let blob_metadata = BlobMetadata::new( blob_id, + 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, &block_group_payload)], ) .unwrap(); @@ -1453,10 +1383,10 @@ mod tests { 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_table_size(), 96); + assert_eq!(loaded.header().block_group_table_size(), 40); + assert_eq!(loaded.header().used_size(), 4096 + 96 + 40); + assert_eq!(loaded.header().padded_size(), 8192); assert_eq!(loaded.header().chunk_size(), EROFS_BLOCK_SIZE); assert_eq!(loaded.header().block_group_block_count(), 2); assert_eq!(loaded.header().compressor(), BlobMetadataCompressor::None); @@ -1640,6 +1570,9 @@ mod tests { let err = BlobMetadataChunk::new([0u8; 32], 0, 0).unwrap_err(); assert!(err.to_string().contains("must be non-zero"), "{err}"); + let err = BlobMetadataChunk::new([0u8; 32], u64::MAX, 1).unwrap_err(); + assert!(err.to_string().contains("overflow"), "{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. @@ -1674,9 +1607,11 @@ mod tests { // irrelevant to this mapping. 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( + let blob_metadata = BlobMetadata::new( [0u8; SHA256_DIGEST_SIZE], + BlobMetadataCompressor::None, 1, + 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,7 +1623,6 @@ 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(); @@ -1728,9 +1662,11 @@ mod tests { // 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( + let err = match BlobMetadata::new( [0u8; SHA256_DIGEST_SIZE], + BlobMetadataCompressor::None, 1, + 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,7 +1678,6 @@ 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, @@ -1758,15 +1693,15 @@ mod tests { // exponent (4 blocks -> bits 2) so every block still shifts to block // group index 0. let three = vec![0x44; 3 * EROFS_BLOCK_SIZE as usize]; - let blob_metadata = BlobMetadata::from_parts( + let blob_metadata = BlobMetadata::new( [0u8; SHA256_DIGEST_SIZE], + BlobMetadataCompressor::None, 1, - vec![block_group(0, 3, 0, 3 * EROFS_BLOCK_SIZE, &three)], 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 { @@ -1785,14 +1720,15 @@ mod tests { // be encoded. 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( + let err = match BlobMetadata::new( [0u8; SHA256_DIGEST_SIZE], + BlobMetadataCompressor::None, 1, + 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, @@ -1807,14 +1743,15 @@ mod tests { // 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( + let blob_metadata = BlobMetadata::new( [0u8; SHA256_DIGEST_SIZE], + BlobMetadataCompressor::None, 1, + 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(); @@ -1827,14 +1764,15 @@ mod tests { 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( + let err = match BlobMetadata::new( [0u8; SHA256_DIGEST_SIZE], + BlobMetadataCompressor::None, 1, + 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, @@ -1918,11 +1856,12 @@ mod tests { .unwrap(), ]; - let blob_metadata = BlobMetadata::from_parts( + let blob_metadata = BlobMetadata::new( [0x9du8; SHA256_DIGEST_SIZE], - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, - block_groups.clone(), + BlobMetadataCompressor::None, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, Vec::new(), + block_groups.clone(), ) .unwrap(); assert!(blob_metadata.is_redirect_blob()); @@ -1930,8 +1869,8 @@ mod tests { // block-to-block group mapping, so the header keeps the default // exponent. 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(); diff --git a/nydus-format/src/blob/mod.rs b/nydus-format/src/blob/mod.rs index a51cf7f64a3..47ce9fa7de2 100644 --- a/nydus-format/src/blob/mod.rs +++ b/nydus-format/src/blob/mod.rs @@ -18,9 +18,9 @@ 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 @@ -46,7 +46,7 @@ pub fn assemble_full_blob( 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_size = blob_metadata.padded_size(); let blob_metadata_blocks = bytes_to_blocks(blob_metadata_size, "blob meta")?; let mut blob_metadata_bytes = Vec::with_capacity( diff --git a/nydus-storage/src/cache/local.rs b/nydus-storage/src/cache/local.rs index de3963b5b40..79638c64e3f 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}; @@ -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 @@ -914,7 +914,7 @@ 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; @@ -928,11 +928,12 @@ mod tests { payload: &[u8], crc32: u32, ) -> BlobMetadata { - BlobMetadata::from_parts( + BlobMetadata::new( blob_id, + 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() } @@ -1206,11 +1207,12 @@ 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( + let redirect_meta = BlobMetadata::new( sha256_bytes(&payload), + 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()); diff --git a/nydus-storage/src/cache/mod.rs b/nydus-storage/src/cache/mod.rs index aa9135c5550..ebe484ef642 100644 --- a/nydus-storage/src/cache/mod.rs +++ b/nydus-storage/src/cache/mod.rs @@ -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..274b610182a 100644 --- a/nydus-storage/src/cache/remote.rs +++ b/nydus-storage/src/cache/remote.rs @@ -120,16 +120,17 @@ impl BlobCache for RemoteBlobCache { 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, write_minimal_full_blob}; use tempfile::tempdir; fn blob_metadata(blob_id: [u8; SHA256_DIGEST_SIZE], payload: &[u8]) -> BlobMetadata { - BlobMetadata::from_parts( + BlobMetadata::new( blob_id, + 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() } diff --git a/nydus/src/bin/nydus/build.rs b/nydus/src/bin/nydus/build.rs index f9507d38452..6c3f1e90bcb 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" )] diff --git a/nydus/src/build/blob_chunk.rs b/nydus/src/build/blob_chunk.rs index 101fdeaaa62..d9c92023273 100644 --- a/nydus/src/build/blob_chunk.rs +++ b/nydus/src/build/blob_chunk.rs @@ -2,7 +2,7 @@ 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; @@ -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(), @@ -135,12 +135,12 @@ impl BlobWriter { blob_id: [u8; EROFS_BLOB_ID_SIZE], source_offset_bias: u64, ) -> Result { - Ok(BlobMetadata::from_parts_with_options( + Ok(BlobMetadata::new( blob_id, - self.file_chunk_size / EROFS_BLOCK_SIZE, 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)?) } @@ -323,7 +323,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 +349,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 +403,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"); @@ -511,12 +518,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 +538,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()), @@ -550,7 +557,7 @@ mod tests { 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) @@ -563,9 +570,9 @@ mod tests { let blob_metadata = BlobMetadata::load(&blob_metadata_path).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/mod.rs b/nydus/src/build/mod.rs index cf8c39c179a..c8d119e4353 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" @@ -208,7 +208,7 @@ pub(crate) fn assemble_ondemand_artifact( blob_metadata: &BlobMetadata, ) -> 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, ); diff --git a/nydus/src/check/mod.rs b/nydus/src/check/mod.rs index 49254a3d59d..1c1e7e361d3 100644 --- a/nydus/src/check/mod.rs +++ b/nydus/src/check/mod.rs @@ -479,7 +479,7 @@ fn blob_metadata_summary_from_bytes(data: &[u8]) -> Result #[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,10 @@ 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( + let blob_metadata = BlobMetadata::new( [0u8; EROFS_BLOB_ID_SIZE], - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, + BlobMetadataCompressor::None, + DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, Vec::new(), Vec::new(), ) diff --git a/nydus/src/optimize/mod.rs b/nydus/src/optimize/mod.rs index 28532ba3636..a5d287af4f1 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}; @@ -167,12 +167,12 @@ 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( + let blob_metadata = BlobMetadata::new( data_digest, - NYDUS_BLOB_METADATA_DEFAULT_CHUNK_BLOCK_COUNT, 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/tests/testsuite/nydus_core.rs b/nydus/tests/testsuite/nydus_core.rs index aa40521cfa8..c67c5720468 100644 --- a/nydus/tests/testsuite/nydus_core.rs +++ b/nydus/tests/testsuite/nydus_core.rs @@ -88,16 +88,19 @@ 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(); @@ -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(); From 19837a509a8d4f5c894cb8a013bd10dd99ab13ba Mon Sep 17 00:00:00 2001 From: Gaius Date: Mon, 24 Aug 2026 10:45:28 +0000 Subject: [PATCH 4/7] refactor(blob/metadata): consolidate validation and parsing into methods Merge `validate_padding` and `validate_blob_metadata_crc32` free functions into a single `BlobMetadata::validate_bytes` method, and simplify chunk/block-group parsing to use `chunks_exact` instead of manual index arithmetic. Signed-off-by: Gaius --- nydus-format/src/blob/metadata.rs | 136 +++++++++++++----------------- 1 file changed, 59 insertions(+), 77 deletions(-) diff --git a/nydus-format/src/blob/metadata.rs b/nydus-format/src/blob/metadata.rs index f656bc62cc5..ffffafbba41 100644 --- a/nydus-format/src/blob/metadata.rs +++ b/nydus-format/src/blob/metadata.rs @@ -826,56 +826,40 @@ impl BlobMetadata { } fn from_bytes( - data: &[u8], + bytes: &[u8], blob_id: [u8; SHA256_DIGEST_SIZE], - check_crc32: bool, + verify_crc32: bool, ) -> Result { - if data.len() < NYDUS_BLOB_METADATA_HEADER_SIZE { + 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.padded_size() { - return Err(Error::InvalidImage(format!( - "blob meta data size mismatch: expected {}, got {}", - header.padded_size(), - data.len() - ))); - } - validate_padding(data, &header)?; - if check_crc32 { - validate_blob_metadata_crc32(data, &header)?; - } + }; - let chunks = (0..header.chunk_count() as usize) - .map(|index| { - let start = - header.chunks_offset() as usize + index * size_of::(); - 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_groups = (0..header.block_group_count() as usize) - .map(|index| { - let start = header.block_groups_offset() as usize - + index * size_of::(); - BlobMetadataBlockGroup::from_bytes( - data[start..start + size_of::()] - .try_into() - .expect("length checked"), - ) - .with_context(|| format!("failed to read blob meta block group {index}")) + 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 blob_metadata = Self { header, blob_id, @@ -894,7 +878,7 @@ impl BlobMetadata { Self::load_inner(path, false) } - fn load_inner(path: &Path, check_crc32: bool) -> Result { + fn load_inner(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(); @@ -908,17 +892,7 @@ impl BlobMetadata { .try_into() .expect("length checked"), )?; - if file_len != header.padded_size() { - return Err(Error::InvalidImage(format!( - "blob meta file size mismatch: expected {}, got {}", - header.padded_size(), - file_len - ))); - } - validate_padding(&mmap, &header)?; - if check_crc32 { - validate_blob_metadata_crc32(&mmap, &header)?; - } + Self::validate_bytes(&mmap, &header, verify_crc32)?; let blob_metadata = Self { header, blob_id: [0u8; SHA256_DIGEST_SIZE], @@ -1034,6 +1008,36 @@ impl BlobMetadata { Ok(()) } + 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 size mismatch: expected {}, got {}", + header.padded_size(), + bytes.len() + ))); + } + + 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(), + )); + } + + 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(()) + } + 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() { @@ -1234,9 +1238,9 @@ impl BlobMetadataLoader { } /// Read blob metadata from an in-memory byte slice. - pub fn from_bytes(self, data: &[u8]) -> Result { + pub fn from_bytes(self, bytes: &[u8]) -> Result { BlobMetadata::from_bytes( - data, + bytes, self.blob_id.unwrap_or([0u8; SHA256_DIGEST_SIZE]), self.verify_crc32, ) @@ -1260,28 +1264,6 @@ fn block_count_to_bits(blocks: u32) -> Result { Ok(bits) } -fn validate_padding(data: &[u8], header: &BlobMetadataHeader) -> Result<()> { - let padding_start = header.used_size() 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() { - return Err(Error::InvalidImage(format!( - "blob meta header crc32 mismatch: stored {:#010x}, computed {:#010x}", - header.crc32(), - computed - ))); - } - Ok(()) -} - 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::(); From bd793620c860a31c76f3815bfd54803370db0abb Mon Sep 17 00:00:00 2001 From: Gaius Date: Tue, 25 Aug 2026 08:19:45 +0000 Subject: [PATCH 5/7] refactor: simplify and consolidate blob metadata API across crates Replace builder-pattern blob metadata loading with direct `from_bytes` calls, rename and inline private helpers, add doc comments, and unify method naming across blob metadata, cache, and storage layers for improved clarity. Signed-off-by: Gaius --- nydus-backend/src/local.rs | 7 +- nydus-backend/src/registry/mod.rs | 4 +- nydus-core/src/blob.rs | 2 +- nydus-core/src/reader/data.rs | 5 +- nydus-core/src/reader/metadata.rs | 4 +- nydus-core/src/reader/mod.rs | 4 +- nydus-format/src/blob/algorithm.rs | 6 + nydus-format/src/blob/metadata.rs | 954 +++++++++++++++++------------ nydus-format/src/blob/mod.rs | 96 ++- nydus-format/src/erofs/inode.rs | 6 +- nydus-format/src/utils/align.rs | 32 +- nydus-format/src/utils/io.rs | 18 +- nydus-format/src/utils/mod.rs | 8 +- nydus-storage/src/cache/caches.rs | 6 +- nydus-storage/src/cache/local.rs | 52 +- nydus-storage/src/cache/mod.rs | 2 +- nydus-storage/src/cache/remote.rs | 14 +- nydus-storage/src/prefetch.rs | 2 +- nydus/src/bin/nydus/build.rs | 4 +- nydus/src/build/blob_chunk.rs | 9 +- nydus/src/build/bootstrap.rs | 5 +- nydus/src/build/inode.rs | 10 +- nydus/src/build/layout.rs | 15 +- nydus/src/build/mod.rs | 10 +- nydus/src/check/mod.rs | 8 +- nydus/src/fanotify/core.rs | 3 +- nydus/src/optimize/mod.rs | 4 +- nydus/src/ublk/core.rs | 4 +- nydus/src/uffd/core.rs | 4 +- nydus/tests/testsuite/fixture.rs | 4 +- 30 files changed, 717 insertions(+), 585 deletions(-) diff --git a/nydus-backend/src/local.rs b/nydus-backend/src/local.rs index 92cb0bdebe0..c9fc72725a5 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, Some(*blob_id), false).map_err(io::Error::other) } fn save_blob_metadata(&self, blob_id: &[u8; SHA256_DIGEST_SIZE], dst: &Path) -> io::Result<()> { @@ -266,7 +263,7 @@ mod tests { fn blob_metadata(blob_id: [u8; SHA256_DIGEST_SIZE], payload: &[u8]) -> BlobMetadata { BlobMetadata::new( - blob_id, + Some(blob_id), BlobMetadataCompressor::None, 1, vec![BlobMetadataChunk::new(*blake3::hash(payload).as_bytes(), 0, 1).unwrap()], diff --git a/nydus-backend/src/registry/mod.rs b/nydus-backend/src/registry/mod.rs index 07e3e5b385a..a58dc4a1e8e 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, Some(*blob_id), 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..34e9cde7841 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(), @@ -42,12 +45,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, diff --git a/nydus-format/src/blob/metadata.rs b/nydus-format/src/blob/metadata.rs index ffffafbba41..2704e16ddd4 100644 --- a/nydus-format/src/blob/metadata.rs +++ b/nydus-format/src/blob/metadata.rs @@ -1,4 +1,5 @@ use crate::blob::algorithm::{BlobMetadataCompressor, BlobMetadataDigester}; +use crate::blob::block_count_to_bits; use crate::blob::flag::validate_incompat_flags; use crate::erofs::EROFS_BLOCK_SIZE; use crate::error::{Context, Error, Result}; @@ -17,28 +18,28 @@ 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. @@ -61,25 +62,27 @@ pub const DEFAULT_NYDUS_BLOB_METADATA_BLOCK_GROUP_BLOCK_COUNT: u32 = pub const NYDUS_BLOB_METADATA_SUFFIX: &str = ".blob.meta"; /// Largest allowed block-count exponent (`chunk_block_count_bits` / -/// `block_group_block_count_bits`): keeps the derived byte size (`4096 << bits`) -/// representable in a `u32` (2 GiB at most). -const NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS: u8 = 19; +/// `block_group_block_count_bits`): keeps the derived byte size +/// (`4096 << bits`) within a `u32` (2 GiB at most). +pub(super) 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; @@ -135,6 +138,10 @@ pub struct BlobMetadataHeader { } 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(), @@ -154,6 +161,10 @@ impl BlobMetadataHeader { 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); @@ -170,6 +181,15 @@ impl BlobMetadataHeader { 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())); @@ -226,68 +246,92 @@ impl BlobMetadataHeader { Ok(()) } + /// 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()).unwrap() } + /// Number of entries in the chunk table. pub fn chunk_count(&self) -> u32 { self.chunk_count } + /// Uncompressed 4KiB blocks per chunk (`1 << chunk_block_count_bits`). pub fn chunk_block_count(&self) -> u32 { 1u32 << self.chunk_block_count_bits } + /// Uncompressed bytes per chunk. pub fn chunk_size(&self) -> u32 { EROFS_BLOCK_SIZE << self.chunk_block_count_bits } + /// Byte offset of the chunk table, always right after the header. pub fn chunks_offset(&self) -> u64 { self.chunks_offset } + /// Byte size of the chunk table. pub fn chunk_table_size(&self) -> u64 { self.chunk_count as u64 * size_of::() as u64 } + /// Number of entries in the block group table. pub fn block_group_count(&self) -> u32 { self.block_group_count } + /// 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 } + /// Byte offset of the block group table, right after the chunk table. pub fn block_groups_offset(&self) -> u64 { self.block_groups_offset } + /// 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 } + /// 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() } + /// The full serialized size: [`Self::used_size`] aligned up to one + /// 4KiB block. pub fn padded_size(&self) -> u64 { - crate::utils::align_up(self.used_size(), EROFS_BLOCK_SIZE as u64) + crate::utils::align_up_u64(self.used_size(), EROFS_BLOCK_SIZE as u64) .expect("blob meta size overflowed") } } @@ -319,6 +363,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 { @@ -366,7 +412,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( @@ -399,24 +445,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 } + /// 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 .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 } @@ -427,6 +481,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 @@ -460,10 +527,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, @@ -486,6 +555,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, @@ -511,6 +583,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), @@ -527,6 +601,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); @@ -540,6 +615,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( @@ -591,6 +669,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( @@ -641,11 +721,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(|| { @@ -658,44 +741,57 @@ 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 } @@ -705,6 +801,10 @@ impl BlobMetadataBlockGroup { 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) { @@ -717,34 +817,11 @@ impl BlobMetadataBlockGroup { [first, ..] => block_count_to_bits(first.uncompressed_block_count()), } } - - fn compute_uncompressed_block_end(block_groups: &[Self]) -> u64 { - block_groups - .last() - .map(|block_group| { - block_group.uncompressed_block_offset() - + block_group.uncompressed_block_count() as u64 - }) - .unwrap_or(0) - } - - fn compute_uncompressed_end(block_groups: &[Self]) -> u64 { - block_groups - .last() - .map(|block_group| block_group.uncompressed_offset() + block_group.uncompressed_size()) - .unwrap_or(0) - } - - fn compute_compressed_end(block_groups: &[Self]) -> u64 { - block_groups - .last() - .map(|block_group| { - block_group.compressed_offset() + block_group.compressed_size() as u64 - }) - .unwrap_or(0) - } } +/// 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, @@ -765,16 +842,17 @@ enum BlobMetadataStorage { /// │ header │ chunk table │ block group table │ zero padding │ /// └────────┴─────────────┴───────────────────┴──────────────┘ /// 0 4096 ▲ EOF -/// the entries end here; the padding +/// 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::new`]) or a shared file mapping read in place -/// ([`Self::load`]), zero-copy thanks to the entries' pinned layout. +/// ([`Self::from_path`]), zero-copy thanks to the entries' pinned layout. +#[derive(Debug)] pub struct BlobMetadata { header: BlobMetadataHeader, - blob_id: [u8; SHA256_DIGEST_SIZE], + blob_id: Option<[u8; SHA256_DIGEST_SIZE]>, storage: BlobMetadataStorage, } @@ -784,7 +862,7 @@ impl BlobMetadata { /// metadata is valid by definition, then the crc32 is computed over the /// final bytes. pub fn new( - blob_id: [u8; SHA256_DIGEST_SIZE], + blob_id: Option<[u8; SHA256_DIGEST_SIZE]>, compressor: BlobMetadataCompressor, chunk_block_count: u32, chunks: Vec, @@ -825,9 +903,12 @@ impl BlobMetadata { Ok(blob_metadata) } - fn from_bytes( + /// Read blob metadata from an in-memory byte slice, optionally attaching + /// the owning blob id and verifying the header crc32 over the full + /// metadata. + pub fn from_bytes( bytes: &[u8], - blob_id: [u8; SHA256_DIGEST_SIZE], + blob_id: Option<[u8; SHA256_DIGEST_SIZE]>, verify_crc32: bool, ) -> Result { let Some((header_bytes, _)) = bytes.split_first_chunk::() @@ -872,41 +953,84 @@ impl BlobMetadata { Ok(blob_metadata) } - /// 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 load_inner(path: &Path, verify_crc32: bool) -> Result { + /// Read blob metadata from a file (mmap-backed), optionally attaching + /// the owning blob id and verifying the header crc32 over the full + /// metadata. + pub fn from_path( + path: &Path, + blob_id: Option<[u8; SHA256_DIGEST_SIZE]>, + 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"), - )?; + + 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, - blob_id: [0u8; SHA256_DIGEST_SIZE], + blob_id, 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(); @@ -916,7 +1040,7 @@ impl BlobMetadata { )); } - let is_redirect_blob = BlobMetadataBlockGroup::has_redirect(block_groups); + 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() { @@ -931,7 +1055,7 @@ impl BlobMetadata { ))); } - if !is_redirect_blob { + if !is_redirect { match ( index + 1 == block_groups.len(), block_group.uncompressed_block_count(), @@ -982,32 +1106,11 @@ impl BlobMetadata { Ok(()) } - fn validate_chunks(&self) -> Result<()> { - let uncompressed_block_end = - BlobMetadataBlockGroup::compute_uncompressed_block_end(self.block_groups()); - 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(()) - } - + /// 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!( @@ -1038,6 +1141,9 @@ impl BlobMetadata { 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() { @@ -1053,31 +1159,23 @@ impl BlobMetadata { ) } - /// Start configuring a blob meta read; finish with - /// [`load`](BlobMetadataLoader::load) or - /// [`from_bytes`](BlobMetadataLoader::from_bytes). - pub fn loader() -> BlobMetadataLoader { - BlobMetadataLoader::default() - } - /// 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)?; } - // The tail padding is sub-block by construction (`padded_size` is - // `used_size` aligned up to one block), so one zero block covers it. + let padding_size = (self.padded_size() - self.header.used_size()) as usize; writer.write_all(&[0u8; EROFS_BLOCK_SIZE as usize][..padding_size])?; Ok(()) @@ -1090,97 +1188,149 @@ 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 + /// The owning blob id, if one was attached at construction or on read. + /// An in-memory tag, never part of the serialized metadata. + pub fn blob_id(&self) -> Option<&[u8; SHA256_DIGEST_SIZE]> { + self.blob_id.as_ref() } + /// 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 { + /// 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 { - BlobMetadataBlockGroup::compute_uncompressed_block_end(self.block_groups()) + /// 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| { + block_group.uncompressed_block_offset() + + block_group.uncompressed_block_count() as u64 + }) + .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_count_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_count_bits).ok() } - pub fn total_uncompressed_size(&self) -> u64 { - BlobMetadataBlockGroup::compute_uncompressed_end(self.block_groups()) + /// 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_compressed_size(&self) -> u64 { - BlobMetadataBlockGroup::compute_compressed_end(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) } + /// The full serialized size, 4KiB aligned. pub fn padded_size(&self) -> u64 { self.header.padded_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(); @@ -1188,6 +1338,9 @@ impl BlobMetadata { crc32c_append(crc32c(&zeroed), tail) } + /// 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 zeroed = self.header.to_bytes(); zeroed[NYDUS_BLOB_METADATA_HEADER_CRC32_FIELD].fill(0); @@ -1206,83 +1359,6 @@ impl BlobMetadata { } } -/// 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, bytes: &[u8]) -> Result { - BlobMetadata::from_bytes( - bytes, - self.blob_id.unwrap_or([0u8; SHA256_DIGEST_SIZE]), - self.verify_crc32, - ) - } -} - -fn block_count_to_bits(blocks: u32) -> Result { - if !blocks.is_power_of_two() { - return Err(Error::InvalidImage(format!( - "blob meta block count must be a non-zero power of two: {blocks}" - ))); - } - - let bits = blocks.ilog2() as u8; - if bits > NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS { - return Err(Error::InvalidImage(format!( - "blob meta block count too large: {blocks}" - ))); - } - - Ok(bits) -} - -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) } -} - #[cfg(test)] mod tests { use super::*; @@ -1292,16 +1368,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), @@ -1309,76 +1389,113 @@ 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(None, 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::new( - [0x7bu8; SHA256_DIGEST_SIZE], + Some([0x7b; SHA256_DIGEST_SIZE]), BlobMetadataCompressor::None, 1, vec![chunk(&payload, 0, 1)], - vec![block_group(0, 1, 0, 4096, &payload)], + 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.blob_id(), Some(&[0x7b; SHA256_DIGEST_SIZE])); + 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 blob_id = [0x5a; 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 both = [payload_a.as_slice(), payload_b.as_slice()].concat(); let blob_metadata = BlobMetadata::new( - blob_id, + Some(blob_id), BlobMetadataCompressor::None, 1, vec![chunk(&payload_a, 0, 1), chunk(&payload_b, 1, 1)], - vec![block_group(0, 2, 8192, 8192, &block_group_payload)], + 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_table_size(), 96); - assert_eq!(loaded.header().block_group_table_size(), 40); - assert_eq!(loaded.header().used_size(), 4096 + 96 + 40); - assert_eq!(loaded.header().padded_size(), 8192); - assert_eq!(loaded.header().chunk_size(), EROFS_BLOCK_SIZE); + + let loaded = BlobMetadata::from_path(&path, None, false).unwrap(); + assert!(loaded.blob_id().is_none()); + 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); + + let tagged = BlobMetadata::from_path(&path, Some(blob_id), true).unwrap(); + assert_eq!(tagged.blob_id(), Some(&blob_id)); } #[test] @@ -1406,38 +1523,21 @@ mod tests { .unwrap(), ); - let loaded = BlobMetadata::loader().from_bytes(&raw).unwrap(); - + let loaded = BlobMetadata::from_bytes(&raw, None, 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, None, 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, None, false).unwrap(); + let err = BlobMetadata::from_bytes(&raw, None, true).unwrap_err(); + assert!(err.to_string().contains("crc32"), "{err}"); } #[test] @@ -1474,9 +1574,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, None, false); match expected_err { None => { result.unwrap_or_else(|err| panic!("{case}: {err}")); @@ -1491,32 +1589,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, None, 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, None, 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], None, false).unwrap_err(); + assert!(err.to_string().contains("too small"), "{err}"); + + let err = BlobMetadata::from_bytes(&raw[..raw.len() - 1], None, 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, None, 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, None, false).unwrap_err(); + assert!(err.to_string().contains("padding must be zero"), "{err}"); } #[test] @@ -1555,9 +1679,6 @@ mod tests { let err = BlobMetadataChunk::new([0u8; 32], u64::MAX, 1).unwrap_err(); assert!(err.to_string().contains("overflow"), "{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 valid = BlobMetadataBlockGroup::new(0, 1, 0, 4096, 0) .unwrap() .to_bytes(); @@ -1582,17 +1703,53 @@ 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::new( - [0u8; SHA256_DIGEST_SIZE], - BlobMetadataCompressor::None, - 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), @@ -1607,33 +1764,25 @@ mod tests { ], ) .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] @@ -1641,13 +1790,8 @@ 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::new( - [0u8; SHA256_DIGEST_SIZE], - BlobMetadataCompressor::None, - 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), @@ -1660,25 +1804,31 @@ mod tests { ), block_group(5, 1, 5 * EROFS_BLOCK_SIZE as u64, EROFS_BLOCK_SIZE, &one), ], - ) { - 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::new( - [0u8; SHA256_DIGEST_SIZE], - BlobMetadataCompressor::None, - 1, + let blob_metadata = build( vec![chunk(&three, 0, 3)], vec![block_group(0, 3, 0, 3 * EROFS_BLOCK_SIZE, &three)], ) @@ -1688,33 +1838,28 @@ mod tests { 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::new( - [0u8; SHA256_DIGEST_SIZE], - BlobMetadataCompressor::None, - 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), ], - ) { - 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}"); } @@ -1722,13 +1867,7 @@ 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::new( - [0u8; SHA256_DIGEST_SIZE], - BlobMetadataCompressor::None, - 1, + let blob_metadata = build( vec![chunk(&two, 0, 2), chunk(&two, 2, 2)], vec![ block_group(0, 2, 0, 5000, &two), @@ -1738,33 +1877,38 @@ mod tests { .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::new( - [0u8; SHA256_DIGEST_SIZE], - BlobMetadataCompressor::None, - 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), ], - ) { - 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, None, 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 = @@ -1783,7 +1927,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(); @@ -1839,25 +1982,22 @@ mod tests { ]; let blob_metadata = BlobMetadata::new( - [0x9du8; SHA256_DIGEST_SIZE], + Some([0x9d; SHA256_DIGEST_SIZE]), 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_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, None, 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 47ce9fa7de2..3925a81c46b 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; @@ -23,70 +23,68 @@ pub use metadata::{ DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, NYDUS_BLOB_METADATA_SUFFIX, }; -/// Append the trailing regions of the full-blob layout +/// Encode a power-of-two 4KiB block count as the log2 stored in the blob +/// meta 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 block count must be a non-zero power of two: {blocks}" + ))); + } + + let bits = blocks.ilog2() as u8; + if bits > metadata::NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS { + return Err(Error::InvalidImage(format!( + "blob meta block count too large: {blocks}" + ))); + } + + Ok(bits) +} + +/// 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.padded_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()))?; + + write_zeros(writer, bootstrap_offset - compressed_data_size)?; + writer + .write_all(bootstrap) + .context("failed to write blob bootstrap")?; - 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, + 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, + bytes_to_blocks(bootstrap_size, "bootstrap")?, blob_metadata_offset, - blob_metadata_blocks, + bytes_to_blocks(blob_metadata.padded_size(), "blob meta")?, )?; - - 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, - blob_metadata_offset, - )?; - 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 79638c64e3f..cd7f5c628fb 100644 --- a/nydus-storage/src/cache/local.rs +++ b/nydus-storage/src/cache/local.rs @@ -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(()); } @@ -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(), Some(blob_id), 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, Some(blob_id), true).map_err(io::Error::other) } /// Drop guard that ensures a leader always signals its flight and cleans up @@ -929,7 +921,7 @@ mod tests { crc32: u32, ) -> BlobMetadata { BlobMetadata::new( - blob_id, + Some(blob_id), BlobMetadataCompressor::None, 1, vec![BlobMetadataChunk::new(*blake3::hash(payload).as_bytes(), 0, 1).unwrap()], @@ -1208,14 +1200,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::new( - sha256_bytes(&payload), + Some(sha256_bytes(&payload)), BlobMetadataCompressor::None, 1, 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); @@ -1359,7 +1351,7 @@ mod tests { 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 ebe484ef642..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 } diff --git a/nydus-storage/src/cache/remote.rs b/nydus-storage/src/cache/remote.rs index 274b610182a..0fb1ce997b3 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() } } @@ -126,7 +126,7 @@ mod tests { fn blob_metadata(blob_id: [u8; SHA256_DIGEST_SIZE], payload: &[u8]) -> BlobMetadata { BlobMetadata::new( - blob_id, + Some(blob_id), BlobMetadataCompressor::None, 1, vec![BlobMetadataChunk::new(*blake3::hash(payload).as_bytes(), 0, 1).unwrap()], 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 6c3f1e90bcb..826167fcb5f 100644 --- a/nydus/src/bin/nydus/build.rs +++ b/nydus/src/bin/nydus/build.rs @@ -410,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 d9c92023273..25f195874fe 100644 --- a/nydus/src/build/blob_chunk.rs +++ b/nydus/src/build/blob_chunk.rs @@ -5,7 +5,7 @@ use nydus_format::blob::{ 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}; @@ -136,7 +136,7 @@ impl BlobWriter { source_offset_bias: u64, ) -> Result { Ok(BlobMetadata::new( - blob_id, + Some(blob_id), self.compressor, self.file_chunk_size / EROFS_BLOCK_SIZE, self.blob_metadata_chunks.clone(), @@ -207,7 +207,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 { @@ -567,7 +568,7 @@ mod tests { // 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, None, 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_table_size(), 48); 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 c8d119e4353..260473796f7 100644 --- a/nydus/src/build/mod.rs +++ b/nydus/src/build/mod.rs @@ -167,7 +167,7 @@ pub fn build_image(options: &BuildImageOptions, writer: impl Write) -> Result 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, None, 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(), }) } @@ -553,7 +553,7 @@ mod tests { let data = [0x5au8; EROFS_BLOCK_SIZE as usize]; let data_digest = sha256_bytes(&data); let blob_metadata = BlobMetadata::new( - [0u8; EROFS_BLOB_ID_SIZE], + None, BlobMetadataCompressor::None, DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, 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 a5d287af4f1..3d105ff90d7 100644 --- a/nydus/src/optimize/mod.rs +++ b/nydus/src/optimize/mod.rs @@ -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}" @@ -168,7 +168,7 @@ pub fn build_ondemand_blob( data_digest.copy_from_slice(&data_hasher.finalize()); let blob_metadata = BlobMetadata::new( - data_digest, + Some(data_digest), BlobMetadataCompressor::Zstd, DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, Vec::new(), diff --git a/nydus/src/ublk/core.rs b/nydus/src/ublk/core.rs index e3ff9e10932..dd5d8293cf1 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 downloads and validates its meta and sizes its cache // file, which takes seconds for a large image. Left to the first block 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/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, From 88b55f55b6bbb740a02699e5b9636bb99d27f955 Mon Sep 17 00:00:00 2001 From: Gaius Date: Tue, 25 Aug 2026 08:57:15 +0000 Subject: [PATCH 6/7] refactor: remove blob_id and data_digest params from blob metadata APIs Remove the `blob_id` field from `BlobMetadata` and eliminate redundant `blob_id`/`data_digest` parameters from related APIs, simplifying the blob metadata construction and usage across build, check, optimize, backend, format, and storage subsystems. Signed-off-by: Gaius --- nydus-backend/src/local.rs | 22 ++---- nydus-backend/src/registry/mod.rs | 2 +- nydus-format/src/blob/metadata.rs | 96 ++++++++++++--------------- nydus-format/src/blob/mod.rs | 19 ------ nydus-storage/src/cache/local.rs | 52 +++++---------- nydus-storage/src/cache/remote.rs | 11 ++- nydus/src/build/blob_chunk.rs | 27 ++------ nydus/src/build/mod.rs | 2 +- nydus/src/check/mod.rs | 3 +- nydus/src/optimize/mod.rs | 1 - nydus/tests/testsuite/erofs_reader.rs | 4 +- nydus/tests/testsuite/nydus_core.rs | 2 +- 12 files changed, 80 insertions(+), 161 deletions(-) diff --git a/nydus-backend/src/local.rs b/nydus-backend/src/local.rs index c9fc72725a5..35e9e10e9eb 100644 --- a/nydus-backend/src/local.rs +++ b/nydus-backend/src/local.rs @@ -198,7 +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::from_bytes(&data, Some(*blob_id), false).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<()> { @@ -261,9 +261,8 @@ mod tests { use nydus_format::utils::sha256_bytes; use tempfile::tempdir; - fn blob_metadata(blob_id: [u8; SHA256_DIGEST_SIZE], payload: &[u8]) -> BlobMetadata { + fn blob_metadata(payload: &[u8]) -> BlobMetadata { BlobMetadata::new( - Some(blob_id), BlobMetadataCompressor::None, 1, vec![BlobMetadataChunk::new(*blake3::hash(payload).as_bytes(), 0, 1).unwrap()], @@ -278,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(); @@ -307,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 a58dc4a1e8e..af8ab9abaab 100644 --- a/nydus-backend/src/registry/mod.rs +++ b/nydus-backend/src/registry/mod.rs @@ -649,7 +649,7 @@ impl Registry { ReadContext::raw(ReadKind::OnDemand), )?; - BlobMetadata::from_bytes(&blob_metadata_bytes, Some(*blob_id), false) + BlobMetadata::from_bytes(&blob_metadata_bytes, false) .map_err(|err| RegistryError::Io(io::Error::other(err))) } diff --git a/nydus-format/src/blob/metadata.rs b/nydus-format/src/blob/metadata.rs index 2704e16ddd4..06354c98e4b 100644 --- a/nydus-format/src/blob/metadata.rs +++ b/nydus-format/src/blob/metadata.rs @@ -1,5 +1,4 @@ use crate::blob::algorithm::{BlobMetadataCompressor, BlobMetadataDigester}; -use crate::blob::block_count_to_bits; use crate::blob::flag::validate_incompat_flags; use crate::erofs::EROFS_BLOCK_SIZE; use crate::error::{Context, Error, Result}; @@ -7,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}; @@ -64,7 +62,7 @@ pub const NYDUS_BLOB_METADATA_SUFFIX: &str = ".blob.meta"; /// 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). -pub(super) const NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS: u8 = 19; +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; @@ -852,7 +850,6 @@ enum BlobMetadataStorage { #[derive(Debug)] pub struct BlobMetadata { header: BlobMetadataHeader, - blob_id: Option<[u8; SHA256_DIGEST_SIZE]>, storage: BlobMetadataStorage, } @@ -862,7 +859,6 @@ impl BlobMetadata { /// metadata is valid by definition, then the crc32 is computed over the /// final bytes. pub fn new( - blob_id: Option<[u8; SHA256_DIGEST_SIZE]>, compressor: BlobMetadataCompressor, chunk_block_count: u32, chunks: Vec, @@ -892,7 +888,6 @@ impl BlobMetadata { let mut blob_metadata = Self { header, - blob_id, storage: BlobMetadataStorage::Owned { chunks, block_groups, @@ -903,14 +898,9 @@ impl BlobMetadata { Ok(blob_metadata) } - /// Read blob metadata from an in-memory byte slice, optionally attaching - /// the owning blob id and verifying the header crc32 over the full - /// metadata. - pub fn from_bytes( - bytes: &[u8], - blob_id: Option<[u8; SHA256_DIGEST_SIZE]>, - verify_crc32: bool, - ) -> Result { + /// 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())); @@ -943,7 +933,6 @@ impl BlobMetadata { let blob_metadata = Self { header, - blob_id, storage: BlobMetadataStorage::Owned { chunks, block_groups, @@ -953,14 +942,9 @@ impl BlobMetadata { Ok(blob_metadata) } - /// Read blob metadata from a file (mmap-backed), optionally attaching - /// the owning blob id and verifying the header crc32 over the full - /// metadata. - pub fn from_path( - path: &Path, - blob_id: Option<[u8; SHA256_DIGEST_SIZE]>, - verify_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 mmap = unsafe { MmapOptions::new().map(&file) } @@ -976,7 +960,6 @@ impl BlobMetadata { let blob_metadata = Self { header, - blob_id, storage: BlobMetadataStorage::Mapped(mmap), }; blob_metadata.validate()?; @@ -1151,7 +1134,6 @@ impl BlobMetadata { } Self::new( - self.blob_id, self.compressor(), self.chunk_block_count(), self.chunks().to_vec(), @@ -1197,12 +1179,6 @@ impl BlobMetadata { &self.header } - /// The owning blob id, if one was attached at construction or on read. - /// An in-memory tag, never part of the serialized metadata. - pub fn blob_id(&self) -> Option<&[u8; SHA256_DIGEST_SIZE]> { - self.blob_id.as_ref() - } - /// Number of entries in the chunk table. pub fn chunk_count(&self) -> usize { self.header.chunk_count() as usize @@ -1359,6 +1335,25 @@ impl BlobMetadata { } } +/// 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 block count must be a non-zero power of two: {blocks}" + ))); + } + + let bits = blocks.ilog2() as u8; + if bits > NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS { + return Err(Error::InvalidImage(format!( + "blob meta block count too large: {blocks}" + ))); + } + + Ok(bits) +} + #[cfg(test)] mod tests { use super::*; @@ -1393,13 +1388,12 @@ mod tests { chunks: Vec, block_groups: Vec, ) -> Result { - BlobMetadata::new(None, BlobMetadataCompressor::None, 1, chunks, block_groups) + BlobMetadata::new(BlobMetadataCompressor::None, 1, chunks, block_groups) } fn blob_metadata() -> BlobMetadata { let payload = vec![0x33; EROFS_BLOCK_SIZE as usize]; BlobMetadata::new( - Some([0x7b; SHA256_DIGEST_SIZE]), BlobMetadataCompressor::None, 1, vec![chunk(&payload, 0, 1)], @@ -1433,7 +1427,6 @@ mod tests { assert_eq!(header.block_group_block_count(), 1); assert_ne!(header.crc32(), 0); - assert_eq!(blob_metadata.blob_id(), Some(&[0x7b; SHA256_DIGEST_SIZE])); assert_eq!(blob_metadata.chunk_count(), 1); assert_eq!(blob_metadata.block_group_count(), 1); assert_eq!(blob_metadata.chunk_block_count(), 1); @@ -1466,12 +1459,10 @@ mod tests { fn round_trips_through_a_sidecar_file() { let dir = tempdir().unwrap(); let path = dir.path().join("blob.meta"); - let blob_id = [0x5a; SHA256_DIGEST_SIZE]; let payload_a = vec![0x11; EROFS_BLOCK_SIZE as usize]; let payload_b = vec![0x22; EROFS_BLOCK_SIZE as usize]; let both = [payload_a.as_slice(), payload_b.as_slice()].concat(); let blob_metadata = BlobMetadata::new( - Some(blob_id), BlobMetadataCompressor::None, 1, vec![chunk(&payload_a, 0, 1), chunk(&payload_b, 1, 1)], @@ -1480,8 +1471,7 @@ mod tests { .unwrap(); blob_metadata.save(&path).unwrap(); - let loaded = BlobMetadata::from_path(&path, None, false).unwrap(); - assert!(loaded.blob_id().is_none()); + 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); @@ -1494,8 +1484,7 @@ mod tests { ); assert_eq!(loaded.uncompressed_size(), 8192); - let tagged = BlobMetadata::from_path(&path, Some(blob_id), true).unwrap(); - assert_eq!(tagged.blob_id(), Some(&blob_id)); + BlobMetadata::from_path(&path, true).unwrap(); } #[test] @@ -1523,10 +1512,10 @@ mod tests { .unwrap(), ); - let loaded = BlobMetadata::from_bytes(&raw, None, false).unwrap(); + let loaded = BlobMetadata::from_bytes(&raw, false).unwrap(); assert_eq!(loaded.header().crc32(), corrupted_crc32); - let err = BlobMetadata::from_bytes(&raw, None, true).unwrap_err(); + let err = BlobMetadata::from_bytes(&raw, true).unwrap_err(); assert!(err.to_string().contains("crc32"), "{err}"); } @@ -1535,8 +1524,8 @@ mod tests { let mut raw = sealed_metadata(); raw[NYDUS_BLOB_METADATA_HEADER_SIZE - 1] = 0xff; - BlobMetadata::from_bytes(&raw, None, false).unwrap(); - let err = BlobMetadata::from_bytes(&raw, None, true).unwrap_err(); + BlobMetadata::from_bytes(&raw, false).unwrap(); + let err = BlobMetadata::from_bytes(&raw, true).unwrap_err(); assert!(err.to_string().contains("crc32"), "{err}"); } @@ -1574,7 +1563,7 @@ mod tests { let mut raw = sealed_metadata(); raw[offset..offset + 4].copy_from_slice(&value); - let result = BlobMetadata::from_bytes(&raw, None, false); + let result = BlobMetadata::from_bytes(&raw, false); match expected_err { None => { result.unwrap_or_else(|err| panic!("{case}: {err}")); @@ -1591,7 +1580,7 @@ mod tests { let mut future = sealed_metadata(); future[8..12].copy_from_slice(&(NYDUS_BLOB_METADATA_VERSION + 1).to_le_bytes()); - let loaded = BlobMetadata::from_bytes(&future, None, false).unwrap(); + let loaded = BlobMetadata::from_bytes(&future, false).unwrap(); assert_eq!(loaded.header().version(), NYDUS_BLOB_METADATA_VERSION + 1); } @@ -1608,7 +1597,7 @@ mod tests { raw[..4].copy_from_slice(&magic.to_le_bytes()); std::fs::write(&path, raw).unwrap(); - let err = match BlobMetadata::from_path(&path, None, false) { + let err = match BlobMetadata::from_path(&path, false) { Ok(_) => panic!("{name}: legacy magic should be rejected"), Err(err) => err, }; @@ -1620,16 +1609,16 @@ mod tests { fn undersized_inputs_reject() { let raw = sealed_metadata(); - let err = BlobMetadata::from_bytes(&raw[..10], None, false).unwrap_err(); + 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], None, false).unwrap_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, None, false).unwrap_err(); + let err = BlobMetadata::from_path(&path, false).unwrap_err(); assert!(err.to_string().contains("too small"), "{err}"); } @@ -1639,7 +1628,7 @@ mod tests { let used_size = blob_metadata().header().used_size() as usize; raw[used_size] = 0xff; - let err = BlobMetadata::from_bytes(&raw, None, false).unwrap_err(); + let err = BlobMetadata::from_bytes(&raw, false).unwrap_err(); assert!(err.to_string().contains("padding must be zero"), "{err}"); } @@ -1904,7 +1893,7 @@ mod tests { let mut raw = Vec::new(); shifted.write_to(&mut raw).unwrap(); - BlobMetadata::from_bytes(&raw, None, true).unwrap(); + BlobMetadata::from_bytes(&raw, true).unwrap(); } #[test] @@ -1982,7 +1971,6 @@ mod tests { ]; let blob_metadata = BlobMetadata::new( - Some([0x9d; SHA256_DIGEST_SIZE]), BlobMetadataCompressor::None, DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, Vec::new(), @@ -1996,7 +1984,7 @@ mod tests { ); blob_metadata.save(&path).unwrap(); - let loaded = BlobMetadata::from_path(&path, None, false).unwrap(); + 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); diff --git a/nydus-format/src/blob/mod.rs b/nydus-format/src/blob/mod.rs index 3925a81c46b..fbc699df484 100644 --- a/nydus-format/src/blob/mod.rs +++ b/nydus-format/src/blob/mod.rs @@ -23,25 +23,6 @@ pub use metadata::{ DEFAULT_NYDUS_BLOB_METADATA_CHUNK_SIZE, NYDUS_BLOB_METADATA_SUFFIX, }; -/// Encode a power-of-two 4KiB block count as the log2 stored in the blob -/// meta 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 block count must be a non-zero power of two: {blocks}" - ))); - } - - let bits = blocks.ilog2() as u8; - if bits > metadata::NYDUS_BLOB_METADATA_MAX_BLOCK_COUNT_BITS { - return Err(Error::InvalidImage(format!( - "blob meta block count too large: {blocks}" - ))); - } - - Ok(bits) -} - /// 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 `compressed_data_size` bytes of blob data. An empty diff --git a/nydus-storage/src/cache/local.rs b/nydus-storage/src/cache/local.rs index cd7f5c628fb..8ae7121d349 100644 --- a/nydus-storage/src/cache/local.rs +++ b/nydus-storage/src/cache/local.rs @@ -854,13 +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::from_path(tmp.path(), Some(blob_id), true) { + 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::from_path(blob_metadata_path, Some(blob_id), true).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 @@ -911,17 +911,12 @@ mod tests { 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 { + fn blob_metadata_with_crc32(payload: &[u8], crc32: u32) -> BlobMetadata { BlobMetadata::new( - Some(blob_id), BlobMetadataCompressor::None, 1, vec![BlobMetadataChunk::new(*blake3::hash(payload).as_bytes(), 0, 1).unwrap()], @@ -975,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())); @@ -994,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())); @@ -1033,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())); @@ -1071,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())); @@ -1102,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())); @@ -1124,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())); @@ -1147,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()); @@ -1200,7 +1188,6 @@ 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::new( - Some(sha256_bytes(&payload)), BlobMetadataCompressor::None, 1, Vec::new(), @@ -1256,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() @@ -1286,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())); @@ -1311,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())); @@ -1345,8 +1326,7 @@ 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())); diff --git a/nydus-storage/src/cache/remote.rs b/nydus-storage/src/cache/remote.rs index 0fb1ce997b3..f73684a8768 100644 --- a/nydus-storage/src/cache/remote.rs +++ b/nydus-storage/src/cache/remote.rs @@ -121,12 +121,11 @@ mod tests { use super::*; use nydus_backend::Local; use nydus_format::blob::{BlobMetadataBlockGroup, BlobMetadataChunk, BlobMetadataCompressor}; - use nydus_format::utils::{sha256_bytes, write_minimal_full_blob}; + use nydus_format::utils::write_minimal_full_blob; use tempfile::tempdir; - fn blob_metadata(blob_id: [u8; SHA256_DIGEST_SIZE], payload: &[u8]) -> BlobMetadata { + fn blob_metadata(payload: &[u8]) -> BlobMetadata { BlobMetadata::new( - Some(blob_id), BlobMetadataCompressor::None, 1, vec![BlobMetadataChunk::new(*blake3::hash(payload).as_bytes(), 0, 1).unwrap()], @@ -139,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())); @@ -164,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/src/build/blob_chunk.rs b/nydus/src/build/blob_chunk.rs index 25f195874fe..e040037ad8c 100644 --- a/nydus/src/build/blob_chunk.rs +++ b/nydus/src/build/blob_chunk.rs @@ -130,13 +130,8 @@ impl BlobWriter { &self.blob_metadata_block_groups } - pub fn blob_metadata( - &self, - blob_id: [u8; EROFS_BLOB_ID_SIZE], - source_offset_bias: u64, - ) -> Result { + pub fn blob_metadata(&self, source_offset_bias: u64) -> Result { Ok(BlobMetadata::new( - Some(blob_id), self.compressor, self.file_chunk_size / EROFS_BLOCK_SIZE, self.blob_metadata_chunks.clone(), @@ -145,16 +140,9 @@ impl BlobWriter { .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<()> { @@ -440,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); @@ -470,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. @@ -554,21 +542,20 @@ 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, 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::from_path(&blob_metadata_path, None, false).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_table_size(), 48); diff --git a/nydus/src/build/mod.rs b/nydus/src/build/mod.rs index 260473796f7..75b46e4c96c 100644 --- a/nydus/src/build/mod.rs +++ b/nydus/src/build/mod.rs @@ -163,7 +163,7 @@ pub fn build_image(options: &BuildImageOptions, writer: impl Write) -> Result Result> { } fn blob_metadata_summary_from_bytes(data: &[u8]) -> Result { - let blob_metadata = BlobMetadata::from_bytes(data, None, false)?; + let blob_metadata = BlobMetadata::from_bytes(data, false)?; Ok(BlobMetadataSummary { chunk_count: blob_metadata.chunk_count(), block_group_count: blob_metadata.block_group_count(), @@ -553,7 +553,6 @@ mod tests { let data = [0x5au8; EROFS_BLOCK_SIZE as usize]; let data_digest = sha256_bytes(&data); let blob_metadata = BlobMetadata::new( - None, BlobMetadataCompressor::None, DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, Vec::new(), diff --git a/nydus/src/optimize/mod.rs b/nydus/src/optimize/mod.rs index 3d105ff90d7..bc6815b11c4 100644 --- a/nydus/src/optimize/mod.rs +++ b/nydus/src/optimize/mod.rs @@ -168,7 +168,6 @@ pub fn build_ondemand_blob( data_digest.copy_from_slice(&data_hasher.finalize()); let blob_metadata = BlobMetadata::new( - Some(data_digest), BlobMetadataCompressor::Zstd, DEFAULT_NYDUS_BLOB_METADATA_CHUNK_BLOCK_COUNT, Vec::new(), 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/nydus_core.rs b/nydus/tests/testsuite/nydus_core.rs index c67c5720468..7718944efeb 100644 --- a/nydus/tests/testsuite/nydus_core.rs +++ b/nydus/tests/testsuite/nydus_core.rs @@ -107,7 +107,7 @@ fn build_test_image_with_layout( 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)]; From 8359ecec1912795b3894c6c6a568f4774126f3e2 Mon Sep 17 00:00:00 2001 From: Gaius Date: Tue, 25 Aug 2026 09:11:10 +0000 Subject: [PATCH 7/7] docs(blob): add Display doc comments for BlobMetadataCompressor and BlobMetadataDigester Clarify that the Display impl emits the lowercase algorithm name used in build and check summaries. Signed-off-by: Gaius --- nydus-format/src/blob/algorithm.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nydus-format/src/blob/algorithm.rs b/nydus-format/src/blob/algorithm.rs index 34e9cde7841..4582d036a1a 100644 --- a/nydus-format/src/blob/algorithm.rs +++ b/nydus-format/src/blob/algorithm.rs @@ -24,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 { @@ -61,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 {