diff --git a/Cargo.toml b/Cargo.toml index ba263d9102..ac56f5dadc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +plain = "0.2" [workspace] members = [ diff --git a/src/types.rs b/src/types.rs index 290c538f92..f1cbb22461 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,50 +1,109 @@ pub const MAGIC_LEN: usize = 8; pub const MAGIC: [u8; 8] = *b"RedoxFtw"; +macro_rules! primitive( + ($wrapper:ident, $bits:expr, $primitive:ident) => { + #[repr(transparent)] + #[derive(Clone, Copy, Debug, Default)] + pub struct $wrapper([u8; $bits / 8]); + + impl $wrapper { + #[inline] + pub fn get(&self) -> $primitive { + <$primitive>::from_le_bytes(self.0) + } + #[inline] + pub fn set(&mut self, new: $primitive) { + self.0 = <$primitive>::to_le_bytes(new); + } + #[inline] + pub fn new(primitive: $primitive) -> Self { + let mut this = Self::default(); + this.set(primitive); + this + } + } + impl From<$primitive> for $wrapper { + fn from(primitive: $primitive) -> Self { + Self::new(primitive) + } + } + impl From<$wrapper> for $primitive { + fn from(wrapper: $wrapper) -> Self { + wrapper.get() + } + } + } +); + +primitive!(U16, 16, u16); +primitive!(U32, 32, u32); +primitive!(U64, 64, u64); + #[repr(transparent)] #[derive(Clone, Copy, Debug)] pub struct Magic(pub [u8; MAGIC_LEN]); #[repr(transparent)] #[derive(Clone, Copy, Debug)] -pub struct Offset(pub u32); +pub struct Offset(pub U32); #[repr(transparent)] #[derive(Clone, Copy, Debug)] -pub struct Length(pub u32); +pub struct Length(pub U32); #[repr(transparent)] #[derive(Clone, Copy, Debug)] -pub struct Inode(pub u16); +pub struct Inode(pub U16); -#[repr(C)] +#[repr(packed)] #[derive(Clone, Copy, Debug)] pub struct Timespec { - pub sec: u64, - pub nsec: u32, + pub sec: U64, + pub nsec: U32, } -#[repr(C)] +#[repr(packed)] #[derive(Clone, Copy, Debug)] pub struct Header { pub magic: Magic, pub inode_table_offset: Offset, pub creation_time: Timespec, - pub inode_count: u16, + pub inode_count: U16, } -#[repr(C)] +#[repr(packed)] +#[derive(Clone, Copy, Debug)] pub struct InodeHeader { - pub type_and_mode: u32, - pub length: u32, + pub type_and_mode: U32, + pub length: U32, pub offset: Offset, - pub uid: u32, - pub gid: u32, + pub uid: U32, + pub gid: U32, } -#[repr(C)] +pub const MODE_MASK: u32 = 0xFFF; +pub const MODE_SHIFT: u8 = 0; + +pub const TYPE_SHIFT: u8 = 28; +pub const TYPE_MASK: u32 = 0xF000_0000; + +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub enum InodeType { + RegularFile = 0x0, + Dir = 0x1, + // All other bit patterns are reserved... for now. TODO: Add symlinks? +} + +#[repr(packed)] +#[derive(Clone, Copy, Debug)] pub struct DirEntry { pub inode: Inode, - pub name_len: u16, + pub name_len: U16, pub name_offset: Offset, } + +unsafe impl plain::Plain for Header {} +unsafe impl plain::Plain for InodeHeader {} +unsafe impl plain::Plain for DirEntry {} diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 1246414d08..86f2970edf 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -13,5 +13,6 @@ name = "redox-initfs-package" [dependencies] anyhow = "1" clap = "2.33" +plain = "0.2" redox-initfs = { path = ".." } diff --git a/utils/src/redox_initfs_package.rs b/utils/src/redox_initfs_package.rs index 53a4ed90f6..9be62d7546 100644 --- a/utils/src/redox_initfs_package.rs +++ b/utils/src/redox_initfs_package.rs @@ -1,9 +1,10 @@ use std::convert::{TryFrom, TryInto}; -use std::fs::{DirEntry, File, FileType, OpenOptions, ReadDir}; +use std::fs::{DirEntry, File, FileType, Metadata, OpenOptions, ReadDir}; +use std::io::{prelude::*, SeekFrom}; use std::path::{Path, PathBuf}; use std::os::unix::ffi::OsStringExt; -use std::os::unix::fs::{FileExt, FileTypeExt}; +use std::os::unix::fs::{FileExt, FileTypeExt, MetadataExt}; use anyhow::{anyhow, Context, Result}; use clap::{App, Arg}; @@ -12,16 +13,18 @@ use redox_initfs::types as initfs; const DEFAULT_MAX_SIZE: u64 = 8 * 1024 * 1024; -enum Entry { +enum EntryKind { File(File), Dir(Dir), } -struct Child { + +struct Entry { name: Vec, - entry: Entry, + kind: EntryKind, + metadata: Metadata, } struct Dir { - children: Vec, + entries: Vec, } struct State { @@ -29,6 +32,7 @@ struct State { offset: u64, max_size: u64, inode_count: u16, + buffer: Box<[u8]>, } fn read_directory(state: &mut State, path: &Path) -> Result { @@ -40,7 +44,7 @@ fn read_directory(state: &mut State, path: &Path) -> Result { ) })?; - let children = read_dir + let entries = read_dir .map(|result| { let entry = result.map_err(|error| { anyhow!( @@ -50,13 +54,14 @@ fn read_directory(state: &mut State, path: &Path) -> Result { ) })?; - let file_type = entry.file_type().map_err(|error| { + let metadata = entry.metadata().map_err(|error| { anyhow!( - "failed to determine file type for `{}`: {}", + "failed to get metadata for `{}`: {}", entry.path().to_string_lossy(), error ) })?; + let file_type = metadata.file_type(); let unsupported_type = |ty: &str, entry: &DirEntry| { Err(anyhow!( @@ -66,7 +71,7 @@ fn read_directory(state: &mut State, path: &Path) -> Result { )) }; let name = entry.path().into_os_string().into_vec(); - let entry = if file_type.is_symlink() { + let entry_kind = if file_type.is_symlink() { return unsupported_type("symlink", &entry); } else if file_type.is_socket() { return unsupported_type("socket", &entry); @@ -77,7 +82,7 @@ fn read_directory(state: &mut State, path: &Path) -> Result { } else if file_type.is_char_device() { return unsupported_type("character device", &entry); } else if file_type.is_dir() { - Entry::File(File::open(&entry.path()).map_err(|error| { + EntryKind::File(File::open(&entry.path()).map_err(|error| { anyhow!( "failed to open file `{}`: {}", entry.path().to_string_lossy(), @@ -85,7 +90,7 @@ fn read_directory(state: &mut State, path: &Path) -> Result { ) })?) } else if file_type.is_file() { - Entry::Dir(read_directory(state, &entry.path())?) + EntryKind::Dir(read_directory(state, &entry.path())?) } else { return Err(anyhow!( "unknown file type at `{}`", @@ -94,14 +99,20 @@ fn read_directory(state: &mut State, path: &Path) -> Result { }; // TODO: Allow the user to specify a lower limit than u16::MAX. - state.inode_count = state.inode_count.checked_add(1) + state.inode_count = state + .inode_count + .checked_add(1) .ok_or_else(|| anyhow!("exceeded the maximum inode limit"))?; - Ok(Child { entry, name }) + Ok(Entry { + kind: entry_kind, + metadata, + name, + }) }) .collect::>>()?; - Ok(Dir { children }) + Ok(Dir { entries }) } fn bump_alloc(state: &mut State, size: u64) -> Result { @@ -113,6 +124,214 @@ fn bump_alloc(state: &mut State, size: u64) -> Result { Err(anyhow!("Bump allocation failed: max limit reached")) } } +struct WriteResult { + size: u32, + offset: u32, +} + +fn allocate_and_write_file(state: &mut State, mut file: &File) -> Result { + let size = file + .seek(SeekFrom::End(0)) + .context("failed to seek to end")?; + + let size: u32 = size.try_into().context("file too large")?; + + let offset: u32 = bump_alloc(state, size.into()) + .context("failed to allocate space for file")? + .try_into() + .context("file offset too high")?; + + let buffer_size: u32 = state.buffer.len().try_into().context("buffer too large")?; + + file.seek(SeekFrom::Start(0)) + .context("failed to seek to start")?; + + state + .file + .seek(SeekFrom::Start(offset.into())) + .context("failed to seek to offset for image")?; + + let mut relative_offset = 0; + + // TODO: If this would ever turn out to be a bottleneck, then perhaps we could use + // copy_file_range in `nix`. + + while relative_offset < size { + let allowed_length = std::cmp::min(buffer_size, size - relative_offset); + let allowed_length = + usize::try_from(allowed_length).expect("expected buffer size not to be outside usize"); + + file.read(&mut state.buffer[..allowed_length]) + .context("failed to read from source file")?; + + state + .file + .write(&state.buffer[..allowed_length]) + .context("failed to write source file into destination image")?; + + relative_offset += buffer_size; + } + + Ok(WriteResult { size, offset }) +} +fn write_inode( + state: &mut State, + ty: initfs::InodeType, + metadata: &Metadata, + write_result: WriteResult, + inode_table_offset: u32, + index: u16, +) -> Result<()> { + let inode_size: u32 = std::mem::size_of::() + .try_into() + .expect("inode header length cannot fit within u32"); + + let type_and_mode = ((ty as u32) << initfs::TYPE_SHIFT) | u32::from(metadata.mode() & 0xFFF); + + // TODO: Use main buffer and write in bulk. + let mut inode_buf = [0_u8; std::mem::size_of::()]; + + let inode = plain::from_mut_bytes::(&mut inode_buf) + .expect("expected inode struct to have alignment 1, and buffer size to match"); + + *inode = initfs::InodeHeader { + type_and_mode: type_and_mode.into(), + length: write_result.size.into(), + offset: initfs::Offset(write_result.offset.into()), + + gid: metadata.gid().into(), + uid: metadata.uid().into(), + }; + + state + .file + .write_all_at( + &inode_buf, + u64::from(inode_table_offset + u32::from(index) * inode_size), + ) + .context("failed to write inode struct to disk image") +} +fn allocate_and_write_dir( + state: &mut State, + dir: &Dir, + inode_table_offset: u32, + start_inode: &mut u16, +) -> Result { + let entry_size = + u16::try_from(std::mem::size_of::()).context("entry size too large")?; + let entry_count = u16::try_from(dir.entries.len()).context("too many subdirectories")?; + + let entry_table_length = u32::from(entry_count) + .checked_mul(u32::from(entry_size)) + .ok_or_else(|| anyhow!("entry table length too large when multiplying by size"))?; + + let entry_table_offset: u32 = bump_alloc(state, entry_table_length.into()) + .context("failed to allocate entry table")? + .try_into() + .context("directory entries offset too high")?; + + let this_start_inode = *start_inode; + + let inode_size: u32 = std::mem::size_of::() + .try_into() + .expect("inode header length cannot fit within u32"); + + *start_inode += entry_count; + + let inode_table_offset = + inode_table_offset + u32::from(this_start_inode) * u32::from(inode_size); + + for (index, entry) in dir.entries.iter().enumerate() { + let (write_result, ty) = match entry.kind { + EntryKind::Dir(ref subdir) => { + let write_result = + allocate_and_write_dir(state, subdir, inode_table_offset, start_inode) + .context("failed to copy directory entries into image")?; + + (write_result, initfs::InodeType::Dir) + } + + EntryKind::File(ref file) => { + let write_result = allocate_and_write_file(state, file) + .context("failed to copy file into image")?; + + (write_result, initfs::InodeType::RegularFile) + } + }; + + let index: u16 = index + .try_into() + .expect("expected dir entry count not to exceed u32"); + + write_inode( + state, + ty, + &entry.metadata, + write_result, + inode_table_offset, + index, + )?; + + let (name_offset, name_len) = { + let name_len: u16 = entry.name.len().try_into().context("file name too long")?; + + let offset: u32 = bump_alloc(state, u64::from(name_len)) + .context("failed to allocate space for file name")? + .try_into() + .context("file name offset too high up")?; + + (offset, name_len) + }; + { + let mut direntry_buf = [0_u8; std::mem::size_of::()]; + + let direntry = plain::from_mut_bytes::(&mut direntry_buf) + .expect("expected dir entry struct to have alignment 1, and buffer size to match"); + + let inode = this_start_inode + index; + + *direntry = initfs::DirEntry { + inode: initfs::Inode(inode.into()), + name_len: name_len.into(), + name_offset: initfs::Offset(name_offset.into()), + }; + + state + .file + .write_all_at( + &direntry_buf, + u64::from(entry_table_offset + u32::from(index) * u32::from(entry_size)), + ) + .context("failed to write dir entry struct to image")?; + } + } + + Ok(WriteResult { + size: entry_table_length, + offset: entry_table_offset, + }) +} +fn allocate_contents_and_write_inodes( + state: &mut State, + dir: &Dir, + inode_table_offset: u32, + root_metadata: Metadata, +) -> Result<()> { + let index = 0; + let mut start_inode = 1; + + let write_result = allocate_and_write_dir(state, dir, inode_table_offset, &mut start_inode) + .context("failed to allocate and write all directories and files")?; + + write_inode( + state, + initfs::InodeType::Dir, + &root_metadata, + write_result, + inode_table_offset, + index, + ) +} fn main() -> Result<()> { let matches = App::new("redox_initfs_package") @@ -182,19 +401,27 @@ fn main() -> Result<()> { .open(destination_temp_path) .context("failed to open destination file")?; + const BUFFER_SIZE: usize = 8192; + let mut state = State { file: destination_temp_file, offset: 0, max_size, inode_count: 0, + buffer: vec![0_u8; BUFFER_SIZE].into_boxed_slice(), }; - let root = read_directory(&mut state, Path::new(source)).context("failed to read root")?; + let root_path = Path::new(source); + let root_metadata = root_path + .metadata() + .context("failed to obtain metadata for root")?; + let root = read_directory(&mut state, root_path).context("failed to read root")?; // NOTE: The header is always stored at offset zero. let header_offset = bump_alloc( &mut state, - std::mem::size_of::().try_into() + std::mem::size_of::() + .try_into() .expect("expected header size to fit"), )?; @@ -208,30 +435,48 @@ fn main() -> Result<()> { .ok_or_else(|| anyhow!("inode table too large"))? }; - let inode_table_offset = bump_alloc( - &mut state, - inode_table_length, - )?; + let inode_table_offset = bump_alloc(&mut state, inode_table_length)?; + + // Finally, write the header to the disk image. + let inode_table_offset = initfs::Offset( u32::try_from(inode_table_offset) .map_err(|_| anyhow!("inode table located too far away"))? + .into(), ); + allocate_contents_and_write_inodes( + &mut state, + &root, + inode_table_offset.0.get(), + root_metadata, + )?; + let current_system_time = std::time::SystemTime::now(); let time_since_epoch = current_system_time .duration_since(std::time::SystemTime::UNIX_EPOCH) .context("could not calculate timestamp")?; - let header = initfs::Header { - magic: initfs::Magic(initfs::MAGIC), - creation_time: initfs::Timespec { - sec: time_since_epoch.as_secs(), - nsec: time_since_epoch.subsec_nanos(), - }, - inode_count: state.inode_count, - inode_table_offset, - }; + { + let mut header_bytes = [0_u8; std::mem::size_of::()]; + let header = plain::from_mut_bytes(&mut header_bytes) + .expect("expected header size to be sufficient and alignment to be 1"); + + *header = initfs::Header { + magic: initfs::Magic(initfs::MAGIC), + creation_time: initfs::Timespec { + sec: time_since_epoch.as_secs().into(), + nsec: time_since_epoch.subsec_nanos().into(), + }, + inode_count: state.inode_count.into(), + inode_table_offset, + }; + state + .file + .write_all_at(&header_bytes, header_offset) + .context("failed to write header")?; + } Ok(()) }