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 Ok((None, None));
110 }
111
112 let sep = if spec.contains(':') {
114 ':'
115 } else if spec.contains('.') {
116 '.'
117 } else {
118 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 let pw = unsafe { libc::getpwuid(u) };
137 if pw.is_null() {
138 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
153pub fn get_reference_ids(path: &Path) -> io::Result<(u32, u32)> {
155 let meta = fs::metadata(path)?;
156 Ok((meta.uid(), meta.gid()))
157}
158
159pub fn chown_file(
164 path: &Path,
165 uid: Option<u32>,
166 gid: Option<u32>,
167 config: &ChownConfig,
168) -> io::Result<bool> {
169 let meta = if config.no_dereference {
171 fs::symlink_metadata(path)?
172 } else {
173 fs::metadata(path)?
174 };
175
176 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 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 if config.verbose {
199 print_verbose(path, uid, gid, false);
200 }
201 return Ok(false);
202 }
203
204 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
225fn 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
256pub 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 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 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 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
361fn 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}