//! The various formats that [fif](crate) can output to. use std::io::{self, Write}; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; use std::path::Path; use snailquote::escape; use crate::scan_error::ScanError; use crate::{Findings, BACKEND}; use std::ffi::OsStr; /// 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> { 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 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::String(s) => write!(f, "{}", s)?, Writable::Path(path) => { if let Some(string) = path.to_str() { write!(f, "{}", escape(string))? } 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())?; write!(f, "'")? } } } } Ok(()) } pub trait Format { fn new() -> Self; fn rename(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()>; fn no_known_extension(&self, f: &mut W, path: &Path) -> io::Result<()>; fn unreadable(&self, f: &mut W, path: &Path) -> io::Result<()>; fn unknown_type(&self, f: &mut W, path: &Path) -> io::Result<()>; fn header(&self, entries: &Entries, f: &mut W) -> io::Result<()>; fn footer(&self, entries: &Entries, f: &mut W) -> io::Result<()>; fn write_all(&self, entries: &Entries, f: &mut W) -> io::Result<()> { // TODO: clean this up - it's kinda messy self.header(entries, f)?; for entry in entries { match entry { Ok(finding) => { 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)? } } Err(error) => { // something went wrong 0uo 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)?, } } } } self.footer(entries, f) } } /// Bourne-Shell compatible script. pub struct Script {} impl Format for Script { fn new() -> Self { Self {} } 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, ], ) } 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], ) } fn unreadable(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write(f, &["# Failed to read ".into(), path.into(), Writable::Newline]) } fn unknown_type(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write( f, &[ "# Failed to detect mime type for ".into(), path.into(), Writable::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.'") } } // 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, &[ "Rename-Item -Path ".into(), from.into(), " -NewName ".into(), to.file_name().unwrap().into(), Writable::Newline, ], ) } 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(), ], ) } fn unreadable(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write( f, &["Write-Output @'\nFailed to read ".into(), path.into(), "\n'@".into()], ) } fn unknown_type(&self, f: &mut W, path: &Path) -> io::Result<()> { smart_write( f, &["<# Failed to detect mime type for ".into(), path.into(), "#>".into()], ) } fn header(&self, _: &Entries, f: &mut W) -> io::Result<()> { writeln!( f, "#!/usr/bin/env pwsh\n# Generated by fif {} ({} backend)", VERSION.unwrap_or("???"), BACKEND ) } fn footer(&self, _: &Entries, f: &mut W) -> io::Result<()> { writeln!(f, "\nWrite-Output 'Done!'") } }