diff --git a/.gitignore b/.gitignore index 96ef6c0b94..a13600d6ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /target Cargo.lock +# TODO: Maybe use an OUT_DIR environment variable, or mktemp, rather than +# writing directly to the git repository root? +/out.img diff --git a/Cargo.toml b/Cargo.toml index 3360d5ccb4..d27ae0e6da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,13 +11,20 @@ license = "MIT" plain = "0.2" [features] -default = [] +default = ["std"] + +std = [] # Enables functionality to actually read the filesystem, which is only done in # the kernel. (Writing to the filesystem is done in utils, and that crate only # needs the type definitions). kernel = [] +[dev-dependencies] +log = "0.4" +env_logger = "0.8" +anyhow = "1" + [workspace] members = [ "redox-initfs-ar", diff --git a/data/file.txt b/data/file.txt new file mode 100644 index 0000000000..98eb3df43f --- /dev/null +++ b/data/file.txt @@ -0,0 +1 @@ +This is a file meant to be used in a redox-initfs test. diff --git a/redox-initfs-ar/Cargo.toml b/redox-initfs-ar/Cargo.toml index 8cc3496070..7b711b9ff4 100644 --- a/redox-initfs-ar/Cargo.toml +++ b/redox-initfs-ar/Cargo.toml @@ -11,6 +11,8 @@ license = "MIT" [dependencies] anyhow = "1" clap = "2.33" +env_logger = "0.8" +log = "0.4" plain = "0.2" redox-initfs = { path = ".." } diff --git a/redox-initfs-ar/src/archive.rs b/redox-initfs-ar/src/archive.rs new file mode 100644 index 0000000000..b006fa6154 --- /dev/null +++ b/redox-initfs-ar/src/archive.rs @@ -0,0 +1,489 @@ +use std::convert::{TryFrom, TryInto}; +use std::fs::{DirEntry, File, Metadata, OpenOptions}; +use std::io::{prelude::*, SeekFrom}; +use std::path::Path; + +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::{FileExt, FileTypeExt, MetadataExt}; + +use anyhow::{anyhow, Context, Result}; + +use redox_initfs::types as initfs; + +pub const KIBIBYTE: u64 = 1024; +pub const MEBIBYTE: u64 = KIBIBYTE * 1024; +pub const DEFAULT_MAX_SIZE: u64 = 64 * MEBIBYTE; + +enum EntryKind { + File(File), + Dir(Dir), +} + +struct Entry { + name: Vec, + kind: EntryKind, + metadata: Metadata, +} +struct Dir { + entries: Vec, +} + +struct State<'path> { + file: OutputImageGuard<'path>, + offset: u64, + max_size: u64, + inode_count: u16, + buffer: Box<[u8]>, +} + +fn write_all_at(file: &File, buf: &[u8], offset: u64, r#where: &str) -> Result<()> { + file.write_all_at(buf, offset)?; + log::debug!("Wrote {}..{} within {}", offset, offset + buf.len() as u64, r#where); + Ok(()) +} + +fn read_directory(state: &mut State, path: &Path) -> Result { + let read_dir = path + .read_dir() + .with_context(|| anyhow!("failed to read directory `{}`", path.to_string_lossy(),))?; + + let entries = read_dir + .map(|result| { + let entry = result.with_context(|| { + anyhow!( + "failed to get a directory entry from `{}`", + path.to_string_lossy(), + ) + })?; + + let metadata = entry.metadata().with_context(|| { + anyhow!( + "failed to get metadata for `{}`", + entry.path().to_string_lossy(), + ) + })?; + let file_type = metadata.file_type(); + + let unsupported_type = |ty: &str, entry: &DirEntry| { + Err(anyhow!( + "failed to include {} at `{}`: not supported by redox-initfs", + ty, + entry.path().to_string_lossy() + )) + }; + let name = entry + .path() + .file_name() + .context("expected path to have a valid filename")? + .as_bytes() + .to_owned(); + + let entry_kind = if file_type.is_symlink() { + return unsupported_type("symlink", &entry); + } else if file_type.is_socket() { + return unsupported_type("socket", &entry); + } else if file_type.is_fifo() { + return unsupported_type("FIFO", &entry); + } else if file_type.is_block_device() { + return unsupported_type("block device", &entry); + } else if file_type.is_char_device() { + return unsupported_type("character device", &entry); + } else if file_type.is_file() { + EntryKind::File(File::open(&entry.path()).with_context(|| { + anyhow!("failed to open file `{}`", entry.path().to_string_lossy(),) + })?) + } else if file_type.is_dir() { + EntryKind::Dir(read_directory(state, &entry.path())?) + } else { + return Err(anyhow!( + "unknown file type at `{}`", + entry.path().to_string_lossy() + )); + }; + + // TODO: Allow the user to specify a lower limit than u16::MAX. + state.inode_count = state + .inode_count + .checked_add(1) + .ok_or_else(|| anyhow!("exceeded the maximum inode limit"))?; + + Ok(Entry { + kind: entry_kind, + metadata, + name, + }) + }) + .collect::>>()?; + + Ok(Dir { entries }) +} + +fn bump_alloc(state: &mut State, size: u64, why: &str) -> Result { + if state.offset + size <= state.max_size { + let offset = state.offset; + state.offset += size; + log::debug!("Allocating range {}..{} in {}", offset, state.offset, why); + Ok(offset) + } else { + 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(), "allocate space for file") + .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")?; + + 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")?; + + write_all_at(&*state.file, &state.buffer[..allowed_length], u64::from(offset + relative_offset), "allocate_and_write_file buffer chunk") + .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(), + }; + + log::debug!("Writing inode index {} from offset {}", index, inode_table_offset); + write_all_at( + &*state.file, + &inode_buf, + u64::from(inode_table_offset + u32::from(index) * inode_size), + "write_inode", + ) + .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(), "allocate entry table") + .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) + .with_context(|| { + anyhow!( + "failed to copy directory entries from `{}` into image", + String::from_utf8_lossy(&entry.name) + ) + })?; + + (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), "allocate file name") + .context("failed to allocate space for file name")? + .try_into() + .context("file name offset too high up")?; + + write_all_at(&*state.file, &entry.name, offset.into(), "writing file name").context("failed to write file name")?; + + (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: inode.into(), + name_len: name_len.into(), + name_offset: initfs::Offset(name_offset.into()), + }; + + write_all_at( + &*state.file, + &direntry_buf, + u64::from(entry_table_offset + u32::from(index) * u32::from(entry_size)), + "allocate_and_write_dir entry", + ) + .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, + ) +} + +struct OutputImageGuard<'a> { + file: File, + path: &'a Path, + ok: bool, +} + +impl std::ops::Deref for OutputImageGuard<'_> { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.file + } +} +impl std::ops::DerefMut for OutputImageGuard<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.file + } +} + +impl Drop for OutputImageGuard<'_> { + fn drop(&mut self) { + if !self.ok { + let _ = std::fs::remove_file(self.path); + } + } +} + +pub struct Args<'a> { + pub destination_path: &'a Path, + pub max_size: u64, + pub source: &'a Path, +} +pub fn archive(&Args { destination_path, max_size, source }: &Args) -> Result<()> { + let previous_extension = destination_path.extension().map_or("", |ext| { + ext.to_str() + .expect("expected destination path to be valid UTF-8") + }); + + if !destination_path + .metadata() + .map_or(true, |metadata| metadata.is_file()) + { + return Err(anyhow!("Destination file must be a file")); + } + + let destination_temp_path = + destination_path.with_extension(format!("{}.partial", previous_extension)); + + let destination_temp_file = OpenOptions::new() + .read(false) + .write(true) + .create(true) + .create_new(false) + .open(&destination_temp_path) + .context("failed to open destination file")?; + + let guard = OutputImageGuard { + file: destination_temp_file, + path: &destination_temp_path, + ok: false, + }; + + const BUFFER_SIZE: usize = 8192; + + let mut state = State { + file: guard, + offset: 0, + max_size, + // Include root directory. + inode_count: 1, + buffer: vec![0_u8; BUFFER_SIZE].into_boxed_slice(), + }; + + let root_path = 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")?; + + log::debug!("there are {} inodes", state.inode_count); + + // NOTE: The header is always stored at offset zero. + let header_offset = bump_alloc( + &mut state, + std::mem::size_of::() + .try_into() + .expect("expected header size to fit"), + "allocate header", + )?; + assert_eq!(header_offset, 0); + + let inode_table_length = { + let inode_entry_size: u64 = std::mem::size_of::() + .try_into() + .expect("expected table entry size to fit"); + + inode_entry_size + .checked_mul(u64::from(state.inode_count)) + .ok_or_else(|| anyhow!("inode table too large"))? + }; + + let inode_table_offset = bump_alloc(&mut state, inode_table_length, "allocate inode table")?; + + // Finally, write the header to the disk image. + + let inode_table_offset = initfs::Offset( + u32::try_from(inode_table_offset) + .with_context(|| "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 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, + }; + write_all_at(&*state.file, &header_bytes, header_offset, "writing header") + .context("failed to write header")?; + } + + std::fs::rename(&destination_temp_path, destination_path) + .context("failed to rename output image")?; + + state.file.ok = true; + + Ok(()) +} diff --git a/redox-initfs-ar/src/main.rs b/redox-initfs-ar/src/main.rs index 707ef5e770..0f3349edf0 100644 --- a/redox-initfs-ar/src/main.rs +++ b/redox-initfs-ar/src/main.rs @@ -1,367 +1,10 @@ -use std::convert::{TryFrom, TryInto}; -use std::fs::{DirEntry, File, Metadata, OpenOptions}; -use std::io::{prelude::*, SeekFrom}; use std::path::Path; -use std::os::unix::ffi::OsStrExt; -use std::os::unix::fs::{FileExt, FileTypeExt, MetadataExt}; - -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use clap::{App, Arg}; -use redox_initfs::types as initfs; - -const KIBIBYTE: u64 = 1024; -const MEBIBYTE: u64 = KIBIBYTE * 1024; -const DEFAULT_MAX_SIZE: u64 = 64 * MEBIBYTE; - -enum EntryKind { - File(File), - Dir(Dir), -} - -struct Entry { - name: Vec, - kind: EntryKind, - metadata: Metadata, -} -struct Dir { - entries: Vec, -} - -struct State<'path> { - file: OutputImageGuard<'path>, - offset: u64, - max_size: u64, - inode_count: u16, - buffer: Box<[u8]>, -} - -fn read_directory(state: &mut State, path: &Path) -> Result { - let read_dir = path - .read_dir() - .with_context(|| anyhow!("failed to read directory `{}`", path.to_string_lossy(),))?; - - let entries = read_dir - .map(|result| { - let entry = result.with_context(|| { - anyhow!( - "failed to get a directory entry from `{}`", - path.to_string_lossy(), - ) - })?; - - let metadata = entry.metadata().with_context(|| { - anyhow!( - "failed to get metadata for `{}`", - entry.path().to_string_lossy(), - ) - })?; - let file_type = metadata.file_type(); - - let unsupported_type = |ty: &str, entry: &DirEntry| { - Err(anyhow!( - "failed to include {} at `{}`: not supported by redox-initfs", - ty, - entry.path().to_string_lossy() - )) - }; - let name = entry - .path() - .file_name() - .context("expected path to have a valid filename")? - .as_bytes() - .to_owned(); - - let entry_kind = if file_type.is_symlink() { - return unsupported_type("symlink", &entry); - } else if file_type.is_socket() { - return unsupported_type("socket", &entry); - } else if file_type.is_fifo() { - return unsupported_type("FIFO", &entry); - } else if file_type.is_block_device() { - return unsupported_type("block device", &entry); - } else if file_type.is_char_device() { - return unsupported_type("character device", &entry); - } else if file_type.is_file() { - EntryKind::File(File::open(&entry.path()).with_context(|| { - anyhow!("failed to open file `{}`", entry.path().to_string_lossy(),) - })?) - } else if file_type.is_dir() { - EntryKind::Dir(read_directory(state, &entry.path())?) - } else { - return Err(anyhow!( - "unknown file type at `{}`", - entry.path().to_string_lossy() - )); - }; - - // TODO: Allow the user to specify a lower limit than u16::MAX. - state.inode_count = state - .inode_count - .checked_add(1) - .ok_or_else(|| anyhow!("exceeded the maximum inode limit"))?; - - Ok(Entry { - kind: entry_kind, - metadata, - name, - }) - }) - .collect::>>()?; - - Ok(Dir { entries }) -} - -fn bump_alloc(state: &mut State, size: u64) -> Result { - if state.offset + size <= state.max_size { - let offset = state.offset; - state.offset += size; - Ok(offset) - } else { - 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) - .with_context(|| { - anyhow!( - "failed to copy directory entries from `{}` into image", - String::from_utf8_lossy(&entry.name) - ) - })?; - - (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: 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, - ) -} - -struct OutputImageGuard<'a> { - file: File, - path: &'a Path, - ok: bool, -} - -impl std::ops::Deref for OutputImageGuard<'_> { - type Target = File; - - fn deref(&self) -> &Self::Target { - &self.file - } -} -impl std::ops::DerefMut for OutputImageGuard<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.file - } -} - -impl Drop for OutputImageGuard<'_> { - fn drop(&mut self) { - if !self.ok { - let _ = std::fs::remove_file(self.path); - } - } -} +mod archive; +use self::archive::*; fn main() -> Result<()> { let matches = App::new(clap::crate_name!()) @@ -392,6 +35,8 @@ fn main() -> Result<()> { ) .get_matches(); + env_logger::init(); + let max_size = if let Some(max_size_str) = matches.value_of("MAX_SIZE") { max_size_str .parse::() @@ -408,118 +53,10 @@ fn main() -> Result<()> { .value_of("OUTPUT") .expect("expected the required arg OUTPUT to exist"); - let destination_path = Path::new(destination); - - let previous_extension = destination_path.extension().map_or("", |ext| { - ext.to_str() - .expect("expected destination path to be valid UTF-8") - }); - - if !destination_path - .metadata() - .map_or(true, |metadata| metadata.is_file()) - { - return Err(anyhow!("Destination file must be a file")); - } - - let destination_temp_path = - destination_path.with_extension(format!("{}.partial", previous_extension)); - - let destination_temp_file = OpenOptions::new() - .read(false) - .write(true) - .create(true) - .create_new(false) - .open(&destination_temp_path) - .context("failed to open destination file")?; - - let guard = OutputImageGuard { - file: destination_temp_file, - path: &destination_temp_path, - ok: false, - }; - - const BUFFER_SIZE: usize = 8192; - - let mut state = State { - file: guard, - offset: 0, + let args = Args { + source: Path::new(source), + destination_path: Path::new(destination), max_size, - inode_count: 0, - buffer: vec![0_u8; BUFFER_SIZE].into_boxed_slice(), }; - - 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() - .expect("expected header size to fit"), - )?; - - let inode_table_length = { - let inode_entry_size: u64 = std::mem::size_of::() - .try_into() - .expect("expected table entry size to fit"); - - inode_entry_size - .checked_mul(u64::from(state.inode_count)) - .ok_or_else(|| anyhow!("inode table too large"))? - }; - - 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) - .with_context(|| "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 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")?; - } - - std::fs::rename(&destination_temp_path, destination_path) - .context("failed to rename output image")?; - - state.file.ok = true; - - Ok(()) + self::archive::archive(&args) } diff --git a/src/lib.rs b/src/lib.rs index a84d9e831a..9a1682b800 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![no_std] +#![cfg_attr(not(any(test, feature = "std")), no_std)] //! A super simple initfs, only meant to be loaded into RAM by the bootloader, and then directly be //! read. @@ -25,6 +25,9 @@ impl core::fmt::Display for Error { } } +#[cfg(any(test, feature = "std"))] +impl std::error::Error for Error {} + type Result = core::result::Result; #[derive(Clone, Copy)] diff --git a/tests/archive_and_read.rs b/tests/archive_and_read.rs new file mode 100644 index 0000000000..d7bd1e2d3c --- /dev/null +++ b/tests/archive_and_read.rs @@ -0,0 +1,46 @@ +#![feature(array_methods)] + +use std::path::Path; + +use anyhow::{anyhow, bail, Context, Result}; + +#[path = "../redox-initfs-ar/src/archive.rs"] +mod archive; + +#[test] +fn archive_and_read() -> Result<()> { + env_logger::init(); + + let args = self::archive::Args { + destination_path: Path::new("out.img"), + source: Path::new("data"), + max_size: self::archive::DEFAULT_MAX_SIZE, + }; + self::archive::archive(&args).context("failed to archive")?; + + let data = std::fs::read(args.destination_path).context("failed to read new archive")?; + let filesystem = redox_initfs::InitFs::new(&data).context("failed to parse archive header")?; + let root_inode = filesystem.get_inode(redox_initfs::InitFs::ROOT_INODE).ok_or_else(|| anyhow!("Failed to get root inode"))?; + + let dir = match root_inode.kind() { + redox_initfs::InodeKind::Dir(dir) => dir, + _ => bail!("root inode was not a directory"), + }; + + for idx in 0..dir.entry_count().context("failed to get inode entry count")? { + let entry = dir.get_entry(idx).context("failed to get entry for index")?.ok_or_else(|| anyhow!("no entry found"))?; + + if entry.name().context("failed to get entry name")? == b"file.txt".as_slice() { + let inode = filesystem.get_inode(entry.inode()).context("failed to load file inode")?; + + let file = match inode.kind() { + redox_initfs::InodeKind::File(file) => file, + _ => bail!("file.txt was a directory"), + }; + let data = file.data().context("failed to get file.txt data")?; + assert_eq!(data, std::fs::read("data/file.txt").context("failed to read the real file.txt")?); + } + } + + Ok(()) +}