//! The various formats that [fif](crate) can output to. use std::ffi::OsStr; use std::io::{self, Write}; #[cfg(unix)] 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() } } #[macro_export] /// Does the same thing as [writables], but adds a Newline to the end. macro_rules! writablesln { [$($args:tt),+] => { &[$(writables!(@do $args),)* writables!(@do Newline)] }; } /// 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>>]; #[derive(Debug, PartialEq)] pub enum Writable<'a> { String(&'a str), Path(&'a Path), Space, Newline, } // the lifetime of a lifetime impl<'a> From<&'a str> for Writable<'a> { fn from(s: &'a str) -> Writable<'a> { Writable::String(s) } } impl<'a> From<&'a Path> for Writable<'a> { fn from(p: &'a Path) -> Writable<'a> { Writable::Path(p) } } impl<'a> From<&'a OsStr> for Writable<'a> { 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 smart_write(f: &mut W, writeables: &[Writable]) -> io::Result<()> { // ehhhh for writeable in writeables { match writeable { Writable::Space => write!(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(path_str) = path.to_str() { let escaped = escape(path_str); if escaped.as_ref() == path_str { // 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_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, "'")? } } } } 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 { fn new() -> Self; fn rename(&self, _f: &mut W, _from: &Path, _to: &Path) -> io::Result<()> { unreachable!() } fn no_known_extension(&self, _f: &mut W, _path: &Path) -> io::Result<()> { unreachable!() } fn unreadable(&self, _f: &mut W, _path: &Path) -> io::Result<()> { unreachable!() } fn unknown_type(&self, _f: &mut W, _path: &Path) -> io::Result<()> { unreachable!() } fn header(&self, _f: &mut W, _entries: &Entries) -> io::Result<()> { unreachable!() } fn footer(&self, _f: &mut W, _entries: &Entries) -> io::Result<()> { unreachable!() } fn write_all(&self, f: &mut W, entries: &Entries) -> io::Result<()> { // TODO: clean this up - it's kinda messy self.header(f, entries)?; // output will be generated in the order: // - files that couldn't be read // - files with no known mime type // - files with no known extension // - files with a known extension // files that already have a correct extension won't be represented in the output. // sort errors so unreadable files appear before files with unknown mimetypes - ScanError impls Ord such that // ScanError::File > ScanError::Mime let errors = entries.iter().filter_map(|e| e.as_ref().err()).sorted(); // sort files so that files with no known extension come before those with known extensions - None > Some("jpg") let findings = entries .iter() .filter_map(|e| e.as_ref().ok()) .sorted_by(|a, b| b.recommended_extension().cmp(&a.recommended_extension()).reverse()); for error in errors { match error { // failed to read the file ScanError::File(path) => self.unreadable(f, path)?, // file was read successfully, but we couldn't determine a mimetype ScanError::Mime(path) => self.unknown_type(f, path)?, } } for finding in findings { if let Some(ext) = finding.recommended_extension() { self.rename(f, finding.file, &finding.file.with_extension(ext.as_str()))? } else { self.no_known_extension(f, finding.file)? } } self.footer(f, entries) } } /// Bourne-Shell compatible script. pub struct Shell {} impl Format for Shell { fn new() -> Self { Self {} } fn rename(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()> { smart_write(f, writablesln!("mv -v -i -- ", from, Space, to)) } fn no_known_extension(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write(f, writablesln!["echo No known extension for ", path]) } fn unreadable(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write(f, writablesln!["# Failed to read", path]) } fn unknown_type(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write(f, writablesln!["# Failed to detect mime type for ", path]) } fn header(&self, f: &mut W, _: &Entries) -> io::Result<()> { smart_write( f, writablesln!["#!/usr/bin/env sh", Newline, "# ", (generated_by().as_str())], )?; if let Ok(working_directory) = std::env::current_dir() { smart_write(f, writablesln!["# Run from ", (working_directory.as_path())])?; } smart_write(f, writablesln![Newline, "set -e", Newline]) } fn footer(&self, f: &mut W, _: &Entries) -> io::Result<()> { smart_write(f, writablesln![Newline, "echo 'Done.'"]) } } // PowerShell is a noun, not a type #[allow(clippy::doc_markdown)] /// PowerShell script. pub struct PowerShell {} impl Format for PowerShell { fn new() -> Self { Self {} } fn rename(&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 // the user to confirm every single rename, and using Move-Item -Force will always overwrite without prompting. // there doesn't seem to be a way to rename the file, prompting only if the target already exists. smart_write( f, writablesln!["Rename-Item -Path ", from, " -NewName ", (to.file_name().unwrap())], ) } fn no_known_extension(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write( f, writables![ "Write-Output @'", Newline, "No known extension for ", path, Newline, "'@" ], ) } fn unreadable(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write( f, writables!["Write-Output @'", Newline, "Failed to read ", path, Newline, "'@"], ) } fn unknown_type(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write(f, writablesln!["<# Failed to detect mime type for ", path, " #>"]) } fn header(&self, f: &mut W, _: &Entries) -> io::Result<()> { smart_write( f, writablesln!["#!/usr/bin/env pwsh", Newline, "<# ", (generated_by().as_str()), " #>"], )?; if let Ok(working_directory) = std::env::current_dir() { smart_write(f, writablesln!["<# Run from ", (working_directory.as_path()), " #>"])?; } smart_write(f, writables![Newline]) } fn footer(&self, f: &mut W, _: &Entries) -> io::Result<()> { 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(&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(()) } }