1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
use std::io::{self, Read, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum EntryKind {
Dir = 0,
File = 1,
Symlink = 2,
}
impl TryFrom<u8> for EntryKind {
type Error = io::Error;
fn try_from(v: u8) -> Result<Self, Self::Error> {
match v {
0 => Ok(EntryKind::Dir),
1 => Ok(EntryKind::File),
2 => Ok(EntryKind::Symlink),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid entry kind: {v}"),
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum WorkingDir {
Inherit = 0,
PackageRoot = 1,
EntrypointParent = 2,
}
impl TryFrom<u8> for WorkingDir {
type Error = io::Error;
fn try_from(v: u8) -> Result<Self, Self::Error> {
match v {
0 => Ok(WorkingDir::Inherit),
1 => Ok(WorkingDir::PackageRoot),
2 => Ok(WorkingDir::EntrypointParent),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid working_dir: {v}"),
)),
}
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EntryPointFlags: u8 {
const MEMFD_ELIGIBLE = 1 << 0;
}
}
/// One block of a file's content.
///
/// In a stipplefs payload ([`crate::Flags::STIPPLE_PAYLOAD`]) a block is a
/// slice of the file rather than a region of the payload: `payload_offset`
/// is where the slice starts within the file and `compressed_size` holds
/// the file's stipplefs inode. See [`crate::payload`].
#[derive(Debug, Clone)]
pub struct Block {
/// Byte offset into the payload section where this block's data begins.
pub payload_offset: u64,
/// Size of the block after compression.
pub compressed_size: u64,
/// Size of the block before compression.
pub original_size: u64,
/// BLAKE3 of this block's *decompressed* bytes.
///
/// Hashing the decompressed form is what lets a random-access reader
/// verify one block without reassembling the whole entry, which is the
/// difference between serving a byte of a large file and buffering all
/// of it. Zero on manifests written before version 2, where the only
/// available check is the whole-entry hash on [`Entry`].
pub content_hash: [u8; 32],
}
/// Serialized width of a block, as written by the current format version.
pub const BLOCK_SIZE: usize = 56;
/// Serialized width in manifest version 1, which carried no per-block hash.
pub const BLOCK_SIZE_V1: usize = 24;
impl Block {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.payload_offset.to_le_bytes())?;
w.write_all(&self.compressed_size.to_le_bytes())?;
w.write_all(&self.original_size.to_le_bytes())?;
w.write_all(&self.content_hash)?;
Ok(())
}
/// Read one block as written by manifest `version`.
pub fn read_from<R: Read>(r: &mut R, version: u16) -> io::Result<Self> {
let mut buf = [0u8; BLOCK_SIZE];
let width = if version >= 2 {
BLOCK_SIZE
} else {
BLOCK_SIZE_V1
};
r.read_exact(&mut buf[..width])?;
Ok(Block {
payload_offset: u64::from_le_bytes(buf[0..8].try_into().unwrap()),
compressed_size: u64::from_le_bytes(buf[8..16].try_into().unwrap()),
original_size: u64::from_le_bytes(buf[16..24].try_into().unwrap()),
// Left zero for version 1, where it means "no per-block check".
content_hash: buf[24..56].try_into().unwrap(),
})
}
/// Whether this block carries a usable per-block hash.
pub fn has_content_hash(&self) -> bool {
self.content_hash != [0u8; 32]
}
}
#[derive(Debug, Clone)]
pub struct EntryPoint {
/// Offset into the string table for this entrypoint's name.
pub name: u32,
/// Index of the filesystem entry this entrypoint executes.
pub target_entry: u32,
/// Offset into the string table for the argument string.
pub args: u32,
/// Working directory strategy when launching this entrypoint.
pub working_dir: WorkingDir,
/// Behavioral flags for this entrypoint.
pub flags: EntryPointFlags,
}
/// Size of serialized EntryPoint: 4+4+4+1+1 = 14 bytes
pub const ENTRYPOINT_SIZE: usize = 14;
impl EntryPoint {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.name.to_le_bytes())?;
w.write_all(&self.target_entry.to_le_bytes())?;
w.write_all(&self.args.to_le_bytes())?;
w.write_all(&[self.working_dir as u8])?;
w.write_all(&[self.flags.bits()])?;
Ok(())
}
pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
let mut buf = [0u8; ENTRYPOINT_SIZE];
r.read_exact(&mut buf)?;
Ok(EntryPoint {
name: u32::from_le_bytes(buf[0..4].try_into().unwrap()),
target_entry: u32::from_le_bytes(buf[4..8].try_into().unwrap()),
args: u32::from_le_bytes(buf[8..12].try_into().unwrap()),
working_dir: WorkingDir::try_from(buf[12])?,
flags: EntryPointFlags::from_bits_retain(buf[13]),
})
}
pub fn is_memfd_eligible(&self) -> bool {
self.flags.contains(EntryPointFlags::MEMFD_ELIGIBLE)
}
}
#[derive(Debug, Clone)]
pub struct Entry {
/// Type of this entry (file, directory, or symlink).
pub kind: EntryKind,
/// Index of the parent directory entry, or `u32::MAX` for top-level entries.
pub parent: u32,
/// Offset into the string table for this entry's name.
pub name: u32,
/// Unix file mode (permissions and type bits).
pub mode: u32,
/// Modification time: seconds since Unix epoch.
pub mtime_secs: u64,
/// Modification time: nanosecond component.
pub mtime_nsec: u32,
/// BLAKE3 hash of the file content (files only).
pub content_hash: [u8; 32],
/// Compressed payload blocks containing this file's data (files only).
/// The serialized block count is derived from `blocks.len()`.
pub blocks: Vec<Block>,
/// Offset into the string table for the symlink target path (symlinks only).
pub symlink_target: u32,
}
/// Size of serialized Entry (without blocks):
/// 1 + 4 + 4 + 4 + 8 + 4 + 32 + 4 + 4 = 65 bytes
/// Plus num_blocks * BLOCK_SIZE bytes of block data
pub const ENTRY_HEADER_SIZE: usize = 65;
impl Entry {
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&[self.kind as u8])?;
w.write_all(&self.parent.to_le_bytes())?;
w.write_all(&self.name.to_le_bytes())?;
w.write_all(&self.mode.to_le_bytes())?;
w.write_all(&self.mtime_secs.to_le_bytes())?;
w.write_all(&self.mtime_nsec.to_le_bytes())?;
w.write_all(&self.content_hash)?;
// Block count is derived from the vec so it can never disagree with
// the blocks that follow.
w.write_all(&(self.blocks.len() as u32).to_le_bytes())?;
w.write_all(&self.symlink_target.to_le_bytes())?;
for block in &self.blocks {
block.write_to(w)?;
}
Ok(())
}
/// Read one entry as written by manifest `version`.
pub fn read_from<R: Read>(r: &mut R, version: u16) -> io::Result<Self> {
let mut buf = [0u8; ENTRY_HEADER_SIZE];
r.read_exact(&mut buf)?;
let kind = EntryKind::try_from(buf[0])?;
let parent = u32::from_le_bytes(buf[1..5].try_into().unwrap());
let name = u32::from_le_bytes(buf[5..9].try_into().unwrap());
let mode = u32::from_le_bytes(buf[9..13].try_into().unwrap());
let mtime_secs = u64::from_le_bytes(buf[13..21].try_into().unwrap());
let mtime_nsec = u32::from_le_bytes(buf[21..25].try_into().unwrap());
let mut content_hash = [0u8; 32];
content_hash.copy_from_slice(&buf[25..57]);
let num_blocks = u32::from_le_bytes(buf[57..61].try_into().unwrap());
let symlink_target = u32::from_le_bytes(buf[61..65].try_into().unwrap());
// Cap the speculative allocation so a crafted `num_blocks` cannot
// request gigabytes before the reader hits EOF. The loop still
// reads exactly `num_blocks` blocks (growing as needed) and fails
// cleanly via `read_exact` when the input is short.
let cap = (num_blocks as usize).min(4096);
let mut blocks = Vec::with_capacity(cap);
for _ in 0..num_blocks {
blocks.push(Block::read_from(r, version)?);
}
Ok(Entry {
kind,
parent,
name,
mode,
mtime_secs,
mtime_nsec,
content_hash,
blocks,
symlink_target,
})
}
}