1use std::{
2 borrow::Cow,
3 fmt, io,
4 path::{self, Component, Path, PathBuf},
5};
6
7use atomic_file_install::{
8 atomic_install, atomic_install_noclobber, atomic_symlink_file, atomic_symlink_file_noclobber,
9};
10use binstalk_types::cargo_toml_binstall::{PkgFmt, PkgMeta};
11use compact_str::{format_compact, CompactString};
12use leon::Template;
13use miette::Diagnostic;
14use normalize_path::NormalizePath;
15use thiserror::Error as ThisError;
16use tracing::debug;
17
18#[derive(Debug, ThisError, Diagnostic)]
19pub enum Error {
20 #[error(
23 "bin-dir configuration provided generates source path outside of the temporary dir: {}", .0.display()
24 )]
25 InvalidSourceFilePath(Box<Path>),
26
27 #[error("bin-dir configuration provided generates empty source path")]
29 EmptySourceFilePath,
30
31 #[error("bin file {} not found", .0.display())]
33 BinFileNotFound(Box<Path>),
34
35 #[error(transparent)]
36 Io(#[from] io::Error),
37
38 #[error("Failed to render template: {0}")]
39 #[diagnostic(transparent)]
40 TemplateRender(#[from] leon::RenderError),
41}
42
43fn is_valid_path(path: &Path) -> bool {
47 !matches!(
48 path.components().next(),
49 Some(Component::Prefix(..) | Component::RootDir)
52 )
53}
54
55pub fn infer_bin_dir_template(
58 data: &Data,
59 has_dir: &mut dyn FnMut(&Path) -> bool,
60) -> Cow<'static, str> {
61 let name = data.name;
62 let target = data.target;
63 let version = data.version;
64
65 let gen_possible_dirs: [for<'r> fn(&'r str, &'r str, &'r str) -> String; 8] = [
69 |name, target, version| format!("{name}-{target}-v{version}"),
70 |name, target, version| format!("{name}-{target}-{version}"),
71 |name, target, version| format!("{name}-{version}-{target}"),
72 |name, target, version| format!("{name}-v{version}-{target}"),
73 |name, target, _version| format!("{name}-{target}"),
74 |name, _target, version| format!("{name}-{version}"),
76 |name, _target, version| format!("{name}-v{version}"),
77 |name, _target, _version| name.to_string(),
78 ];
79
80 let default_bin_dir_template = Cow::Borrowed("{ bin }{ binary-ext }");
81
82 gen_possible_dirs
83 .into_iter()
84 .map(|gen_possible_dir| gen_possible_dir(name, target, version))
85 .find(|dirname| has_dir(Path::new(&dirname)))
86 .map(|mut dir| {
87 dir.reserve_exact(1 + default_bin_dir_template.len());
88 dir += "/";
89 dir += &default_bin_dir_template;
90 Cow::Owned(dir)
91 })
92 .unwrap_or(default_bin_dir_template)
94}
95
96pub struct BinFile {
97 pub base_name: CompactString,
98 pub source: PathBuf,
99 pub archive_source_path: PathBuf,
100 pub dest: PathBuf,
101 pub link: Option<PathBuf>,
102}
103
104impl BinFile {
105 pub fn new(
107 data: &Data<'_>,
108 base_name: &str,
109 tt: &Template<'_>,
110 no_symlinks: bool,
111 ) -> Result<Self, Error> {
112 let binary_ext = if data.target.contains("windows") {
113 ".exe"
114 } else {
115 ""
116 };
117
118 let ctx = Context {
119 name: data.name,
120 repo: data.repo,
121 target: data.target,
122 version: data.version,
123 bin: base_name,
124 binary_ext,
125
126 target_related_info: data.target_related_info,
127 };
128
129 let (source, archive_source_path) = if data.meta.pkg_fmt == Some(PkgFmt::Bin) {
130 (
131 data.bin_path.to_path_buf(),
132 data.bin_path.file_name().unwrap().into(),
133 )
134 } else {
135 let path = tt.render(&ctx)?;
138
139 let path_normalized = Path::new(&path).normalize();
140
141 if path_normalized.components().next().is_none() {
142 return Err(Error::EmptySourceFilePath);
143 }
144
145 if !is_valid_path(&path_normalized) {
146 return Err(Error::InvalidSourceFilePath(path_normalized.into()));
147 }
148
149 (data.bin_path.join(&path_normalized), path_normalized)
150 };
151
152 let mut dest = data.install_path.join(ctx.bin);
154 if !binary_ext.is_empty() {
155 let binary_ext = binary_ext.strip_prefix('.').unwrap();
156
157 assert!(dest.set_extension(binary_ext));
161 }
162
163 let (dest, link) = if no_symlinks {
164 (dest, None)
165 } else {
166 let dest_file_path_with_ver = format!("{}-v{}{}", ctx.bin, ctx.version, ctx.binary_ext);
168 let dest_with_ver = data.install_path.join(dest_file_path_with_ver);
169
170 (dest_with_ver, Some(dest))
171 };
172
173 Ok(Self {
174 base_name: format_compact!("{base_name}{binary_ext}"),
175 source,
176 archive_source_path,
177 dest,
178 link,
179 })
180 }
181
182 pub fn preview_bin(&self) -> impl fmt::Display + '_ {
183 struct PreviewBin<'a> {
184 base_name: &'a str,
185 dest: path::Display<'a>,
186 }
187
188 impl fmt::Display for PreviewBin<'_> {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 write!(f, "{} => {}", self.base_name, self.dest)
191 }
192 }
193
194 PreviewBin {
195 base_name: &self.base_name,
196 dest: self.dest.display(),
197 }
198 }
199
200 pub fn preview_link(&self) -> impl fmt::Display + '_ {
201 OptionalLazyFormat(self.link.as_ref().map(|link| LazyFormat {
202 base_name: &self.base_name,
203 source: link.display(),
204 dest: self.link_dest().display(),
205 }))
206 }
207
208 pub fn check_source_exists(
210 &self,
211 has_file: &mut dyn FnMut(&Path) -> bool,
212 ) -> Result<(), Error> {
213 if has_file(&self.archive_source_path) {
214 Ok(())
215 } else {
216 Err(Error::BinFileNotFound((&*self.source).into()))
217 }
218 }
219
220 fn pre_install_bin(&self) -> Result<(), Error> {
221 if !self.source.try_exists()? {
222 return Err(Error::BinFileNotFound((&*self.source).into()));
223 }
224
225 #[cfg(unix)]
226 std::fs::set_permissions(
227 &self.source,
228 std::os::unix::fs::PermissionsExt::from_mode(0o755),
229 )?;
230
231 Ok(())
232 }
233
234 pub fn install_bin(&self) -> Result<(), Error> {
235 self.pre_install_bin()?;
236
237 debug!(
238 "Atomically install file from '{}' to '{}'",
239 self.source.display(),
240 self.dest.display()
241 );
242
243 atomic_install(&self.source, &self.dest)?;
244
245 Ok(())
246 }
247
248 pub fn install_bin_noclobber(&self) -> Result<(), Error> {
249 self.pre_install_bin()?;
250
251 debug!(
252 "Installing file from '{}' to '{}' only if dst not exists",
253 self.source.display(),
254 self.dest.display()
255 );
256
257 atomic_install_noclobber(&self.source, &self.dest)?;
258
259 Ok(())
260 }
261
262 pub fn install_link(&self) -> Result<(), Error> {
263 if let Some(link) = &self.link {
264 let dest = self.link_dest();
265 debug!(
266 "Create link '{}' pointing to '{}'",
267 link.display(),
268 dest.display()
269 );
270 atomic_symlink_file(dest, link)?;
271 }
272
273 Ok(())
274 }
275
276 pub fn install_link_noclobber(&self) -> Result<(), Error> {
277 if let Some(link) = &self.link {
278 let dest = self.link_dest();
279 debug!(
280 "Create link '{}' pointing to '{}' only if dst not exists",
281 link.display(),
282 dest.display()
283 );
284 atomic_symlink_file_noclobber(dest, link)?;
285 }
286
287 Ok(())
288 }
289
290 fn link_dest(&self) -> &Path {
291 if cfg!(target_family = "unix") {
292 Path::new(self.dest.file_name().unwrap())
293 } else {
294 &self.dest
295 }
296 }
297}
298
299pub struct Data<'a> {
301 pub name: &'a str,
302 pub target: &'a str,
303 pub version: &'a str,
304 pub repo: Option<&'a str>,
305 pub meta: PkgMeta,
306 pub bin_path: &'a Path,
307 pub install_path: &'a Path,
308 pub target_related_info: &'a dyn leon::Values,
314}
315
316#[derive(Clone)]
317struct Context<'c> {
318 name: &'c str,
319 repo: Option<&'c str>,
320 target: &'c str,
321 version: &'c str,
322 bin: &'c str,
323
324 binary_ext: &'c str,
326
327 target_related_info: &'c dyn leon::Values,
328}
329
330impl leon::Values for Context<'_> {
331 fn get_value<'s>(&'s self, key: &str) -> Option<Cow<'s, str>> {
332 match key {
333 "name" => Some(Cow::Borrowed(self.name)),
334 "repo" => self.repo.map(Cow::Borrowed),
335 "target" => Some(Cow::Borrowed(self.target)),
336 "version" => Some(Cow::Borrowed(self.version)),
337 "bin" => Some(Cow::Borrowed(self.bin)),
338 "binary-ext" => Some(Cow::Borrowed(self.binary_ext)),
339 "format" => Some(Cow::Borrowed(self.binary_ext)),
341
342 key => self.target_related_info.get_value(key),
343 }
344 }
345}
346
347struct LazyFormat<'a> {
348 base_name: &'a str,
349 source: path::Display<'a>,
350 dest: path::Display<'a>,
351}
352
353impl fmt::Display for LazyFormat<'_> {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 write!(f, "{} ({} -> {})", self.base_name, self.source, self.dest)
356 }
357}
358
359struct OptionalLazyFormat<'a>(Option<LazyFormat<'a>>);
360
361impl fmt::Display for OptionalLazyFormat<'_> {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 if let Some(lazy_format) = self.0.as_ref() {
364 fmt::Display::fmt(lazy_format, f)
365 } else {
366 Ok(())
367 }
368 }
369}