Skip to content
28 changes: 26 additions & 2 deletions src/uu/chown/src/chown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,16 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
assert!(['.', ':'].contains(&sep));
let mut args = spec.splitn(2, sep);
let user = args.next().unwrap_or("");
let group = args.next().unwrap_or("");
let mut implicit_group_is_user = false;
let group = args.next().map_or("", |g| {
if g.is_empty() && sep == ':' {
// argument ended with a colon, implicit group == user
implicit_group_is_user = true;
user
} else {
g
}
});

// dot separator: try as username first, fall back to owner.group (like GNU)
if sep == ':' && !spec.contains(':') && spec.contains('.') {
Expand All @@ -221,7 +230,10 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
let uid = parse_uid(user, spec)?;
let gid = parse_gid(group, spec)?;

if user.chars().next().is_some_and(char::is_numeric) && group.is_empty() && spec != user {
if user.chars().next().is_some_and(char::is_numeric)
&& (group.is_empty() || implicit_group_is_user)
&& spec != user
{
// if the arg starts with an id numeric value, the group isn't set but the separator is provided,
// we should fail with an error
return Err(USimpleError::new(
Expand Down Expand Up @@ -263,5 +275,17 @@ mod test {
parse_spec("12345:54321", ':'),
Ok((Some(12345), Some(54321)))
));
// Implicit group-is-user does not work with IDs
assert_eq!(
"chown-error-invalid-spec",
format!("{}", parse_spec("0:", ':').err().unwrap()),
);
}

/// root user uid/gid unresolvable in some environments
#[test]
#[cfg(target_os = "linux")]
fn test_parse_spec_named() {
assert!(matches!(parse_spec("root:", ':'), Ok((Some(0), Some(0)))));
}
}
62 changes: 52 additions & 10 deletions tests/by-util/test_chown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,46 @@ fn test_chown_only_owner_colon() {
let file1 = "test_chown_file1";
at.touch(file1);

scene
.ucmd()
.arg(format!("{user_name}:"))
.arg("--verbose")
.arg(file1)
.succeeds()
.stderr_contains("retained as");
let result = scene.cmd("id").arg("-gn").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return;
}
let group_name = String::from(result.stdout_str().trim());
assert!(!group_name.is_empty());

// In some test environments user and group are different, and the user
// is not allowed the file to its current group, in those cases, it should fail.
if group_name == user_name {
let success = scene
.ucmd()
.arg(format!("{user_name}:"))
.arg("--verbose")
.arg(file1)
.succeeds();
let stderr = success.stderr_str();
#[cfg(target_os = "openbsd")]
{
// Ugly hack because the openbsd runner has sticky file creation set to wheel for
// our directory, which make the group change when invoked
assert!(stderr.contains("retained as") || stderr.contains("changed ownership"));
}
#[cfg(not(target_os = "openbsd"))]
{
assert!(stderr.contains("retained as"));
}
} else {
let failure = scene
.ucmd()
.arg(format!("{user_name}:"))
.arg("--verbose")
.arg(file1)
.fails();
// Depending on the environment, it may be a forbidden group (failed to change) or group not existing (invalid group)
assert!(
failure.stderr_str().contains("failed to change")
|| failure.stderr_str().contains("invalid group")
);
}

scene
.ucmd()
Expand All @@ -150,13 +183,17 @@ fn test_chown_only_owner_colon() {
.stderr_contains("retained as")
.stderr_contains("warning: '.' should be ':'");

scene
let failure = scene
.ucmd()
.arg("root:")
.arg("--verbose")
.arg(file1)
.fails()
.stderr_contains("failed to change");
.fails();
// Depending on the environment, it may be a forbidden group (failed to change) or group not existing (invalid group)
assert!(
failure.stderr_str().contains("failed to change")
|| failure.stderr_str().contains("invalid group")
);
}

#[test]
Expand Down Expand Up @@ -210,6 +247,11 @@ fn test_chown_dot_separator_warning() {
result.stderr_str()
);

let result = scene.cmd("id").arg("-gn").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return;
}

// chown user: file should not warn
scene
.ucmd()
Expand Down
Loading