Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/windirstat/ntfs.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorColin Finck <colin@reactos.org>2021-10-03 17:03:03 +0300
committerColin Finck <colin@reactos.org>2021-10-03 17:03:03 +0300
commit5be9002f7a983df0e99a84fe991a42a9acd1c09c (patch)
tree7c0790cf07fb2f28faff230191e55378f1996a3d
parenteaf86401ce5ee42153b70963fcbd67799926bf32 (diff)
Add ntfs-shell, an example app to demonstrate all features of the crate.
ntfs-shell is a command-line shell to navigate through an NTFS filesystem. It works read-only on image files, loopback devices, and raw partitions. Very useful for debugging the crate, examining the structure of an NTFS filesystem, and extracting data from it.
-rw-r--r--Cargo.toml7
-rw-r--r--examples/ntfs-shell/main.rs598
-rw-r--r--examples/ntfs-shell/sector_reader.rs130
3 files changed, 735 insertions, 0 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 957eba8..70ccd5e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,13 @@ enumn = "0.1.3"
memoffset = "0.6.4"
strum_macros = "0.21.1"
+[dev-dependencies]
+anyhow = "1.0"
+
[features]
default = ["std"]
std = ["arrayvec/std", "binread/std", "byteorder/std"]
+
+[[example]]
+name = "ntfs-shell"
+required-features = ["chrono"]
diff --git a/examples/ntfs-shell/main.rs b/examples/ntfs-shell/main.rs
new file mode 100644
index 0000000..06e7376
--- /dev/null
+++ b/examples/ntfs-shell/main.rs
@@ -0,0 +1,598 @@
+// Copyright 2021 Colin Finck <colin@reactos.org>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+mod sector_reader;
+
+use std::env;
+use std::fs::{File, OpenOptions};
+use std::io;
+use std::io::{BufReader, Read, Seek, Write};
+
+use anyhow::{anyhow, bail, Context, Result};
+use chrono::{DateTime, Utc};
+use ntfs::indexes::NtfsFileNameIndex;
+use ntfs::structured_values::{NtfsAttributeList, NtfsFileName, NtfsStandardInformation};
+use ntfs::value::NtfsValue;
+use ntfs::{Ntfs, NtfsAttribute, NtfsAttributeType, NtfsFile, NtfsReadSeek};
+
+use sector_reader::SectorReader;
+
+struct CommandInfo<'n, T>
+where
+ T: Read + Seek,
+{
+ current_directory: NtfsFile<'n>,
+ current_directory_string: String,
+ fs: T,
+}
+
+fn main() -> Result<()> {
+ let args: Vec<String> = env::args().collect();
+ if args.len() != 2 {
+ eprintln!("Usage: ntfs-shell FILESYSTEM");
+ eprintln!();
+ eprintln!("FILESYSTEM can be a path to any NTFS filesystem image.");
+ eprintln!("Under Windows and when run with administrative privileges, FILESYSTEM can also");
+ eprintln!("be the special path \\\\.\\C: to access the filesystem of the C: partition.");
+ bail!("Aborted");
+ }
+
+ let f = File::open(&args[1])?;
+ let sr = SectorReader::new(f, 512)?;
+ let mut fs = BufReader::new(sr);
+ let mut ntfs = Ntfs::new(&mut fs)?;
+ ntfs.read_upcase_table(&mut fs)?;
+ let current_directory = ntfs.root_directory(&mut fs)?;
+
+ let mut info = CommandInfo {
+ current_directory,
+ current_directory_string: String::new(),
+ fs,
+ };
+
+ println!("**********************************************************************");
+ println!("ntfs-shell - Demonstration of rust-ntfs");
+ println!("by Colin Finck <colin@reactos.org>");
+ println!("**********************************************************************");
+ println!();
+ println!("Opened \"{}\" read-only.", args[1]);
+ println!();
+
+ loop {
+ print!("ntfs-shell:\\{}> ", info.current_directory_string);
+ io::stdout().flush()?;
+
+ let mut input_string = String::new();
+ io::stdin().read_line(&mut input_string).unwrap();
+ if input_string.is_empty() {
+ // An empty `input_string` without even a newline looks like STDIN was closed.
+ break;
+ }
+
+ let input = input_string.trim();
+ let mid = input.find(' ').unwrap_or(input.len());
+ let (command, arg) = input.split_at(mid);
+ let arg = arg.trim_start();
+
+ let result = match command {
+ "attr" => attr(false, arg, &mut info),
+ "attr_runs" => attr(true, arg, &mut info),
+ "cd" => cd(arg, &mut info),
+ "dir" => dir(&mut info),
+ "exit" | "quit" => break,
+ "fileinfo" => fileinfo(arg, &mut info),
+ "fsinfo" => fsinfo(&mut info),
+ "get" => get(arg, &mut info),
+ "help" => help(arg),
+ "" => continue,
+ _ => Err(anyhow!(
+ "Invalid command \"{}\". Type \"help\" to get a list of all commands.",
+ command
+ )),
+ };
+ if let Err(e) = result {
+ eprintln!("Error: {:?}", e);
+ }
+ }
+
+ Ok(())
+}
+
+fn attr<T>(with_runs: bool, arg: &str, info: &mut CommandInfo<T>) -> Result<()>
+where
+ T: Read + Seek,
+{
+ let ntfs = info.current_directory.ntfs();
+ let file = parse_file_arg(arg, info)?;
+
+ println!("{:=<110}", "");
+ println!(
+ "{:<10} | {:<20} | {:<8} | {:<13} | {:<18} | {:<13} | {}",
+ "INSTANCE", "TYPE", "RESIDENT", "RECORD NUMBER", "START", "LENGTH", "NAME"
+ );
+ println!("{:=<110}", "");
+
+ let attributes = file.attributes_raw();
+ for attribute in attributes {
+ let ty = attribute.ty()?;
+
+ attr_print_attribute(
+ with_runs,
+ &attribute,
+ file.file_record_number(),
+ "● ",
+ " ■ ",
+ )?;
+
+ if ty == NtfsAttributeType::AttributeList {
+ let list = attribute.structured_value::<_, NtfsAttributeList>(&mut info.fs)?;
+ let mut list_iter = list.iter();
+
+ while let Some(entry) = list_iter.next(&mut info.fs) {
+ let entry = entry?;
+
+ let entry_record_number = entry.base_file_reference().file_record_number();
+ if entry_record_number == file.file_record_number() {
+ continue;
+ }
+
+ let entry_file = entry.to_file(ntfs, &mut info.fs)?;
+ let entry_attribute = entry.to_attribute(&entry_file)?;
+
+ attr_print_attribute(
+ with_runs,
+ &entry_attribute,
+ entry_record_number,
+ " ○ ",
+ " □ ",
+ )?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn attr_print_attribute<'n>(
+ with_runs: bool,
+ attribute: &NtfsAttribute<'n, '_>,
+ record_number: u64,
+ attribute_prefix: &str,
+ data_run_prefix: &str,
+) -> Result<()> {
+ let instance = format!("{}{}", attribute_prefix, attribute.instance());
+ let ty = attribute.ty()?;
+ let resident = attribute.is_resident();
+ let start = attribute.position();
+ let length = attribute.value_length();
+ let name = attribute.name()?.to_string_lossy();
+
+ println!(
+ "{:<10} | {:<20} | {:<8} | {:#13x} | {:#18x} | {:>13} | \"{}\"",
+ instance, ty, resident, record_number, start, length, name
+ );
+
+ if with_runs {
+ if let NtfsValue::NonResidentAttribute(non_resident_value) = attribute.value()? {
+ for (i, data_run) in non_resident_value.data_runs().enumerate() {
+ let data_run = data_run?;
+ let instance = format!("{}{}", data_run_prefix, i);
+ let start = data_run.data_position().unwrap_or(0);
+ let length = data_run.len();
+
+ println!(
+ "{:<10} | {:<20} | {:<8} | {:>13} | {:#18x} | {:>13} |",
+ instance, "DataRun", "", "", start, length
+ );
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn cd<T>(arg: &str, info: &mut CommandInfo<T>) -> Result<()>
+where
+ T: Read + Seek,
+{
+ if arg.is_empty() {
+ return Ok(());
+ }
+
+ let ntfs = info.current_directory.ntfs();
+
+ if arg == ".." {
+ if info.current_directory_string.is_empty() {
+ return Ok(());
+ }
+
+ let file_name = info.current_directory.name(&mut info.fs)?;
+ let parent_ref = file_name.parent_directory_reference();
+ info.current_directory = parent_ref.to_file(ntfs, &mut info.fs)?;
+
+ let new_len = info.current_directory_string.rfind('\\').unwrap_or(0);
+ info.current_directory_string.truncate(new_len);
+ } else {
+ let index = info.current_directory.directory_index(&mut info.fs)?;
+ let mut finder = index.finder();
+ let maybe_entry = NtfsFileNameIndex::find(&mut finder, &ntfs, &mut info.fs, arg);
+
+ if maybe_entry.is_none() {
+ println!("Cannot find subdirectory \"{}\".", arg);
+ return Ok(());
+ }
+
+ let entry = maybe_entry.unwrap()?;
+ let file_name = entry
+ .key()
+ .expect("key must exist for a found index entry")?;
+
+ if !file_name.is_directory() {
+ println!("\"{}\" is not a directory.", arg);
+ return Ok(());
+ }
+
+ info.current_directory = entry.to_file(ntfs, &mut info.fs)?;
+
+ let file_name = info.current_directory.name(&mut info.fs)?;
+ if !info.current_directory_string.is_empty() {
+ info.current_directory_string += "\\";
+ }
+ info.current_directory_string += &file_name.name().to_string_lossy();
+ }
+
+ Ok(())
+}
+
+fn dir<T>(info: &mut CommandInfo<T>) -> Result<()>
+where
+ T: Read + Seek,
+{
+ let index = info.current_directory.directory_index(&mut info.fs)?;
+ let mut iter = index.iter();
+
+ while let Some(entry) = iter.next(&mut info.fs) {
+ let entry = entry?;
+ let file_name = entry
+ .key()
+ .expect("key must exist for a found index entry")?;
+
+ let prefix = if file_name.is_directory() {
+ "<DIR>"
+ } else {
+ ""
+ };
+ println!("{:5} {}", prefix, file_name.name());
+ }
+
+ Ok(())
+}
+
+fn fileinfo<T>(arg: &str, info: &mut CommandInfo<T>) -> Result<()>
+where
+ T: Read + Seek,
+{
+ let file = parse_file_arg(arg, info)?;
+
+ println!("{:=^72}", " FILE RECORD ");
+ println!("{:34}{}", "Allocated Size:", file.allocated_size());
+ println!("{:34}{:#x}", "Byte Position:", file.position());
+ println!("{:34}{}", "Hard-Link Count:", file.hard_link_count());
+ println!("{:34}{}", "Is Directory:", file.is_directory());
+ println!("{:34}{:#x}", "Record Number:", file.file_record_number());
+ println!("{:34}{}", "Sequence Number:", file.sequence_number());
+ println!("{:34}{}", "Used Size:", file.used_size());
+
+ let mut attributes = file.attributes();
+ while let Some(attribute_item) = attributes.next(&mut info.fs) {
+ let attribute_item = attribute_item?;
+ let attribute = attribute_item.to_attribute();
+
+ match attribute.ty() {
+ Ok(NtfsAttributeType::StandardInformation) => fileinfo_std(attribute)?,
+ Ok(NtfsAttributeType::FileName) => fileinfo_filename(info, attribute)?,
+ Ok(NtfsAttributeType::Data) => fileinfo_data(attribute)?,
+ _ => continue,
+ }
+ }
+
+ Ok(())
+}
+
+fn fileinfo_std(attribute: NtfsAttribute) -> Result<()> {
+ println!();
+ println!("{:=^72}", " STANDARD INFORMATION ");
+
+ let info = attribute.resident_structured_value::<NtfsStandardInformation>()?;
+
+ println!("{:34}{:?}", "Attributes:", info.file_attributes());
+
+ let format = "%F %T UTC";
+ let atime = DateTime::<Utc>::from(info.access_time()).format(format);
+ let ctime = DateTime::<Utc>::from(info.creation_time()).format(format);
+ let mtime = DateTime::<Utc>::from(info.modification_time()).format(format);
+ let mmtime = DateTime::<Utc>::from(info.mft_record_modification_time()).format(format);
+ println!("{:34}{}", "Access Time:", atime);
+ println!("{:34}{}", "Creation Time:", ctime);
+ println!("{:34}{}", "Modification Time:", mtime);
+ println!("{:34}{}", "MFT Record Modification Time:", mmtime);
+
+ // NTFS 3.x extended information
+ let class_id = info
+ .class_id()
+ .map(|x| x.to_string())
+ .unwrap_or_else(|| "<NONE>".to_string());
+ let maximum_versions = info
+ .maximum_versions()
+ .map(|x| x.to_string())
+ .unwrap_or_else(|| "<NONE>".to_string());
+ let owner_id = info
+ .owner_id()
+ .map(|x| x.to_string())
+ .unwrap_or_else(|| "<NONE>".to_string());
+ let quota_charged = info
+ .quota_charged()
+ .map(|x| x.to_string())
+ .unwrap_or_else(|| "<NONE>".to_string());
+ let security_id = info
+ .security_id()
+ .map(|x| x.to_string())
+ .unwrap_or_else(|| "<NONE>".to_string());
+ let usn = info
+ .usn()
+ .map(|x| x.to_string())
+ .unwrap_or_else(|| "<NONE>".to_string());
+ let version = info
+ .version()
+ .map(|x| x.to_string())
+ .unwrap_or_else(|| "<NONE>".to_string());
+ println!("{:34}{}", "Class ID:", class_id);
+ println!("{:34}{}", "Maximum Versions:", maximum_versions);
+ println!("{:34}{}", "Owner ID:", owner_id);
+ println!("{:34}{}", "Quota Charged:", quota_charged);
+ println!("{:34}{}", "Security ID:", security_id);
+ println!("{:34}{}", "USN:", usn);
+ println!("{:34}{}", "Version:", version);
+
+ Ok(())
+}
+
+fn fileinfo_filename<T>(info: &mut CommandInfo<T>, attribute: NtfsAttribute) -> Result<()>
+where
+ T: Read + Seek,
+{
+ println!();
+ println!("{:=^72}", " FILE NAME ");
+
+ let file_name = attribute.structured_value::<_, NtfsFileName>(&mut info.fs)?;
+
+ println!("{:34}\"{}\"", "Name:", file_name.name().to_string_lossy());
+ println!("{:34}{:?}", "Namespace:", file_name.namespace());
+ println!(
+ "{:34}{:#x}",
+ "Parent Directory Record Number:",
+ file_name.parent_directory_reference().file_record_number()
+ );
+
+ Ok(())
+}
+
+fn fileinfo_data(attribute: NtfsAttribute) -> Result<()> {
+ println!();
+ println!("{:=^72}", " DATA STREAM ");
+
+ println!("{:34}\"{}\"", "Name:", attribute.name()?.to_string_lossy());
+ println!("{:34}{}", "Size:", attribute.value_length());
+
+ Ok(())
+}
+
+fn fsinfo<T>(info: &mut CommandInfo<T>) -> Result<()>
+where
+ T: Read + Seek,
+{
+ let ntfs = info.current_directory.ntfs();
+
+ println!("{:20}{}", "Cluster Size:", ntfs.cluster_size());
+ println!("{:20}{}", "File Record Size:", ntfs.file_record_size());
+ println!("{:20}{:#x}", "MFT Byte Position:", ntfs.mft_position());
+
+ let volume_info = ntfs.volume_info(&mut info.fs)?;
+ let ntfs_version = format!(
+ "{}.{}",
+ volume_info.major_version(),
+ volume_info.minor_version()
+ );
+ println!("{:20}{}", "NTFS Version:", ntfs_version);
+
+ println!("{:20}{}", "Sector Size:", ntfs.sector_size());
+ println!("{:20}{}", "Serial Number:", ntfs.serial_number());
+ println!("{:20}{}", "Size:", ntfs.size());
+
+ let volume_name = if let Some(Ok(volume_name)) = ntfs.volume_name(&mut info.fs) {
+ volume_name.name().to_string_lossy()
+ } else {
+ "<NONE>".to_string()
+ };
+ println!("{:20}{}", "Volume Name:", volume_name);
+
+ Ok(())
+}
+
+fn get<T>(arg: &str, info: &mut CommandInfo<T>) -> Result<()>
+where
+ T: Read + Seek,
+{
+ // Extract any specific $DATA stream name from the file.
+ let mid = arg.find(':').unwrap_or(arg.len());
+ let (file_name, data_stream_name) = arg.split_at(mid);
+
+ // Compose the output file name and try to create it.
+ // It must not yet exist, as we don't want to accidentally overwrite things.
+ let output_file_name = if data_stream_name.is_empty() {
+ file_name.to_string()
+ } else {
+ format!("{}_{}", file_name, data_stream_name)
+ };
+ let mut output_file = OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .open(&output_file_name)
+ .with_context(|| format!("Tried to open \"{}\" for writing", output_file_name))?;
+
+ // Open the desired file and find the $DATA attribute we are looking for.
+ let file = parse_file_arg(file_name, info)?;
+ let data_item = match file.data(&mut info.fs, data_stream_name) {
+ Some(data_item) => data_item,
+ None => {
+ println!(
+ "The file does not have a \"{}\" $DATA attribute.",
+ data_stream_name
+ );
+ return Ok(());
+ }
+ };
+ let data_item = data_item?;
+ let data_attribute = data_item.to_attribute();
+ let mut data_value = data_attribute.value()?;
+
+ println!(
+ "Saving {} bytes of data in \"{}\"...",
+ data_value.len(),
+ output_file_name
+ );
+ let mut buf = [0u8; 4096];
+
+ loop {
+ let bytes_read = data_value.read(&mut info.fs, &mut buf)?;
+ if bytes_read == 0 {
+ break;
+ }
+
+ output_file.write(&buf[..bytes_read])?;
+ }
+
+ Ok(())
+}
+
+fn help(arg: &str) -> Result<()> {
+ match arg {
+ "attr" => {
+ println!("Usage: attr FILE");
+ println!();
+ println!("Shows the structure of all NTFS attributes of a single file, not including their data runs.");
+ println!("Try \"attr_runs\" if you are also interested in data run information.");
+ help_file("attr");
+ }
+ "attr_runs" => {
+ println!("Usage: attr_runs FILE");
+ println!();
+ println!("Shows the structure of all NTFS attributes of a single file, including their data runs.");
+ println!("Try \"attr\" if you don't need the data run information.");
+ help_file("attr_runs");
+ }
+ "cd" => {
+ println!("Usage: cd SUBDIRECTORY");
+ println!();
+ println!("Changes the current directory to SUBDIRECTORY.");
+ println!("This implementation of \"cd\" only supports subdirectories of the current directory.");
+ println!("\"cd ..\" moves back into the parent directory.");
+ }
+ "dir" => {
+ println!("Usage: dir");
+ println!();
+ println!("Lists filenames in the current directory (like \"ls\" on UNIX systems).");
+ println!("No additional parameters are supported.");
+ println!("Try \"fileinfo\" to get additional information about a single file.");
+ }
+ "fileinfo" => {
+ println!("Usage: fileinfo FILE");
+ println!();
+ println!("Shows information about a single file (by parsing its NTFS attributes).");
+ help_file("fileinfo");
+ }
+ "get" => {
+ println!("Usage:");
+ println!(" get FILE");
+ println!(" get FILE:STREAM");
+ println!();
+ println!("Copies the data of a single file from the NTFS filesystem to the current directory of your local filesystem.");
+ println!("Optionally, you can append a colon and a data stream name to copy a specific data stream of that file.");
+ println!();
+ println!("This command will fail if the file already exists in the current directory.");
+ help_file("get");
+ }
+ _ => {
+ println!("Available Commands:");
+ println!(" attr - Show structure of NTFS attributes of a particular file");
+ println!(" attr_runs - Show structure of NTFS attributes of a particular file, including data runs");
+ println!(" cd - Change the current directory");
+ println!(" dir - Show files of the current directory");
+ println!(" exit - Quit ntfs-shell");
+ println!(" fileinfo - Show information about a particular file");
+ println!(" fsinfo - Show general filesystem information");
+ println!(" get - Copy a file from the NTFS filesystem");
+ println!(" help - Show this help");
+ println!(" quit - Quit ntfs-shell");
+ println!();
+ println!(
+ "You can also enter \"help COMMAND\" to get additional help about some commands."
+ );
+ }
+ }
+
+ Ok(())
+}
+
+fn help_file(command: &str) {
+ println!();
+ println!("FILE can have one of the following formats:");
+ println!(" ● A name of a file in the current directory.");
+ println!(" Enter the filename as is, including any spaces. Don't put it into additional quotation marks.");
+ println!(" Examples:");
+ println!(" ○ {} ntoskrnl.exe", command);
+ println!(" ○ {} File with spaces.exe", command);
+ println!(" ● A file record number anywhere on the filesystem.");
+ println!(" This is indicated through a leading slash (/). A hexadecimal file record number is indicated via 0x.");
+ println!(" Examples:");
+ println!(" ○ {} /5", command);
+ println!(" ○ {} /0xa299", command);
+}
+
+fn parse_file_arg<'n, T>(arg: &str, info: &mut CommandInfo<'n, T>) -> Result<NtfsFile<'n>>
+where
+ T: Read + Seek,
+{
+ if arg.is_empty() {
+ bail!("Missing argument!");
+ }
+
+ let ntfs = info.current_directory.ntfs();
+
+ if let Some(record_number_arg) = arg.strip_prefix("/") {
+ let record_number = match record_number_arg.strip_prefix("0x") {
+ Some(hex_record_number_arg) => u64::from_str_radix(hex_record_number_arg, 16),
+ None => u64::from_str_radix(record_number_arg, 10),
+ };
+
+ if let Ok(record_number) = record_number {
+ let file = ntfs.file(&mut info.fs, record_number)?;
+ Ok(file)
+ } else {
+ bail!(
+ "Cannot parse record number argument \"{}\"",
+ record_number_arg
+ )
+ }
+ } else {
+ let index = info.current_directory.directory_index(&mut info.fs)?;
+ let mut finder = index.finder();
+ if let Some(entry) = NtfsFileNameIndex::find(&mut finder, &ntfs, &mut info.fs, arg) {
+ let entry = entry?;
+ let file = entry.to_file(ntfs, &mut info.fs)?;
+ Ok(file)
+ } else {
+ bail!("No such file or directory \"{}\".", arg)
+ }
+ }
+}
diff --git a/examples/ntfs-shell/sector_reader.rs b/examples/ntfs-shell/sector_reader.rs
new file mode 100644
index 0000000..690d3ed
--- /dev/null
+++ b/examples/ntfs-shell/sector_reader.rs
@@ -0,0 +1,130 @@
+// Copyright 2021 Colin Finck <colin@reactos.org>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+use std::io;
+use std::io::{Read, Seek, SeekFrom};
+
+/// `SectorReader` encapsulates any reader and only performs read and seek operations on it
+/// on boundaries of the given sector size.
+///
+/// This can be very useful for readers that only accept sector-sized reads (like reading
+/// from a raw partition on Windows).
+/// The sector size must be a power of two.
+///
+/// This reader does not keep any buffer.
+/// You are advised to encapsulate `SectorReader` in a buffered reader, as unbuffered reads of
+/// just a few bytes here and there are highly inefficient.
+pub struct SectorReader<R>
+where
+ R: Read + Seek,
+{
+ /// The inner reader stream.
+ inner: R,
+ /// The sector size set at creation.
+ sector_size: usize,
+ /// The current stream position as requested by the caller through `read` or `seek`.
+ /// The implementation will internally make sure to only read/seek on sector boundaries.
+ stream_position: u64,
+ /// This buffer is only part of the struct as a small performance optimization (keeping it allocated between reads).
+ temp_buf: Vec<u8>,
+}
+
+impl<R> SectorReader<R>
+where
+ R: Read + Seek,
+{
+ pub fn new(inner: R, sector_size: usize) -> io::Result<Self> {
+ if !sector_size.is_power_of_two() {
+ return Err(io::Error::new(
+ io::ErrorKind::Other,
+ "sector_size is not a power of two",
+ ));
+ }
+
+ Ok(Self {
+ inner,
+ sector_size,
+ stream_position: 0,
+ temp_buf: Vec::new(),
+ })
+ }
+
+ fn align_down_to_sector_size(&self, n: u64) -> u64 {
+ n / self.sector_size as u64 * self.sector_size as u64
+ }
+
+ fn align_up_to_sector_size(&self, n: u64) -> u64 {
+ self.align_down_to_sector_size(n) + self.sector_size as u64
+ }
+}
+
+impl<R> Read for SectorReader<R>
+where
+ R: Read + Seek,
+{
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ // We can only read from a sector boundary, and `self.stream_position` specifies the position where the
+ // caller thinks we are.
+ // Align down to a sector boundary to determine the position where we really are (see our `seek` implementation).
+ let aligned_position = self.align_down_to_sector_size(self.stream_position);
+
+ // We have to read more bytes now to make up for the alignment difference.
+ // We can also only read in multiples of the sector size, so align up to the next sector boundary.
+ let start = (self.stream_position - aligned_position) as usize;
+ let end = start + buf.len();
+ let aligned_bytes_to_read = self.align_up_to_sector_size(end as u64) as usize;
+
+ // Perform the sector-sized read and copy the actually requested bytes into the given buffer.
+ self.temp_buf.resize(aligned_bytes_to_read, 0);
+ self.inner.read_exact(&mut self.temp_buf)?;
+ buf.copy_from_slice(&self.temp_buf[start..end]);
+
+ // We are done.
+ self.stream_position += buf.len() as u64;
+ Ok(buf.len())
+ }
+}
+
+impl<R> Seek for SectorReader<R>
+where
+ R: Read + Seek,
+{
+ fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
+ let new_pos = match pos {
+ SeekFrom::Start(n) => Some(n),
+ SeekFrom::End(_n) => {
+ // This is unsupported, because it's not safely possible under Windows.
+ // We cannot seek to the end to determine the raw partition size.
+ // Which makes it impossible to set `self.stream_position`.
+ return Err(io::Error::new(
+ io::ErrorKind::Other,
+ "SeekFrom::End is unsupported for SectorReader",
+ ));
+ }
+ SeekFrom::Current(n) => {
+ if n >= 0 {
+ self.stream_position.checked_add(n as u64)
+ } else {
+ self.stream_position.checked_sub(n.wrapping_neg() as u64)
+ }
+ }
+ };
+
+ match new_pos {
+ Some(n) => {
+ // We can only seek on sector boundaries, so align down the requested seek position and seek to that.
+ let aligned_n = self.align_down_to_sector_size(n);
+ self.inner.seek(SeekFrom::Start(aligned_n))?;
+
+ // Make the caller believe that we seeked to the actually requested position.
+ // Our `read` implementation will cover the difference.
+ self.stream_position = n;
+ Ok(self.stream_position)
+ }
+ None => Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "invalid seek to a negative or overflowing position",
+ )),
+ }
+ }
+}