Skip to content

Commit

Permalink
Merge pull request #3438 from jfinkels/chown-nonexistent-user-id
Browse files Browse the repository at this point in the history
chown: allow setting arbitrary numeric user ID
  • Loading branch information
sylvestre authored May 14, 2022
2 parents 9044d96 + 5713de4 commit 40095e1
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 31 deletions.
62 changes: 45 additions & 17 deletions src/uu/chown/src/chown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -198,22 +199,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 @@ -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)))
));
}
}
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

0 comments on commit 40095e1

Please sign in to comment.