Skip to main content

coreutils_rs/install/
core.rs

1use std::fs;
2use std::io;
3use std::path::Path;
4
5/// Backup mode for destination files (shared with mv).
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum BackupMode {
8    Simple,
9    Numbered,
10    Existing,
11    None,
12}
13
14/// Configuration for install operations.
15#[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
54/// Parse a backup control string.
55pub 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
65/// Generate a backup file name for a given destination path.
66pub 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
118/// Parse a mode string (octal or symbolic like chmod) into a u32.
119///
120/// For install, symbolic modes are resolved relative to a base of 0
121/// and without umask filtering (GNU behaviour).
122pub fn parse_mode(mode_str: &str) -> Result<u32, String> {
123    // Use the no-umask variant: install -m applies modes exactly as
124    // specified, without filtering through the process umask.
125    crate::chmod::parse_mode_no_umask(mode_str, 0)
126}
127
128/// Install a single file from `src` to `dst`.
129pub fn install_file(src: &Path, dst: &Path, config: &InstallConfig) -> io::Result<()> {
130    // Create leading directories if -D
131    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    // Handle backup
140    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    // Compare if -C: skip copy if files are identical
148    if config.compare && dst.exists() {
149        if files_are_identical(src, dst)? {
150            return Ok(());
151        }
152    }
153
154    // Copy file — use optimized path on Linux
155    #[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    // Set mode
165    #[cfg(unix)]
166    {
167        use std::os::unix::fs::PermissionsExt;
168        fs::set_permissions(dst, fs::Permissions::from_mode(config.mode))?;
169    }
170
171    // Set ownership if specified
172    #[cfg(unix)]
173    if config.owner.is_some() || config.group.is_some() {
174        set_ownership(dst, &config.owner, &config.group)?;
175    }
176
177    // Preserve timestamps
178    if config.preserve_timestamps {
179        preserve_times(src, dst)?;
180    }
181
182    // Strip if requested
183    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
194/// Create directories (install -d).
195pub fn install_directories(dirs: &[&Path], config: &InstallConfig) -> io::Result<()> {
196    for dir in dirs {
197        // Normalize the path to handle trailing "." (e.g. "d1/.") which
198        // causes create_dir_all to fail on Linux.
199        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
218/// Check if two files have identical contents.
219fn 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    // Quick check: different sizes means different
224    if meta_a.len() != meta_b.len() {
225        return Ok(false);
226    }
227
228    // For large files, use mmap to avoid double allocation
229    #[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/// Set ownership on a file using chown(2).
244#[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 // -1 means "don't change"
252    };
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/// Resolve a username or numeric UID to a uid_t.
272#[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/// Resolve a group name or numeric GID to a gid_t.
291#[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
309/// Preserve access and modification times from src to dst.
310fn 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/// Optimized file copy on Linux: try FICLONE (CoW reflink), then copy_file_range,
341/// then fall back to fs::copy.
342#[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    // Create destination with same permissions initially
351    let dst_file = fs::OpenOptions::new()
352        .write(true)
353        .create(true)
354        .truncate(true)
355        .open(dst)?;
356
357    // Try FICLONE first (instant CoW copy on btrfs/XFS/OCFS2)
358    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    // Try copy_file_range for zero-copy in-kernel copy
365    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; // 1GB max per call
372        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                // copy_file_range not supported, fall back
386                drop(dst_file);
387                drop(src_file);
388                return fs::copy(src, dst);
389            }
390            // Partial failure after some success — this is an error
391            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
400/// Strip symbol tables from a binary using an external strip program.
401fn 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}