Skip to content

Commit 51fbfb8

Browse files
authored
Merge pull request #374 from JRF63/ftw-chgrp
Finish chgrp
2 parents 1cf04d8 + 864b066 commit 51fbfb8

File tree

2 files changed

+571
-47
lines changed

2 files changed

+571
-47
lines changed

tree/chgrp.rs

Lines changed: 125 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,36 @@
66
// file in the root directory of this project.
77
// SPDX-License-Identifier: MIT
88
//
9-
// TODO:
10-
// - implement -h, -H, -L, -P
11-
//
129

10+
mod common;
11+
12+
use self::common::error_string;
1313
use clap::Parser;
14-
use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory};
15-
use std::ffi::CString;
16-
use std::path::Path;
17-
use std::{fs, io};
14+
use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory};
15+
use std::{cell::RefCell, ffi::CString, io, os::unix::fs::MetadataExt};
1816

1917
/// chgrp - change file group ownership
2018
#[derive(Parser)]
21-
#[command(version, about)]
19+
#[command(version, about, disable_help_flag = true)]
2220
struct Args {
21+
#[arg(long, action = clap::ArgAction::HelpLong)] // Bec. help clashes with -h
22+
help: Option<bool>,
23+
2324
/// Change symbolic links, rather than the files they point to
24-
#[arg(short = 'h', long)]
25+
#[arg(short = 'h', long, default_value_t = false)]
2526
no_derereference: bool,
2627

2728
/// Follow command line symlinks during -R recursion
28-
#[arg(short = 'H', long)]
29+
#[arg(short = 'H', overrides_with_all = ["follow_cli", "follow_symlinks", "follow_none"])]
2930
follow_cli: bool,
3031

3132
/// Follow symlinks during -R recursion
32-
#[arg(short = 'L', group = "deref")]
33-
dereference: bool,
33+
#[arg(short = 'L', overrides_with_all = ["follow_cli", "follow_symlinks", "follow_none"])]
34+
follow_symlinks: bool,
3435

3536
/// Never follow symlinks during -R recursion
36-
#[arg(short = 'P', group = "deref")]
37-
no_dereference2: bool,
37+
#[arg(short = 'P', overrides_with_all = ["follow_cli", "follow_symlinks", "follow_none"], default_value_t = true)]
38+
follow_none: bool,
3839

3940
/// Recursively change groups of directories and their contents
4041
#[arg(short, short_alias = 'R', long)]
@@ -47,66 +48,143 @@ struct Args {
4748
files: Vec<String>,
4849
}
4950

50-
fn chgrp_file(filename: &str, gid: u32, recurse: bool) -> Result<(), io::Error> {
51-
let path = Path::new(filename);
52-
let metadata = fs::metadata(path)?;
53-
54-
// recurse into directories
55-
if metadata.is_dir() && recurse {
56-
for entry in fs::read_dir(path)? {
57-
let entry = entry?;
58-
let entry_path = entry.path();
59-
let entry_filename = entry_path.to_str().unwrap();
60-
chgrp_file(entry_filename, gid, recurse)?;
61-
}
62-
}
51+
fn chgrp_file(filename: &str, gid: Option<u32>, args: &Args) -> bool {
52+
let recurse = args.recurse;
53+
let no_derereference = args.no_derereference;
6354

64-
// change the group
65-
let pathstr = CString::new(filename).unwrap();
66-
unsafe {
67-
if libc::chown(pathstr.as_ptr(), libc::geteuid(), gid) != 0 {
68-
return Err(io::Error::last_os_error());
69-
}
70-
}
55+
let terminate = RefCell::new(false);
56+
57+
ftw::traverse_directory(
58+
filename,
59+
|entry| {
60+
if *terminate.borrow() {
61+
return Ok(false);
62+
}
7163

72-
Ok(())
64+
let md = entry.metadata().unwrap();
65+
66+
// According to the spec:
67+
// "The user ID of the file shall be used as the owner argument."
68+
let uid = md.uid();
69+
70+
// Don't change the group ID if the group argument is empty
71+
let gid = gid.unwrap_or(libc::gid_t::MAX);
72+
73+
let ret = unsafe {
74+
libc::fchownat(
75+
entry.dir_fd(),
76+
entry.file_name().as_ptr(),
77+
uid,
78+
gid,
79+
// Default is to change the file that the symbolic link points to unless the
80+
// -h flag is specified.
81+
if no_derereference {
82+
libc::AT_SYMLINK_NOFOLLOW
83+
} else {
84+
0
85+
},
86+
)
87+
};
88+
if ret != 0 {
89+
let e = io::Error::last_os_error();
90+
let err_str = match e.kind() {
91+
io::ErrorKind::PermissionDenied => {
92+
gettext!("cannot access '{}': {}", entry.path(), error_string(&e))
93+
}
94+
_ => {
95+
gettext!("changing group of '{}': {}", entry.path(), error_string(&e))
96+
}
97+
};
98+
eprintln!("chgrp: {}", err_str);
99+
*terminate.borrow_mut() = true;
100+
return Err(());
101+
}
102+
103+
Ok(recurse)
104+
},
105+
|_| Ok(()), // Do nothing on `postprocess_dir`
106+
|entry, error| {
107+
let e = error.inner();
108+
let err_str = match e.kind() {
109+
io::ErrorKind::PermissionDenied => {
110+
gettext!(
111+
"cannot read directory '{}': {}",
112+
entry.path(),
113+
error_string(&e)
114+
)
115+
}
116+
_ => {
117+
gettext!("changing group of '{}': {}", entry.path(), error_string(&e))
118+
}
119+
};
120+
eprintln!("chgrp: {}", err_str);
121+
*terminate.borrow_mut() = true;
122+
},
123+
ftw::TraverseDirectoryOpts {
124+
follow_symlinks_on_args: args.follow_cli,
125+
follow_symlinks: args.follow_symlinks,
126+
..Default::default()
127+
},
128+
);
129+
130+
let failed = *terminate.borrow();
131+
!failed
73132
}
74133

75134
// lookup string group by name, or parse numeric group ID
76-
fn parse_group(group: &str) -> Result<u32, &'static str> {
135+
fn parse_group(group: &str) -> Result<Option<u32>, String> {
136+
// empty strings are accepted without errors
137+
if group.is_empty() {
138+
return Ok(None);
139+
}
140+
77141
match group.parse::<u32>() {
78-
Ok(gid) => Ok(gid),
142+
Ok(gid) => Ok(Some(gid)),
79143
Err(_) => {
80144
// lookup group by name
81145
let group_cstr = CString::new(group).unwrap();
82-
let group = unsafe { libc::getgrnam(group_cstr.as_ptr()) };
83-
if group.is_null() {
84-
return Err("group not found");
146+
let group_st = unsafe { libc::getgrnam(group_cstr.as_ptr()) };
147+
if group_st.is_null() {
148+
let err_str = gettext!("invalid group: '{}'", group);
149+
return Err(err_str);
85150
}
86151

87-
let gid = unsafe { (*group).gr_gid };
88-
Ok(gid)
152+
let gid = unsafe { (*group_st).gr_gid };
153+
Ok(Some(gid))
89154
}
90155
}
91156
}
92157

93158
fn main() -> Result<(), Box<dyn std::error::Error>> {
159+
// parse command line arguments
160+
let mut args = Args::parse();
161+
162+
// Enable `no_derereference` if `-R` is enabled without either `-H` or `-L`
163+
if args.recurse && !(args.follow_cli || args.follow_symlinks) {
164+
args.no_derereference = true;
165+
}
166+
167+
// initialize translations
94168
setlocale(LocaleCategory::LcAll, "");
95169
textdomain(env!("PROJECT_NAME"))?;
96170
bind_textdomain_codeset(env!("PROJECT_NAME"), "UTF-8")?;
97171

98-
let args = Args::parse();
99-
100172
let mut exit_code = 0;
101173

102174
// lookup string group by name, or parse numeric group ID
103-
let gid = parse_group(&args.group)?;
175+
let gid = match parse_group(&args.group) {
176+
Ok(gid) => gid,
177+
Err(e) => {
178+
eprintln!("chgrp: {}", e);
179+
std::process::exit(1);
180+
}
181+
};
104182

105183
// apply the group to each file
106184
for filename in &args.files {
107-
if let Err(e) = chgrp_file(filename, gid, args.recurse) {
185+
let success = chgrp_file(filename, gid, &args);
186+
if !success {
108187
exit_code = 1;
109-
eprintln!("{}: {}", filename, e);
110188
}
111189
}
112190

0 commit comments

Comments
 (0)