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        // GNU chown treats '' as a no-op (no owner/group change)
109        return Ok((None, None));
110    }
111
112    // Determine separator: prefer ':', fall back to '.' (deprecated)
113    let sep = if spec.contains(':') {
114        ':'
115    } else if spec.contains('.') {
116        '.'
117    } else {
118        // No separator -- just a user
119        let uid = resolve_user(spec).ok_or_else(|| format!("invalid user: '{}'", spec))?;
120        return Ok((Some(uid), None));
121    };
122
123    let idx = spec.find(sep).unwrap();
124    let user_part = &spec[..idx];
125    let group_part = &spec[idx + 1..];
126
127    let uid = if user_part.is_empty() {
128        None
129    } else {
130        Some(resolve_user(user_part).ok_or_else(|| format!("invalid user: '{}'", user_part))?)
131    };
132
133    let gid = if group_part.is_empty() {
134        if let Some(u) = uid {
135            // "USER:" means use the user's login group
136            let pw = unsafe { libc::getpwuid(u) };
137            if pw.is_null() {
138                // For numeric UIDs that don't map to a user, we can't resolve
139                // their login group -- GNU chown errors out here
140                return Err(format!("failed to get login group for uid '{}'", u));
141            }
142            Some(unsafe { (*pw).pw_gid })
143        } else {
144            None
145        }
146    } else {
147        Some(resolve_group(group_part).ok_or_else(|| format!("invalid group: '{}'", group_part))?)
148    };
149
150    Ok((uid, gid))
151}
152
153/// Get the owner and group of a reference file.
154pub fn get_reference_ids(path: &Path) -> io::Result<(u32, u32)> {
155    let meta = fs::metadata(path)?;
156    Ok((meta.uid(), meta.gid()))
157}
158
159/// Change the owner and/or group of a single file.
160///
161/// Returns `Ok(true)` if the ownership was actually changed,
162/// `Ok(false)` if it was already correct (or skipped due to `--from`).
163pub fn chown_file(
164    path: &Path,
165    uid: Option<u32>,
166    gid: Option<u32>,
167    config: &ChownConfig,
168) -> io::Result<bool> {
169    // Read current metadata to check --from filter and detect no-op
170    let meta = if config.no_dereference {
171        fs::symlink_metadata(path)?
172    } else {
173        fs::metadata(path)?
174    };
175
176    // --from filter: skip if current owner/group does not match
177    if let Some(from_uid) = config.from_owner {
178        if meta.uid() != from_uid {
179            return Ok(false);
180        }
181    }
182    if let Some(from_gid) = config.from_group {
183        if meta.gid() != from_gid {
184            return Ok(false);
185        }
186    }
187
188    let new_uid = uid.map(|u| u as libc::uid_t).unwrap_or(u32::MAX);
189    let new_gid = gid.map(|g| g as libc::gid_t).unwrap_or(u32::MAX);
190
191    // Detect no-op
192    let current_uid = meta.uid();
193    let current_gid = meta.gid();
194    let uid_match = uid.is_none() || uid == Some(current_uid);
195    let gid_match = gid.is_none() || gid == Some(current_gid);
196    if uid_match && gid_match {
197        // No change needed
198        if config.verbose {
199            print_verbose(path, uid, gid, false);
200        }
201        return Ok(false);
202    }
203
204    // Use -1 (u32::MAX cast) to mean "don't change" for lchown/chown
205    let c_path = CString::new(path.as_os_str().as_encoded_bytes())
206        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
207
208    let ret = if config.no_dereference {
209        unsafe { libc::lchown(c_path.as_ptr(), new_uid, new_gid) }
210    } else {
211        unsafe { libc::chown(c_path.as_ptr(), new_uid, new_gid) }
212    };
213
214    if ret != 0 {
215        return Err(io::Error::last_os_error());
216    }
217
218    if config.verbose || config.changes {
219        print_verbose(path, uid, gid, true);
220    }
221
222    Ok(true)
223}
224
225/// Print a verbose message about an ownership change.
226fn print_verbose(path: &Path, uid: Option<u32>, gid: Option<u32>, changed: bool) {
227    let action = if changed { "changed" } else { "retained" };
228    let display = path.display();
229    match (uid, gid) {
230        (Some(u), Some(g)) => {
231            eprintln!(
232                "ownership of '{}' {} to {}:{}",
233                display,
234                action,
235                uid_to_name(u),
236                gid_to_name(g)
237            );
238        }
239        (Some(u), None) => {
240            eprintln!(
241                "ownership of '{}' {} to {}",
242                display,
243                action,
244                uid_to_name(u)
245            );
246        }
247        (None, Some(g)) => {
248            eprintln!("group of '{}' {} to {}", display, action, gid_to_name(g));
249        }
250        (None, None) => {
251            eprintln!("ownership of '{}' {}", display, action);
252        }
253    }
254}
255
256/// Recursively change ownership of a directory tree.
257/// Uses rayon for parallel processing when verbose/changes output is not needed.
258pub fn chown_recursive(
259    path: &Path,
260    uid: Option<u32>,
261    gid: Option<u32>,
262    config: &ChownConfig,
263    is_command_line_arg: bool,
264    tool_name: &str,
265) -> i32 {
266    // Preserve-root check
267    if config.preserve_root && path == Path::new("/") {
268        eprintln!(
269            "{}: it is dangerous to operate recursively on '/'",
270            tool_name
271        );
272        eprintln!(
273            "{}: use --no-preserve-root to override this failsafe",
274            tool_name
275        );
276        return 1;
277    }
278
279    // For non-verbose mode, use parallel traversal
280    if !config.verbose && !config.changes {
281        let error_count = std::sync::atomic::AtomicI32::new(0);
282        chown_recursive_parallel(
283            path,
284            uid,
285            gid,
286            config,
287            is_command_line_arg,
288            tool_name,
289            &error_count,
290        );
291        return error_count.load(std::sync::atomic::Ordering::Relaxed);
292    }
293
294    // Sequential path for verbose/changes mode
295    let mut errors = 0;
296
297    if let Err(e) = chown_file(path, uid, gid, config) {
298        if !config.silent {
299            eprintln!(
300                "{}: changing ownership of '{}': {}",
301                tool_name,
302                path.display(),
303                crate::common::io_error_msg(&e)
304            );
305        }
306        errors += 1;
307    }
308
309    let should_follow = match config.symlink_follow {
310        SymlinkFollow::Always => true,
311        SymlinkFollow::CommandLine => is_command_line_arg,
312        SymlinkFollow::Never => false,
313    };
314
315    let is_dir = if should_follow {
316        fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
317    } else {
318        fs::symlink_metadata(path)
319            .map(|m| m.is_dir())
320            .unwrap_or(false)
321    };
322
323    if is_dir {
324        let entries = match fs::read_dir(path) {
325            Ok(entries) => entries,
326            Err(e) => {
327                if !config.silent {
328                    eprintln!(
329                        "{}: cannot read directory '{}': {}",
330                        tool_name,
331                        path.display(),
332                        crate::common::io_error_msg(&e)
333                    );
334                }
335                return errors + 1;
336            }
337        };
338        for entry in entries {
339            match entry {
340                Ok(entry) => {
341                    errors += chown_recursive(&entry.path(), uid, gid, config, false, tool_name);
342                }
343                Err(e) => {
344                    if !config.silent {
345                        eprintln!(
346                            "{}: cannot access entry in '{}': {}",
347                            tool_name,
348                            path.display(),
349                            crate::common::io_error_msg(&e)
350                        );
351                    }
352                    errors += 1;
353                }
354            }
355        }
356    }
357
358    errors
359}
360
361/// Parallel recursive chown using rayon.
362fn chown_recursive_parallel(
363    path: &Path,
364    uid: Option<u32>,
365    gid: Option<u32>,
366    config: &ChownConfig,
367    is_command_line_arg: bool,
368    tool_name: &str,
369    error_count: &std::sync::atomic::AtomicI32,
370) {
371    if let Err(e) = chown_file(path, uid, gid, config) {
372        if !config.silent {
373            eprintln!(
374                "{}: changing ownership of '{}': {}",
375                tool_name,
376                path.display(),
377                crate::common::io_error_msg(&e)
378            );
379        }
380        error_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
381    }
382
383    let should_follow = match config.symlink_follow {
384        SymlinkFollow::Always => true,
385        SymlinkFollow::CommandLine => is_command_line_arg,
386        SymlinkFollow::Never => false,
387    };
388
389    let is_dir = if should_follow {
390        fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
391    } else {
392        fs::symlink_metadata(path)
393            .map(|m| m.is_dir())
394            .unwrap_or(false)
395    };
396
397    if is_dir {
398        let entries = match fs::read_dir(path) {
399            Ok(entries) => entries,
400            Err(e) => {
401                if !config.silent {
402                    eprintln!(
403                        "{}: cannot read directory '{}': {}",
404                        tool_name,
405                        path.display(),
406                        crate::common::io_error_msg(&e)
407                    );
408                }
409                error_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
410                return;
411            }
412        };
413        let entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
414
415        use rayon::prelude::*;
416        entries.par_iter().for_each(|entry| {
417            chown_recursive_parallel(
418                &entry.path(),
419                uid,
420                gid,
421                config,
422                false,
423                tool_name,
424                error_count,
425            );
426        });
427    }
428}