Skip to main content

onelf_format/
payload.rs

1//! Turning a package's payload back into file content.
2//!
3//! A payload is either a run of separately compressed blocks or a stipplefs image.
4//! A stipplefs payload keeps the manifest exactly as it is otherwise, with
5//! every file and its blocks, but a block is then a slice of its file:
6//! `payload_offset` is where the slice starts within the file,
7//! `original_size` is its length, and `compressed_size` is the file's inode
8//! in the image. The BLAKE3 a block carries covers the slice, so a reader
9//! verifies what the image hands it the same way it verifies a decompressed
10//! onelf block, and the manifest stays the only thing a package's integrity
11//! rests on.
12
13use std::fs::File;
14use std::io;
15use std::os::unix::fs::FileExt;
16use std::sync::Arc;
17
18use stipplefs::read::{Image, Reader};
19use stipplefs::source::ByteSource;
20
21use crate::{Block, EntryKind, Flags, Footer, Manifest};
22
23/// Size of the slices a stipplefs payload's files are verified and cached
24/// in, whatever block size the image itself was built with.
25pub const SLICE_SIZE: u64 = 64 * 1024;
26
27/// How a package's blocks become file content.
28#[derive(Clone)]
29pub enum Codec {
30    /// A blocks payload: zstd frames, compressed with the package's
31    /// dictionary when it has one, or raw bytes when the footer says the
32    /// payload is stored.
33    Zstd(Option<Arc<[u8]>>),
34    /// A stipplefs image, opened once for the life of the process.
35    Stipple(&'static Reader<'static>),
36}
37
38impl Codec {
39    /// The codec for the payload `footer` describes. The footer must
40    /// already have passed [`crate::reader::validate_footer`].
41    pub fn open(file: &File, footer: &Footer) -> io::Result<Codec> {
42        if footer.is_stipple() {
43            let source = FileSource {
44                size: file.metadata()?.len(),
45                file: file.try_clone()?,
46            };
47            let image = Image::new(source, footer.payload_offset, footer.payload_size)
48                .map_err(stipple_error)?;
49            let image: &'static Image<'static> = Box::leak(Box::new(image));
50            let reader = image.reader().map_err(stipple_error)?;
51            return Ok(Codec::Stipple(Box::leak(Box::new(reader))));
52        }
53        if !footer.flags.contains(Flags::HAS_DICT) || footer.dict_size == 0 {
54            return Ok(Codec::Zstd(None));
55        }
56        let mut dict = vec![0u8; footer.dict_size as usize];
57        file.read_exact_at(&mut dict, footer.dict_offset)?;
58        Ok(Codec::Zstd(Some(dict.into())))
59    }
60
61    /// The dictionary a blocks payload was compressed with, if any.
62    pub fn dict(&self) -> Option<&[u8]> {
63        match self {
64            Codec::Zstd(dict) => dict.as_deref(),
65            Codec::Stipple(_) => None,
66        }
67    }
68}
69
70/// The slice of a file `block` describes, read out of a stipplefs image.
71/// Unverified: the caller checks it against the block's hash.
72pub fn read_slice(reader: &Reader<'_>, block: &Block) -> io::Result<Vec<u8>> {
73    let ino = u32::try_from(block.compressed_size)
74        .map_err(|_| invalid("a block names an inode outside the image"))?;
75    let mut buf = vec![0u8; crate::reader::block_original_size(block)?];
76    let read = reader
77        .read(ino, block.payload_offset, &mut buf)
78        .map_err(stipple_error)?;
79    if read != buf.len() {
80        return Err(invalid(
81            "a file in the image is shorter than the manifest says",
82        ));
83    }
84    Ok(buf)
85}
86
87/// The file entries of `manifest`, in the order to read many of them in.
88///
89/// That is the order their content sits in the payload. A stipplefs image
90/// places files by type and by start-up order rather than by path, and
91/// reading them by path would decode a large block again for every file
92/// it holds.
93pub fn file_order(manifest: &Manifest, codec: &Codec) -> Vec<usize> {
94    let mut files: Vec<usize> = (0..manifest.entries.len())
95        .filter(|&i| manifest.entries[i].kind == EntryKind::File)
96        .collect();
97    if let Codec::Stipple(reader) = codec {
98        files.sort_by_cached_key(|&i| {
99            manifest.entries[i]
100                .blocks
101                .first()
102                .and_then(|b| u32::try_from(b.compressed_size).ok())
103                .and_then(|ino| reader.file_extents(ino).first().map(|e| e.0))
104                .unwrap_or(u64::MAX)
105        });
106    }
107    files
108}
109
110/// The package file, as the byte source an image is read from.
111struct FileSource {
112    file: File,
113    size: u64,
114}
115
116impl ByteSource for FileSource {
117    fn size(&self) -> u64 {
118        self.size
119    }
120
121    fn read_at(&self, offset: u64, buf: &mut [u8]) -> stipplefs::error::Result<()> {
122        Ok(self.file.read_exact_at(buf, offset)?)
123    }
124}
125
126fn invalid(msg: &str) -> io::Error {
127    io::Error::new(io::ErrorKind::InvalidData, msg.to_string())
128}
129
130fn stipple_error(e: stipplefs::error::Error) -> io::Error {
131    io::Error::new(io::ErrorKind::InvalidData, format!("stipplefs: {e}"))
132}