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) into a u32.
119pub 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
123/// Install a single file from `src` to `dst`.
124pub fn install_file(src: &Path, dst: &Path, config: &InstallConfig) -> io::Result<()> {
125    // Create leading directories if -D
126    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    // Handle backup
135    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    // Compare if -C: skip copy if files are identical
143    if config.compare && dst.exists() {
144        if files_are_identical(src, dst)? {
145            return Ok(());
146        }
147    }
148
149    // Copy file — use optimized path on Linux
150    #[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    // Set mode
160    #[cfg(unix)]
161    {
162        use std::os::unix::fs::PermissionsExt;
163        fs::set_permissions(dst, fs::Permissions::from_mode(config.mode))?;
164    }
165
166    // Set ownership if specified
167    #[cfg(unix)]
168    if config.owner.is_some() || config.group.is_some() {
169        set_ownership(dst, &config.owner, &config.group)?;
170    }
171
172    // Preserve timestamps
173    if config.preserve_timestamps {
174        preserve_times(src, dst)?;
175    }
176
177    // Strip if requested
178    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
189/// Create directories (install -d).
190pub 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
205/// Check if two files have identical contents.
206fn 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    // Quick check: different sizes means different
211    if meta_a.len() != meta_b.len() {
212        return Ok(false);
213    }
214
215    // For large files, use mmap to avoid double allocation
216    #[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/// Set ownership on a file using chown(2).
231#[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 // -1 means "don't change"
239    };
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/// Resolve a username or numeric UID to a uid_t.
259#[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/// Resolve a group name or numeric GID to a gid_t.
278#[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
296/// Preserve access and modification times from src to dst.
297fn 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/// Optimized file copy on Linux: try FICLONE (CoW reflink), then copy_file_range,
328/// then fall back to fs::copy.
329#[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    // Create destination with same permissions initially
338    let dst_file = fs::OpenOptions::new()
339        .write(true)
340        .create(true)
341        .truncate(true)
342        .open(dst)?;
343
344    // Try FICLONE first (instant CoW copy on btrfs/XFS/OCFS2)
345    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    // Try copy_file_range for zero-copy in-kernel copy
352    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; // 1GB max per call
359        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                // copy_file_range not supported, fall back
373                drop(dst_file);
374                drop(src_file);
375                return fs::copy(src, dst);
376            }
377            // Partial failure after some success — this is an error
378            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
387/// Strip symbol tables from a binary using an external strip program.
388fn 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}