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:
commit
40095e1b50
3 changed files with 111 additions and 31 deletions
|
@ -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
|
||||
/// 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);
|
||||
|
@ -198,10 +199,17 @@ 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()),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -209,11 +217,18 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
|
|||
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
|
||||
};
|
||||
|
@ -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: "));
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
};
|
||||
};
|
||||
|
@ -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())
|
||||
)
|
||||
};
|
||||
}
|
||||
|
@ -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())
|
||||
)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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"))]
|
||||
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue