diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a04db..7da2dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,15 +3,22 @@ Dates are given in YYYY-MM-DD format. ## v0.2 ### v0.2.13 (2021-???) +#### Features +- Added system extension set (`.dll`, `.so`, `.exe`...) +- Output is now sorted: Files that couldn't be read, then files with no known mimetype, then files with no known + extensions, then files with the wrong extension +#### Bugfixes +- Fixed some bad formatting in PowerShell output +- Always quote file paths in output, even when not necessary - This makes output more portable and less likely to break + in future, or if [`snailquote`] misses something +#### Other - Added Apple iWork document formats to documents extension set - Cleaned up and properly documented tests - Renamed `Script` (in `formats.rs`) to `Shell`, in line with renaming in `parameters.rs` - Added .rpa (Ren'Py archive) support to infer backend -- Added system extension set - [`xdg-mime`] no longer uses git version -- Output is sorted: Files that couldn't be read, then files with no known mimetype, then files with no known extensions, - then files with the wrong extension -- Fixed some bad formatting in PowerShell output +- Output `\r\n` on Windows +- Use a macro to generate `Writable` arrays, making the code a little bit cleaner and nicer to write ### v0.2.12 (2021-04-14) #### Features @@ -88,7 +95,7 @@ Dates are given in YYYY-MM-DD format. - Automatically disable [`xdg-mime`] backend on Windows - Exit codes - Improved error handling -- Retrieve extension sets from [`mime_guess`] rather than hardcoding them in +- Retrieve extension sets from [`mime_guess`] rather than hardcoding them #### Bugfixes - Improved SVG detection #### Other @@ -135,4 +142,5 @@ Initial commit! [`structopt`]: https://crates.io/crates/structopt [`clap`]: https://crates.io/crates/clap [`infer`]: https://crates.io/crates/infer -[`mime_guess`]: https://crates.io/crates/mime_guess \ No newline at end of file +[`mime_guess`]: https://crates.io/crates/mime_guess +[`snailquote`]: https://crates.io/crates/snailquote \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c4c529a..5e0e1a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,9 +184,9 @@ checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" [[package]] name = "fastrand" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3" +checksum = "77b705829d1e87f762c2df6da140b26af5839e1033aa84aa5f56bb688e4e1bdb" dependencies = [ "instant", ] @@ -300,9 +300,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lexical-core" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ "arrayvec", "bitflags", @@ -578,9 +578,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" +checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 87e65ff..dbaa5f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ default-features = false [dev-dependencies] tempfile = "3.2.0" -fastrand = "1.4.0" +fastrand = "1.4.1" [profile.release] lto = "thin" diff --git a/src/formats.rs b/src/formats.rs index 9ebab73..b57fab7 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -6,19 +6,49 @@ use std::io::{self, Write}; use std::os::unix::ffi::OsStrExt; use std::path::Path; +use cfg_if::cfg_if; use snailquote::escape; use crate::scan_error::ScanError; use crate::{Findings, BACKEND}; use itertools::Itertools; +/// A macro for creating an array of `Writable`s without needing to pepper your code with `into()`s. +/// # Usage +/// ``` +/// let f = std::io::stdout(); +/// // Instead of... +/// smart_write(f, &["hello".into(), Writable::Newline]); +/// // ...just use: +/// smart_write(f, writables!["hello", Newline]); +/// ``` +#[macro_export] +macro_rules! writables { + [$($args:tt),+] => { + &[$(writables!(@do $args),)*] + }; + + (@do Newline) => { + $crate::formats::Writable::Newline + }; + + (@do Space) => { + $crate::formats::Writable::Space + }; + + (@do $arg:expr) => { + $arg.into() + } +} + /// The current version of fif, as defined in Cargo.toml. const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); #[doc(hidden)] type Entries<'a> = [Result, ScanError<'a>>]; -enum Writable<'a> { +#[derive(Debug, PartialEq)] +pub enum Writable<'a> { String(&'a str), Path(&'a Path), Space, @@ -44,25 +74,48 @@ impl<'a> From<&'a OsStr> for Writable<'a> { } } +fn generated_by() -> String { + format!("Generated by fif {} ({} backend)", VERSION.unwrap_or("???"), BACKEND) +} + fn smart_write(f: &mut W, writeables: &[Writable]) -> io::Result<()> { // ehhhh for writeable in writeables { match writeable { Writable::Space => write!(f, " ")?, - Writable::Newline => writeln!(f,)?, + Writable::Newline => { + cfg_if! { + if #[cfg(windows)] { + write!(f, "\r\n")? + } else { + writeln!(f,)? + } + } + } Writable::String(s) => write!(f, "{}", s)?, Writable::Path(path) => { if let Some(string) = path.to_str() { - write!(f, "{}", escape(string))? + let escaped = escape(string); + if escaped == string { + // 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 + // escaped string with single quotes. + write!(f, "'{}'", escaped)? + } else { + write!(f, "{}", escaped)? + } } else { write!(f, "'")?; - #[cfg(unix)] - f.write_all(&*path.as_os_str().as_bytes())?; - // TODO: implement bonked strings for windows - // something like: - // f.write_all(&*path.as_os_str().encode_wide().collect::>())?; - #[cfg(windows)] - write!(f, "{}", path.as_os_str().to_string_lossy())?; + cfg_if! { + if #[cfg(windows)] { + // TODO: implement bonked strings for windows + // something like: + // f.write_all(&*path.as_os_str().encode_wide().collect::>())?; + write!(f, "{}", path.as_os_str().to_string_lossy())?; + } else { + f.write_all(&*path.as_os_str().as_bytes())?; + } + } write!(f, "'")? } } @@ -130,52 +183,38 @@ impl Format for Shell { } fn rename(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()> { - smart_write( - f, - &[ - "mv -v -i -- ".into(), - from.into(), - Writable::Space, - to.into(), - Writable::Newline, - ], - ) + smart_write(f, writables!("mv -v -i -- ", from, Space, to, Newline)) } fn no_known_extension(&self, f: &mut W, path: &Path) -> io::Result<()> { - smart_write( - f, - &["echo No known extension for ".into(), path.into(), Writable::Newline], - ) + smart_write(f, writables!["echo No known extension for ", path, Newline]) } fn unreadable(&self, f: &mut W, path: &Path) -> io::Result<()> { - smart_write(f, &["# Failed to read ".into(), path.into(), Writable::Newline]) + smart_write(f, writables!["# Failed to read", path, Newline]) } fn unknown_type(&self, f: &mut W, path: &Path) -> io::Result<()> { + smart_write(f, writables!["# Failed to detect mime type for ", path, Newline]) + } + + fn header(&self, _: &Entries, f: &mut W) -> io::Result<()> { smart_write( f, - &[ - "# Failed to detect mime type for ".into(), - path.into(), - Writable::Newline, + writables![ + "#!/usr/bin/env sh", + Newline, + "# ", + (generated_by().as_str()), + Newline, + "set -e", + Newline ], ) } - fn header(&self, _: &Entries, f: &mut W) -> io::Result<()> { - writeln!( - f, - "#!/usr/bin/env sh\n# Generated by fif {} ({} backend)", - VERSION.unwrap_or("???"), - BACKEND - )?; - writeln!(f, "\nset -e\n") - } - fn footer(&self, _: &Entries, f: &mut W) -> io::Result<()> { - writeln!(f, "\necho 'Done.'") + smart_write(f, writables![Newline, "echo 'Done.'", Newline]) } } @@ -195,12 +234,12 @@ impl Format for PowerShell { // there doesn't seem to be a way to rename the file, prompting only if the target already exists. smart_write( f, - &[ - "Rename-Item -Path ".into(), - from.into(), - " -NewName ".into(), - to.file_name().unwrap().into(), - Writable::Newline, + writables![ + "Rename-Item -Path ", + from, + " -NewName ", + (to.file_name().unwrap()), + Newline ], ) } @@ -208,10 +247,13 @@ impl Format for PowerShell { fn no_known_extension(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write( f, - &[ - "Write-Output @'\nNo known extension for ".into(), - path.into(), - "\n'@".into(), + writables![ + "Write-Output @'", + Newline, + "No known extension for ", + path, + Newline, + "'@" ], ) } @@ -219,27 +261,32 @@ impl Format for PowerShell { fn unreadable(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write( f, - &["Write-Output @'\nFailed to read ".into(), path.into(), "\n'@".into()], + writables!["Write-Output @'", Newline, "Failed to read ", path, Newline, "'@"], ) } fn unknown_type(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write( f, - &["<# Failed to detect mime type for ".into(), path.into(), "#>".into(), Writable::Newline], + writables!["<# Failed to detect mime type for ", path, " #>", Newline], ) } fn header(&self, _: &Entries, f: &mut W) -> io::Result<()> { - writeln!( + smart_write( f, - "#!/usr/bin/env pwsh\n# Generated by fif {} ({} backend)", - VERSION.unwrap_or("???"), - BACKEND + writables![ + "#!/usr/bin/env pwsh", + Newline, + "<# ", + (generated_by().as_str()), + " #>", + Newline + ], ) } fn footer(&self, _: &Entries, f: &mut W) -> io::Result<()> { - writeln!(f, "\nWrite-Output 'Done!'") + smart_write(f, writables![Newline, "Write-Output 'Done!'", Newline]) } } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 22036bc..48b697c 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -276,6 +276,7 @@ fn outputs_move_commands() { } #[test] +/// Ensure that the Media extension set contains all (is a superset) of Audio, Video, and Images. fn media_contains_audio_video_images() { use crate::extension_set::ExtensionSet::{Audio, Images, Media, Videos}; let media_exts = Media.extensions(); @@ -286,3 +287,20 @@ fn media_contains_audio_video_images() { .into_iter() .for_each(|ext| assert!(media_exts.contains(&ext))); } + +#[test] +/// Ensure that the `writables!` macro produces the output it should. +fn writables_is_correct() { + use crate::formats::Writable; + use crate::writables; + + assert_eq!( + &[ + "henlo".into(), + Path::new("henlo").into(), + Writable::Newline, + Writable::Space + ], + writables!["henlo", (Path::new("henlo")), Newline, Space] + ) +}