1#![allow(clippy::let_and_return, clippy::let_unit_value)]
5
6use std::ffi::OsStr;
7use std::ffi::OsString;
8use std::path::Path;
9use std::path::PathBuf;
10use std::process::Output;
11use std::process::Stdio;
12
13use anyhow::bail;
14use anyhow::Context as _;
15use anyhow::Result;
16
17use tempfile::tempdir;
18
19use tokio::fs::canonicalize;
20use tokio::fs::read_dir;
21use tokio::fs::write;
22use tokio::process::Command;
23
24
25fn concat_command<C, A, S>(command: C, args: A) -> OsString
27where
28 C: AsRef<OsStr>,
29 A: IntoIterator<Item = S>,
30 S: AsRef<OsStr>,
31{
32 args
33 .into_iter()
34 .fold(command.as_ref().to_os_string(), |mut cmd, arg| {
35 cmd.push(OsStr::new(" "));
36 cmd.push(arg.as_ref());
37 cmd
38 })
39}
40
41#[doc(hidden)]
43pub fn format_command<C, A, S>(command: C, args: A) -> String
44where
45 C: AsRef<OsStr>,
46 A: IntoIterator<Item = S>,
47 S: AsRef<OsStr>,
48{
49 concat_command(command, args).to_string_lossy().to_string()
50}
51
52
53#[doc(hidden)]
54pub fn evaluate<C, A, S>(output: &Output, command: C, args: A) -> Result<()>
55where
56 C: AsRef<OsStr>,
57 A: IntoIterator<Item = S>,
58 S: AsRef<OsStr>,
59{
60 if !output.status.success() {
61 let code = if let Some(code) = output.status.code() {
62 format!(" ({code})")
63 } else {
64 " (terminated by signal)".to_string()
65 };
66
67 let stderr = String::from_utf8_lossy(&output.stderr);
68 let stderr = stderr.trim_end();
69 let stderr = if !stderr.is_empty() {
70 format!(": {stderr}")
71 } else {
72 String::new()
73 };
74
75 bail!(
76 "`{}` reported non-zero exit-status{code}{stderr}",
77 format_command(command, args),
78 );
79 }
80 Ok(())
81}
82
83
84async fn run_in_impl<C, A, S, D>(command: C, args: A, dir: D, stdout: Stdio) -> Result<Output>
86where
87 C: AsRef<OsStr>,
88 A: IntoIterator<Item = S> + Clone,
89 S: AsRef<OsStr>,
90 D: AsRef<Path>,
91{
92 let output = Command::new(command.as_ref())
93 .current_dir(dir)
94 .stdin(Stdio::null())
95 .stdout(stdout)
96 .args(args.clone())
97 .output()
98 .await
99 .with_context(|| {
100 format!(
101 "failed to run `{}`",
102 format_command(command.as_ref(), args.clone())
103 )
104 })?;
105
106 let () = evaluate(&output, command, args)?;
107 Ok(output)
108}
109
110async fn run_in<C, A, S, D>(command: C, args: A, dir: D) -> Result<()>
112where
113 C: AsRef<OsStr>,
114 A: IntoIterator<Item = S> + Clone,
115 S: AsRef<OsStr>,
116 D: AsRef<Path>,
117{
118 let _output = run_in_impl(command, args, dir, Stdio::null()).await?;
119 Ok(())
120}
121
122
123pub async fn rename(file: &Path, command: &[OsString], dry_run: bool) -> Result<PathBuf> {
128 let tmp = tempdir().context("failed to create temporary directory")?;
129 let path = canonicalize(file)
130 .await
131 .with_context(|| format!("failed to canonicalize `{}`", file.display()))?;
132 let dir = path
133 .parent()
134 .with_context(|| format!("`{}` does not contain a parent", path.display()))?;
135 let file = path
136 .file_name()
137 .with_context(|| format!("path `{}` does not have file name", path.display()))?;
138 let tmp_file = tmp.path().join(file);
139 let () = write(&tmp_file, b"")
140 .await
141 .with_context(|| format!("failed to create `{}`", tmp_file.display()))?;
142
143 let (cmd, cmd_args) = command.split_first().context("rename command is missing")?;
144 let () = run_in(
146 cmd,
147 cmd_args.iter().chain([&file.to_os_string()]),
148 tmp.path(),
149 )
150 .await?;
151
152 let new = read_dir(tmp.path())
153 .await
154 .with_context(|| {
155 format!(
156 "failed to read contents of directory `{}`",
157 tmp.path().display()
158 )
159 })?
160 .next_entry()
161 .await
162 .with_context(|| {
163 format!(
164 "no file found in `{}`; did the rename operation delete instead?",
165 tmp.path().display()
166 )
167 })?
168 .with_context(|| format!("failed to read first file of `{}`", tmp.path().display()))?;
169
170 if !dry_run {
171 let () = run_in(cmd, cmd_args.iter().chain([&file.to_os_string()]), dir).await?;
173 }
174
175 let new_path = dir.join(new.file_name());
176 Ok(new_path)
177}