writables!, windows newlines, sorted changelog, always quote paths
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
6a7577d874
commit
d6974b4959
5 changed files with 143 additions and 70 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -3,15 +3,22 @@ Dates are given in YYYY-MM-DD format.
|
||||||
|
|
||||||
## v0.2
|
## v0.2
|
||||||
### v0.2.13 (2021-???)
|
### 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
|
- Added Apple iWork document formats to documents extension set
|
||||||
- Cleaned up and properly documented tests
|
- Cleaned up and properly documented tests
|
||||||
- Renamed `Script` (in `formats.rs`) to `Shell`, in line with renaming in `parameters.rs`
|
- Renamed `Script` (in `formats.rs`) to `Shell`, in line with renaming in `parameters.rs`
|
||||||
- Added .rpa (Ren'Py archive) support to infer backend
|
- Added .rpa (Ren'Py archive) support to infer backend
|
||||||
- Added system extension set
|
|
||||||
- [`xdg-mime`] no longer uses git version
|
- [`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,
|
- Output `\r\n` on Windows
|
||||||
then files with the wrong extension
|
- Use a macro to generate `Writable` arrays, making the code a little bit cleaner and nicer to write
|
||||||
- Fixed some bad formatting in PowerShell output
|
|
||||||
|
|
||||||
### v0.2.12 (2021-04-14)
|
### v0.2.12 (2021-04-14)
|
||||||
#### Features
|
#### Features
|
||||||
|
@ -88,7 +95,7 @@ Dates are given in YYYY-MM-DD format.
|
||||||
- Automatically disable [`xdg-mime`] backend on Windows
|
- Automatically disable [`xdg-mime`] backend on Windows
|
||||||
- Exit codes
|
- Exit codes
|
||||||
- Improved error handling
|
- 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
|
#### Bugfixes
|
||||||
- Improved SVG detection
|
- Improved SVG detection
|
||||||
#### Other
|
#### Other
|
||||||
|
@ -135,4 +142,5 @@ Initial commit!
|
||||||
[`structopt`]: https://crates.io/crates/structopt
|
[`structopt`]: https://crates.io/crates/structopt
|
||||||
[`clap`]: https://crates.io/crates/clap
|
[`clap`]: https://crates.io/crates/clap
|
||||||
[`infer`]: https://crates.io/crates/infer
|
[`infer`]: https://crates.io/crates/infer
|
||||||
[`mime_guess`]: https://crates.io/crates/mime_guess
|
[`mime_guess`]: https://crates.io/crates/mime_guess
|
||||||
|
[`snailquote`]: https://crates.io/crates/snailquote
|
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -184,9 +184,9 @@ checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "1.4.0"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3"
|
checksum = "77b705829d1e87f762c2df6da140b26af5839e1033aa84aa5f56bb688e4e1bdb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"instant",
|
"instant",
|
||||||
]
|
]
|
||||||
|
@ -300,9 +300,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lexical-core"
|
name = "lexical-core"
|
||||||
version = "0.7.5"
|
version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374"
|
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
@ -578,9 +578,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.69"
|
version = "1.0.70"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb"
|
checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
@ -61,7 +61,7 @@ default-features = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.2.0"
|
tempfile = "3.2.0"
|
||||||
fastrand = "1.4.0"
|
fastrand = "1.4.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|
161
src/formats.rs
161
src/formats.rs
|
@ -6,19 +6,49 @@ use std::io::{self, Write};
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use cfg_if::cfg_if;
|
||||||
use snailquote::escape;
|
use snailquote::escape;
|
||||||
|
|
||||||
use crate::scan_error::ScanError;
|
use crate::scan_error::ScanError;
|
||||||
use crate::{Findings, BACKEND};
|
use crate::{Findings, BACKEND};
|
||||||
use itertools::Itertools;
|
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.
|
/// The current version of fif, as defined in Cargo.toml.
|
||||||
const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
|
const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
type Entries<'a> = [Result<Findings<'a>, ScanError<'a>>];
|
type Entries<'a> = [Result<Findings<'a>, ScanError<'a>>];
|
||||||
|
|
||||||
enum Writable<'a> {
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum Writable<'a> {
|
||||||
String(&'a str),
|
String(&'a str),
|
||||||
Path(&'a Path),
|
Path(&'a Path),
|
||||||
Space,
|
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<W: Write>(f: &mut W, writeables: &[Writable]) -> io::Result<()> {
|
fn smart_write<W: Write>(f: &mut W, writeables: &[Writable]) -> io::Result<()> {
|
||||||
// ehhhh
|
// ehhhh
|
||||||
for writeable in writeables {
|
for writeable in writeables {
|
||||||
match writeable {
|
match writeable {
|
||||||
Writable::Space => write!(f, " ")?,
|
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::String(s) => write!(f, "{}", s)?,
|
||||||
Writable::Path(path) => {
|
Writable::Path(path) => {
|
||||||
if let Some(string) = path.to_str() {
|
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 {
|
} else {
|
||||||
write!(f, "'")?;
|
write!(f, "'")?;
|
||||||
#[cfg(unix)]
|
cfg_if! {
|
||||||
f.write_all(&*path.as_os_str().as_bytes())?;
|
if #[cfg(windows)] {
|
||||||
// TODO: implement bonked strings for windows
|
// TODO: implement bonked strings for windows
|
||||||
// something like:
|
// something like:
|
||||||
// f.write_all(&*path.as_os_str().encode_wide().collect::<Vec<u16>>())?;
|
// f.write_all(&*path.as_os_str().encode_wide().collect::<Vec<u16>>())?;
|
||||||
#[cfg(windows)]
|
write!(f, "{}", path.as_os_str().to_string_lossy())?;
|
||||||
write!(f, "{}", path.as_os_str().to_string_lossy())?;
|
} else {
|
||||||
|
f.write_all(&*path.as_os_str().as_bytes())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
write!(f, "'")?
|
write!(f, "'")?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,52 +183,38 @@ impl Format for Shell {
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
smart_write(f, writables!("mv -v -i -- ", from, Space, to, Newline))
|
||||||
f,
|
|
||||||
&[
|
|
||||||
"mv -v -i -- ".into(),
|
|
||||||
from.into(),
|
|
||||||
Writable::Space,
|
|
||||||
to.into(),
|
|
||||||
Writable::Newline,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_known_extension<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
fn no_known_extension<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
||||||
smart_write(
|
smart_write(f, writables!["echo No known extension for ", path, Newline])
|
||||||
f,
|
|
||||||
&["echo No known extension for ".into(), path.into(), Writable::Newline],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unreadable<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
fn unreadable<W: Write>(&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<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
fn unknown_type<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
||||||
|
smart_write(f, writables!["# Failed to detect mime type for ", path, Newline])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header<W: Write>(&self, _: &Entries, f: &mut W) -> io::Result<()> {
|
||||||
smart_write(
|
smart_write(
|
||||||
f,
|
f,
|
||||||
&[
|
writables![
|
||||||
"# Failed to detect mime type for ".into(),
|
"#!/usr/bin/env sh",
|
||||||
path.into(),
|
Newline,
|
||||||
Writable::Newline,
|
"# ",
|
||||||
|
(generated_by().as_str()),
|
||||||
|
Newline,
|
||||||
|
"set -e",
|
||||||
|
Newline
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header<W: Write>(&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<W: Write>(&self, _: &Entries, f: &mut W) -> io::Result<()> {
|
fn footer<W: Write>(&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.
|
// there doesn't seem to be a way to rename the file, prompting only if the target already exists.
|
||||||
smart_write(
|
smart_write(
|
||||||
f,
|
f,
|
||||||
&[
|
writables![
|
||||||
"Rename-Item -Path ".into(),
|
"Rename-Item -Path ",
|
||||||
from.into(),
|
from,
|
||||||
" -NewName ".into(),
|
" -NewName ",
|
||||||
to.file_name().unwrap().into(),
|
(to.file_name().unwrap()),
|
||||||
Writable::Newline,
|
Newline
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -208,10 +247,13 @@ impl Format for PowerShell {
|
||||||
fn no_known_extension<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
fn no_known_extension<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
||||||
smart_write(
|
smart_write(
|
||||||
f,
|
f,
|
||||||
&[
|
writables![
|
||||||
"Write-Output @'\nNo known extension for ".into(),
|
"Write-Output @'",
|
||||||
path.into(),
|
Newline,
|
||||||
"\n'@".into(),
|
"No known extension for ",
|
||||||
|
path,
|
||||||
|
Newline,
|
||||||
|
"'@"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -219,27 +261,32 @@ impl Format for PowerShell {
|
||||||
fn unreadable<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
fn unreadable<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
||||||
smart_write(
|
smart_write(
|
||||||
f,
|
f,
|
||||||
&["Write-Output @'\nFailed to read ".into(), path.into(), "\n'@".into()],
|
writables!["Write-Output @'", Newline, "Failed to read ", path, Newline, "'@"],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unknown_type<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
fn unknown_type<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
||||||
smart_write(
|
smart_write(
|
||||||
f,
|
f,
|
||||||
&["<# Failed to detect mime type for ".into(), path.into(), "#>".into(), Writable::Newline],
|
writables!["<# Failed to detect mime type for ", path, " #>", Newline],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header<W: Write>(&self, _: &Entries, f: &mut W) -> io::Result<()> {
|
fn header<W: Write>(&self, _: &Entries, f: &mut W) -> io::Result<()> {
|
||||||
writeln!(
|
smart_write(
|
||||||
f,
|
f,
|
||||||
"#!/usr/bin/env pwsh\n# Generated by fif {} ({} backend)",
|
writables![
|
||||||
VERSION.unwrap_or("???"),
|
"#!/usr/bin/env pwsh",
|
||||||
BACKEND
|
Newline,
|
||||||
|
"<# ",
|
||||||
|
(generated_by().as_str()),
|
||||||
|
" #>",
|
||||||
|
Newline
|
||||||
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn footer<W: Write>(&self, _: &Entries, f: &mut W) -> io::Result<()> {
|
fn footer<W: Write>(&self, _: &Entries, f: &mut W) -> io::Result<()> {
|
||||||
writeln!(f, "\nWrite-Output 'Done!'")
|
smart_write(f, writables![Newline, "Write-Output 'Done!'", Newline])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -276,6 +276,7 @@ fn outputs_move_commands() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
/// Ensure that the Media extension set contains all (is a superset) of Audio, Video, and Images.
|
||||||
fn media_contains_audio_video_images() {
|
fn media_contains_audio_video_images() {
|
||||||
use crate::extension_set::ExtensionSet::{Audio, Images, Media, Videos};
|
use crate::extension_set::ExtensionSet::{Audio, Images, Media, Videos};
|
||||||
let media_exts = Media.extensions();
|
let media_exts = Media.extensions();
|
||||||
|
@ -286,3 +287,20 @@ fn media_contains_audio_video_images() {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|ext| assert!(media_exts.contains(&ext)));
|
.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]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue