Compare commits
No commits in common. "64efb0de2716681e1bb35d4e6fac8af5ebf97142" and "80f14085cf42c15bcb513ff69c325e22350da6b8" have entirely different histories.
64efb0de27
...
80f14085cf
11 changed files with 115 additions and 258 deletions
|
@ -39,13 +39,13 @@ stages:
|
||||||
- .gitlab-ci.yml
|
- .gitlab-ci.yml
|
||||||
|
|
||||||
script:
|
script:
|
||||||
cargo build --no-default-features --locked --features="json $FEATURES"
|
cargo build --no-default-features --locked --features="$FEATURES"
|
||||||
|
|
||||||
.cargo-test:
|
.cargo-test:
|
||||||
extends: .cargo-build
|
extends: .cargo-build
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
cargo test --no-default-features --locked --verbose --features="json $FEATURES"
|
cargo test --no-default-features --locked --verbose --features="$FEATURES"
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
stage: lint
|
stage: lint
|
||||||
|
|
|
@ -2,10 +2,6 @@
|
||||||
Dates are given in YYYY-MM-DD format.
|
Dates are given in YYYY-MM-DD format.
|
||||||
|
|
||||||
## v0.3
|
## v0.3
|
||||||
### v0.3.1 (2021-xx-yy)
|
|
||||||
#### Features
|
|
||||||
- Added JSON output support via `-o json`
|
|
||||||
|
|
||||||
### v0.3.0 (2021-04-28)
|
### v0.3.0 (2021-04-28)
|
||||||
#### Features
|
#### Features
|
||||||
- Added `-x`/`--exclude` flag for excluding file extensions (overrides `-e` or `-E` - `-E images -x jpg` scans all image
|
- Added `-x`/`--exclude` flag for excluding file extensions (overrides `-e` or `-E` - `-E images -x jpg` scans all image
|
||||||
|
|
83
Cargo.lock
generated
83
Cargo.lock
generated
|
@ -116,9 +116,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-epoch"
|
name = "crossbeam-epoch"
|
||||||
version = "0.9.4"
|
version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94"
|
checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
|
@ -129,9 +129,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.4"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278"
|
checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
@ -182,6 +182,15 @@ version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193"
|
checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77b705829d1e87f762c2df6da140b26af5839e1033aa84aa5f56bb688e4e1bdb"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fif"
|
name = "fif"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -191,15 +200,13 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"exitcode",
|
"exitcode",
|
||||||
|
"fastrand",
|
||||||
"infer",
|
"infer",
|
||||||
"itertools",
|
"itertools",
|
||||||
"log",
|
"log",
|
||||||
"new_mime_guess",
|
"new_mime_guess",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand",
|
|
||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"smartstring",
|
"smartstring",
|
||||||
"snailquote",
|
"snailquote",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
@ -267,6 +274,15 @@ dependencies = [
|
||||||
"cfb",
|
"cfb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
@ -276,12 +292,6 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "0.4.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -318,9 +328,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.4.0"
|
version = "2.3.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
|
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memoffset"
|
name = "memoffset"
|
||||||
|
@ -495,9 +505,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.8"
|
version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
|
checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
@ -542,37 +552,6 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.125"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.125"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_json"
|
|
||||||
version = "1.0.64"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
|
|
||||||
dependencies = [
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smartstring"
|
name = "smartstring"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
@ -600,9 +579,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.72"
|
version = "1.0.71"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
|
checksum = "ad184cc9470f9117b2ac6817bfe297307418819ba40552f9b3846f05c33d5373"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -695,9 +674,9 @@ checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.2"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode_categories"
|
name = "unicode_categories"
|
||||||
|
|
|
@ -18,11 +18,10 @@ exclude = [".idea/", "*.toml", "!Cargo.toml", "*.sh", "*.py", "*.yml", "*.md", "
|
||||||
maintenance = { status = "experimental" }
|
maintenance = { status = "experimental" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["multi-threaded", "json"]
|
default = ["multi-threaded"]
|
||||||
multi-threaded = ["rayon"]
|
multi-threaded = ["rayon"]
|
||||||
infer-backend = []
|
infer-backend = []
|
||||||
xdg-mime-backend = []
|
xdg-mime-backend = []
|
||||||
json = ["serde", "serde_json"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
|
@ -35,8 +34,6 @@ rayon = { version = "1.5.0", optional = true }
|
||||||
exitcode = "1.1.2"
|
exitcode = "1.1.2"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
itertools = "0.10.0"
|
itertools = "0.10.0"
|
||||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
|
||||||
serde_json = { version = "1.0", optional = true }
|
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
xdg-mime = "0.3.3"
|
xdg-mime = "0.3.3"
|
||||||
|
@ -60,7 +57,7 @@ default-features = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.2.0"
|
tempfile = "3.2.0"
|
||||||
rand = "0.8.3"
|
fastrand = "1.4.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
max_width = 120
|
max_width = 120
|
||||||
fn_single_line = true
|
|
||||||
hard_tabs = true
|
hard_tabs = true
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
newline_style = "Unix"
|
newline_style = "Unix"
|
|
@ -5,10 +5,6 @@ use mime_guess::Mime;
|
||||||
use crate::inspectors::mime_extension_lookup;
|
use crate::inspectors::mime_extension_lookup;
|
||||||
use crate::string_type::String;
|
use crate::string_type::String;
|
||||||
|
|
||||||
#[cfg(feature = "json")]
|
|
||||||
use serde::{ser::SerializeStruct, Serializer};
|
|
||||||
use std::fmt::{Display, Formatter};
|
|
||||||
|
|
||||||
/// Information about a scanned file.
|
/// Information about a scanned file.
|
||||||
#[derive(Ord, PartialOrd, Eq, PartialEq)]
|
#[derive(Ord, PartialOrd, Eq, PartialEq)]
|
||||||
pub struct Findings<'a> {
|
pub struct Findings<'a> {
|
||||||
|
@ -20,50 +16,8 @@ pub struct Findings<'a> {
|
||||||
pub mime: Mime,
|
pub mime: Mime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "json")]
|
|
||||||
impl<'a> serde::Serialize for Findings<'a> {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
// the second parameter is the number of fields in the struct -- in this case, 3
|
|
||||||
let mut state = serializer.serialize_struct("Findings", 3)?;
|
|
||||||
|
|
||||||
state.serialize_field("file", &self.file)?;
|
|
||||||
state.serialize_field("valid", &self.valid)?;
|
|
||||||
state.serialize_field("mime", &self.mime.essence_str())?;
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Findings<'a> {
|
impl<'a> Findings<'a> {
|
||||||
pub fn recommended_extension(&self) -> Option<String> {
|
pub fn recommended_extension(&self) -> Option<String> {
|
||||||
mime_extension_lookup(self.mime.clone()).map(|extensions| extensions[0].clone())
|
mime_extension_lookup(self.mime.clone()).map(|extensions| extensions[0].clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq)]
|
|
||||||
#[cfg_attr(feature = "json", derive(serde::Serialize))]
|
|
||||||
#[cfg_attr(feature = "json", serde(tag = "type", content = "path"))]
|
|
||||||
pub enum ScanError<'a> {
|
|
||||||
/// Something went wrong while trying to read the given file.
|
|
||||||
File(&'a Path),
|
|
||||||
/// Failed to determine the mimetype of the given file.
|
|
||||||
Mime(&'a Path),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Display for ScanError<'a> {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Couldn't {} file: {}",
|
|
||||||
match self {
|
|
||||||
Self::File(_) => "read",
|
|
||||||
Self::Mime(_) => "determine mime type of",
|
|
||||||
},
|
|
||||||
match self {
|
|
||||||
Self::File(f) | Self::Mime(f) => f.to_string_lossy(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ use std::path::Path;
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use snailquote::escape;
|
use snailquote::escape;
|
||||||
|
|
||||||
use crate::findings::ScanError;
|
use crate::scan_error::ScanError;
|
||||||
use crate::{Findings, BACKEND};
|
use crate::{Findings, BACKEND};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
@ -65,18 +65,26 @@ pub enum Writable<'a> {
|
||||||
|
|
||||||
// the lifetime of a lifetime
|
// the lifetime of a lifetime
|
||||||
impl<'a> From<&'a str> for Writable<'a> {
|
impl<'a> From<&'a str> for Writable<'a> {
|
||||||
fn from(s: &'a str) -> Writable<'a> { Writable::String(s) }
|
fn from(s: &'a str) -> Writable<'a> {
|
||||||
|
Writable::String(s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a Path> for Writable<'a> {
|
impl<'a> From<&'a Path> for Writable<'a> {
|
||||||
fn from(p: &'a Path) -> Writable<'a> { Writable::Path(p) }
|
fn from(p: &'a Path) -> Writable<'a> {
|
||||||
|
Writable::Path(p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a OsStr> for Writable<'a> {
|
impl<'a> From<&'a OsStr> for Writable<'a> {
|
||||||
fn from(p: &'a OsStr) -> Writable<'a> { Writable::Path(p.as_ref()) }
|
fn from(p: &'a OsStr) -> Writable<'a> {
|
||||||
|
Writable::Path(p.as_ref())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generated_by() -> String { format!("Generated by fif {} ({} backend)", VERSION.unwrap_or("???"), BACKEND) }
|
fn generated_by() -> String {
|
||||||
|
format!("Generated by fif {} ({} backend)", VERSION.unwrap_or("???"), BACKEND)
|
||||||
|
}
|
||||||
|
|
||||||
fn smart_write<W: Write>(f: &mut W, writeables: &[Writable]) -> io::Result<()> {
|
fn smart_write<W: Write>(f: &mut W, writeables: &[Writable]) -> io::Result<()> {
|
||||||
// ehhhh
|
// ehhhh
|
||||||
|
@ -94,9 +102,9 @@ fn smart_write<W: Write>(f: &mut W, writeables: &[Writable]) -> io::Result<()> {
|
||||||
}
|
}
|
||||||
Writable::String(s) => write!(f, "{}", s)?,
|
Writable::String(s) => write!(f, "{}", s)?,
|
||||||
Writable::Path(path) => {
|
Writable::Path(path) => {
|
||||||
if let Some(path_str) = path.to_str() {
|
if let Some(string) = path.to_str() {
|
||||||
let escaped = escape(path_str);
|
let escaped = escape(string);
|
||||||
if escaped.as_ref() == path_str {
|
if escaped == string {
|
||||||
// the escaped string is the same as the input - this will occur for inputs like "file.txt" which don't
|
// the escaped string is the same as the input - this will occur for inputs like "file.txt" which don't
|
||||||
// need to be escaped. however, it's Best Practice™ to escape such strings anyway, so we prefix/suffix the
|
// need to be escaped. however, it's Best Practice™ to escape such strings anyway, so we prefix/suffix the
|
||||||
// escaped string with single quotes.
|
// escaped string with single quotes.
|
||||||
|
@ -124,21 +132,14 @@ fn smart_write<W: Write>(f: &mut W, writeables: &[Writable]) -> io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this might need a restructure.
|
|
||||||
// it would be nice if i didn't have to write a case for every OutputFormat variant that looked like
|
|
||||||
// OutputFormat::PowerShell => PowerShell::new().write_all(...)
|
|
||||||
// also, JSON's implementation differs vastly from PowerShell and Shell's implementations. Maybe they shouldn't be
|
|
||||||
// treated as implementing the same trait, since in that case, the format trait is more of a concept rather than an
|
|
||||||
// actual definition of behaviour.
|
|
||||||
// structuring code is *hard*
|
|
||||||
pub trait Format {
|
pub trait Format {
|
||||||
fn new() -> Self;
|
fn new() -> Self;
|
||||||
fn rename<W: Write>(&self, _f: &mut W, _from: &Path, _to: &Path) -> io::Result<()> { unreachable!() }
|
fn rename<W: Write>(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()>;
|
||||||
fn no_known_extension<W: Write>(&self, _f: &mut W, _path: &Path) -> io::Result<()> { unreachable!() }
|
fn no_known_extension<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()>;
|
||||||
fn unreadable<W: Write>(&self, _f: &mut W, _path: &Path) -> io::Result<()> { unreachable!() }
|
fn unreadable<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()>;
|
||||||
fn unknown_type<W: Write>(&self, _f: &mut W, _path: &Path) -> io::Result<()> { unreachable!() }
|
fn unknown_type<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()>;
|
||||||
fn header<W: Write>(&self, _f: &mut W, _entries: &Entries) -> io::Result<()> { unreachable!() }
|
fn header<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()>;
|
||||||
fn footer<W: Write>(&self, _f: &mut W, _entries: &Entries) -> io::Result<()> { unreachable!() }
|
fn footer<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()>;
|
||||||
|
|
||||||
fn write_all<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()> {
|
fn write_all<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()> {
|
||||||
// TODO: clean this up - it's kinda messy
|
// TODO: clean this up - it's kinda messy
|
||||||
|
@ -185,7 +186,9 @@ pub trait Format {
|
||||||
pub struct Shell {}
|
pub struct Shell {}
|
||||||
|
|
||||||
impl Format for Shell {
|
impl Format for Shell {
|
||||||
fn new() -> Self { Self {} }
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
fn rename<W: Write>(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()> {
|
fn rename<W: Write>(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()> {
|
||||||
smart_write(f, writablesln!("mv -v -i -- ", from, Space, to))
|
smart_write(f, writablesln!("mv -v -i -- ", from, Space, to))
|
||||||
|
@ -227,7 +230,9 @@ impl Format for Shell {
|
||||||
pub struct PowerShell {}
|
pub struct PowerShell {}
|
||||||
|
|
||||||
impl Format for PowerShell {
|
impl Format for PowerShell {
|
||||||
fn new() -> Self { Self {} }
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
fn rename<W: Write>(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()> {
|
fn rename<W: Write>(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()> {
|
||||||
// unfortunately there doesn't seem to be an equivalent of sh's `mv -i` -- passing the '-Confirm' flag will prompt
|
// unfortunately there doesn't seem to be an equivalent of sh's `mv -i` -- passing the '-Confirm' flag will prompt
|
||||||
|
@ -281,34 +286,3 @@ impl Format for PowerShell {
|
||||||
smart_write(f, writablesln![Newline, "Write-Output 'Done!'"])
|
smart_write(f, writablesln![Newline, "Write-Output 'Done!'"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "json")]
|
|
||||||
pub struct Json;
|
|
||||||
|
|
||||||
#[cfg(feature = "json")]
|
|
||||||
impl Format for Json {
|
|
||||||
fn new() -> Self { Self {} }
|
|
||||||
|
|
||||||
fn write_all<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()> {
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
struct SerdeEntries<'a> {
|
|
||||||
errors: &'a Vec<&'a ScanError<'a>>,
|
|
||||||
findings: &'a Vec<&'a Findings<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = serde_json::to_writer_pretty(
|
|
||||||
f,
|
|
||||||
&SerdeEntries {
|
|
||||||
errors: &entries.iter().filter_map(|e| e.as_ref().err()).sorted().collect(),
|
|
||||||
findings: &entries.iter().filter_map(|f| f.as_ref().ok()).sorted().collect(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Err(err) = result {
|
|
||||||
log::error!("Error while serialising: {}", err);
|
|
||||||
return Err(err.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -30,10 +30,10 @@ use once_cell::sync::OnceCell;
|
||||||
use walkdir::{DirEntry, WalkDir};
|
use walkdir::{DirEntry, WalkDir};
|
||||||
|
|
||||||
use crate::findings::Findings;
|
use crate::findings::Findings;
|
||||||
use crate::findings::ScanError;
|
|
||||||
use crate::formats::{Format, PowerShell, Shell};
|
use crate::formats::{Format, PowerShell, Shell};
|
||||||
use crate::mime_db::MimeDb;
|
use crate::mime_db::MimeDb;
|
||||||
use crate::parameters::{OutputFormat, ScanOpts};
|
use crate::parameters::{OutputFormat, ScanOpts};
|
||||||
|
use crate::scan_error::ScanError;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
mod findings;
|
mod findings;
|
||||||
|
@ -41,6 +41,7 @@ mod formats;
|
||||||
mod inspectors;
|
mod inspectors;
|
||||||
mod mime_db;
|
mod mime_db;
|
||||||
mod parameters;
|
mod parameters;
|
||||||
|
mod scan_error;
|
||||||
pub(crate) mod string_type;
|
pub(crate) mod string_type;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -117,7 +118,7 @@ fn main() {
|
||||||
.filter(
|
.filter(
|
||||||
|result| result.is_err() || !result.as_ref().unwrap().valid,
|
|result| result.is_err() || !result.as_ref().unwrap().valid,
|
||||||
// TODO: find a way to trace! the valid files without doing ↓
|
// TODO: find a way to trace! the valid files without doing ↓
|
||||||
// || if result.as_ref().unwrap().valid { trace!("{:?} ok", result.as_ref().unwrap().file); false } else { true }
|
// || if result.as_ref().unwrap().valid { trace!("{:?} is fine", result.as_ref().unwrap().file); false } else { true }
|
||||||
)
|
)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -147,8 +148,6 @@ fn main() {
|
||||||
let result = match args.output_format {
|
let result = match args.output_format {
|
||||||
OutputFormat::Sh => Shell::new().write_all(&mut buffered_stdout, &results),
|
OutputFormat::Sh => Shell::new().write_all(&mut buffered_stdout, &results),
|
||||||
OutputFormat::PowerShell => PowerShell::new().write_all(&mut buffered_stdout, &results),
|
OutputFormat::PowerShell => PowerShell::new().write_all(&mut buffered_stdout, &results),
|
||||||
#[cfg(feature = "json")]
|
|
||||||
OutputFormat::Json => formats::Json::new().write_all(&mut buffered_stdout, &results),
|
|
||||||
OutputFormat::Text => todo!(),
|
OutputFormat::Text => todo!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -229,7 +228,9 @@ fn wanted_file(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given a file path, returns its extension, using [`std::path::Path::extension`].
|
/// Given a file path, returns its extension, using [`std::path::Path::extension`].
|
||||||
fn extension_from_path(path: &Path) -> Option<&OsStr> { path.extension() }
|
fn extension_from_path(path: &Path) -> Option<&OsStr> {
|
||||||
|
path.extension()
|
||||||
|
}
|
||||||
|
|
||||||
/// Inspects the given entry, returning a [`Findings`] on success and a [`ScanError`] on failure.
|
/// Inspects the given entry, returning a [`Findings`] on success and a [`ScanError`] on failure.
|
||||||
///
|
///
|
||||||
|
|
|
@ -24,9 +24,6 @@ pub enum OutputFormat {
|
||||||
PowerShell,
|
PowerShell,
|
||||||
/// Plain text.
|
/// Plain text.
|
||||||
Text,
|
Text,
|
||||||
/// JSON.
|
|
||||||
#[cfg(feature = "json")]
|
|
||||||
Json,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: convert this to macro style?: https://docs.rs/clap/3.0.0-beta.2/clap/index.html#using-macros
|
// TODO: convert this to macro style?: https://docs.rs/clap/3.0.0-beta.2/clap/index.html#using-macros
|
||||||
|
@ -51,37 +48,23 @@ pub struct Parameters {
|
||||||
/// Only examine files with these extensions.
|
/// Only examine files with these extensions.
|
||||||
/// Multiple extensions can be specified by either using the flag multiple times (`-e jpg -e png -e gif`), or by
|
/// Multiple extensions can be specified by either using the flag multiple times (`-e jpg -e png -e gif`), or by
|
||||||
/// separating them with commas (`-e jpg,png,gif`).
|
/// separating them with commas (`-e jpg,png,gif`).
|
||||||
#[clap(short, long, use_delimiter = true, require_delimiter = true, value_name = "ext", validator = lowercase_exts)]
|
#[clap(short, long, use_delimiter = true, require_delimiter = true, value_name = "ext")]
|
||||||
pub exts: Option<Vec<StringType>>,
|
pub exts: Option<Vec<StringType>>,
|
||||||
|
|
||||||
/// Use these preset lists of extensions as the search filter (comma-separated list).
|
/// Use these preset lists of extensions as the search filter (comma-separated list).
|
||||||
/// `media` includes all extensions from the `audio`, `video`, and `images` sets, making `-E media` equivalent to
|
/// `media` includes all extensions from the `audio`, `video`, and `images` sets, making `-E media` equivalent to
|
||||||
/// `-E audio,video,images`.
|
/// `-E audio,video,images`.
|
||||||
#[clap(
|
#[clap(short = 'E', long, arg_enum, use_delimiter = true, require_delimiter = true, value_name = "set")]
|
||||||
short = 'E',
|
|
||||||
long,
|
|
||||||
arg_enum,
|
|
||||||
use_delimiter = true,
|
|
||||||
require_delimiter = true,
|
|
||||||
value_name = "set"
|
|
||||||
)]
|
|
||||||
pub ext_set: Vec<ExtensionSet>,
|
pub ext_set: Vec<ExtensionSet>,
|
||||||
|
|
||||||
/// Don't scan files with these extensions.
|
/// Don't scan files with these extensions.
|
||||||
/// This option takes precedence over extensions specified with `-e` or `-E`.
|
/// This option takes precedence over extensions specified with `-e` or `-E`.
|
||||||
#[clap(short = 'x', long, use_delimiter = true, require_delimiter = true, value_name = "ext", validator = lowercase_exts)]
|
#[clap(short = 'x', long, use_delimiter = true, require_delimiter = true, value_name = "ext")]
|
||||||
pub exclude: Option<Vec<StringType>>,
|
pub exclude: Option<Vec<StringType>>,
|
||||||
|
|
||||||
/// Exclude files using a preset list of extensions.
|
/// Exclude files using a preset list of extensions.
|
||||||
/// This option takes precedence over extensions specified with `-e` or `-E`.
|
/// This option takes precedence over extensions specified with `-e` or `-E`.
|
||||||
#[clap(
|
#[clap(short = 'X', long, arg_enum, use_delimiter = true, require_delimiter = true, value_name = "set")]
|
||||||
short = 'X',
|
|
||||||
long,
|
|
||||||
arg_enum,
|
|
||||||
use_delimiter = true,
|
|
||||||
require_delimiter = true,
|
|
||||||
value_name = "set"
|
|
||||||
)]
|
|
||||||
pub exclude_set: Vec<ExtensionSet>,
|
pub exclude_set: Vec<ExtensionSet>,
|
||||||
|
|
||||||
/// Don't skip hidden files and directories.
|
/// Don't skip hidden files and directories.
|
||||||
|
@ -120,14 +103,6 @@ pub struct Parameters {
|
||||||
pub dirs: PathBuf,
|
pub dirs: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lowercase_exts(exts: &str) -> Result<(), String> {
|
|
||||||
// TODO: i would much rather accept uppercase exts and convert them to lowercase than just rejecting lowercase exts...
|
|
||||||
if exts.to_lowercase() != exts {
|
|
||||||
return Err(String::from("Supplied extensions must be lowercase"));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Further options relating to scanning.
|
/// Further options relating to scanning.
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub struct ScanOpts {
|
pub struct ScanOpts {
|
||||||
|
@ -216,12 +191,12 @@ impl Parameters {
|
||||||
match self.verbose {
|
match self.verbose {
|
||||||
0 => "info", // no verbosity flags specified
|
0 => "info", // no verbosity flags specified
|
||||||
1 => "debug", // -v
|
1 => "debug", // -v
|
||||||
_ => "trace", // -vv...
|
_ => "trace" // -vv...
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
1 => "warn", // -q
|
1 => "warn", // -q
|
||||||
2 => "error", // -qq
|
2 => "error", // -qq
|
||||||
_ => "off", // -qqq...
|
_ => "off" // -qqq...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
26
src/scan_error.rs
Normal file
26
src/scan_error.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq)]
|
||||||
|
pub enum ScanError<'a> {
|
||||||
|
/// Something went wrong while trying to read the given file.
|
||||||
|
File(&'a Path),
|
||||||
|
/// Failed to determine the mimetype of the given file.
|
||||||
|
Mime(&'a Path),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for ScanError<'a> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Couldn't {} file: {}",
|
||||||
|
match self {
|
||||||
|
Self::File(_) => "read",
|
||||||
|
Self::Mime(_) => "determine mime type of",
|
||||||
|
},
|
||||||
|
match self {
|
||||||
|
Self::File(f) | Self::Mime(f) => f.to_string_lossy(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::findings::Findings;
|
use crate::findings::Findings;
|
||||||
use crate::formats::{Format, PowerShell, Shell};
|
use crate::formats::{Format, Shell};
|
||||||
use crate::inspectors::{mime_extension_lookup, BUF_SIZE};
|
use crate::inspectors::{mime_extension_lookup, BUF_SIZE};
|
||||||
use crate::mime_db::MimeDb;
|
use crate::mime_db::MimeDb;
|
||||||
use crate::string_type::String;
|
use crate::string_type::String;
|
||||||
|
@ -304,15 +304,14 @@ fn rejects_bad_args() {
|
||||||
/// Generate random series of bytes and try to identify them. This test makes no assertions and can only fail if the
|
/// Generate random series of bytes and try to identify them. This test makes no assertions and can only fail if the
|
||||||
/// mime database somehow panics or hangs.
|
/// mime database somehow panics or hangs.
|
||||||
fn identify_random_bytes() {
|
fn identify_random_bytes() {
|
||||||
use rand::RngCore;
|
|
||||||
let db = get_mime_db();
|
let db = get_mime_db();
|
||||||
let mut rng = rand::thread_rng();
|
let rng = fastrand::Rng::new();
|
||||||
let mut bytes: [u8; BUF_SIZE * 2] = [0; BUF_SIZE * 2];
|
let mut bytes: Vec<u8>;
|
||||||
let mut results: HashMap<Mime, i32> = HashMap::new();
|
let mut results: HashMap<Mime, i32> = HashMap::new();
|
||||||
|
|
||||||
for _ in 1..1000 {
|
for _ in 1..500 {
|
||||||
rng.fill_bytes(&mut bytes);
|
bytes = std::iter::repeat_with(|| rng.u8(..)).take(BUF_SIZE * 2).collect();
|
||||||
if let Some(detected_type) = db.get_type(&bytes) {
|
if let Some(detected_type) = db.get_type(&*bytes) {
|
||||||
*results.entry(detected_type).or_insert(0) += 1;
|
*results.entry(detected_type).or_insert(0) += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -336,17 +335,12 @@ fn outputs_move_commands() {
|
||||||
mime: IMAGE_JPEG,
|
mime: IMAGE_JPEG,
|
||||||
})];
|
})];
|
||||||
|
|
||||||
for format in &["Shell", "PowerShell"] {
|
|
||||||
let mut cursor = std::io::Cursor::new(Vec::new());
|
let mut cursor = std::io::Cursor::new(Vec::new());
|
||||||
let mut contents = std::string::String::new();
|
let mut contents = std::string::String::new();
|
||||||
|
|
||||||
match *format {
|
Shell::new()
|
||||||
"Shell" => Shell::new().write_all(&mut cursor, &entries),
|
.write_all(&mut cursor, &entries)
|
||||||
"PowerShell" => PowerShell::new().write_all(&mut cursor, &entries),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
.expect("Failed to write to cursor");
|
.expect("Failed to write to cursor");
|
||||||
|
|
||||||
cursor.set_position(0);
|
cursor.set_position(0);
|
||||||
cursor
|
cursor
|
||||||
.read_to_string(&mut contents)
|
.read_to_string(&mut contents)
|
||||||
|
@ -355,42 +349,7 @@ fn outputs_move_commands() {
|
||||||
// the output should contain a command like "mv -i misnamed_file.png misnamed_file.jpg"
|
// the output should contain a command like "mv -i misnamed_file.png misnamed_file.jpg"
|
||||||
assert!(
|
assert!(
|
||||||
contents.contains("misnamed_file.jpg"),
|
contents.contains("misnamed_file.jpg"),
|
||||||
"{} output doesn't contain move command!\n===\n{}",
|
"Output doesn't contain move command!"
|
||||||
format,
|
|
||||||
contents
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Ensure JSON output is valid.
|
|
||||||
fn test_json() {
|
|
||||||
use crate::formats::Json;
|
|
||||||
use std::io::Read;
|
|
||||||
// create an example finding stating that "misnamed_file.png" has been identified as a jpeg file
|
|
||||||
let entries = vec![Ok(Findings {
|
|
||||||
file: Path::new("misnamed_file.png"),
|
|
||||||
valid: false,
|
|
||||||
mime: IMAGE_JPEG,
|
|
||||||
})];
|
|
||||||
|
|
||||||
let mut cursor = std::io::Cursor::new(Vec::new());
|
|
||||||
let mut contents = std::string::String::new();
|
|
||||||
|
|
||||||
Json::new()
|
|
||||||
.write_all(&mut cursor, &entries)
|
|
||||||
.expect("Failed to write to cursor");
|
|
||||||
|
|
||||||
cursor.set_position(0);
|
|
||||||
cursor
|
|
||||||
.read_to_string(&mut contents)
|
|
||||||
.expect("Failed to read from cursor to string");
|
|
||||||
|
|
||||||
// the output should contain the file's mime type
|
|
||||||
assert!(
|
|
||||||
contents.contains(IMAGE_JPEG.essence_str()),
|
|
||||||
"JSON output doesn't contain move command!\n===\n{}",
|
|
||||||
contents
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,10 +391,7 @@ fn writables_is_correct() {
|
||||||
#[test]
|
#[test]
|
||||||
/// Test various combinations of verbosity flags.
|
/// Test various combinations of verbosity flags.
|
||||||
fn verbosity() {
|
fn verbosity() {
|
||||||
assert!(
|
assert!(Parameters::try_parse_from(&["fif", "-q", "-v"]).is_err(), "Failed to reject usage of both -q and -v!");
|
||||||
Parameters::try_parse_from(&["fif", "-q", "-v"]).is_err(),
|
|
||||||
"Failed to reject usage of both -q and -v!"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut expected_results = HashMap::new();
|
let mut expected_results = HashMap::new();
|
||||||
expected_results.insert("-qqqqqqqq", "off");
|
expected_results.insert("-qqqqqqqq", "off");
|
||||||
|
|
Loading…
Reference in a new issue