Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 45 additions & 17 deletions src/uu/chown/src/chown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,18 @@ pub fn uu_app<'a>() -> Command<'a> {
)
}

/// Parse the username and groupname
/// Parse the owner/group specifier string into a user ID and a group ID.
///
/// In theory, it should be username:groupname
/// but ...
/// it can user.name:groupname
/// or username.groupname
/// The `spec` can be of the form:
///
/// # Arguments
/// * `"owner:group"`,
/// * `"owner"`,
/// * `":group"`,
///
/// * `spec` - The input from the user
/// * `sep` - Should be ':' or '.'
/// and the owner or group can be specified either as an ID or a
/// name. The `sep` argument specifies which character to use as a
/// separator between the owner and group; calling code should set
/// this to `':'`.
fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
assert!(['.', ':'].contains(&sep));
let mut args = spec.splitn(2, sep);
Expand All @@ -197,22 +198,36 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
// So, try to parse it this way
return parse_spec(spec, '.');
} else {
return Err(USimpleError::new(
1,
format!("invalid user: {}", spec.quote()),
));
// It's possible that the `user` string contains a
// numeric user ID, in which case, we respect that.
match user.parse() {
Ok(uid) => uid,
Err(_) => {
return Err(USimpleError::new(
1,
format!("invalid user: {}", spec.quote()),
))
}
}
}
}
})
} else {
None
};
let gid = if !group.is_empty() {
Some(
Group::locate(group)
.map_err(|_| USimpleError::new(1, format!("invalid group: {}", spec.quote())))?
.gid,
)
Some(match Group::locate(group) {
Ok(g) => g.gid,
Err(_) => match group.parse() {
Ok(gid) => gid,
Err(_) => {
return Err(USimpleError::new(
1,
format!("invalid group: {}", spec.quote()),
));
}
},
})
} else {
None
};
Expand All @@ -231,4 +246,17 @@ mod test {
assert!(format!("{}", parse_spec("::", ':').err().unwrap()).starts_with("invalid group: "));
assert!(format!("{}", parse_spec("..", ':').err().unwrap()).starts_with("invalid group: "));
}

/// Test for parsing IDs that don't correspond to a named user or group.
#[test]
fn test_parse_spec_nameless_ids() {
// This assumes that there is no named user with ID 12345.
assert!(matches!(parse_spec("12345", ':'), Ok((Some(12345), None))));
// This assumes that there is no named group with ID 54321.
assert!(matches!(parse_spec(":54321", ':'), Ok((None, Some(54321)))));
assert!(matches!(
parse_spec("12345:54321", ':'),
Ok((Some(12345), Some(54321)))
));
}
}
34 changes: 20 additions & 14 deletions src/uucore/src/lib/features/perms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,25 @@ pub fn wrap_chown<P: AsRef<Path>>(
);
if level == VerbosityLevel::Verbose {
out = if verbosity.groups_only {
let gid = meta.gid();
format!(
"{}\nfailed to change group of {} from {} to {}",
out,
path.quote(),
entries::gid2grp(meta.gid()).unwrap(),
entries::gid2grp(dest_gid).unwrap()
entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
)
} else {
let uid = meta.uid();
let gid = meta.gid();
format!(
"{}\nfailed to change ownership of {} from {}:{} to {}:{}",
out,
path.quote(),
entries::uid2usr(meta.uid()).unwrap(),
entries::gid2grp(meta.gid()).unwrap(),
entries::uid2usr(dest_uid).unwrap(),
entries::gid2grp(dest_gid).unwrap()
entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()),
entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
entries::uid2usr(dest_uid).unwrap_or_else(|_| dest_uid.to_string()),
entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
)
};
};
Expand All @@ -119,21 +122,24 @@ pub fn wrap_chown<P: AsRef<Path>>(
if changed {
match verbosity.level {
VerbosityLevel::Changes | VerbosityLevel::Verbose => {
let gid = meta.gid();
out = if verbosity.groups_only {
format!(
"changed group of {} from {} to {}",
path.quote(),
entries::gid2grp(meta.gid()).unwrap(),
entries::gid2grp(dest_gid).unwrap()
entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
)
} else {
let gid = meta.gid();
let uid = meta.uid();
format!(
"changed ownership of {} from {}:{} to {}:{}",
path.quote(),
entries::uid2usr(meta.uid()).unwrap(),
entries::gid2grp(meta.gid()).unwrap(),
entries::uid2usr(dest_uid).unwrap(),
entries::gid2grp(dest_gid).unwrap()
entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()),
entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
entries::uid2usr(dest_uid).unwrap_or_else(|_| dest_uid.to_string()),
entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
)
};
}
Expand All @@ -150,8 +156,8 @@ pub fn wrap_chown<P: AsRef<Path>>(
format!(
"ownership of {} retained as {}:{}",
path.quote(),
entries::uid2usr(dest_uid).unwrap(),
entries::gid2grp(dest_gid).unwrap()
entries::uid2usr(dest_uid).unwrap_or_else(|_| dest_uid.to_string()),
entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
)
};
}
Expand Down
46 changes: 46 additions & 0 deletions tests/by-util/test_chown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,29 @@ fn test_chown_only_user_id() {
.stderr_contains(&"failed to change");
}

/// Test for setting the owner to a user ID for a user that does not exist.
///
/// For example:
///
/// $ touch f && chown 12345 f
///
/// succeeds with exit status 0 and outputs nothing. The owner of the
/// file is set to 12345, even though no user with that ID exists.
///
/// This test must be run as root, because only the root user can
/// transfer ownership of a file.
#[test]
fn test_chown_only_user_id_nonexistent_user() {
let ts = TestScenario::new(util_name!());
let at = ts.fixtures.clone();
at.touch("f");
if let Ok(result) = run_ucmd_as_root(&ts, &["12345", "f"]) {
result.success().no_stdout().no_stderr();
} else {
print!("Test skipped; requires root user");
}
}

#[test]
// FixME: stderr = chown: ownership of 'test_chown_file1' retained as cuuser:wheel
#[cfg(not(target_os = "freebsd"))]
Expand Down Expand Up @@ -461,6 +484,29 @@ fn test_chown_only_group_id() {
.stderr_contains(&"failed to change");
}

/// Test for setting the group to a group ID for a group that does not exist.
///
/// For example:
///
/// $ touch f && chown :12345 f
///
/// succeeds with exit status 0 and outputs nothing. The group of the
/// file is set to 12345, even though no group with that ID exists.
///
/// This test must be run as root, because only the root user can
/// transfer ownership of a file.
#[test]
fn test_chown_only_group_id_nonexistent_group() {
let ts = TestScenario::new(util_name!());
let at = ts.fixtures.clone();
at.touch("f");
if let Ok(result) = run_ucmd_as_root(&ts, &[":12345", "f"]) {
result.success().no_stdout().no_stderr();
} else {
print!("Test skipped; requires root user");
}
}

#[test]
fn test_chown_owner_group_id() {
// test chown 1111:1111 file.txt
Expand Down