diff --git a/Cargo.toml b/Cargo.toml index 2427e96..3b21d32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ anyhow = { version = "1.0.58", features = ["backtrace"] } bitflags = "1" byteorder = "1" crc = "1" -positioned-io = "0.2" thiserror = "1" [dev-dependencies] diff --git a/examples/walk.rs b/examples/walk.rs index 26beedc..b780487 100644 --- a/examples/walk.rs +++ b/examples/walk.rs @@ -7,7 +7,7 @@ fn main() { let r = fs::File::open(env::args().nth(1).expect("one argument")).expect("openable file"); let mut options = ext4::Options::default(); options.checksums = ext4::Checksums::Enabled; - let vol = ext4::SuperBlock::new_with_options(r, &options).expect("ext4 volume"); + let mut vol = ext4::SuperBlock::new_with_options(r, &options).expect("ext4 volume"); let root = vol.root().expect("root"); vol.walk(&root, "/", &mut |_, path, _, _| { println!("{}", path); diff --git a/src/extents.rs b/src/extents.rs index 73caa90..980004c 100644 --- a/src/extents.rs +++ b/src/extents.rs @@ -4,11 +4,10 @@ use std::io; use anyhow::ensure; use anyhow::Error; -use positioned_io::ReadAt; use crate::{ assumption_failed, map_lib_error_to_io, read_le16, read_le32, Crypto, InnerReader, - MetadataCrypto, + MetadataCrypto, ReadAt, }; #[derive(Debug)] @@ -20,7 +19,7 @@ struct Extent { } pub struct TreeReader<'a, R: ReadAt, C: Crypto, M: MetadataCrypto> { - inner: &'a InnerReader, + inner: &'a mut InnerReader, pos: u64, len: u64, block_size: u32, @@ -31,7 +30,7 @@ pub struct TreeReader<'a, R: ReadAt, C: Crypto, M: MetadataCrypto> { impl<'a, R: ReadAt, C: Crypto, M: MetadataCrypto> TreeReader<'a, R, C, M> { pub fn new( - inner: &'a InnerReader, + inner: &'a mut InnerReader, block_size: u32, size: u64, core: [u8; crate::INODE_CORE_SIZE], @@ -55,7 +54,7 @@ impl<'a, R: ReadAt, C: Crypto, M: MetadataCrypto> TreeReader<'a, R, C, M> { } fn create( - inner: &'a InnerReader, + inner: &'a mut InnerReader, block_size: u32, size: u64, extents: Vec, @@ -320,9 +319,11 @@ mod tests { let size = 4 + 4 * 2; let crypto = NoneCrypto {}; let metadata_crypto = NoneCrypto {}; - let data = InnerReader::new((0..255u8).collect::>(), metadata_crypto); + + let cursor = std::io::Cursor::new((0..255u8).collect::>()); + let mut data = InnerReader::new(cursor, metadata_crypto); let mut reader = TreeReader::create( - &data, + &mut data, 4, u64::try_from(size).expect("infallible u64 conversion"), vec![ diff --git a/src/inner_reader.rs b/src/inner_reader.rs index a6167b1..53eb59c 100644 --- a/src/inner_reader.rs +++ b/src/inner_reader.rs @@ -2,7 +2,8 @@ use std::io; use std::io::ErrorKind; use anyhow::Error; -use positioned_io::ReadAt; + +use crate::ReadAt; pub trait MetadataCrypto { fn decrypt(&self, page: &mut [u8], page_addr: u64) -> Result<(), Error>; @@ -22,15 +23,15 @@ impl InnerReader { } } - pub fn read_at_without_decrypt(&self, pos: u64, buf: &mut [u8]) -> io::Result { + pub fn read_at_without_decrypt(&mut self, pos: u64, buf: &mut [u8]) -> io::Result { self.inner.read_at(pos, buf) } - fn decrypt io::Result>( - &self, + fn decrypt, u64, &mut [u8]) -> io::Result>( + &mut self, pos: u64, buf: &mut [u8], - read_fn: F, + mut read_fn: F, ) -> io::Result { let mut read_offset = 0; const CHUNK_SIZE: usize = 0x1000; @@ -51,7 +52,7 @@ impl InnerReader { for block_buffer in buffer.chunks_mut(CHUNK_SIZE) { let address = aligned_address + read_offset as u64; - read_fn(address, block_buffer)?; + read_fn(self, address, block_buffer)?; self.metadata_crypto .decrypt(block_buffer, address) @@ -67,15 +68,15 @@ impl InnerReader { } impl ReadAt for InnerReader { - fn read_at(&self, pos: u64, buf: &mut [u8]) -> io::Result { - self.decrypt(pos, buf, |offset, buffer| { - self.read_at_without_decrypt(offset, buffer) + fn read_at(&mut self, pos: u64, buf: &mut [u8]) -> io::Result { + self.decrypt(pos, buf, |reader, offset, buffer| { + reader.read_at_without_decrypt(offset, buffer) }) } - fn read_exact_at(&self, pos: u64, buf: &mut [u8]) -> io::Result<()> { - self.decrypt(pos, buf, |offset, buffer| { - self.inner.read_exact_at(offset, buffer)?; + fn read_exact_at(&mut self, pos: u64, buf: &mut [u8]) -> io::Result<()> { + self.decrypt(pos, buf, |reader, offset, buffer| { + reader.inner.read_exact_at(offset, buffer)?; Ok(0) })?; diff --git a/src/lib.rs b/src/lib.rs index ba13c2a..22f45d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ file on the filesystem. You can grant yourself temporary access with use std::collections::HashMap; use std::convert::TryFrom; use std::io; -use std::io::Seek; +use std::io::{Seek, SeekFrom}; use std::io::{ErrorKind, Read}; use anyhow::anyhow; @@ -29,7 +29,6 @@ use anyhow::Context; use anyhow::Error; use bitflags::bitflags; use byteorder::{LittleEndian, ReadBytesExt}; -use positioned_io::ReadAt; mod block_groups; mod extents; @@ -43,6 +42,46 @@ use crate::extents::TreeReader; pub use crate::none_crypto::NoneCrypto; pub use inner_reader::{InnerReader, MetadataCrypto}; +pub trait ReadAt { + /// Read bytes from an offset in this source into a buffer, returning how many bytes were read. + /// + /// This function may yield fewer bytes than the size of `buf`, if it was interrupted or hit + /// end-of-file. + /// + /// See [`Read::read()`](https://doc.rust-lang.org/std/io/trait.Read.html#tymethod.read). + fn read_at(&mut self, pos: u64, buf: &mut [u8]) -> io::Result; + + /// Read the exact number of bytes required to fill `buf`, from an offset. + /// + /// If only a lesser number of bytes can be read, will yield an error. + fn read_exact_at(&mut self, mut pos: u64, mut buf: &mut [u8]) -> io::Result<()> { + while !buf.is_empty() { + match self.read_at(pos, buf) { + Ok(0) => break, + Ok(n) => { + let tmp = buf; + buf = &mut tmp[n..]; + pos += n as u64; + } + Err(ref e) if e.kind() == ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + } + if !buf.is_empty() { + Err(io::Error::new(ErrorKind::UnexpectedEof, "failed to fill whole buffer")) + } else { + Ok(()) + } + } +} + +impl ReadAt for T where T: Read + Seek { + fn read_at(&mut self, pos: u64, buf: &mut [u8]) -> io::Result { + self.seek(SeekFrom::Start(pos))?; + self.read(buf) + } +} + #[derive(Debug, thiserror::Error)] pub enum ParseError { /// The filesystem doesn't meet the code's expectations; @@ -202,7 +241,7 @@ pub trait Crypto { } /// An actual disc metadata entry. -pub struct Inode<'a, C: Crypto> { +pub struct Inode { pub stat: Stat, pub number: u32, flags: InodeFlags, @@ -213,7 +252,6 @@ pub struct Inode<'a, C: Crypto> { /// I made up a new name. core: [u8; INODE_CORE_SIZE], block_size: u32, - crypto: &'a C, } /// The critical core of the filesystem. @@ -353,7 +391,7 @@ impl SuperBlock { } /// Load a filesystem entry by inode number. - pub fn load_inode(&self, inode: u32) -> Result, Error> { + pub fn load_inode(&mut self, inode: u32) -> Result { let data = self .load_inode_bytes(inode) .with_context(|| anyhow!("failed to find inode <{}> on disc", inode))?; @@ -374,23 +412,22 @@ impl SuperBlock { core: parsed.core, checksum_prefix: parsed.checksum_prefix, block_size: self.groups.block_size, - crypto: &self.crypto, }) } - fn load_inode_bytes(&self, inode: u32) -> Result, Error> { + fn load_inode_bytes(&mut self, inode: u32) -> Result, Error> { let offset = self.groups.index_of(inode)?; let mut data = vec![0u8; usize::try_from(self.groups.inode_size)?]; self.inner.read_exact_at(offset, &mut data)?; Ok(data) } - fn load_disc_bytes(&self, block: u64) -> Result, Error> { - load_disc_bytes(&self.inner, self.groups.block_size, block) + fn load_disc_bytes(&mut self, block: u64) -> Result, Error> { + load_disc_bytes(&mut self.inner, self.groups.block_size, block) } /// Load the root node of the filesystem (typically `/`). - pub fn root(&self) -> Result, Error> { + pub fn root(&mut self) -> Result { Ok(self .load_inode(2) .with_context(|| anyhow!("failed to load root inode"))?) @@ -399,11 +436,11 @@ impl SuperBlock { /// Visit every entry in the filesystem in an arbitrary order. /// The closure should return `true` if it wants walking to continue. /// The method returns `true` if the closure always returned true. - pub fn walk(&self, inode: &Inode, path: &str, visit: &mut F) -> Result + pub fn walk(&mut self, inode: &Inode, path: &str, visit: &mut F) -> Result where - F: FnMut(&Self, &str, &Inode, &Enhanced) -> Result, + F: FnMut(&mut Self, &str, &Inode, &Enhanced) -> Result, { - let enhanced = inode.enhance(&self.inner)?; + let enhanced = inode.enhance(&mut self.inner, &self.crypto)?; if !visit(self, path, inode, &enhanced).with_context(|| anyhow!("user closure failed"))? { return Ok(false); @@ -438,7 +475,7 @@ impl SuperBlock { /// Parse a path, and find the directory entry it represents. /// Note that "/foo/../bar" will be treated literally, not resolved to "/bar" then looked up. - pub fn resolve_path(&self, path: &str) -> Result { + pub fn resolve_path(&mut self, path: &str) -> Result { let path = path.replace('\\', "/"); let path = path.trim_end_matches('/'); @@ -469,7 +506,7 @@ impl SuperBlock { self.dir_entry_named(&curr, last) } - fn dir_entry_named(&self, inode: &Inode, name: &str) -> Result { + fn dir_entry_named(&mut self, inode: &Inode, name: &str) -> Result { if let Enhanced::Directory(entries) = self.enhance(inode)? { if let Some(en) = entries.into_iter().find(|entry| entry.name == name) { Ok(en) @@ -482,18 +519,18 @@ impl SuperBlock { } /// Read the data from an inode. You might not want to call this on thigns that aren't regular files. - pub fn open<'a>(&'a self, inode: &'a Inode) -> Result, Error> { - inode.reader(&self.inner) + pub fn open<'a>(&'a mut self, inode: &'a Inode) -> Result, Error> { + inode.reader(&mut self.inner, &self.crypto) } /// Load extra metadata about some types of entries. - pub fn enhance(&self, inode: &Inode) -> Result { - inode.enhance(&self.inner) + pub fn enhance(&mut self, inode: &Inode) -> Result { + inode.enhance(&mut self.inner, &self.crypto) } } fn load_disc_bytes( - inner: &InnerReader, + inner: &mut InnerReader, block_size: u32, block: u64, ) -> Result, Error> { @@ -503,10 +540,11 @@ fn load_disc_bytes( Ok(data) } -impl<'a, C: Crypto> Inode<'a, C> { - fn reader( - &self, - inner: &'a InnerReader, +impl Inode { + fn reader<'a, R: ReadAt, C: Crypto, M: MetadataCrypto>( + &'a self, + inner: &'a mut InnerReader, + crypto: &'a C ) -> Result, Error> { let context = if matches!(self.stat.extracted_type, FileType::RegularFile) { self.get_encryption_context() @@ -521,21 +559,22 @@ impl<'a, C: Crypto> Inode<'a, C> { self.core, self.checksum_prefix, context, - self.crypto, + crypto, ) .with_context(|| anyhow!("opening inode <{}>", self.number))?) } - fn enhance( + fn enhance( &self, - inner: &InnerReader, + inner: &mut InnerReader, + crypto: &C ) -> Result { Ok(match self.stat.extracted_type { FileType::RegularFile => Enhanced::RegularFile, FileType::Socket => Enhanced::Socket, FileType::Fifo => Enhanced::Fifo, - FileType::Directory => Enhanced::Directory(self.read_directory(inner)?), + FileType::Directory => Enhanced::Directory(self.read_directory(inner, crypto)?), FileType::SymbolicLink => { Enhanced::SymbolicLink(if self.stat.size < u64::try_from(INODE_CORE_SIZE)? { ensure!( @@ -556,7 +595,7 @@ impl<'a, C: Crypto> Inode<'a, C> { self.flags )) ); - std::str::from_utf8(&self.load_all(inner)?) + std::str::from_utf8(&self.load_all(inner, crypto)?) .with_context(|| anyhow!("long symlink is invalid utf-8"))? .to_string() }) @@ -572,14 +611,15 @@ impl<'a, C: Crypto> Inode<'a, C> { }) } - fn load_all( + fn load_all( &self, - inner: &InnerReader, + inner: &mut InnerReader, + crypto: &C ) -> Result, Error> { let size = usize::try_from(self.stat.size)?; let mut ret = vec![0u8; size]; - self.reader(inner)?.read_exact(&mut ret)?; + self.reader(inner, crypto)?.read_exact(&mut ret)?; Ok(ret) } @@ -588,9 +628,10 @@ impl<'a, C: Crypto> Inode<'a, C> { self.stat.xattrs.get("encryption.c") } - fn read_directory( + fn read_directory( &self, - inner: &InnerReader, + inner: &mut InnerReader, + crypto: &C ) -> Result, Error> { let mut dirs = Vec::with_capacity(40); @@ -604,7 +645,7 @@ impl<'a, C: Crypto> Inode<'a, C> { )) ); - self.load_all(inner)? + self.load_all(inner, crypto)? }; let total_len = data.len(); @@ -633,7 +674,7 @@ impl<'a, C: Crypto> Inode<'a, C> { self.get_encryption_context(), [b".".as_slice(), b"..".as_slice()].contains(&name.as_slice()), ) { - self.crypto.decrypt_filename(context, &name)? + crypto.decrypt_filename(context, &name)? } else { name }; diff --git a/src/parse.rs b/src/parse.rs index 9f36e41..50927f3 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -3,6 +3,7 @@ use std::convert::TryFrom; use std::io; use std::io::Read; use std::io::Seek; +use std::io::Cursor; use anyhow::anyhow; use anyhow::bail; @@ -11,8 +12,6 @@ use anyhow::Context; use anyhow::Error; use bitflags::bitflags; use byteorder::{ByteOrder, LittleEndian, ReadBytesExt}; -use positioned_io::Cursor; -use positioned_io::ReadAt; use crate::unsupported_feature; use crate::Time; @@ -21,6 +20,7 @@ use crate::{map_lib_error_to_io, parse_error}; use crate::{not_found, Crypto}; use crate::{read_le16, MetadataCrypto}; use crate::{read_le32, InnerReader}; +use crate::ReadAt; const EXT4_SUPER_MAGIC: u16 = 0xEF53; const INODE_BASE_LEN: usize = 128; @@ -82,7 +82,7 @@ pub fn superblock( crypto: C, metadata_crypto: M, ) -> Result, Error> { - let reader = InnerReader::new(raw_reader, metadata_crypto); + let mut reader = InnerReader::new(raw_reader, metadata_crypto); let mut entire_superblock = [0u8; 1024]; reader.read_exact_at(1024, &mut entire_superblock)?; diff --git a/tests/generated-images.rs b/tests/generated-images.rs index dfaada2..648adf6 100644 --- a/tests/generated-images.rs +++ b/tests/generated-images.rs @@ -6,14 +6,91 @@ use std::ffi::OsStr; use std::fs; use std::io; use std::io::Read; +use std::io::Seek; +use std::io::SeekFrom; use std::path::PathBuf; use std::process::Stdio; use anyhow::Result; -use positioned_io::ReadAt; -use tempfile::NamedTempFile; use tempfile::TempDir; +fn calculate_position_after_seek( + position: SeekFrom, + current_offset: u64, + total_size: u64, +) -> io::Result { + let new_offset = match position { + SeekFrom::Current(offset) => current_offset + .checked_add_signed(offset) + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Numeric overflow"))?, + SeekFrom::End(offset) => total_size + .checked_add_signed(offset) + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Numeric overflow"))?, + SeekFrom::Start(offset) => offset, + }; + + if new_offset > total_size { + return Err(io::Error::new( + io::ErrorKind::Other, + "Out of sub-stream bounds", + )); + } + + Ok(new_offset) +} + +pub struct StreamSlice { + base_stream: T, + start_offset: u64, + size: u64, + current_offset: u64, +} + +impl StreamSlice +where + T: Seek, +{ + pub fn new(base_stream: T, start_offset: u64, size: u64) -> Result> { + let mut slice = StreamSlice { + base_stream, + start_offset, + size, + current_offset: 0, + }; + slice.seek(SeekFrom::Start(0))?; + + Ok(slice) + } +} + +impl Seek for StreamSlice +where + T: Seek, +{ + fn seek(&mut self, pos: SeekFrom) -> io::Result { + self.current_offset = calculate_position_after_seek(pos, self.current_offset, self.size)?; + self.base_stream + .seek(SeekFrom::Start(self.start_offset + self.current_offset))?; + Ok(self.current_offset) + } +} + +impl Read for StreamSlice +where + T: Seek + Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.current_offset > self.size { + return Err(io::Error::new(io::ErrorKind::Other, "End of stream")); + } + let size_to_read = std::cmp::min((self.size - self.current_offset) as usize, buf.len()); + + let size = self.base_stream.read(&mut buf[..size_to_read])?; + self.current_offset += size as u64; + Ok(size) + } +} + #[test] fn all_types() -> Result<()> { let mut files_successfully_processed = 0u64; @@ -34,8 +111,8 @@ fn all_types() -> Result<()> { _ => panic!("unexpected partition table"), } - let part_reader = positioned_io::Slice::new(&mut img, part.first_byte, Some(part.len)); - let superblock = ext4::SuperBlock::new(part_reader).unwrap(); + let part_reader = StreamSlice::new(&mut img, part.first_byte, part.len)?; + let mut superblock = ext4::SuperBlock::new(part_reader).unwrap(); let root = superblock.root().unwrap(); superblock .walk(&root, "", &mut |fs, path, inode, enhanced| { @@ -68,10 +145,11 @@ fn all_types() -> Result<()> { .unwrap(); assert_eq!("Hello, world!\n", s); + let future_file_inode = superblock.resolve_path("future-file").unwrap().inode; assert_eq!( 11847456550, superblock - .load_inode(superblock.resolve_path("future-file").unwrap().inode) + .load_inode(future_file_inode) .unwrap() .stat .mtime @@ -85,16 +163,6 @@ fn all_types() -> Result<()> { Ok(()) } -struct ReadAtTempFile { - inner: NamedTempFile, -} - -impl ReadAt for ReadAtTempFile { - fn read_at(&self, pos: u64, buf: &mut [u8]) -> io::Result { - self.inner.as_file().read_at(pos, buf) - } -} - struct Assets { tempdir: TempDir, }