Skip to content

Commit

Permalink
Merge pull request #374 from JRF63/ftw-chgrp
Browse files Browse the repository at this point in the history
Finish chgrp
  • Loading branch information
jgarzik authored Nov 3, 2024
2 parents 1cf04d8 + 864b066 commit 51fbfb8
Show file tree
Hide file tree
Showing 2 changed files with 571 additions and 47 deletions.
172 changes: 125 additions & 47 deletions tree/chgrp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,36 @@
// file in the root directory of this project.
// SPDX-License-Identifier: MIT
//
// TODO:
// - implement -h, -H, -L, -P
//

mod common;

use self::common::error_string;
use clap::Parser;
use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory};
use std::ffi::CString;
use std::path::Path;
use std::{fs, io};
use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory};
use std::{cell::RefCell, ffi::CString, io, os::unix::fs::MetadataExt};

/// chgrp - change file group ownership
#[derive(Parser)]
#[command(version, about)]
#[command(version, about, disable_help_flag = true)]
struct Args {
#[arg(long, action = clap::ArgAction::HelpLong)] // Bec. help clashes with -h
help: Option<bool>,

/// Change symbolic links, rather than the files they point to
#[arg(short = 'h', long)]
#[arg(short = 'h', long, default_value_t = false)]
no_derereference: bool,

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

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

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

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

fn chgrp_file(filename: &str, gid: u32, recurse: bool) -> Result<(), io::Error> {
let path = Path::new(filename);
let metadata = fs::metadata(path)?;

// recurse into directories
if metadata.is_dir() && recurse {
for entry in fs::read_dir(path)? {
let entry = entry?;
let entry_path = entry.path();
let entry_filename = entry_path.to_str().unwrap();
chgrp_file(entry_filename, gid, recurse)?;
}
}
fn chgrp_file(filename: &str, gid: Option<u32>, args: &Args) -> bool {
let recurse = args.recurse;
let no_derereference = args.no_derereference;

// change the group
let pathstr = CString::new(filename).unwrap();
unsafe {
if libc::chown(pathstr.as_ptr(), libc::geteuid(), gid) != 0 {
return Err(io::Error::last_os_error());
}
}
let terminate = RefCell::new(false);

ftw::traverse_directory(
filename,
|entry| {
if *terminate.borrow() {
return Ok(false);
}

Ok(())
let md = entry.metadata().unwrap();

// According to the spec:
// "The user ID of the file shall be used as the owner argument."
let uid = md.uid();

// Don't change the group ID if the group argument is empty
let gid = gid.unwrap_or(libc::gid_t::MAX);

let ret = unsafe {
libc::fchownat(
entry.dir_fd(),
entry.file_name().as_ptr(),
uid,
gid,
// Default is to change the file that the symbolic link points to unless the
// -h flag is specified.
if no_derereference {
libc::AT_SYMLINK_NOFOLLOW
} else {
0
},
)
};
if ret != 0 {
let e = io::Error::last_os_error();
let err_str = match e.kind() {
io::ErrorKind::PermissionDenied => {
gettext!("cannot access '{}': {}", entry.path(), error_string(&e))
}
_ => {
gettext!("changing group of '{}': {}", entry.path(), error_string(&e))
}
};
eprintln!("chgrp: {}", err_str);
*terminate.borrow_mut() = true;
return Err(());
}

Ok(recurse)
},
|_| Ok(()), // Do nothing on `postprocess_dir`
|entry, error| {
let e = error.inner();
let err_str = match e.kind() {
io::ErrorKind::PermissionDenied => {
gettext!(
"cannot read directory '{}': {}",
entry.path(),
error_string(&e)
)
}
_ => {
gettext!("changing group of '{}': {}", entry.path(), error_string(&e))
}
};
eprintln!("chgrp: {}", err_str);
*terminate.borrow_mut() = true;
},
ftw::TraverseDirectoryOpts {
follow_symlinks_on_args: args.follow_cli,
follow_symlinks: args.follow_symlinks,
..Default::default()
},
);

let failed = *terminate.borrow();
!failed
}

// lookup string group by name, or parse numeric group ID
fn parse_group(group: &str) -> Result<u32, &'static str> {
fn parse_group(group: &str) -> Result<Option<u32>, String> {
// empty strings are accepted without errors
if group.is_empty() {
return Ok(None);
}

match group.parse::<u32>() {
Ok(gid) => Ok(gid),
Ok(gid) => Ok(Some(gid)),
Err(_) => {
// lookup group by name
let group_cstr = CString::new(group).unwrap();
let group = unsafe { libc::getgrnam(group_cstr.as_ptr()) };
if group.is_null() {
return Err("group not found");
let group_st = unsafe { libc::getgrnam(group_cstr.as_ptr()) };
if group_st.is_null() {
let err_str = gettext!("invalid group: '{}'", group);
return Err(err_str);
}

let gid = unsafe { (*group).gr_gid };
Ok(gid)
let gid = unsafe { (*group_st).gr_gid };
Ok(Some(gid))
}
}
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
// parse command line arguments
let mut args = Args::parse();

// Enable `no_derereference` if `-R` is enabled without either `-H` or `-L`
if args.recurse && !(args.follow_cli || args.follow_symlinks) {
args.no_derereference = true;
}

// initialize translations
setlocale(LocaleCategory::LcAll, "");
textdomain(env!("PROJECT_NAME"))?;
bind_textdomain_codeset(env!("PROJECT_NAME"), "UTF-8")?;

let args = Args::parse();

let mut exit_code = 0;

// lookup string group by name, or parse numeric group ID
let gid = parse_group(&args.group)?;
let gid = match parse_group(&args.group) {
Ok(gid) => gid,
Err(e) => {
eprintln!("chgrp: {}", e);
std::process::exit(1);
}
};

// apply the group to each file
for filename in &args.files {
if let Err(e) = chgrp_file(filename, gid, args.recurse) {
let success = chgrp_file(filename, gid, &args);
if !success {
exit_code = 1;
eprintln!("{}: {}", filename, e);
}
}

Expand Down
Loading

0 comments on commit 51fbfb8

Please sign in to comment.