1#![doc(hidden)]
7
8use std::ffi::OsStr;
9use std::ffi::OsString;
10use std::path::Path;
11use std::process::Output;
12use std::process::Stdio;
13
14use anyhow::bail;
15use anyhow::Context as _;
16use anyhow::Result;
17
18use tempfile::tempdir;
19
20use tokio::fs::canonicalize;
21use tokio::fs::read_dir;
22use tokio::fs::write;
23use tokio::process::Command;
24
25
26fn concat_command<C, A, S>(command: C, args: A) -> OsString
28where
29 C: AsRef<OsStr>,
30 A: IntoIterator<Item = S>,
31 S: AsRef<OsStr>,
32{
33 args
34 .into_iter()
35 .fold(command.as_ref().to_os_string(), |mut cmd, arg| {
36 cmd.push(OsStr::new(" "));
37 cmd.push(arg.as_ref());
38 cmd
39 })
40}
41
42pub 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
53pub 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 .stderr(Stdio::piped())
97 .args(args.clone())
98 .output()
99 .await
100 .with_context(|| {
101 format!(
102 "failed to run `{}`",
103 format_command(command.as_ref(), args.clone())
104 )
105 })?;
106
107 let () = evaluate(&output, command, args)?;
108 Ok(output)
109}
110
111async fn run_in<C, A, S, D>(command: C, args: A, dir: D) -> Result<()>
113where
114 C: AsRef<OsStr>,
115 A: IntoIterator<Item = S> + Clone,
116 S: AsRef<OsStr>,
117 D: AsRef<Path>,
118{
119 let _output = run_in_impl(command, args, dir, Stdio::null()).await?;
120 Ok(())
121}
122
123
124pub async fn simulate_rename(path: &Path, command: &[OsString]) -> Result<OsString> {
129 let tmp = tempdir().context("failed to create temporary directory")?;
130 let path = canonicalize(path)
131 .await
132 .with_context(|| format!("failed to canonicalize `{}`", path.display()))?;
133 let file = path
134 .file_name()
135 .with_context(|| format!("path `{}` does not have file name", path.display()))?;
136 let tmp_file = tmp.path().join(file);
137 let () = write(&tmp_file, b"")
138 .await
139 .with_context(|| format!("failed to create `{}`", tmp_file.display()))?;
140
141 let (cmd, cmd_args) = command.split_first().context("rename command is missing")?;
142 let () = run_in(
144 cmd,
145 cmd_args.iter().chain([&file.to_os_string()]),
146 tmp.path(),
147 )
148 .await?;
149
150 let new = read_dir(tmp.path())
151 .await
152 .with_context(|| {
153 format!(
154 "failed to read contents of directory `{}`",
155 tmp.path().display()
156 )
157 })?
158 .next_entry()
159 .await
160 .with_context(|| {
161 format!(
162 "no file found in `{}`; did the rename operation delete instead?",
163 tmp.path().display()
164 )
165 })?
166 .with_context(|| format!("failed to read first file of `{}`", tmp.path().display()))?;
167
168 Ok(new.file_name())
169}