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> {
123 crate::chmod::parse_mode_no_umask(mode_str, 0)
126}
127
128pub fn install_file(src: &Path, dst: &Path, config: &InstallConfig) -> io::Result<()> {
130 if config.create_leading {
132 if let Some(parent) = dst.parent() {
133 if !parent.as_os_str().is_empty() {
134 fs::create_dir_all(parent)?;
135 }
136 }
137 }
138
139 if dst.exists() {
141 if let Some(ref mode) = config.backup {
142 let backup_name = make_backup_name(dst, mode, &config.suffix);
143 fs::rename(dst, &backup_name)?;
144 }
145 }
146
147 if config.compare && dst.exists() {
149 if files_are_identical(src, dst)? {
150 return Ok(());
151 }
152 }
153
154 #[cfg(target_os = "linux")]
156 {
157 optimized_copy(src, dst)?;
158 }
159 #[cfg(not(target_os = "linux"))]
160 {
161 fs::copy(src, dst)?;
162 }
163
164 #[cfg(unix)]
166 {
167 use std::os::unix::fs::PermissionsExt;
168 fs::set_permissions(dst, fs::Permissions::from_mode(config.mode))?;
169 }
170
171 #[cfg(unix)]
173 if config.owner.is_some() || config.group.is_some() {
174 set_ownership(dst, &config.owner, &config.group)?;
175 }
176
177 if config.preserve_timestamps {
179 preserve_times(src, dst)?;
180 }
181
182 if config.strip {
184 strip_binary(dst, &config.strip_program)?;
185 }
186
187 if config.verbose {
188 eprintln!("'{}' -> '{}'", src.display(), dst.display());
189 }
190
191 Ok(())
192}
193
194pub fn install_directories(dirs: &[&Path], config: &InstallConfig) -> io::Result<()> {
196 for dir in dirs {
197 let normalized: std::path::PathBuf = dir.components().collect();
200 let target = if normalized.as_os_str().is_empty() {
201 dir
202 } else {
203 normalized.as_path()
204 };
205 fs::create_dir_all(target)?;
206 #[cfg(unix)]
207 {
208 use std::os::unix::fs::PermissionsExt;
209 fs::set_permissions(target, fs::Permissions::from_mode(config.mode))?;
210 }
211 if config.verbose {
212 eprintln!("creating directory '{}'", dir.display());
213 }
214 }
215 Ok(())
216}
217
218fn files_are_identical(a: &Path, b: &Path) -> io::Result<bool> {
220 let meta_a = fs::metadata(a)?;
221 let meta_b = fs::metadata(b)?;
222
223 if meta_a.len() != meta_b.len() {
225 return Ok(false);
226 }
227
228 #[cfg(target_os = "linux")]
230 if meta_a.len() > 1024 * 1024 {
231 let file_a = fs::File::open(a)?;
232 let file_b = fs::File::open(b)?;
233 let mmap_a = unsafe { memmap2::MmapOptions::new().map(&file_a)? };
234 let mmap_b = unsafe { memmap2::MmapOptions::new().map(&file_b)? };
235 return Ok(mmap_a[..] == mmap_b[..]);
236 }
237
238 let data_a = fs::read(a)?;
239 let data_b = fs::read(b)?;
240 Ok(data_a == data_b)
241}
242
243#[cfg(unix)]
245fn set_ownership(path: &Path, owner: &Option<String>, group: &Option<String>) -> io::Result<()> {
246 use std::ffi::CString;
247
248 let uid = if let Some(name) = owner {
249 resolve_uid(name)?
250 } else {
251 u32::MAX };
253
254 let gid = if let Some(name) = group {
255 resolve_gid(name)?
256 } else {
257 u32::MAX
258 };
259
260 let c_path = CString::new(path.as_os_str().as_encoded_bytes())
261 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?;
262
263 let ret = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
264 if ret != 0 {
265 Err(io::Error::last_os_error())
266 } else {
267 Ok(())
268 }
269}
270
271#[cfg(unix)]
273fn resolve_uid(name: &str) -> io::Result<u32> {
274 if let Ok(uid) = name.parse::<u32>() {
275 return Ok(uid);
276 }
277 let c_name = std::ffi::CString::new(name)
278 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid username"))?;
279 let pw = unsafe { libc::getpwnam(c_name.as_ptr()) };
280 if pw.is_null() {
281 Err(io::Error::new(
282 io::ErrorKind::NotFound,
283 format!("invalid user: '{}'", name),
284 ))
285 } else {
286 Ok(unsafe { (*pw).pw_uid })
287 }
288}
289
290#[cfg(unix)]
292fn resolve_gid(name: &str) -> io::Result<u32> {
293 if let Ok(gid) = name.parse::<u32>() {
294 return Ok(gid);
295 }
296 let c_name = std::ffi::CString::new(name)
297 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid group name"))?;
298 let gr = unsafe { libc::getgrnam(c_name.as_ptr()) };
299 if gr.is_null() {
300 Err(io::Error::new(
301 io::ErrorKind::NotFound,
302 format!("invalid group: '{}'", name),
303 ))
304 } else {
305 Ok(unsafe { (*gr).gr_gid })
306 }
307}
308
309fn preserve_times(src: &Path, dst: &Path) -> io::Result<()> {
311 #[cfg(unix)]
312 {
313 use std::os::unix::fs::MetadataExt;
314 let meta = fs::metadata(src)?;
315 let atime = libc::timespec {
316 tv_sec: meta.atime(),
317 tv_nsec: meta.atime_nsec(),
318 };
319 let mtime = libc::timespec {
320 tv_sec: meta.mtime(),
321 tv_nsec: meta.mtime_nsec(),
322 };
323 let times = [atime, mtime];
324 let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
325 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?;
326 let ret = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) };
327 if ret != 0 {
328 return Err(io::Error::last_os_error());
329 }
330 }
331
332 #[cfg(not(unix))]
333 {
334 let _ = (src, dst);
335 }
336
337 Ok(())
338}
339
340#[cfg(target_os = "linux")]
343fn optimized_copy(src: &Path, dst: &Path) -> io::Result<u64> {
344 use std::os::unix::io::AsRawFd;
345
346 let src_file = fs::File::open(src)?;
347 let src_meta = src_file.metadata()?;
348 let file_size = src_meta.len();
349
350 let dst_file = fs::OpenOptions::new()
352 .write(true)
353 .create(true)
354 .truncate(true)
355 .open(dst)?;
356
357 const FICLONE: libc::c_ulong = 0x40049409;
359 let ret = unsafe { libc::ioctl(dst_file.as_raw_fd(), FICLONE, src_file.as_raw_fd()) };
360 if ret == 0 {
361 return Ok(file_size);
362 }
363
364 let mut off_in: i64 = 0;
366 let mut off_out: i64 = 0;
367 let mut remaining = file_size;
368 let mut used_cfr = false;
369
370 while remaining > 0 {
371 let chunk = remaining.min(1 << 30) as usize; let n = unsafe {
373 libc::syscall(
374 libc::SYS_copy_file_range,
375 src_file.as_raw_fd(),
376 &mut off_in as *mut i64,
377 dst_file.as_raw_fd(),
378 &mut off_out as *mut i64,
379 chunk,
380 0u32,
381 )
382 };
383 if n <= 0 {
384 if !used_cfr {
385 drop(dst_file);
387 drop(src_file);
388 return fs::copy(src, dst);
389 }
390 return Err(io::Error::last_os_error());
392 }
393 used_cfr = true;
394 remaining -= n as u64;
395 }
396
397 Ok(file_size)
398}
399
400fn strip_binary(path: &Path, strip_program: &str) -> io::Result<()> {
402 let status = std::process::Command::new(strip_program)
403 .arg(path)
404 .status()?;
405 if !status.success() {
406 return Err(io::Error::new(
407 io::ErrorKind::Other,
408 format!(
409 "{} failed with exit code {}",
410 strip_program,
411 status.code().unwrap_or(-1)
412 ),
413 ));
414 }
415 Ok(())
416}