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

github.com/windirstat/mft.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOmer Ben-Amram <omerbenamram@gmail.com>2019-06-03 16:06:28 +0300
committerOmer Ben-Amram <omerbenamram@gmail.com>2019-06-03 16:06:28 +0300
commitb4725823ce8a834fb4bc2f924d181f4efd12cd48 (patch)
tree3f81a7500b2bcbddddbbaf307962cb71f5d2edbe
parent809cc220afa8facd149196cd1d921541c2bca0c6 (diff)
overhauled CLI
-rw-r--r--Cargo.toml17
-rw-r--r--README.md67
-rw-r--r--build.rs3
-rw-r--r--src/attribute/mod.rs30
-rw-r--r--src/bin/mft_dump.rs376
-rw-r--r--tests/fixtures.rs27
-rw-r--r--tests/skeptic.rs1
-rw-r--r--tests/test_cli.rs62
-rw-r--r--tests/test_cli_interactive.rs82
9 files changed, 611 insertions, 54 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 3bc1afd..101d28a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,12 +27,29 @@ winstructs = "0.2.0"
lru = "0.1.15"
itertools = "0.8.0"
+# `mft_dump` dependencies
+simplelog = "0.5.3"
+dialoguer = "0.4.0"
+indoc = "0.3"
+
[dependencies.chrono]
version = "0.4.6"
features = ["serde"]
[dev-dependencies]
criterion = "0.2"
+skeptic = "0.13"
+assert_cmd = "0.11"
+predicates = "1"
+env_logger = "0.6.1"
+tempfile = "3"
+
+# rexpect relies on unix process semantics, but it's only used for process interaction tests.
+[target.'cfg(not(target_os = "windows"))'.dev-dependencies]
+rexpect = "0.3"
+
+[build-dependencies]
+skeptic = "0.13"
[[bin]]
name = "mft_dump"
diff --git a/README.md b/README.md
index f9df1e0..2a8428e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,70 @@
[![Build Status](https://dev.azure.com/benamram/DFIR/_apis/build/status/omerbenamram.mft?branchName=master)](https://dev.azure.com/benamram/DFIR/_build/latest?definitionId=5&branchName=master)
![crates.io](https://img.shields.io/crates/v/mft.svg)
-A work-in-progress cross platform MFT (master file table) parser.
+# MFT
+
+This is a parser for the MFT (master file table) format.
+Supported rust version is latest stable rust (minimum 1.34) or nightly.
-Inspired by the work at: https://github.com/forensicmatt/RustyMft, but is not API compatible.
+[Documentation](https://docs.rs/mft)
+
+Python bindings are available as well at https://github.com/omerbenamram/pymft-rs (and at PyPi https://pypi.org/project/mft/)
+
+## Features
+ - Implemented using 100% safe rust - and works on all platforms supported by rust (that have stdlib).
+ - Supports JSON and CSV outputs.
+ - Supports extracting resident data streams.
+
+## Installation (associated binary utility):
+ - Download latest executable release from https://github.com/omerbenamram/mft/releases
+ - Releases are automatically built for for Windows, macOS, and Linux. (64-bit executables only)
+ - Build from sources using `cargo install mft`
+
+# `mft_dump` (Binary utility):
+The main binary utility provided with this crate is `mft_dump`, and it provides a quick way to convert mft snapshots to different output formats.
+
+Some examples
+ - `mft_dump <evtx_file>` will dump contents of mft entries as JSON.
+ - `mft_dump -o csv <evtx_file>` will dump contents of mft entries as CSV.
+ - `mft_dump -e <output_directory> -o json <input_file>` will extract all resident streams in MFT to files.
+
+# Library usage:
+```rust
+use mft::MftParser;
+use mft::attribute::MftAttributeContent;
+use std::path::PathBuf;
+
+fn main() {
+ // Change this to a path of your .evtx sample.
+ let fp = PathBuf::from(format!("{}/samples/MFT", std::env::var("CARGO_MANIFEST_DIR").unwrap()));
+
+ let mut parser = MftParser::from_path(fp).unwrap();
+ for entry in parser.iter_entries() {
+ match entry {
+ Ok(e) => {
+ for attribute in e.iter_attributes().filter_map(|attr| attr.ok()) {
+ match attribute.data {
+ MftAttributeContent::AttrX10(standard_info) => {
+ println!("\tX10 attribute: {:#?}", standard_info)
+ },
+ MftAttributeContent::AttrX30(filename_attribute) => {
+ println!("\tX30 attribute: {:#?}", filename_attribute)
+ },
+ _ => {
+ println!("\tSome other attribute: {:#?}", attribute)
+ }
+ }
+
+ }
+ }
+ Err(err) => eprintln!("{}", err),
+ }
+ }
+}
+```
+
+## Thanks/Resources:
+ - https://docs.microsoft.com/en-us/windows/desktop/DevNotes/master-file-table
+ - https://github.com/libyal/libfsntfs/blob/master/documentation/New%20Technologies%20File%20System%20(NTFS).asciidoc
+ - https://github.com/forensicmatt/RustyMft
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..64b479b
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ skeptic::generate_doc_tests(&["README.md"]);
+}
diff --git a/src/attribute/mod.rs b/src/attribute/mod.rs
index c397a2e..e50f21c 100644
--- a/src/attribute/mod.rs
+++ b/src/attribute/mod.rs
@@ -67,6 +67,36 @@ impl MftAttributeContent {
}
}
+ /// Converts the given attributes into a `IndexRootAttr`, consuming the object attribute object.
+ pub fn into_index_root(self) -> Option<IndexRootAttr> {
+ match self {
+ MftAttributeContent::AttrX90(content) => Some(content),
+ _ => None,
+ }
+ }
+ /// Converts the given attributes into a `ObjectIdAttr`, consuming the object attribute object.
+ pub fn into_object_id(self) -> Option<ObjectIdAttr> {
+ match self {
+ MftAttributeContent::AttrX40(content) => Some(content),
+ _ => None,
+ }
+ }
+ /// Converts the given attributes into a `StandardInfoAttr`, consuming the object attribute object.
+ pub fn into_standard_info(self) -> Option<StandardInfoAttr> {
+ match self {
+ MftAttributeContent::AttrX10(content) => Some(content),
+ _ => None,
+ }
+ }
+ /// Converts the given attributes into a `DataAttr`, consuming the object attribute object.
+ pub fn into_data(self) -> Option<DataAttr> {
+ match self {
+ MftAttributeContent::AttrX80(content) => Some(content),
+ _ => None,
+ }
+ }
+
+ /// Converts the given attributes into a `FileNameAttr`, consuming the object attribute object.
pub fn into_file_name(self) -> Option<FileNameAttr> {
match self {
MftAttributeContent::AttrX30(content) => Some(content),
diff --git a/src/bin/mft_dump.rs b/src/bin/mft_dump.rs
index fdb1f26..aff2cb0 100644
--- a/src/bin/mft_dump.rs
+++ b/src/bin/mft_dump.rs
@@ -1,17 +1,34 @@
use clap::{App, Arg, ArgMatches};
use env_logger;
-use log::info;
+use indoc::indoc;
+use log::{info, Level};
+use mft::attribute::{MftAttributeContent, MftAttributeType};
use mft::mft::MftParser;
use mft::{MftEntry, ReadSeek};
+use dialoguer::Confirmation;
use mft::csv::FlatMftEntryWithName;
-use std::io;
+
+use snafu::{Backtrace, ErrorCompat, Snafu};
+use std::fs::File;
use std::io::Write;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
+use std::process::exit;
+
+use std::{fs, io, path};
+
+/// Simple error macro for use inside of internal errors in `EvtxDump`
+macro_rules! err {
+ ($($tt:tt)*) => { Err(Box::<dyn std::error::Error>::from(format!($($tt)*))) }
+}
+
+type StdErr = Box<dyn std::error::Error>;
+#[derive(Debug, PartialOrd, PartialEq)]
enum OutputFormat {
JSON,
+ JSONL,
CSV,
}
@@ -19,6 +36,7 @@ impl OutputFormat {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"json" => Some(OutputFormat::JSON),
+ "jsonl" => Some(OutputFormat::JSONL),
"csv" => Some(OutputFormat::CSV),
_ => None,
}
@@ -27,87 +45,295 @@ impl OutputFormat {
struct MftDump {
filepath: PathBuf,
- indent: bool,
+ // We use an option here to be able to move the output out of mftdump from a mutable reference.
+ output: Option<Box<dyn Write>>,
+ data_streams_output: Option<PathBuf>,
+ verbosity_level: Option<Level>,
output_format: OutputFormat,
+ backtraces: bool,
}
impl MftDump {
- pub fn from_cli_matches(matches: &ArgMatches) -> Self {
- MftDump {
+ pub fn from_cli_matches(matches: &ArgMatches) -> Result<Self, StdErr> {
+ let output_format =
+ OutputFormat::from_str(matches.value_of("output-format").unwrap_or_default())
+ .expect("Validated with clap default values");
+
+ let backtraces = matches.is_present("backtraces");
+
+ let output: Option<Box<dyn Write>> = if let Some(path) = matches.value_of("output-target") {
+ match Self::create_output_file(path, !matches.is_present("no-confirm-overwrite")) {
+ Ok(f) => Some(Box::new(f)),
+ Err(e) => {
+ return err!(
+ "An error occurred while creating output file at `{}` - `{}`",
+ path,
+ e
+ );
+ }
+ }
+ } else {
+ Some(Box::new(io::stdout()))
+ };
+
+ let data_streams_output = if let Some(path) = matches.value_of("data-streams-target") {
+ let path = PathBuf::from(path);
+ Self::create_output_dir(&path)?;
+ Some(path)
+ } else {
+ None
+ };
+
+ let verbosity_level = match matches.occurrences_of("verbose") {
+ 0 => None,
+ 1 => Some(Level::Info),
+ 2 => Some(Level::Debug),
+ 3 => Some(Level::Trace),
+ _ => {
+ eprintln!("using more than -vvv does not affect verbosity level");
+ Some(Level::Trace)
+ }
+ };
+
+ Ok(MftDump {
filepath: PathBuf::from(matches.value_of("INPUT").expect("Required argument")),
- indent: !matches.is_present("no-indent"),
- output_format: OutputFormat::from_str(
- matches.value_of("output-format").unwrap_or_default(),
- )
- .expect("Validated with clap default values"),
- }
+ output,
+ data_streams_output,
+ verbosity_level,
+ output_format,
+ backtraces,
+ })
}
- pub fn print_json_entry(&self, entry: &MftEntry) {
- let json_str = if self.indent {
- serde_json::to_string_pretty(&entry).expect("It should be valid UTF-8")
+ fn create_output_dir(path: impl AsRef<Path>) -> Result<(), StdErr> {
+ let p = path.as_ref();
+
+ if p.exists() {
+ if !p.is_dir() {
+ return err!("There is a file at {}, refusing to overwrite", p.display());
+ }
+ // p exists and is a directory, it's ok to add files.
} else {
- serde_json::to_string(&entry).expect("It should be valid UTF-8")
- };
+ fs::create_dir_all(path)?
+ }
- println!("{}", json_str);
+ Ok(())
}
- pub fn print_csv_entry<W: Write>(
- &self,
- entry: &MftEntry,
- parser: &mut MftParser<impl ReadSeek>,
- writer: &mut csv::Writer<W>,
- ) {
- let flat_entry = FlatMftEntryWithName::from_entry(&entry, parser);
+ /// If `prompt` is passed, will display a confirmation prompt before overwriting files.
+ fn create_output_file(
+ path: impl AsRef<Path>,
+ prompt: bool,
+ ) -> Result<File, Box<dyn std::error::Error>> {
+ let p = path.as_ref();
- writer.serialize(flat_entry).expect("Writing to CSV failed");
+ if p.is_dir() {
+ return err!(
+ "There is a directory at {}, refusing to overwrite",
+ p.display()
+ );
+ }
+
+ if p.exists() {
+ if prompt {
+ match Confirmation::new()
+ .with_text(&format!(
+ "Are you sure you want to override output file at {}",
+ p.display()
+ ))
+ .default(false)
+ .interact()
+ {
+ Ok(true) => Ok(File::create(p)?),
+ Ok(false) => err!("Cancelled"),
+ Err(e) => err!(
+ "Failed to write confirmation prompt to term caused by\n{}",
+ e
+ ),
+ }
+ } else {
+ Ok(File::create(p)?)
+ }
+ } else {
+ // Ok to assume p is not an existing directory
+ match p.parent() {
+ Some(parent) =>
+ // Parent exist
+ {
+ if parent.exists() {
+ Ok(File::create(p)?)
+ } else {
+ fs::create_dir_all(parent)?;
+ Ok(File::create(p)?)
+ }
+ }
+ None => err!("Output file cannot be root."),
+ }
+ }
}
- pub fn parse_file(&self) {
- info!("Opening file {:?}", &self.filepath);
- let mut mft_handler = match MftParser::from_path(&self.filepath) {
- Ok(mft_handler) => mft_handler,
- Err(error) => {
- eprintln!(
- "Failed to parse {:?}, failed with: [{}]",
- &self.filepath, error
- );
- std::process::exit(-1);
+ /// Main entry point for `EvtxDump`
+ pub fn run(&mut self) -> Result<(), StdErr> {
+ self.try_to_initialize_logging();
+
+ let mut parser = match MftParser::from_path(&self.filepath) {
+ Ok(parser) => parser,
+ Err(e) => {
+ return err!(
+ "Failed to open file {}.\n\tcaused by: {}",
+ self.filepath.display(),
+ &e
+ )
}
};
+ // Since the JSON parser can do away with a &mut Write, but the csv parser needs ownership
+ // of `Write`, we eagerly create the csv writer here, moving the Box<Write> out from
+ // `Mftdump` and replacing it with None placeholder.
let mut csv_writer = match self.output_format {
- OutputFormat::CSV => Some(csv::Writer::from_writer(io::stdout())),
+ OutputFormat::CSV => {
+ Some(csv::Writer::from_writer(self.output.take().expect(
+ "There can only be one flow accessing the output at a time",
+ )))
+ }
_ => None,
};
- let number_of_entries = mft_handler.get_entry_count();
+ let number_of_entries = parser.get_entry_count();
for i in 0..number_of_entries {
- let entry = mft_handler.get_entry(i);
+ let entry = parser.get_entry(i);
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
- eprintln!("Failed to parse MFT entry {}, failed with: [{}]", i, error);
+ eprintln!("{}", error);
+
+ if self.backtraces {
+ if let Some(bt) = error.backtrace() {
+ eprintln!("{}", bt);
+ }
+ }
continue;
}
};
+ if let Some(data_streams_dir) = &self.data_streams_output {
+ if let Ok(Some(path)) = parser.get_full_path_for_entry(&entry) {
+ let sanitized_path = sanitized(&path.to_string_lossy().to_string());
+
+ for (i, (name, stream)) in entry
+ .iter_attributes()
+ .filter_map(|a| a.ok())
+ .filter_map(|a| {
+ if a.header.type_code == MftAttributeType::DATA {
+ // resident
+ let name = a.header.name.clone();
+ if let Some(data) = a.data.into_data() {
+ Some((name, data))
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ })
+ .enumerate()
+ {
+ let orig_path_component: String = data_streams_dir
+ .join(&sanitized_path)
+ .to_string_lossy()
+ .to_string()
+ .chars()
+ .take(150)
+ .collect();
+
+ let data_stream_path = format!(
+ "{parent_name}__{stream_number}_{stream_name}.dontrun",
+ parent_name = orig_path_component,
+ stream_number = i,
+ stream_name = name
+ );
+
+ let mut f = File::create(&data_stream_path)?;
+ f.write_all(stream.data())?;
+ }
+ }
+ }
+
match self.output_format {
- OutputFormat::JSON => self.print_json_entry(&entry),
+ OutputFormat::JSON | OutputFormat::JSONL => self.print_json_entry(&entry)?,
OutputFormat::CSV => self.print_csv_entry(
&entry,
- &mut mft_handler,
+ &mut parser,
csv_writer
.as_mut()
.expect("CSV Writer is for OutputFormat::CSV"),
- ),
+ )?,
}
}
+
+ Ok(())
+ }
+
+ fn try_to_initialize_logging(&self) {
+ if let Some(level) = self.verbosity_level {
+ match simplelog::WriteLogger::init(
+ level.to_level_filter(),
+ simplelog::Config::default(),
+ io::stderr(),
+ ) {
+ Ok(_) => {}
+ Err(e) => eprintln!("Failed to initialize logging: {:?}", e),
+ };
+ }
+ }
+
+ pub fn print_json_entry(&mut self, entry: &MftEntry) -> Result<(), Box<dyn std::error::Error>> {
+ let out = self
+ .output
+ .as_mut()
+ .expect("CSV Flow cannot occur, so `Mftdump` should still Own `output`");
+
+ let json_str = if self.output_format == OutputFormat::JSON {
+ serde_json::to_vec_pretty(&entry).expect("It should be valid UTF-8")
+ } else {
+ serde_json::to_vec(&entry).expect("It should be valid UTF-8")
+ };
+
+ out.write_all(&json_str)?;
+ out.write_all(b"\n")?;
+
+ Ok(())
+ }
+
+ pub fn print_csv_entry<W: Write>(
+ &self,
+ entry: &MftEntry,
+ parser: &mut MftParser<impl ReadSeek>,
+ writer: &mut csv::Writer<W>,
+ ) -> Result<(), Box<dyn std::error::Error>> {
+ let flat_entry = FlatMftEntryWithName::from_entry(&entry, parser);
+
+ writer.serialize(flat_entry)?;
+
+ Ok(())
}
}
+// adapter from python version
+// https://github.com/pallets/werkzeug/blob/9394af646038abf8b59d6f866a1ea5189f6d46b8/src/werkzeug/utils.py#L414
+pub fn sanitized(component: &str) -> String {
+ let mut buf = String::with_capacity(component.len());
+ for c in component.chars() {
+ match c {
+ _sep if path::is_separator(c) => buf.push('_'),
+ _ => buf.push(c),
+ }
+ }
+
+ buf
+}
+
fn main() {
env_logger::init();
@@ -117,22 +343,68 @@ fn main() {
.about("Utility for parsing MFT snapshots")
.arg(Arg::with_name("INPUT").required(true))
.arg(
- Arg::with_name("no-indent")
- .long("--no-indent")
- .takes_value(false)
- .help("When set, output will not be indented (works only with JSON output)."),
- )
- .arg(
Arg::with_name("output-format")
.short("-o")
.long("--output-format")
.takes_value(true)
- .possible_values(&["csv", "json"])
+ .possible_values(&["csv", "json", "jsonl"])
.default_value("json")
.help("Output format."),
)
+ .arg(
+ Arg::with_name("output-target")
+ .long("--output")
+ .short("-f")
+ .takes_value(true)
+ .help(indoc!("Writes output to the file specified instead of stdout, errors will still be printed to stderr.
+ Will ask for confirmation before overwriting files, to allow overwriting, pass `--no-confirm-overwrite`
+ Will create parent directories if needed.")),
+ )
+ .arg(
+ Arg::with_name("data-streams-target")
+ .long("--extract-resident-streams")
+ .short("-e")
+ .takes_value(true)
+ .help(indoc!("Writes resident data streams to the given directory, resident streams will be named\
+ <original_file_path>_<stream_number>/<stream_name>.norun")),
+ )
+ .arg(
+ Arg::with_name("no-confirm-overwrite")
+ .long("--no-confirm-overwrite")
+ .takes_value(false)
+ .help(indoc!("When set, will not ask for confirmation before overwriting files, useful for automation")),
+ )
+ .arg(Arg::with_name("verbose")
+ .short("-v")
+ .multiple(true)
+ .takes_value(false)
+ .help(indoc!(r#"
+ Sets debug prints level for the application:
+ -v - info
+ -vv - debug
+ -vvv - trace
+ NOTE: trace output is only available in debug builds, as it is extremely verbose."#))
+ )
+ .arg(
+ Arg::with_name("backtraces")
+ .long("--backtraces")
+ .takes_value(false)
+ .help("If set, a backtrace will be printed with some errors if available"))
.get_matches();
- let app = MftDump::from_cli_matches(&matches);
- app.parse_file();
+ let mut app = match MftDump::from_cli_matches(&matches) {
+ Ok(app) => app,
+ Err(e) => {
+ eprintln!("An error occurred while setting up the app: {}", &e);
+ exit(1);
+ }
+ };
+
+ match app.run() {
+ Ok(()) => {}
+ Err(e) => {
+ eprintln!("A runtime error has occurred {}", &e);
+ exit(1);
+ }
+ };
}
diff --git a/tests/fixtures.rs b/tests/fixtures.rs
new file mode 100644
index 0000000..17dfed6
--- /dev/null
+++ b/tests/fixtures.rs
@@ -0,0 +1,27 @@
+#![allow(dead_code)]
+use std::path::PathBuf;
+
+use std::sync::{Once, ONCE_INIT};
+
+static LOGGER_INIT: Once = ONCE_INIT;
+
+// Rust runs the tests concurrently, so unless we synchronize logging access
+// it will crash when attempting to run `cargo test` with some logging facilities.
+pub fn ensure_env_logger_initialized() {
+ LOGGER_INIT.call_once(env_logger::init);
+}
+
+pub fn samples_dir() -> PathBuf {
+ PathBuf::from(file!())
+ .parent()
+ .unwrap()
+ .parent()
+ .unwrap()
+ .join("samples")
+ .canonicalize()
+ .unwrap()
+}
+
+pub fn mft_sample() -> PathBuf {
+ samples_dir().join("MFT")
+}
diff --git a/tests/skeptic.rs b/tests/skeptic.rs
new file mode 100644
index 0000000..ff46c9c
--- /dev/null
+++ b/tests/skeptic.rs
@@ -0,0 +1 @@
+include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs"));
diff --git a/tests/test_cli.rs b/tests/test_cli.rs
new file mode 100644
index 0000000..890b055
--- /dev/null
+++ b/tests/test_cli.rs
@@ -0,0 +1,62 @@
+mod fixtures;
+
+use fixtures::*;
+
+use assert_cmd::prelude::*;
+use std::fs;
+use std::fs::File;
+use std::io::{Read, Write};
+use std::process::Command;
+use tempfile::tempdir;
+
+#[test]
+fn it_respects_directory_output() {
+ let d = tempdir().unwrap();
+ let f = d.as_ref().join("test.out");
+
+ let sample = mft_sample();
+
+ let mut cmd = Command::cargo_bin("mft_dump").expect("failed to find binary");
+ cmd.args(&["-f", &f.to_string_lossy(), sample.to_str().unwrap()]);
+
+ assert!(
+ cmd.output().unwrap().stdout.is_empty(),
+ "Expected output to be printed to file, but was printed to stdout"
+ );
+
+ let mut expected = vec![];
+
+ File::open(&f).unwrap().read_to_end(&mut expected).unwrap();
+ assert!(
+ !expected.is_empty(),
+ "Expected output to be printed to file"
+ )
+}
+
+#[test]
+fn test_it_refuses_to_overwrite_directory() {
+ let d = tempdir().unwrap();
+
+ let sample = mft_sample();
+ let mut cmd = Command::cargo_bin("mft_dump").expect("failed to find binary");
+ cmd.args(&["-f", &d.path().to_string_lossy(), sample.to_str().unwrap()]);
+
+ cmd.assert().failure().code(1);
+}
+
+#[test]
+fn test_it_exports_resident_streams() {
+ let d = tempdir().unwrap();
+
+ let sample = mft_sample();
+ let mut cmd = Command::cargo_bin("mft_dump").expect("failed to find binary");
+ cmd.args(&[
+ "-e",
+ &d.path().to_string_lossy().to_string(),
+ &sample.to_string_lossy().to_string(),
+ ]);
+
+ cmd.assert().success();
+
+ assert_eq!(fs::read_dir(d.path()).unwrap().count(), 1108)
+}
diff --git a/tests/test_cli_interactive.rs b/tests/test_cli_interactive.rs
new file mode 100644
index 0000000..5f7111f
--- /dev/null
+++ b/tests/test_cli_interactive.rs
@@ -0,0 +1,82 @@
+// The interactive tests are in a separate file,
+// since they use `rexpect`, which internally uses quirky fork semantics to open a pty.
+// They will fail if tried to be executed concurrently any other CLI test.
+
+mod fixtures;
+
+use fixtures::*;
+
+use std::fs::File;
+use std::io::{Read, Write};
+use tempfile::tempdir;
+
+use assert_cmd::cargo::cargo_bin;
+#[cfg(not(target_os = "windows"))]
+use rexpect::spawn;
+
+// It should behave the same on windows, but interactive testing relies on unix pty internals.
+#[test]
+#[cfg(not(target_os = "windows"))]
+fn test_it_confirms_before_overwriting_a_file() {
+ let d = tempdir().unwrap();
+ let f = d.as_ref().join("test.out");
+
+ let mut file = File::create(&f).unwrap();
+ file.write_all(b"I'm a file!").unwrap();
+
+ let sample = mft_sample();
+
+ let cmd_string = format!(
+ "{bin} -f {output_file} {sample}",
+ bin = cargo_bin("mft_dump").display(),
+ output_file = f.to_string_lossy(),
+ sample = sample.to_str().unwrap()
+ );
+
+ let mut p = spawn(&cmd_string, Some(10000)).unwrap();
+ p.exp_regex(r#"Are you sure you want to override.*"#)
+ .unwrap();
+ p.send_line("y").unwrap();
+ p.exp_eof().unwrap();
+
+ let mut expected = vec![];
+
+ File::open(&f).unwrap().read_to_end(&mut expected).unwrap();
+ assert!(
+ !expected.len() > 100,
+ "Expected output to be printed to file"
+ )
+}
+
+#[test]
+#[cfg(not(target_os = "windows"))]
+fn test_it_confirms_before_overwriting_a_file_and_quits() {
+ let d = tempdir().unwrap();
+ let f = d.as_ref().join("test.out");
+
+ let mut file = File::create(&f).unwrap();
+ file.write_all(b"I'm a file!").unwrap();
+
+ let sample = mft_sample();
+
+ let cmd_string = format!(
+ "{bin} -f {output_file} {sample}",
+ bin = cargo_bin("mft_dump").display(),
+ output_file = f.to_string_lossy(),
+ sample = sample.to_str().unwrap()
+ );
+
+ let mut p = spawn(&cmd_string, Some(10000)).unwrap();
+ p.exp_regex(r#"Are you sure you want to override.*"#)
+ .unwrap();
+ p.send_line("n").unwrap();
+ p.exp_eof().unwrap();
+
+ let mut expected = vec![];
+
+ File::open(&f).unwrap().read_to_end(&mut expected).unwrap();
+ assert!(
+ !expected.len() > 100,
+ "Expected output to be printed to file"
+ )
+}