1use std::fs;
2use std::io;
3use std::path::Path;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum BackupMode {
8 Simple,
9 Numbered,
10 Existing,
11 None,
12}
13
14#[derive(Debug, Clone)]
16pub struct InstallConfig {
17 pub mode: u32,
18 pub owner: Option<String>,
19 pub group: Option<String>,
20 pub directory_mode: bool,
21 pub create_leading: bool,
22 pub compare: bool,
23 pub preserve_timestamps: bool,
24 pub strip: bool,
25 pub strip_program: String,
26 pub verbose: bool,
27 pub backup: Option<BackupMode>,
28 pub suffix: String,
29 pub target_directory: Option<String>,
30 pub no_target_directory: bool,
31}
32
33impl Default for InstallConfig {
34 fn default() -> Self {
35 Self {
36 mode: 0o755,
37 owner: None,
38 group: None,
39 directory_mode: false,
40 create_leading: false,
41 compare: false,
42 preserve_timestamps: false,
43 strip: false,
44 strip_program: "strip".to_string(),
45 verbose: false,
46 backup: None,
47 suffix: "~".to_string(),
48 target_directory: None,
49 no_target_directory: false,
50 }
51 }
52}
53
54pub fn parse_backup_mode(s: &str) -> Option<BackupMode> {
56 match s {
57 "none" | "off" => Some(BackupMode::None),
58 "simple" | "never" => Some(BackupMode::Simple),
59 "numbered" | "t" => Some(BackupMode::Numbered),
60 "existing" | "nil" => Some(BackupMode::Existing),
61 _ => Option::None,
62 }
63}
64
65pub fn make_backup_name(dst: &Path, mode: &BackupMode, suffix: &str) -> std::path::PathBuf {
67 match mode {
68 BackupMode::Simple | BackupMode::None => {
69 let mut name = dst.as_os_str().to_os_string();
70 name.push(suffix);
71 std::path::PathBuf::from(name)
72 }
73 BackupMode::Numbered => make_numbered_backup(dst),
74 BackupMode::Existing => {
75 if has_numbered_backup(dst) {
76 make_numbered_backup(dst)
77 } else {
78 let mut name = dst.as_os_str().to_os_string();
79 name.push(suffix);
80 std::path::PathBuf::from(name)
81 }
82 }
83 }
84}
85
86fn has_numbered_backup(path: &Path) -> bool {
87 let file_name = match path.file_name() {
88 Some(n) => n.to_string_lossy().to_string(),
89 None => return false,
90 };
91 let parent = path.parent().unwrap_or_else(|| Path::new("."));
92 if let Ok(entries) = fs::read_dir(parent) {
93 for entry in entries.flatten() {
94 let name = entry.file_name().to_string_lossy().to_string();
95 if name.starts_with(&format!("{}.~", file_name)) && name.ends_with('~') {
96 let middle = &name[file_name.len() + 2..name.len() - 1];
97 if middle.parse::<u64>().is_ok() {
98 return true;
99 }
100 }
101 }
102 }
103 false
104}
105
106fn make_numbered_backup(path: &Path) -> std::path::PathBuf {
107 let mut n = 1u64;
108 loop {
109 let candidate = format!("{}.~{}~", path.display(), n);
110 let p = std::path::PathBuf::from(&candidate);
111 if !p.exists() {
112 return p;
113 }
114 n += 1;
115 }
116}
117
118pub fn parse_mode(mode_str: &str) -> Result<u32, String> {
120 u32::from_str_radix(mode_str, 8).map_err(|_| format!("invalid mode: '{}'", mode_str))
121}
122
123pub fn install_file(src: &Path, dst: &Path, config: &InstallConfig) -> io::Result<()> {
125 if config.create_leading {
127 if let Some(parent) = dst.parent() {
128 if !parent.as_os_str().is_empty() {
129 fs::create_dir_all(parent)?;
130 }
131 }
132 }
133
134 if dst.exists() {
136 if let Some(ref mode) = config.backup {
137 let backup_name = make_backup_name(dst, mode, &config.suffix);
138 fs::rename(dst, &backup_name)?;
139 }
140 }
141
142 if config.compare && dst.exists() {
144 if files_are_identical(src, dst)? {
145 return Ok(());
146 }
147 }
148
149 #[cfg(target_os = "linux")]
151 {
152 optimized_copy(src, dst)?;
153 }
154 #[cfg(not(target_os = "linux"))]
155 {
156 fs::copy(src, dst)?;
157 }
158
159 #[cfg(unix)]
161 {
162 use std::os::unix::fs::PermissionsExt;
163 fs::set_permissions(dst, fs::Permissions::from_mode(config.mode))?;
164 }
165
166 #[cfg(unix)]
168 if config.owner.is_some() || config.group.is_some() {
169 set_ownership(dst, &config.owner, &config.group)?;
170 }
171
172 if config.preserve_timestamps {
174 preserve_times(src, dst)?;
175 }
176
177 if config.strip {
179 strip_binary(dst, &config.strip_program)?;
180 }
181
182 if config.verbose {
183 eprintln!("'{}' -> '{}'", src.display(), dst.display());
184 }
185
186 Ok(())
187}
188
189pub fn install_directories(dirs: &[&Path], config: &InstallConfig) -> io::Result<()> {
191 for dir in dirs {
192 fs::create_dir_all(dir)?;
193 #[cfg(unix)]
194 {
195 use std::os::unix::fs::PermissionsExt;
196 fs::set_permissions(dir, fs::Permissions::from_mode(config.mode))?;
197 }
198 if config.verbose {
199 eprintln!("creating directory '{}'", dir.display());
200 }
201 }
202 Ok(())
203}
204
205fn files_are_identical(a: &Path, b: &Path) -> io::Result<bool> {
207 let meta_a = fs::metadata(a)?;
208 let meta_b = fs::metadata(b)?;
209
210 if meta_a.len() != meta_b.len() {
212 return Ok(false);
213 }
214
215 #[cfg(target_os = "linux")]
217 if meta_a.len() > 1024 * 1024 {
218 let file_a = fs::File::open(a)?;
219 let file_b = fs::File::open(b)?;
220 let mmap_a = unsafe { memmap2::MmapOptions::new().map(&file_a)? };
221 let mmap_b = unsafe { memmap2::MmapOptions::new().map(&file_b)? };
222 return Ok(mmap_a[..] == mmap_b[..]);
223 }
224
225 let data_a = fs::read(a)?;
226 let data_b = fs::read(b)?;
227 Ok(data_a == data_b)
228}
229
230#[cfg(unix)]
232fn set_ownership(path: &Path, owner: &Option<String>, group: &Option<String>) -> io::Result<()> {
233 use std::ffi::CString;
234
235 let uid = if let Some(name) = owner {
236 resolve_uid(name)?
237 } else {
238 u32::MAX };
240
241 let gid = if let Some(name) = group {
242 resolve_gid(name)?
243 } else {
244 u32::MAX
245 };
246
247 let c_path = CString::new(path.as_os_str().as_encoded_bytes())
248 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?;
249
250 let ret = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
251 if ret != 0 {
252 Err(io::Error::last_os_error())
253 } else {
254 Ok(())
255 }
256}
257
258#[cfg(unix)]
260fn resolve_uid(name: &str) -> io::Result<u32> {
261 if let Ok(uid) = name.parse::<u32>() {
262 return Ok(uid);
263 }
264 let c_name = std::ffi::CString::new(name)
265 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid username"))?;
266 let pw = unsafe { libc::getpwnam(c_name.as_ptr()) };
267 if pw.is_null() {
268 Err(io::Error::new(
269 io::ErrorKind::NotFound,
270 format!("invalid user: '{}'", name),
271 ))
272 } else {
273 Ok(unsafe { (*pw).pw_uid })
274 }
275}
276
277#[cfg(unix)]
279fn resolve_gid(name: &str) -> io::Result<u32> {
280 if let Ok(gid) = name.parse::<u32>() {
281 return Ok(gid);
282 }
283 let c_name = std::ffi::CString::new(name)
284 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid group name"))?;
285 let gr = unsafe { libc::getgrnam(c_name.as_ptr()) };
286 if gr.is_null() {
287 Err(io::Error::new(
288 io::ErrorKind::NotFound,
289 format!("invalid group: '{}'", name),
290 ))
291 } else {
292 Ok(unsafe { (*gr).gr_gid })
293 }
294}
295
296fn preserve_times(src: &Path, dst: &Path) -> io::Result<()> {
298 #[cfg(unix)]
299 {
300 use std::os::unix::fs::MetadataExt;
301 let meta = fs::metadata(src)?;
302 let atime = libc::timespec {
303 tv_sec: meta.atime(),
304 tv_nsec: meta.atime_nsec(),
305 };
306 let mtime = libc::timespec {
307 tv_sec: meta.mtime(),
308 tv_nsec: meta.mtime_nsec(),
309 };
310 let times = [atime, mtime];
311 let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
312 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?;
313 let ret = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) };
314 if ret != 0 {
315 return Err(io::Error::last_os_error());
316 }
317 }
318
319 #[cfg(not(unix))]
320 {
321 let _ = (src, dst);
322 }
323
324 Ok(())
325}
326
327#[cfg(target_os = "linux")]
330fn optimized_copy(src: &Path, dst: &Path) -> io::Result<u64> {
331 use std::os::unix::io::AsRawFd;
332
333 let src_file = fs::File::open(src)?;
334 let src_meta = src_file.metadata()?;
335 let file_size = src_meta.len();
336
337 let dst_file = fs::OpenOptions::new()
339 .write(true)
340 .create(true)
341 .truncate(true)
342 .open(dst)?;
343
344 const FICLONE: libc::c_ulong = 0x40049409;
346 let ret = unsafe { libc::ioctl(dst_file.as_raw_fd(), FICLONE, src_file.as_raw_fd()) };
347 if ret == 0 {
348 return Ok(file_size);
349 }
350
351 let mut off_in: i64 = 0;
353 let mut off_out: i64 = 0;
354 let mut remaining = file_size;
355 let mut used_cfr = false;
356
357 while remaining > 0 {
358 let chunk = remaining.min(1 << 30) as usize; let n = unsafe {
360 libc::syscall(
361 libc::SYS_copy_file_range,
362 src_file.as_raw_fd(),
363 &mut off_in as *mut i64,
364 dst_file.as_raw_fd(),
365 &mut off_out as *mut i64,
366 chunk,
367 0u32,
368 )
369 };
370 if n <= 0 {
371 if !used_cfr {
372 drop(dst_file);
374 drop(src_file);
375 return fs::copy(src, dst);
376 }
377 return Err(io::Error::last_os_error());
379 }
380 used_cfr = true;
381 remaining -= n as u64;
382 }
383
384 Ok(file_size)
385}
386
387fn strip_binary(path: &Path, strip_program: &str) -> io::Result<()> {
389 let status = std::process::Command::new(strip_program)
390 .arg(path)
391 .status()?;
392 if !status.success() {
393 return Err(io::Error::new(
394 io::ErrorKind::Other,
395 format!(
396 "{} failed with exit code {}",
397 strip_program,
398 status.code().unwrap_or(-1)
399 ),
400 ));
401 }
402 Ok(())
403}