format: read the superblock extension and cache image over Storage

read_superblock_extension_in, cache_image_state_in, CacheImage::decode_in
and CacheImage::block_in take &dyn Storage (whose length is the end of
file); the image block is one bounded read. The &[u8] functions are
wrappers; applying an image in place still needs the bytes in memory.
New test: extension messages, a cache image and a corrupt one decode to
the same results through a read_at-only CountingStorage.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 12:47:59 -05:00
co-authored by Claude Opus 5.5
parent cd828725c7
commit 0d908facd3
+92 -12
View File
@@ -21,13 +21,14 @@
//! file is never copied whole. //! file is never copied whole.
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
use alloc::{collections::BTreeSet, vec::Vec}; use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec};
#[cfg(feature = "std")] #[cfg(feature = "std")]
use std::collections::BTreeSet; use std::{borrow::Cow, collections::BTreeSet};
use crate::error::FormatError; use crate::error::FormatError;
use crate::message_type::MessageType; use crate::message_type::MessageType;
use crate::object_header::ObjectHeader; use crate::object_header::ObjectHeader;
use crate::storage::{Storage, read_exact_at};
use crate::superblock::Superblock; use crate::superblock::Superblock;
/// Message type of the File Space Info message. /// Message type of the File Space Info message.
@@ -169,6 +170,15 @@ impl<'a> Cursor<'a> {
pub fn read_superblock_extension( pub fn read_superblock_extension(
data: &[u8], data: &[u8],
sb: &Superblock, sb: &Superblock,
) -> Result<Option<SuperblockExtension>, FormatError> {
read_superblock_extension_in(&data, sb)
}
/// [`read_superblock_extension`] over any [`Storage`]; its length is the
/// end of file.
pub fn read_superblock_extension_in(
file: &dyn Storage,
sb: &Superblock,
) -> Result<Option<SuperblockExtension>, FormatError> { ) -> Result<Option<SuperblockExtension>, FormatError> {
let os = sb.offset_size; let os = sb.offset_size;
let ls = sb.length_size; let ls = sb.length_size;
@@ -181,8 +191,8 @@ pub fn read_superblock_extension(
return Ok(None); return Ok(None);
}; };
let addr = usize::try_from(addr).map_err(|_| ext_err("address out of range"))?; let addr = usize::try_from(addr).map_err(|_| ext_err("address out of range"))?;
let header = ObjectHeader::parse(data, addr, os, ls)?; let header = ObjectHeader::parse_in(file, addr as u64, os, ls)?;
let eoa = data.len() as u64; let eoa = file.len();
let mut ext = SuperblockExtension::default(); let mut ext = SuperblockExtension::default();
for msg in &header.messages { for msg in &header.messages {
@@ -340,12 +350,21 @@ impl CacheImage {
data: &[u8], data: &[u8],
location: CacheImageLocation, location: CacheImageLocation,
sb: &Superblock, sb: &Superblock,
) -> Result<Self, FormatError> {
Self::decode_in(&data, location, sb)
}
/// [`Self::decode`] over any [`Storage`]: one read of the image block.
pub fn decode_in(
file: &dyn Storage,
location: CacheImageLocation,
sb: &Superblock,
) -> Result<Self, FormatError> { ) -> Result<Self, FormatError> {
let (offset_size, length_size) = (sb.offset_size, sb.length_size); let (offset_size, length_size) = (sb.offset_size, sb.length_size);
let bad = FormatError::InvalidCacheImage; let bad = FormatError::InvalidCacheImage;
let block = image_block(data, location)?; let block = image_block_in(file, location)?;
let eoa = data.len() as u64; let eoa = file.len();
let mut c = Cursor::new(block, bad(RAN_OFF)); let mut c = Cursor::new(&block, bad(RAN_OFF));
// Header: signature, version, flags, image data length, entry count. // Header: signature, version, flags, image data length, entry count.
if c.take(4)? != MDCI_SIGNATURE { if c.take(4)? != MDCI_SIGNATURE {
@@ -463,6 +482,11 @@ impl CacheImage {
image_block(data, self.location) image_block(data, self.location)
} }
/// [`Self::block`] over any [`Storage`].
pub fn block_in<'a>(&self, file: &'a dyn Storage) -> Result<Cow<'a, [u8]>, FormatError> {
image_block_in(file, self.location)
}
/// Write every entry over `dst`, the file's bytes from the superblock /// Write every entry over `dst`, the file's bytes from the superblock
/// on (as long as the `data` the image was decoded from), taking the /// on (as long as the `data` the image was decoded from), taking the
/// entries from `block` (the image block, see [`Self::block`]). `block` /// entries from `block` (the image block, see [`Self::block`]). `block`
@@ -483,13 +507,28 @@ impl CacheImage {
} }
fn image_block(data: &[u8], location: CacheImageLocation) -> Result<&[u8], FormatError> { fn image_block(data: &[u8], location: CacheImageLocation) -> Result<&[u8], FormatError> {
let (start, len) = image_block_range(data.len() as u64, location)?;
Ok(&data[start as usize..start as usize + len])
}
fn image_block_in(
file: &dyn Storage,
location: CacheImageLocation,
) -> Result<Cow<'_, [u8]>, FormatError> {
let (start, len) = image_block_range(file.len(), location)?;
read_exact_at(file, start, len)
}
/// Where the image block is, checked against a file of `file_len` bytes.
fn image_block_range(file_len: u64, location: CacheImageLocation) -> Result<(u64, usize), FormatError> {
let bad = FormatError::InvalidCacheImage; let bad = FormatError::InvalidCacheImage;
let start = usize::try_from(location.address).map_err(|_| bad("address out of range"))?; let start = usize::try_from(location.address).map_err(|_| bad("address out of range"))?;
let len = usize::try_from(location.length).map_err(|_| bad("length out of range"))?; let len = usize::try_from(location.length).map_err(|_| bad("length out of range"))?;
start start
.checked_add(len) .checked_add(len)
.and_then(|end| data.get(start..end)) .filter(|&end| end as u64 <= file_len)
.ok_or(bad("image block extends past the end of the file")) .ok_or(bad("image block extends past the end of the file"))?;
Ok((start as u64, len))
} }
/// What an opener must do before reading a file's metadata: check the /// What an opener must do before reading a file's metadata: check the
@@ -498,11 +537,19 @@ fn image_block(data: &[u8], location: CacheImageLocation) -> Result<&[u8], Forma
/// ([`CacheImage::decode`]). `data` is the file from the superblock on, up /// ([`CacheImage::decode`]). `data` is the file from the superblock on, up
/// to its recorded end of file. /// to its recorded end of file.
pub fn cache_image_state(data: &[u8], sb: &Superblock) -> Result<CacheImageState, FormatError> { pub fn cache_image_state(data: &[u8], sb: &Superblock) -> Result<CacheImageState, FormatError> {
match read_superblock_extension(data, sb)? { cache_image_state_in(&data, sb)
}
/// [`cache_image_state`] over any [`Storage`].
pub fn cache_image_state_in(
file: &dyn Storage,
sb: &Superblock,
) -> Result<CacheImageState, FormatError> {
match read_superblock_extension_in(file, sb)? {
Some(SuperblockExtension { Some(SuperblockExtension {
cache_image: Some(location), cache_image: Some(location),
.. ..
}) => Ok(match CacheImage::decode(data, location, sb) { }) => Ok(match CacheImage::decode_in(file, location, sb) {
Ok(image) => CacheImageState::Loaded(image), Ok(image) => CacheImageState::Loaded(image),
Err(e) => CacheImageState::Unloadable(e), Err(e) => CacheImageState::Unloadable(e),
}), }),
@@ -566,7 +613,7 @@ mod tests {
/// holds the given messages, padded to `len` bytes. /// holds the given messages, padded to `len` bytes.
fn file_with_ext(messages: &[(u16, &[u8])], len: usize) -> Vec<u8> { fn file_with_ext(messages: &[(u16, &[u8])], len: usize) -> Vec<u8> {
let mut body = Vec::new(); let mut body = Vec::new();
for (t, d) in messages { for &(t, d) in messages {
let padded = d.len().div_ceil(8) * 8; let padded = d.len().div_ceil(8) * 8;
body.extend_from_slice(&t.to_le_bytes()); body.extend_from_slice(&t.to_le_bytes());
body.extend_from_slice(&(padded as u16).to_le_bytes()); body.extend_from_slice(&(padded as u16).to_le_bytes());
@@ -809,4 +856,37 @@ mod tests {
// An entry cannot be its own parent. // An entry cannot be its own parent.
assert!(load(image_with_deps(&[(40, b"C", 1, Some(40))])).is_err()); assert!(load(image_with_deps(&[(40, b"C", 1, Some(40))])).is_err());
} }
/// The extension and cache image decode identically through a
/// `read_at`-only storage, errors included.
#[test]
fn storage_parse_matches_slice_parse() {
use crate::storage::CountingStorage;
let img = image(&[(16, b"HEADER"), (40, b"NODE")]);
let mut with_image = file_with_ext(&[(MSG_MDCI, &mdci(256, img.len() as u64))], 256);
with_image.extend_from_slice(&img);
let mut bad_image = with_image.clone();
bad_image[256] = b'X';
let files = [
file_with_ext(&[(MSG_FSINFO, &fsinfo_v1(4096, false, 0))], 256),
file_with_ext(&[(MSG_FSINFO, &fsinfo_v1(256, false, 0))], 256),
file_with_ext(&[(MSG_MDCI, &mdci(128, 64))], 192),
file_with_ext(&[(MSG_MDCI, &mdci(0x10100, 0x1000_0000))], 2565),
file_with_ext(&[(MSG_FSINFO, &fsinfo_v1(4096, true, 12))], 60),
with_image,
bad_image,
];
for f in files {
let storage = CountingStorage::new(f.clone());
let sb = sb_v2(48);
assert_eq!(
read_superblock_extension_in(&storage, &sb),
read_superblock_extension(&f, &sb)
);
assert_eq!(cache_image_state_in(&storage, &sb), cache_image_state(&f, &sb));
if let Ok(CacheImageState::Loaded(image)) = cache_image_state(&f, &sb) {
assert_eq!(&*image.block_in(&storage).unwrap(), image.block(&f).unwrap());
}
}
}
} }