use std::collections::{BTreeMap, HashMap};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use clap::Parser;
use fif::files::{mime_extension_lookup, scan_directory, scan_from_walkdir, BUF_SIZE};
use fif::findings::Findings;
use fif::formats::{Format, PowerShell, Shell};
use fif::mime_db::MimeDb;
use fif::utils::APPLICATION_ZIP;
use fif::{String, MIMEDB};
use itertools::Itertools;
use maplit::{btreeset, hashmap};
use mime::{Mime, APPLICATION_OCTET_STREAM, APPLICATION_PDF, IMAGE_JPEG, IMAGE_PNG};
use crate::parameters::ExtensionSet;
use crate::parameters::Parameters;
const JPEG_BYTES: &[u8] = b"\xFF\xD8\xFF";
const PNG_BYTES: &[u8] = b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
const PDF_BYTES: &[u8] = b"%PDF-";
const ZIP_BYTES: &[u8] = b"PK\x03\x04";
#[test]
fn get_ext() {
let ext_checks: HashMap<_, Option<&OsStr>> = hashmap![
Path::new("test.txt") => Some(OsStr::new("txt")),
Path::new("test.zip") => Some(OsStr::new("zip")),
Path::new("test.tar.gz") => Some(OsStr::new("gz")),
Path::new("test.") => Some(OsStr::new("")),
Path::new("test") => None,
Path::new(".hidden") => None,
];
for (path, ext) in ext_checks {
assert_eq!(path.extension(), ext);
}
}
#[test]
fn detect_type() {
assert_eq!(MIMEDB.get_type(JPEG_BYTES), Some(IMAGE_JPEG));
assert_eq!(MIMEDB.get_type(PNG_BYTES), Some(IMAGE_PNG));
assert_eq!(MIMEDB.get_type(PDF_BYTES), Some(APPLICATION_PDF));
assert_eq!(MIMEDB.get_type(ZIP_BYTES), Some(APPLICATION_ZIP.clone()));
}
#[test]
fn recommend_ext() {
let tests = hashmap![
&IMAGE_JPEG => "jpg",
&IMAGE_PNG => "png",
&APPLICATION_PDF => "pdf",
&*APPLICATION_ZIP => "zip",
];
for (mime, ext) in tests {
assert!(
mime_extension_lookup(mime.essence_str().into())
.unwrap()
.contains(&String::from(ext)),
"mime_extension_lookup for {} didn't contain {}!",
mime.essence_str(),
ext
);
}
}
#[test]
fn simple_directory() {
use std::borrow::Borrow;
use std::env::set_current_dir;
use std::fs::{canonicalize, File};
use std::io::Write;
use tempfile::tempdir;
use crate::parameters::ScanOpts;
let files = hashmap![
"test.jpg" => JPEG_BYTES,
"test.jpeg" => JPEG_BYTES,
"test.png" => PNG_BYTES,
"test.pdf" => PDF_BYTES,
"test.zip" => ZIP_BYTES,
"wrong.jpg" => PNG_BYTES,
"ignore.fake_ext" => ZIP_BYTES,
];
let dir = tempdir().expect("Failed to create temporary directory.");
set_current_dir(dir.path()).expect("Failed to change directory.");
for (name, bytes) in &files {
let mut file = File::create(dir.path().join(name)).unwrap_or_else(|_| panic!("Failed to create file: {name}"));
file
.write_all(bytes)
.unwrap_or_else(|_| panic!("Failed to write to file: {name}"));
drop(file);
}
let scan_opts = ScanOpts {
hidden: true,
extensionless: false,
follow_symlinks: false,
ignore_unknown_exts: true,
};
let entries = scan_directory(dir.path(), None, None, &scan_opts).expect("Directory scan failed.");
assert_eq!(entries.len(), files.len() - 1);
let use_threads = cfg!(feature = "multi-threaded");
let results = scan_from_walkdir(&entries, false, use_threads).0;
let canonical_results = scan_from_walkdir(&entries, true, use_threads).0;
assert_eq!(results.len(), canonical_results.len());
for (result, canonical_result) in results.iter().zip(canonical_results.iter()) {
assert_eq!(canonicalize(&result.file).unwrap(), canonical_result.file);
if !result.valid {
assert_eq!(result.file.as_path().extension().unwrap(), OsStr::new("jpg"));
assert_eq!(result.mime, IMAGE_PNG);
assert_eq!(&result.recommended_extension().unwrap(), &String::from("png"));
assert_eq!(result.recommended_path().unwrap().file_name(), Some(OsStr::new("wrong.png")));
continue;
}
assert!(mime_extension_lookup(result.mime.essence_str().into())
.unwrap()
.contains(&result.recommended_extension().unwrap()));
assert!(result
.recommended_path()
.unwrap()
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("test"));
let ext = result.file.as_path().extension().unwrap();
assert_eq!(
result.mime,
match ext.to_string_lossy().borrow() {
"jpg" | "jpeg" => IMAGE_JPEG,
"png" => IMAGE_PNG,
"pdf" => APPLICATION_PDF,
"zip" => APPLICATION_ZIP.clone(),
_ => APPLICATION_OCTET_STREAM, },
"Incorrect MIME type detected - got {:?} for a {:?} file",
result.mime,
ext
);
}
}
#[test]
fn argument_parsing() {
use crate::parameters::ScanOpts;
let args: Parameters = Parameters::parse_from(vec!["fif", "-f", "-E", "images"]);
assert!(
args
.extensions()
.expect("args.extensions() should be Some(_)!")
.contains(&"jpg"),
"args.extensions() should contain the `images` set!"
);
assert!(!args.scan_hidden);
assert!(args.exts.is_none());
assert!(args.excluded_extensions().is_none());
assert_eq!(
args.get_scan_opts(),
ScanOpts {
hidden: false,
extensionless: false,
follow_symlinks: true,
ignore_unknown_exts: false,
},
"ScanOpts are incorrect"
);
}
#[test]
fn positional_args() {
for flag in ["-x", "-e", "-X", "-E"] {
assert_eq!(
Parameters::parse_from(vec!["fif", flag, "images", "directory"]).dir,
PathBuf::from("directory")
);
}
}
#[test]
fn exclude_overrides() {
let args: Parameters = Parameters::parse_from(vec!["fif", "-x", "jpg,png", "-E", "images"]);
let extensions = args.extensions();
assert!(extensions.is_some(), "Extensions should contain the `images` set!");
let extensions = extensions.unwrap();
assert!(!extensions.contains(&"jpg"), "\"jpg\" should be excluded!");
assert!(!extensions.contains(&"png"), "\"png\" should be excluded!");
assert!(extensions.contains(&"jpeg"), "\"jpeg\" should be included!");
let args: Parameters = Parameters::parse_from(vec!["fif", "-e", "abc,def,ghi,jkl", "-x", "abc,def"]);
let extensions = args.extensions();
assert!(extensions.is_some(), "Extensions should be set!");
assert_eq!(extensions, Some(btreeset!["ghi", "jkl"]));
}
#[test]
fn exclude_set_overrides_includes() {
let args: Parameters = Parameters::parse_from(vec!["fif", "-e", "jpg,flac", "-X", "images"]);
let extensions = args.extensions();
assert!(extensions.is_some(), "Extensions should be set!");
assert_eq!(extensions, Some(btreeset!["flac"]));
}
#[test]
fn exclude_set_overrides_include_set() {
let args: Parameters = Parameters::parse_from(vec!["fif", "-E", "media", "-X", "images"]);
let extensions = args.extensions();
assert!(extensions.is_some(), "Extensions should be set!");
let extensions = extensions.unwrap();
for &ext in ExtensionSet::Audio
.extensions()
.iter()
.chain(ExtensionSet::Video.extensions().iter())
{
assert!(extensions.contains(&ext), "Extensions should contain {ext}!");
}
for ext in ExtensionSet::Images.extensions() {
assert!(!extensions.contains(&ext), "Extensions should not contain {ext}!");
}
}
#[test]
fn identify_random_bytes() {
use rand::prelude::*;
let mut rng = rand::rng();
let mut bytes: [u8; BUF_SIZE * 2] = [0; BUF_SIZE * 2];
let mut results: BTreeMap<Mime, i32> = BTreeMap::new();
for _ in 1..1000 {
rng.fill_bytes(&mut bytes);
if let Some(detected_type) = MIMEDB.get_type(&bytes) {
*results.entry(detected_type).or_insert(0) += 1;
}
}
for (mime, count) in &results {
println!("{mime}:\t{count} counts");
}
println!("No type found:\t{} counts", 1000 - results.values().sum::<i32>());
}
#[test]
fn outputs_move_commands() {
use std::io::Read;
let findings = vec![Findings {
file: Path::new("misnamed_file.png").to_path_buf(),
valid: false,
mime: IMAGE_JPEG,
}];
for format in &["Shell", "PowerShell"] {
let mut cursor = std::io::Cursor::new(Vec::new());
let mut contents = std::string::String::new();
match *format {
"Shell" => Shell.write_all(&mut cursor, &findings, &[]),
"PowerShell" => PowerShell.write_all(&mut cursor, &findings, &[]),
_ => unreachable!(),
}
.expect("Failed to write to cursor");
cursor.set_position(0);
cursor
.read_to_string(&mut contents)
.expect("Failed to read from cursor to string");
assert!(
contents.contains("misnamed_file.jpg") && contents.contains("misnamed_file.png"),
"{format} output doesn't contain move command!\n===\n{contents}"
);
}
}
#[test]
#[cfg(feature = "json")]
fn test_json() {
use std::io::Read;
use crate::formats::Json;
let findings = vec![Findings {
file: Path::new("misnamed_file.png").to_path_buf(),
valid: false,
mime: IMAGE_JPEG,
}];
let mut cursor = std::io::Cursor::new(Vec::new());
let mut contents = std::string::String::new();
Json
.write_all(&mut cursor, &findings, &[])
.expect("Failed to write to cursor");
cursor.set_position(0);
cursor
.read_to_string(&mut contents)
.expect("Failed to read from cursor to string");
assert!(
contents.contains(IMAGE_JPEG.essence_str()),
"JSON output doesn't contain move command!\n===\n{contents}"
);
}
#[test]
fn media_contains_audio_video_images() {
use crate::parameters::ExtensionSet::{Audio, Images, Media, Video};
let media_exts = Media.extensions();
[Audio.extensions(), Video.extensions(), Images.extensions()]
.concat()
.into_iter()
.for_each(|ext| assert!(media_exts.contains(&ext)));
assert_eq!(
Parameters::parse_from(["fif", "-E", "media"]).extensions(),
Parameters::parse_from(["fif", "-E", "audio,video,images"]).extensions()
);
}
#[test]
fn writables_is_correct() {
use fif::formats::Writable;
use fif::{writables, writablesln};
assert_eq!(
&["henlo".into(), Path::new("henlo").into(), Writable::Newline,],
writables!["henlo", (Path::new("henlo")), Newline]
);
assert_eq!(
&["henlo".into(), Path::new("henlo").into(), Writable::Newline, Writable::Newline],
writablesln!["henlo", (Path::new("henlo")), Newline]
);
}
#[test]
fn verbosity() {
use log::LevelFilter;
assert!(
Parameters::try_parse_from(["fif", "-q", "-v"]).is_err(),
"Failed to reject usage of both -q and -v!"
);
let expected_results = hashmap![
"-qqqqqqqq" => LevelFilter::Off,
"-qqq" => LevelFilter::Off,
"-qq" => LevelFilter::Error,
"-q" => LevelFilter::Warn,
"-s" => LevelFilter::Info,
"-v" => LevelFilter::Debug,
"-vv" => LevelFilter::Trace,
"-vvv" => LevelFilter::Trace,
"-vvvvvvvv" => LevelFilter::Trace,
];
for (flags, level) in expected_results {
assert_eq!(Parameters::parse_from(["fif", flags]).get_verbosity(), level);
}
}
#[test]
fn validate_os_name() {
assert_eq!(fif::utils::os_name().to_lowercase(), std::env::consts::OS.to_lowercase());
}
#[test]
fn sort_findings() {
let findings = [Findings {
file: Path::new("ccc").to_path_buf(),
valid: false,
mime: IMAGE_JPEG,
},
Findings {
file: Path::new("bbb.xyz").to_path_buf(),
valid: true,
mime: IMAGE_PNG,
},
Findings {
file: Path::new("aaa").to_path_buf(),
valid: true,
mime: APPLICATION_PDF,
}];
let mut findings = findings.iter().sorted_unstable();
assert_eq!(findings.next().unwrap().file, Path::new("aaa"));
assert_eq!(findings.next().unwrap().file, Path::new("bbb.xyz"));
assert_eq!(findings.next().unwrap().file, Path::new("ccc"));
assert_eq!(findings.next(), None);
}