From 0218e1a4327f317f3aa16ea83b1a52019d5c37a0 Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Sat, 29 Jul 2023 01:07:35 +0200 Subject: Genesis commit --- src/error.rs | 63 +++++++++++++ src/main.rs | 255 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main_result.rs | 32 +++++++ 3 files changed, 350 insertions(+) create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/main_result.rs (limited to 'src') diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e592e50 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,63 @@ +use std::{ + env, + ffi::OsString, + fmt::{self, Display, Formatter}, + io, + path::PathBuf, +}; + +pub enum Error { + BadArgs(Option), + BadDecoding(String), + BadLengths, + DuplicateInput(PathBuf), + DuplicateOutput(PathBuf), + IO(io::Error), + Nop, + SpawnFailed(OsString, io::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let p = env::args().next().unwrap(); + match self { + Self::BadArgs(o) => { + if let Some(v) = o { + writeln!(f, "{p}: {v}")?; + } + writeln!(f, "Usage: {p} [-0eiv] command [argument ...]") + } + Self::BadDecoding(s) => writeln!(f, "{p}: Decoding the file “{s}” failed!"), + Self::BadLengths => writeln!(f, "{p}: Files have been added or removed during editing"), + Self::DuplicateInput(s) => writeln!( + f, + "{p}: Input file “{}” specified more than once", + s.to_string_lossy() + ), + Self::DuplicateOutput(s) => writeln!( + f, + "{p}: Output file “{}” specified more than once", + s.to_string_lossy() + ), + Self::IO(e) => writeln!(f, "{p}: {e}"), + Self::Nop => Ok(()), + Self::SpawnFailed(ed, e) => writeln!( + f, + "{p}: Failed to spawn utility “{}”: {e}", + ed.to_string_lossy() + ), + } + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Self::IO(e) + } +} + +impl From for Error { + fn from(e: lexopt::Error) -> Self { + Self::BadArgs(Some(e)) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c841a2f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,255 @@ +mod error; +mod main_result; + +use std::{ + cmp::Reverse, + collections::{HashSet, hash_map::DefaultHasher}, + env, + ffi::OsString, + fs, + io::{self, BufRead, BufReader, BufWriter, Write}, + iter, + path::{Component, Path, PathBuf}, + process::{Command, Stdio}, hash::{Hash, Hasher}, fmt::{Display, Formatter, self}, +}; + +use tempfile::tempdir; + +use {error::Error, main_result::MainResult}; + +#[derive(Default)] +struct Flags { + pub dryrun: bool, + pub encode: bool, + pub individual: bool, + pub nul: bool, + pub verbose: bool, +} + +fn main() -> MainResult { + work().into() +} + +fn work() -> Result<(), Error> { + let (flags, rest) = parse_args()?; + let (cmd, args) = rest.split_first().ok_or(Error::BadArgs(None))?; + + // Collect sources from standard input + let srcs = io::stdin() + .lines() + .map(|l| { + l.map_err(Error::from).and_then(|l| { + if l.is_empty() { + Err(Error::BadArgs(None)) + } else { + Ok(l) + } + }) + }) + .collect::, Error>>()?; + + // Spawn the child process + let mut child = Command::new(cmd) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .map_err(|e| Error::SpawnFailed(cmd.to_owned(), e))?; + + // Pass the source files to the child process. + // TODO: Don’t use expect; create a custom error + { + let ci = child + .stdin + .take() + .expect("Could not open the child process’ stdin"); + let mut ci = BufWriter::new(ci); + if flags.encode { + srcs.iter() + .try_for_each(|src| writeln!(ci, "{}", encode_string(src)))?; + } else { + srcs.iter().try_for_each(|src| writeln!(ci, "{}", src))?; + } + } + + // Read the destination file list from the process. + let mut dsts = Vec::with_capacity(srcs.len()); + { + let co = child + .stdout + .take() + .expect("Could not open the child process’ stdout."); + let co = BufReader::new(co); + + // TODO: Don’t allocate an intermediary String per line, by using the BufReader buffer. + co.lines().try_for_each(|dst| -> Result<(), Error> { + if flags.encode { + dsts.push(decode_string(&dst?)?); + } else { + dsts.push(dst?); + } + Ok(()) + })?; + + if dsts.len() != srcs.len() { + return Err(Error::BadLengths); + } + } + + /* If the process failed, it is expected to print an error message; as such, + we exit directly. */ + if !child.wait()?.success() { + return Err(Error::Nop); + } + + let mut uniq_srcs: HashSet = HashSet::with_capacity(srcs.len()); + let mut uniq_dsts: HashSet = HashSet::with_capacity(dsts.len()); + + let mut src_dst = srcs + .iter() + .zip(dsts) + .map(|(s, d)| -> Result<(PathBuf, PathBuf), Error> { + let s = fs::canonicalize(s)?; + let d = env::current_dir()?.join(Path::new(&d)); + let d = normalize_path(&d); + + if !uniq_srcs.insert(s.clone()) { + Err(Error::DuplicateInput(s)) + } else if !uniq_dsts.insert(d.clone()) { + Err(Error::DuplicateOutput(d)) + } else { + Ok((s, d)) + } + }) + .collect::, Error>>()?; + + /* Sort the src/dst pairs so that the sources with the longest componenets + * come first. + */ + src_dst.sort_by_key(|(s, _)| Reverse(s.components().count())); + + let dir = tempdir()?; + for s_d in src_dst.iter_mut() { + let (s, d) = s_d; + let mut hasher = DefaultHasher::new(); + s.hash(&mut hasher); + let file = hasher.finish().to_string(); + let dst = dir.path().join(&file); + + if flags.dryrun { + println!("{} -> {}", s.as_path().display(), dst.as_path().display()); + } else { + fs::rename(s, &dst)?; + } + + *s_d = (dst, d.to_path_buf()) + } + + for (s, d) in src_dst.iter().rev() { + if flags.dryrun { + println!("{} -> {}", s.as_path().display(), d.as_path().display()); + } else { + fs::rename(s, d)?; + } + } + + Ok(()) +} + +fn parse_args() -> Result<(Flags, Vec), lexopt::Error> { + use lexopt::prelude::*; + + let mut rest = Vec::with_capacity(env::args().len()); + let mut flags = Flags::default(); + let mut parser = lexopt::Parser::from_env(); + while let Some(arg) = parser.next()? { + match arg { + Short('0') | Long("nul") => flags.nul = true, + Short('d') | Long("dryrun") => flags.dryrun = true, + Short('e') | Long("encode") => flags.encode = true, + Short('i') | Long("individual") => flags.individual = true, + Short('v') | Long("verbose") => flags.verbose = true, + Value(v) => { + rest.push(v); + rest.extend(iter::from_fn(|| parser.value().ok())); + } + _ => return Err(arg.unexpected()), + } + } + + Ok((flags, rest)) +} + +fn encode_string(s: &str) -> String { + s.chars() + .flat_map(|c| { + let cs = match c { + '\\' => ['\\', '\\'], + '\n' => ['\\', 'n'], + _ => [c, '\0'], + }; + cs.into_iter() + .enumerate() + .filter(|(i, c)| *i != 1 || *c != '\0') + .map(|(_, c)| c) + }) + .collect::() +} + +fn decode_string(s: &str) -> Result { + let mut pv = false; + + s.chars() + .map(|c| { + Ok(match (pv, c) { + (true, '\\') => { + pv = false; + Some('\\') + } + (true, 'n') => { + pv = false; + Some('\n') + } + (true, _) => { + pv = false; + return Err(()); + } + (false, '\\') => { + pv = true; + None + } + (false, _) => Some(c), + }) + }) + .filter_map(Result::transpose) + .collect::>() + .map_err(|()| Error::BadDecoding(s.to_string())) +} + +/* Blatantly stolen from the Cargo source code. This is MIT licensed. */ +fn normalize_path(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} diff --git a/src/main_result.rs b/src/main_result.rs new file mode 100644 index 0000000..36e3096 --- /dev/null +++ b/src/main_result.rs @@ -0,0 +1,32 @@ +use std::{ + io::{self, Write}, + process::{ExitCode, Termination}, +}; + +use super::error::Error; + +pub enum MainResult { + Success, + Failure(Error), +} + +impl Termination for MainResult { + fn report(self) -> ExitCode { + match self { + Self::Success => ExitCode::SUCCESS, + Self::Failure(e) => { + let _ = write!(io::stderr(), "{e}"); + ExitCode::FAILURE + } + } + } +} + +impl From> for MainResult { + fn from(r: Result<(), Error>) -> Self { + match r { + Ok(()) => Self::Success, + Err(e) => Self::Failure(e), + } + } +} -- cgit v1.2.3