1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

Merge pull request #3438 from jfinkels/chown-nonexistent-user-id

chown: allow setting arbitrary numeric user ID
This commit is contained in:
Sylvestre Ledru 2022-05-14 09:20:01 +02:00 committed by GitHub
commit 40095e1b50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 111 additions and 31 deletions

View file

@ -168,17 +168,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 /// The `spec` can be of the form:
/// but ...
/// it can user.name:groupname
/// or username.groupname
/// ///
/// # Arguments /// * `"owner:group"`,
/// * `"owner"`,
/// * `":group"`,
/// ///
/// * `spec` - The input from the user /// and the owner or group can be specified either as an ID or a
/// * `sep` - Should be ':' or '.' /// 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>)> { fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
assert!(['.', ':'].contains(&sep)); assert!(['.', ':'].contains(&sep));
let mut args = spec.splitn(2, sep); let mut args = spec.splitn(2, sep);
@ -198,10 +199,17 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
// So, try to parse it this way // So, try to parse it this way
return parse_spec(spec, '.'); return parse_spec(spec, '.');
} else { } else {
return Err(USimpleError::new( // It's possible that the `user` string contains a
1, // numeric user ID, in which case, we respect that.
format!("invalid user: {}", spec.quote()), match user.parse() {
)); Ok(uid) => uid,
Err(_) => {
return Err(USimpleError::new(
1,
format!("invalid user: {}", spec.quote()),
))
}
}
} }
} }
}) })
@ -209,11 +217,18 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
None None
}; };
let gid = if !group.is_empty() { let gid = if !group.is_empty() {
Some( Some(match Group::locate(group) {
Group::locate(group) Ok(g) => g.gid,
.map_err(|_| USimpleError::new(1, format!("invalid group: {}", spec.quote())))? Err(_) => match group.parse() {
.gid, Ok(gid) => gid,
) Err(_) => {
return Err(USimpleError::new(
1,
format!("invalid group: {}", spec.quote()),
));
}
},
})
} else { } else {
None None
}; };
@ -232,4 +247,17 @@ mod test {
assert!(format!("{}", parse_spec("::", ':').err().unwrap()).starts_with("invalid group: ")); assert!(format!("{}", parse_spec("::", ':').err().unwrap()).starts_with("invalid group: "));
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)))
));
}
} }

View file

@ -92,22 +92,25 @@ pub fn wrap_chown<P: AsRef<Path>>(
); );
if level == VerbosityLevel::Verbose { if level == VerbosityLevel::Verbose {
out = if verbosity.groups_only { out = if verbosity.groups_only {
let gid = meta.gid();
format!( format!(
"{}\nfailed to change group of {} from {} to {}", "{}\nfailed to change group of {} from {} to {}",
out, out,
path.quote(), path.quote(),
entries::gid2grp(meta.gid()).unwrap(), entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
entries::gid2grp(dest_gid).unwrap() entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
) )
} else { } else {
let uid = meta.uid();
let gid = meta.gid();
format!( format!(
"{}\nfailed to change ownership of {} from {}:{} to {}:{}", "{}\nfailed to change ownership of {} from {}:{} to {}:{}",
out, out,
path.quote(), path.quote(),
entries::uid2usr(meta.uid()).unwrap(), entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()),
entries::gid2grp(meta.gid()).unwrap(), entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
entries::uid2usr(dest_uid).unwrap(), entries::uid2usr(dest_uid).unwrap_or_else(|_| dest_uid.to_string()),
entries::gid2grp(dest_gid).unwrap() entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
) )
}; };
}; };
@ -119,21 +122,24 @@ pub fn wrap_chown<P: AsRef<Path>>(
if changed { if changed {
match verbosity.level { match verbosity.level {
VerbosityLevel::Changes | VerbosityLevel::Verbose => { VerbosityLevel::Changes | VerbosityLevel::Verbose => {
let gid = meta.gid();
out = if verbosity.groups_only { out = if verbosity.groups_only {
format!( format!(
"changed group of {} from {} to {}", "changed group of {} from {} to {}",
path.quote(), path.quote(),
entries::gid2grp(meta.gid()).unwrap(), entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
entries::gid2grp(dest_gid).unwrap() entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
) )
} else { } else {
let gid = meta.gid();
let uid = meta.uid();
format!( format!(
"changed ownership of {} from {}:{} to {}:{}", "changed ownership of {} from {}:{} to {}:{}",
path.quote(), path.quote(),
entries::uid2usr(meta.uid()).unwrap(), entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()),
entries::gid2grp(meta.gid()).unwrap(), entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
entries::uid2usr(dest_uid).unwrap(), entries::uid2usr(dest_uid).unwrap_or_else(|_| dest_uid.to_string()),
entries::gid2grp(dest_gid).unwrap() entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
) )
}; };
} }
@ -150,8 +156,8 @@ pub fn wrap_chown<P: AsRef<Path>>(
format!( format!(
"ownership of {} retained as {}:{}", "ownership of {} retained as {}:{}",
path.quote(), path.quote(),
entries::uid2usr(dest_uid).unwrap(), entries::uid2usr(dest_uid).unwrap_or_else(|_| dest_uid.to_string()),
entries::gid2grp(dest_gid).unwrap() entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string())
) )
}; };
} }

View file

@ -418,6 +418,29 @@ fn test_chown_only_user_id() {
.stderr_contains(&"failed to change"); .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] #[test]
// FixME: stderr = chown: ownership of 'test_chown_file1' retained as cuuser:wheel // FixME: stderr = chown: ownership of 'test_chown_file1' retained as cuuser:wheel
#[cfg(not(target_os = "freebsd"))] #[cfg(not(target_os = "freebsd"))]
@ -461,6 +484,29 @@ fn test_chown_only_group_id() {
.stderr_contains(&"failed to change"); .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] #[test]
fn test_chown_owner_group_id() { fn test_chown_owner_group_id() {
// test chown 1111:1111 file.txt // test chown 1111:1111 file.txt