use std::io::Read;
use std::path::Path;
use sha2::{Digest as Sha2Digest, Sha256};
use super::format::{EXT4_BLOCK_SIZE, EXT4_BLOCKS_PER_GROUP, EXT4_INODES_PER_GROUP};
use super::formatter::{Ext4Error, Ext4FormatOptions, format_ext4_rootfs_with_tree_and_uuid};
use super::resizer::validate_rootfs_image;
use crate::tree::FileTree;
pub const EXT4_ROOTFS_MATERIALIZER_ABI: u32 = 2;
const DEFAULT_ROOTFS_JOURNAL_BLOCKS: u32 = 16_384;
const WORST_CASE_METADATA_BLOCKS_PER_GROUP: u64 = 772;
const MIN_FREE_RESERVE_BLOCKS: u64 = 16_384;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ext4RootfsOptions {
pub journal_blocks: u32,
pub derivation_digest: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ext4Artifact {
pub materializer_abi: u32,
pub uuid: [u8; 16],
pub virtual_size_bytes: u64,
pub inode_count: u64,
pub content_bytes: u64,
pub sha256: [u8; 32],
}
impl Default for Ext4RootfsOptions {
fn default() -> Self {
Self {
journal_blocks: DEFAULT_ROOTFS_JOURNAL_BLOCKS,
derivation_digest: [0u8; 32],
}
}
}
pub fn materialize_ext4_rootfs(
path: &Path,
tree: FileTree,
options: &Ext4RootfsOptions,
) -> Result<Ext4Artifact, Ext4Error> {
let inode_count = unique_inode_count(&tree);
let content_bytes = tree.total_data_size();
let size_bytes = canonical_size_bytes(inode_count, content_bytes, options.journal_blocks)?;
let format_options = Ext4FormatOptions {
size_bytes,
journal_blocks: options.journal_blocks,
};
let uuid = deterministic_uuid(&options.derivation_digest);
format_ext4_rootfs_with_tree_and_uuid(path, &format_options, tree, uuid)?;
validate_rootfs_image(path)?;
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)?
.sync_all()?;
Ok(Ext4Artifact {
materializer_abi: EXT4_ROOTFS_MATERIALIZER_ABI,
uuid,
virtual_size_bytes: size_bytes,
inode_count,
content_bytes,
sha256: sha256_file(path)?,
})
}
fn unique_inode_count(tree: &FileTree) -> u64 {
let duplicate_hardlink_aliases = tree
.regular_file_link_counts()
.values()
.map(|count| u64::from(count.saturating_sub(1)))
.sum::<u64>();
tree.node_count() + 1 - duplicate_hardlink_aliases
}
fn canonical_size_bytes(
inode_count: u64,
content_bytes: u64,
journal_blocks: u32,
) -> Result<u64, Ext4Error> {
let data_blocks = content_bytes.div_ceil(u64::from(EXT4_BLOCK_SIZE));
let reserve_blocks = MIN_FREE_RESERVE_BLOCKS.max(data_blocks.div_ceil(20));
let inode_overhead_blocks = inode_count
.checked_mul(2)
.ok_or_else(|| Ext4Error::InvalidSize("rootfs inode overhead exceeds u64".to_string()))?;
let required_usable_blocks = data_blocks
.checked_add(reserve_blocks)
.and_then(|blocks| blocks.checked_add(u64::from(journal_blocks)))
.and_then(|blocks| blocks.checked_add(inode_overhead_blocks))
.ok_or_else(|| Ext4Error::InvalidSize("rootfs size calculation overflowed".to_string()))?;
let inode_slots = inode_count.saturating_add(9).div_ceil(10) * 11;
let inode_groups = inode_slots.div_ceil(u64::from(EXT4_INODES_PER_GROUP));
let usable_per_group =
u64::from(EXT4_BLOCKS_PER_GROUP).saturating_sub(WORST_CASE_METADATA_BLOCKS_PER_GROUP);
let data_groups = required_usable_blocks.div_ceil(usable_per_group);
let groups = inode_groups.max(data_groups).max(2);
groups
.checked_mul(u64::from(EXT4_BLOCKS_PER_GROUP))
.and_then(|blocks| blocks.checked_mul(u64::from(EXT4_BLOCK_SIZE)))
.ok_or_else(|| Ext4Error::InvalidSize("rootfs virtual size exceeds u64".to_string()))
}
fn sha256_file(path: &Path) -> Result<[u8; 32], Ext4Error> {
let mut file = std::fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 64 * 1024];
loop {
let len = file.read(&mut buffer)?;
if len == 0 {
break;
}
hasher.update(&buffer[..len]);
}
Ok(hasher.finalize().into())
}
fn deterministic_uuid(digest: &[u8; 32]) -> [u8; 16] {
let mut uuid = [0u8; 16];
uuid.copy_from_slice(&digest[..16]);
uuid[6] = (uuid[6] & 0x0f) | 0x40;
uuid[8] = (uuid[8] & 0x3f) | 0x80;
uuid
}
#[cfg(test)]
mod tests {
use std::io::{Read, Seek, SeekFrom, Write};
use super::*;
use crate::tree::{FileData, InodeMetadata, RegularFileNode, TreeNode, Xattr};
const TEST_JOURNAL_BLOCKS: u32 = 1024;
fn test_options(digest: [u8; 32]) -> Ext4RootfsOptions {
Ext4RootfsOptions {
journal_blocks: TEST_JOURNAL_BLOCKS,
derivation_digest: digest,
}
}
fn inode_table_block() -> u64 {
1 + 1 + super::super::layout::RESERVED_GDT_BLOCKS as u64 + 2
}
fn read_inode(path: &Path, inode: u32) -> Vec<u8> {
let offset = inode_table_block() * 4096 + u64::from(inode - 1) * 256;
let mut file = std::fs::File::open(path).unwrap();
file.seek(SeekFrom::Start(offset)).unwrap();
let mut bytes = vec![0u8; 256];
file.read_exact(&mut bytes).unwrap();
bytes
}
fn le_u16(bytes: &[u8], offset: usize) -> u16 {
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
}
fn le_u32(bytes: &[u8], offset: usize) -> u32 {
u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
])
}
fn regular_file(id: crate::tree::RegularFileId) -> RegularFileNode {
RegularFileNode {
id,
metadata: InodeMetadata {
uid: 0x12345,
gid: 0x23456,
mode: 0o4750,
mtime: 1_800_000_000,
mtime_nsec: 123_456_789,
},
xattrs: Vec::new(),
data: FileData::Memory(b"rootfs-data".to_vec()),
nlink: 1,
}
}
#[test]
fn deterministic_uuid_has_rfc4122_version_and_variant() {
let digest = [0xff; 32];
let uuid = deterministic_uuid(&digest);
assert_eq!(uuid[6] >> 4, 4);
assert_eq!(uuid[8] >> 6, 2);
assert_eq!(&uuid[..6], &[0xff; 6]);
}
#[test]
fn materializer_preserves_inode_metadata_and_hardlinks() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rootfs.raw");
let mut tree = FileTree::new();
let file = regular_file(crate::tree::RegularFileId::new());
tree.insert(b"usr/bin/tool", TreeNode::RegularFile(file.clone()))
.unwrap();
tree.insert(b"usr/bin/tool-link", TreeNode::RegularFile(file))
.unwrap();
let artifact = materialize_ext4_rootfs(&path, tree, &test_options([7u8; 32])).unwrap();
assert_eq!(artifact.inode_count, 4);
assert_eq!(artifact.content_bytes, b"rootfs-data".len() as u64);
let inode = read_inode(&path, 13);
assert_eq!(le_u16(&inode, 0x00), 0o100000 | 0o4750);
assert_eq!(le_u16(&inode, 0x02), 0x2345);
assert_eq!(le_u16(&inode, 0x78), 0x0001);
assert_eq!(le_u16(&inode, 0x18), 0x3456);
assert_eq!(le_u16(&inode, 0x7a), 0x0002);
assert_eq!(le_u16(&inode, 0x1a), 2);
assert_eq!(le_u32(&inode, 0x10), 1_800_000_000);
assert_eq!(le_u32(&inode, 0x88) >> 2, 123_456_789);
}
#[test]
fn materializer_is_byte_deterministic_for_same_inputs() {
let dir = tempfile::tempdir().unwrap();
let first_path = dir.path().join("first.raw");
let second_path = dir.path().join("second.raw");
let mut tree = FileTree::new();
tree.insert(
b"payload",
TreeNode::RegularFile(regular_file(crate::tree::RegularFileId::new())),
)
.unwrap();
let options = test_options([9u8; 32]);
let first_artifact = materialize_ext4_rootfs(&first_path, tree.clone(), &options).unwrap();
let second_artifact = materialize_ext4_rootfs(&second_path, tree, &options).unwrap();
assert_eq!(first_artifact, second_artifact);
assert_eq!(first_artifact.virtual_size_bytes, 256 * 1024 * 1024);
assert_eq!(first_artifact.inode_count, 2);
let mut first = std::fs::File::open(first_path).unwrap();
let mut second = std::fs::File::open(second_path).unwrap();
let mut first_buf = vec![0u8; 1024 * 1024];
let mut second_buf = vec![0u8; 1024 * 1024];
loop {
let first_len = first.read(&mut first_buf).unwrap();
let second_len = second.read(&mut second_buf).unwrap();
assert_eq!(first_len, second_len);
assert_eq!(&first_buf[..first_len], &second_buf[..second_len]);
if first_len == 0 {
break;
}
}
}
#[test]
fn materializer_links_external_xattr_block_from_inode() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rootfs.raw");
let mut tree = FileTree::new();
let mut file = regular_file(crate::tree::RegularFileId::new());
file.xattrs.push(Xattr {
name: b"security.capability".to_vec(),
value: (0u8..128).collect(),
});
tree.insert(b"usr/bin/tool", TreeNode::RegularFile(file))
.unwrap();
materialize_ext4_rootfs(&path, tree, &test_options([11u8; 32])).unwrap();
let inode = read_inode(&path, 13);
let xattr_block = u64::from(le_u32(&inode, 0x68)) | (u64::from(le_u16(&inode, 0x76)) << 32);
assert_ne!(xattr_block, 0);
let mut image = std::fs::File::open(&path).unwrap();
image.seek(SeekFrom::Start(xattr_block * 4096)).unwrap();
let mut block = vec![0u8; 4096];
image.read_exact(&mut block).unwrap();
assert_eq!(le_u32(&block, 0), 0xEA02_0000);
assert_eq!(block[33], 6);
assert_eq!(&block[48..58], b"capability");
}
#[test]
fn validator_rejects_corrupted_inode() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rootfs.raw");
let mut tree = FileTree::new();
tree.insert(
b"payload",
TreeNode::RegularFile(regular_file(crate::tree::RegularFileId::new())),
)
.unwrap();
materialize_ext4_rootfs(&path, tree, &test_options([13u8; 32])).unwrap();
let root_mode_offset = inode_table_block() * 4096 + 256;
let mut image = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
image.seek(SeekFrom::Start(root_mode_offset)).unwrap();
image.write_all(&[0]).unwrap();
image.flush().unwrap();
assert!(validate_rootfs_image(&path).is_err());
}
#[test]
fn validator_rejects_corrupted_external_xattr() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rootfs.raw");
let mut tree = FileTree::new();
let mut file = regular_file(crate::tree::RegularFileId::new());
file.xattrs.push(Xattr {
name: b"security.capability".to_vec(),
value: vec![0x5a; 128],
});
tree.insert(b"payload", TreeNode::RegularFile(file))
.unwrap();
materialize_ext4_rootfs(&path, tree, &test_options([15u8; 32])).unwrap();
let inode = read_inode(&path, 11);
let xattr_block = u64::from(le_u32(&inode, 0x68)) | (u64::from(le_u16(&inode, 0x76)) << 32);
let mut image = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
image
.seek(SeekFrom::Start(xattr_block * 4096 + 32))
.unwrap();
image.write_all(&[0xff]).unwrap();
image.flush().unwrap();
assert!(validate_rootfs_image(&path).is_err());
}
}