1use std::ffi::CString;
2use std::fs;
3use std::io;
4use std::os::unix::fs::MetadataExt;
5use std::path::Path;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SymlinkFollow {
10 CommandLine,
12 Always,
14 Never,
16}
17
18#[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
48pub 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
62pub 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
76pub 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
86pub 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
96pub 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 let sep = if spec.contains(':') {
113 ':'
114 } else if spec.contains('.') {
115 '.'
116 } else {
117 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 let pw = unsafe { libc::getpwuid(u) };
136 if pw.is_null() {
137 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
152pub fn get_reference_ids(path: &Path) -> io::Result<(u32, u32)> {
154 let meta = fs::metadata(path)?;
155 Ok((meta.uid(), meta.gid()))
156}
157
158pub fn chown_file(
163 path: &Path,
164 uid: Option<u32>,
165 gid: Option<u32>,
166 config: &ChownConfig,
167) -> io::Result<bool> {
168 let meta = if config.no_dereference {
170 fs::symlink_metadata(path)?
171 } else {
172 fs::metadata(path)?
173 };
174
175 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 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 if config.verbose {
198 print_verbose(path, uid, gid, false);
199 }
200 return Ok(false);
201 }
202
203 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
224fn 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
255pub 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 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 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 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
360fn 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}