1use crate::cursor::ManifestCursor;
9use crate::error::CoreError;
10use limnifs_format::DropId;
11
12pub const INODE_FIXED_PREFIX_LEN: usize = 41;
14
15pub const INODE_FLAG_ATIME: u8 = 0x01;
17pub const INODE_FLAG_HAS_XATTRS: u8 = 0x02;
19pub const INODE_FLAG_INLINE_DATA: u8 = 0x04;
21pub const INODE_FLAG_SHARED_INLINE: u8 = 0x08;
26pub const INODE_FLAG_RESERVED_MASK: u8 = 0xF0;
31
32pub const S_IFMT: u32 = 0xF000;
34pub const S_IFREG: u32 = 0x8000;
35pub const S_IFDIR: u32 = 0x4000;
36pub const S_IFLNK: u32 = 0xA000;
37pub const S_IFBLK: u32 = 0x6000;
38pub const S_IFCHR: u32 = 0x2000;
39pub const S_IFIFO: u32 = 0x1000;
40pub const S_IFSOCK: u32 = 0xC000;
41
42pub const DEFAULT_INLINE_DATA_MAX_BYTES: u32 = 4 * 1024;
44
45#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct XAttr {
48 pub namespace: u8,
49 pub key: String,
50 pub value: Vec<u8>,
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
55pub enum ContentHandle {
56 InlineData(Vec<u8>),
58 SharedInline(usize),
62 SliceMap(Vec<SliceRef>),
64 Directory([u8; 32]),
66 Symlink(String),
68 Device(u64),
70 Pipe(u64),
72}
73
74#[derive(Copy, Clone, Debug, Eq, PartialEq)]
77pub struct SliceRef {
78 pub file_byte_start: u64,
79 pub file_byte_end: u64,
80 pub drop_id: DropId,
81 pub drop_byte_start: u32,
82 pub drop_byte_len: u32,
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct Inode {
88 pub number: u64,
89 pub mode: u32,
90 pub uid: u32,
91 pub gid: u32,
92 pub mtime_ns: u64,
93 pub ctime_ns: u64,
94 pub nlink: u32,
95 pub atime_ns: Option<u64>,
96 pub xattrs: Vec<XAttr>,
97 pub content_handle: ContentHandle,
98}
99
100impl Inode {
101 #[must_use]
103 pub fn file_type(&self) -> u32 {
104 self.mode & S_IFMT
105 }
106
107 #[must_use]
109 pub fn is_regular(&self) -> bool {
110 self.file_type() == S_IFREG
111 }
112
113 #[must_use]
115 pub fn is_directory(&self) -> bool {
116 self.file_type() == S_IFDIR
117 }
118}
119
120pub fn parse_inode(cursor: &mut ManifestCursor<'_>) -> Result<Inode, CoreError> {
131 parse_inode_with_ceiling(cursor, DEFAULT_INLINE_DATA_MAX_BYTES)
132}
133
134pub fn parse_inode_with_ceiling(
140 cursor: &mut ManifestCursor<'_>,
141 max_inline_bytes: u32,
142) -> Result<Inode, CoreError> {
143 let number = cursor.read_u64_le()?;
144 let mode = cursor.read_u32_le()?;
145 let uid = cursor.read_u32_le()?;
146 let gid = cursor.read_u32_le()?;
147 let mtime_ns = cursor.read_u64_le()?;
148 let ctime_ns = cursor.read_u64_le()?;
149 let nlink = cursor.read_u32_le()?;
150 let flags = cursor.read_u8()?;
151
152 if flags & INODE_FLAG_RESERVED_MASK != 0 {
153 return Err(CoreError::Corrupt {
154 reason: format!("inode {number}: reserved flag bits set (0x{flags:02X})"),
155 });
156 }
157
158 let atime_ns = if flags & INODE_FLAG_ATIME != 0 {
159 Some(cursor.read_u64_le()?)
160 } else {
161 None
162 };
163
164 let xattrs = if flags & INODE_FLAG_HAS_XATTRS != 0 {
165 parse_xattr_block(cursor)?
166 } else {
167 Vec::new()
168 };
169
170 let content_handle = parse_content_handle(cursor, mode, flags, number, max_inline_bytes)?;
171
172 Ok(Inode {
173 number,
174 mode,
175 uid,
176 gid,
177 mtime_ns,
178 ctime_ns,
179 nlink,
180 atime_ns,
181 xattrs,
182 content_handle,
183 })
184}
185
186fn parse_xattr_block(cursor: &mut ManifestCursor<'_>) -> Result<Vec<XAttr>, CoreError> {
187 let count = cursor.read_u32_le()?;
188 let count_us = usize::try_from(count).map_err(|_| CoreError::Corrupt {
189 reason: format!("xattr_count {count} exceeds usize"),
190 })?;
191 let mut xattrs = Vec::with_capacity(count_us);
192 for _ in 0..count_us {
193 let namespace = cursor.read_u8()?;
194 if namespace > 0x03 {
195 return Err(CoreError::Corrupt {
196 reason: format!("xattr namespace 0x{namespace:02X} out of range (0x00..0x03)"),
197 });
198 }
199 let key_len = cursor.read_u32_le()?;
200 let key_len_us = usize::try_from(key_len).map_err(|_| CoreError::Corrupt {
201 reason: format!("xattr key_len {key_len} exceeds usize"),
202 })?;
203 let key_bytes = cursor.read_n(key_len_us)?;
204 let key = std::str::from_utf8(key_bytes).map_err(|_| CoreError::Corrupt {
205 reason: "xattr key is not valid UTF-8".into(),
206 })?;
207 if key.contains('\0') {
208 return Err(CoreError::Corrupt {
209 reason: "xattr key contains NUL byte".into(),
210 });
211 }
212 let value_len = cursor.read_u32_le()?;
213 let value_len_us = usize::try_from(value_len).map_err(|_| CoreError::Corrupt {
214 reason: format!("xattr value_len {value_len} exceeds usize"),
215 })?;
216 let value = cursor.read_n_owned(value_len_us)?;
217 xattrs.push(XAttr {
218 namespace,
219 key: key.to_owned(),
220 value,
221 });
222 }
223 Ok(xattrs)
224}
225
226fn parse_content_handle(
227 cursor: &mut ManifestCursor<'_>,
228 mode: u32,
229 flags: u8,
230 inode_number: u64,
231 max_inline_bytes: u32,
232) -> Result<ContentHandle, CoreError> {
233 let file_type = mode & S_IFMT;
234 match file_type {
235 S_IFREG => {
236 if flags & INODE_FLAG_SHARED_INLINE != 0 {
237 let index = cursor.read_u32_le()?;
241 let index_us = usize::try_from(index).map_err(|_| CoreError::Corrupt {
242 reason: format!("shared_inline_index {index} exceeds usize"),
243 })?;
244 Ok(ContentHandle::SharedInline(index_us))
245 } else if flags & INODE_FLAG_INLINE_DATA != 0 {
246 let inline_len = cursor.read_u32_le()?;
247 if inline_len > max_inline_bytes {
248 return Err(CoreError::Corrupt {
249 reason: format!(
250 "inode {inode_number}: inline_data_len {inline_len} exceeds ceiling {max_inline_bytes}"
251 ),
252 });
253 }
254 let inline_len_us =
255 usize::try_from(inline_len).map_err(|_| CoreError::Corrupt {
256 reason: format!("inline_data_len {inline_len} exceeds usize"),
257 })?;
258 let data = cursor.read_n_owned(inline_len_us)?;
259 Ok(ContentHandle::InlineData(data))
260 } else {
261 let slice_count = cursor.read_u32_le()?;
262 let count_us = usize::try_from(slice_count).map_err(|_| CoreError::Corrupt {
263 reason: format!("slice_count {slice_count} exceeds usize"),
264 })?;
265 let mut slices = Vec::with_capacity(count_us);
266 for _ in 0..count_us {
267 let file_byte_start = cursor.read_u64_le()?;
268 let file_byte_end = cursor.read_u64_le()?;
269 if file_byte_start >= file_byte_end {
270 return Err(CoreError::Corrupt {
271 reason: format!(
272 "inode {inode_number}: slice has file_byte_start ({file_byte_start}) >= file_byte_end ({file_byte_end})"
273 ),
274 });
275 }
276 let drop_id_bytes = cursor.read_n(32)?;
277 let mut drop_id_arr = [0u8; 32];
278 drop_id_arr.copy_from_slice(drop_id_bytes);
279 let drop_id = DropId::from_bytes(drop_id_arr);
280 let drop_byte_start = cursor.read_u32_le()?;
281 let drop_byte_len = cursor.read_u32_le()?;
282 slices.push(SliceRef {
283 file_byte_start,
284 file_byte_end,
285 drop_id,
286 drop_byte_start,
287 drop_byte_len,
288 });
289 }
290 Ok(ContentHandle::SliceMap(slices))
291 }
292 }
293 S_IFDIR => {
294 let hash_bytes = cursor.read_n(32)?;
295 let mut hash = [0u8; 32];
296 hash.copy_from_slice(hash_bytes);
297 Ok(ContentHandle::Directory(hash))
298 }
299 S_IFLNK => {
300 let target_len = cursor.read_u32_le()?;
301 let target_len_us = usize::try_from(target_len).map_err(|_| CoreError::Corrupt {
302 reason: format!("target_len {target_len} exceeds usize"),
303 })?;
304 let target_bytes = cursor.read_n(target_len_us)?;
305 let target = std::str::from_utf8(target_bytes).map_err(|_| CoreError::Corrupt {
306 reason: "symlink target is not valid UTF-8".into(),
307 })?;
308 Ok(ContentHandle::Symlink(target.to_owned()))
309 }
310 S_IFBLK | S_IFCHR => {
311 let dev = cursor.read_u64_le()?;
312 Ok(ContentHandle::Device(dev))
313 }
314 S_IFIFO | S_IFSOCK => {
315 let pipe_id = cursor.read_u64_le()?;
316 Ok(ContentHandle::Pipe(pipe_id))
317 }
318 _ => Err(CoreError::Corrupt {
319 reason: format!("inode {inode_number}: unknown file type 0x{file_type:04X}"),
320 }),
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 fn make_regular_inline_inode(number: u64, mode: u32, inline_data: &[u8]) -> Vec<u8> {
329 let mut bytes = Vec::new();
330 bytes.extend_from_slice(&number.to_le_bytes());
331 bytes.extend_from_slice(&mode.to_le_bytes());
332 bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.push(INODE_FLAG_INLINE_DATA); let inline_len = u32::try_from(inline_data.len()).unwrap();
339 bytes.extend_from_slice(&inline_len.to_le_bytes());
340 bytes.extend_from_slice(inline_data);
341 bytes
342 }
343
344 fn make_directory_inode(number: u64) -> Vec<u8> {
345 let mode = S_IFDIR | 0o755;
346 let mut bytes = Vec::new();
347 bytes.extend_from_slice(&number.to_le_bytes());
348 bytes.extend_from_slice(&mode.to_le_bytes());
349 bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&2u32.to_le_bytes()); bytes.push(0); bytes.extend_from_slice(&[0xBB; 32]); bytes
357 }
358
359 #[test]
360 fn parses_regular_inline_file() {
361 let data = b"hello world";
362 let bytes = make_regular_inline_inode(42, S_IFREG | 0o644, data);
363 let mut cursor = ManifestCursor::new(&bytes);
364 let inode = parse_inode(&mut cursor).expect("inline file parses");
365 assert_eq!(inode.number, 42);
366 assert!(inode.is_regular());
367 assert!(!inode.is_directory());
368 assert!(inode.atime_ns.is_none());
369 assert!(inode.xattrs.is_empty());
370 match &inode.content_handle {
371 ContentHandle::InlineData(d) => assert_eq!(d, data),
372 other => panic!("expected InlineData, got {other:?}"),
373 }
374 assert_eq!(cursor.position(), bytes.len());
375 }
376
377 #[test]
378 fn parses_directory() {
379 let bytes = make_directory_inode(0);
380 let mut cursor = ManifestCursor::new(&bytes);
381 let inode = parse_inode(&mut cursor).expect("directory parses");
382 assert_eq!(inode.number, 0);
383 assert!(inode.is_directory());
384 match &inode.content_handle {
385 ContentHandle::Directory(hash) => assert_eq!(hash, &[0xBB; 32]),
386 other => panic!("expected Directory, got {other:?}"),
387 }
388 }
389
390 #[test]
391 fn rejects_reserved_flag_bits() {
392 let mut bytes = make_regular_inline_inode(1, S_IFREG | 0o644, b"x");
393 bytes[INODE_FIXED_PREFIX_LEN - 1] |= 0x10;
396 let mut cursor = ManifestCursor::new(&bytes);
397 match parse_inode(&mut cursor) {
398 Err(CoreError::Corrupt { reason }) => {
399 assert!(reason.contains("reserved"), "got: {reason}");
400 }
401 other => panic!("expected Corrupt, got {other:?}"),
402 }
403 }
404
405 #[test]
406 fn rejects_unknown_file_type() {
407 let mut bytes = Vec::new();
408 bytes.extend_from_slice(&1u64.to_le_bytes());
409 bytes.extend_from_slice(&0x0000u32.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.push(0); let mut cursor = ManifestCursor::new(&bytes);
417 match parse_inode(&mut cursor) {
418 Err(CoreError::Corrupt { reason }) => {
419 assert!(reason.contains("unknown file type"), "got: {reason}");
420 }
421 other => panic!("expected Corrupt, got {other:?}"),
422 }
423 }
424
425 #[test]
426 fn parses_with_atime() {
427 let mode = S_IFREG | 0o644;
428 let mut bytes = Vec::new();
429 bytes.extend_from_slice(&1u64.to_le_bytes());
430 bytes.extend_from_slice(&mode.to_le_bytes());
431 bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.push(INODE_FLAG_ATIME); bytes.extend_from_slice(&999u64.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); let mut cursor = ManifestCursor::new(&bytes);
440 let inode = parse_inode(&mut cursor).expect("atime inode parses");
441 assert_eq!(inode.atime_ns, Some(999));
442 }
443
444 #[test]
445 fn rejects_inline_above_ceiling() {
446 let oversized = vec![0xFF; (DEFAULT_INLINE_DATA_MAX_BYTES as usize) + 1];
447 let bytes = make_regular_inline_inode(1, S_IFREG | 0o644, &oversized);
448 let mut cursor = ManifestCursor::new(&bytes);
449 match parse_inode(&mut cursor) {
450 Err(CoreError::Corrupt { reason }) => {
451 assert!(reason.contains("ceiling"), "got: {reason}");
452 }
453 other => panic!("expected Corrupt, got {other:?}"),
454 }
455 }
456}