Skip to main content

coreutils_rs/chown/
core.rs

1use std::ffi::CString;
2use std::fs;
3use std::io;
4use std::os::unix::fs::MetadataExt;
5use std::path::Path;
6
7/// How to handle symlinks during recursive traversal.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SymlinkFollow {
10    /// -H: follow symlinks given on the command line only
11    CommandLine,
12    /// -L: follow all symlinks
13    Always,
14    /// -P: never follow symlinks (default)
15    Never,
16}
17
18/// Configuration for chown/chgrp operations.
19#[derive(Debug, Clone)]
20pub struct ChownConfig {
21    pub verbose: bool,
22    pub changes: bool,
23    pub silent: bool,
24    pub recursive: bool,
25    pub no_dereference: bool,
26    pub preserve_root: bool,
27    pub from_owner: Option<u32>,
28    pub from_group: Option<u32>,
29    pub symlink_follow: SymlinkFollow,
30}
31
32impl Default for ChownConfig {
33    fn default() -> Self {
34        Self {
35            verbose: false,
36            changes: false,
37            silent: false,
38            recursive: false,
39            no_dereference: false,
40            preserve_root: false,
41            from_owner: None,
42            from_group: None,
43            symlink_follow: SymlinkFollow::Never,
44        }
45    }
46}
47
48/// Resolve a username to a UID, or parse a numeric UID.
49pub fn resolve_user(name: &str) -> Option<u32> {
50    if let Ok(uid) = name.parse::<u32>() {
51        return Some(uid);
52    }
53    let c_name = CString::new(name).ok()?;
54    let pw = unsafe { libc::getpwnam(c_name.as_ptr()) };
55    if pw.is_null() {
56        None
57    } else {
58        Some(unsafe { (*pw).pw_uid })
59    }
60}
61
62/// Resolve a group name to a GID, or parse a numeric GID.
63pub fn resolve_group(name: &str) -> Option<u32> {
64    if let Ok(gid) = name.parse::<u32>() {
65        return Some(gid);
66    }
67    let c_name = CString::new(name).ok()?;
68    let gr = unsafe { libc::getgrnam(c_name.as_ptr()) };
69    if gr.is_null() {
70        None
71    } else {
72        Some(unsafe { (*gr).gr_gid })
73    }
74}
75
76/// Convert a UID to a username string. Falls back to the numeric string.
77pub fn uid_to_name(uid: u32) -> String {
78    let pw = unsafe { libc::getpwuid(uid) };
79    if pw.is_null() {
80        return uid.to_string();
81    }
82    let name = unsafe { std::ffi::CStr::from_ptr((*pw).pw_name) };
83    name.to_string_lossy().into_owned()
84}
85
86/// Convert a GID to a group name string. Falls back to the numeric string.
87pub fn gid_to_name(gid: u32) -> String {
88    let gr = unsafe { libc::getgrgid(gid) };
89    if gr.is_null() {
90        return gid.to_string();
91    }
92    let name = unsafe { std::ffi::CStr::from_ptr((*gr).gr_name) };
93    name.to_string_lossy().into_owned()
94}
95
96/// Parse an ownership specification string.
97///
98/// Accepted formats:
99/// - `USER` -- set owner only
100/// - `USER:GROUP` or `USER.GROUP` -- set both (dot form is deprecated)
101/// - `USER:` -- set owner and group to that user's login group
102/// - `:GROUP` -- set group only
103/// - numeric IDs are accepted anywhere a name is accepted
104///
105/// Returns `(Option<uid>, Option<gid>)`.
106pub fn parse_owner_spec(spec: &str) -> Result<(Option<u32>, Option<u32>), String> {
107    if spec.is_empty() {
108        return Err("invalid spec: ''".to_string());
109    }
110
111    // Determine separator: prefer ':', fall back to '.' (deprecated)
112    let sep = if spec.contains(':') {
113        ':'
114    } else if spec.contains('.') {
115        '.'
116    } else {
117        // No separator -- just a user
118        let uid = resolve_user(spec).ok_or_else(|| format!("invalid user: '{}'", spec))?;
119        return Ok((Some(uid), None));
120    };
121
122    let idx = spec.find(sep).unwrap();
123    let user_part = &spec[..idx];
124    let group_part = &spec[idx + 1..];
125
126    let uid = if user_part.is_empty() {
127        None
128    } else {
129        Some(resolve_user(user_part).ok_or_else(|| format!("invalid user: '{}'", user_part))?)
130    };
131
132    let gid = if group_part.is_empty() {
133        if let Some(u) = uid {
134            // "USER:" means use the user's login group
135            let pw = unsafe { libc::getpwuid(u) };
136            if pw.is_null() {
137                // For numeric UIDs that don't map to a user, we can't resolve
138                // their login group -- GNU chown errors out here
139                return Err(format!("failed to get login group for uid '{}'", u));
140            }
141            Some(unsafe { (*pw).pw_gid })
142        } else {
143            None
144        }
145    } else {
146        Some(resolve_group(group_part).ok_or_else(|| format!("invalid group: '{}'", group_part))?)
147    };
148
149    Ok((uid, gid))
150}
151
152/// Get the owner and group of a reference file.
153pub fn get_reference_ids(path: &Path) -> io::Result<(u32, u32)> {
154    let meta = fs::metadata(path)?;
155    Ok((meta.uid(), meta.gid()))
156}
157
158/// Change the owner and/or group of a single file.
159///
160/// Returns `Ok(true)` if the ownership was actually changed,
161/// `Ok(false)` if it was already correct (or skipped due to `--from`).
162pub fn chown_file(
163    path: &Path,
164    uid: Option<u32>,
165    gid: Option<u32>,
166    config: &ChownConfig,
167) -> io::Result<bool> {
168    // Read current metadata to check --from filter and detect no-op
169    let meta = if config.no_dereference {
170        fs::symlink_metadata(path)?
171    } else {
172        fs::metadata(path)?
173    };
174
175    // --from filter: skip if current owner/group does not match
176    if let Some(from_uid) = config.from_owner {
177        if meta.uid() != from_uid {
178            return Ok(false);
179        }
180    }
181    if let Some(from_gid) = config.from_group {
182        if meta.gid() != from_gid {
183            return Ok(false);
184        }
185    }
186
187    let new_uid = uid.map(|u| u as libc::uid_t).unwrap_or(u32::MAX);
188    let new_gid = gid.map(|g| g as libc::gid_t).unwrap_or(u32::MAX);
189
190    // Detect no-op
191    let current_uid = meta.uid();
192    let current_gid = meta.gid();
193    let uid_match = uid.is_none() || uid == Some(current_uid);
194    let gid_match = gid.is_none() || gid == Some(current_gid);
195    if uid_match && gid_match {
196        // No change needed
197        if config.verbose {
198            print_verbose(path, uid, gid, false);
199        }
200        return Ok(false);
201    }
202
203    // Use -1 (u32::MAX cast) to mean "don't change" for lchown/chown
204    let c_path = CString::new(path.as_os_str().as_encoded_bytes())
205        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
206
207    let ret = if config.no_dereference {
208        unsafe { libc::lchown(c_path.as_ptr(), new_uid, new_gid) }
209    } else {
210        unsafe { libc::chown(c_path.as_ptr(), new_uid, new_gid) }
211    };
212
213    if ret != 0 {
214        return Err(io::Error::last_os_error());
215    }
216
217    if config.verbose || config.changes {
218        print_verbose(path, uid, gid, true);
219    }
220
221    Ok(true)
222}
223
224/// Print a verbose message about an ownership change.
225fn print_verbose(path: &Path, uid: Option<u32>, gid: Option<u32>, changed: bool) {
226    let action = if changed { "changed" } else { "retained" };
227    let display = path.display();
228    match (uid, gid) {
229        (Some(u), Some(g)) => {
230            eprintln!(
231                "ownership of '{}' {} to {}:{}",
232                display,
233                action,
234                uid_to_name(u),
235                gid_to_name(g)
236            );
237        }
238        (Some(u), None) => {
239            eprintln!(
240                "ownership of '{}' {} to {}",
241                display,
242                action,
243                uid_to_name(u)
244            );
245        }
246        (None, Some(g)) => {
247            eprintln!("group of '{}' {} to {}", display, action, gid_to_name(g));
248        }
249        (None, None) => {
250            eprintln!("ownership of '{}' {}", display, action);
251        }
252    }
253}
254
255/// Recursively change ownership of a directory tree.
256/// Uses rayon for parallel processing when verbose/changes output is not needed.
257pub fn chown_recursive(
258    path: &Path,
259    uid: Option<u32>,
260    gid: Option<u32>,
261    config: &ChownConfig,
262    is_command_line_arg: bool,
263    tool_name: &str,
264) -> i32 {
265    // Preserve-root check
266    if config.preserve_root && path == Path::new("/") {
267        eprintln!(
268            "{}: it is dangerous to operate recursively on '/'",
269            tool_name
270        );
271        eprintln!(
272            "{}: use --no-preserve-root to override this failsafe",
273            tool_name
274        );
275        return 1;
276    }
277
278    // For non-verbose mode, use parallel traversal
279    if !config.verbose && !config.changes {
280        let error_count = std::sync::atomic::AtomicI32::new(0);
281        chown_recursive_parallel(
282            path,
283            uid,
284            gid,
285            config,
286            is_command_line_arg,
287            tool_name,
288            &error_count,
289        );
290        return error_count.load(std::sync::atomic::Ordering::Relaxed);
291    }
292
293    // Sequential path for verbose/changes mode
294    let mut errors = 0;
295
296    if let Err(e) = chown_file(path, uid, gid, config) {
297        if !config.silent {
298            eprintln!(
299                "{}: changing ownership of '{}': {}",
300                tool_name,
301                path.display(),
302                crate::common::io_error_msg(&e)
303            );
304        }
305        errors += 1;
306    }
307
308    let should_follow = match config.symlink_follow {
309        SymlinkFollow::Always => true,
310        SymlinkFollow::CommandLine => is_command_line_arg,
311        SymlinkFollow::Never => false,
312    };
313
314    let is_dir = if should_follow {
315        fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
316    } else {
317        fs::symlink_metadata(path)
318            .map(|m| m.is_dir())
319            .unwrap_or(false)
320    };
321
322    if is_dir {
323        let entries = match fs::read_dir(path) {
324            Ok(entries) => entries,
325            Err(e) => {
326                if !config.silent {
327                    eprintln!(
328                        "{}: cannot read directory '{}': {}",
329                        tool_name,
330                        path.display(),
331                        crate::common::io_error_msg(&e)
332                    );
333                }
334                return errors + 1;
335            }
336        };
337        for entry in entries {
338            match entry {
339                Ok(entry) => {
340                    errors += chown_recursive(&entry.path(), uid, gid, config, false, tool_name);
341                }
342                Err(e) => {
343                    if !config.silent {
344                        eprintln!(
345                            "{}: cannot access entry in '{}': {}",
346                            tool_name,
347                            path.display(),
348                            crate::common::io_error_msg(&e)
349                        );
350                    }
351                    errors += 1;
352                }
353            }
354        }
355    }
356
357    errors
358}
359
360/// Parallel recursive chown using rayon.
361fn chown_recursive_parallel(
362    path: &Path,
363    uid: Option<u32>,
364    gid: Option<u32>,
365    config: &ChownConfig,
366    is_command_line_arg: bool,
367    tool_name: &str,
368    error_count: &std::sync::atomic::AtomicI32,
369) {
370    if let Err(e) = chown_file(path, uid, gid, config) {
371        if !config.silent {
372            eprintln!(
373                "{}: changing ownership of '{}': {}",
374                tool_name,
375                path.display(),
376                crate::common::io_error_msg(&e)
377            );
378        }
379        error_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
380    }
381
382    let should_follow = match config.symlink_follow {
383        SymlinkFollow::Always => true,
384        SymlinkFollow::CommandLine => is_command_line_arg,
385        SymlinkFollow::Never => false,
386    };
387
388    let is_dir = if should_follow {
389        fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
390    } else {
391        fs::symlink_metadata(path)
392            .map(|m| m.is_dir())
393            .unwrap_or(false)
394    };
395
396    if is_dir {
397        let entries = match fs::read_dir(path) {
398            Ok(entries) => entries,
399            Err(e) => {
400                if !config.silent {
401                    eprintln!(
402                        "{}: cannot read directory '{}': {}",
403                        tool_name,
404                        path.display(),
405                        crate::common::io_error_msg(&e)
406                    );
407                }
408                error_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
409                return;
410            }
411        };
412        let entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
413
414        use rayon::prelude::*;
415        entries.par_iter().for_each(|entry| {
416            chown_recursive_parallel(
417                &entry.path(),
418                uid,
419                gid,
420                config,
421                false,
422                tool_name,
423                error_count,
424            );
425        });
426    }
427}