Part 2. Looking up users (20 points)
In this part, you’re going to implement a function username()
which takes a
user ID (UID) and returns a string containing the username.
The libc function you called in Part 1 was very simple: It took no arguments and it could never fail. The libc function you’ll call in this part is much more complicated.
POSIX-compatible operating systems provide functions to get information
about a user. Take a look at the man page for
getpwnam_r(3)
(this is in section 3 of the manual which is the “Library Functions Manual”).
The four functions described all behave similarly: They look up a user in the
user database. getpwnam()
, and getpwnam_r()
look up a user given a user
name whereas getpwuid()
and getpwuid_r()
look up a user given a UID.
The information returned by these functions is a passwd
structure. In C, this structure looks like this.
struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
Here’s the same definition in
Rust,
from the libc
crate.
#![allow(unused)] fn main() { type c_char = i8; type uid_t = u32; type gid_t = u32; #[repr(C)] pub struct passwd { pub pw_name: *mut c_char, pub pw_passwd: *mut c_char, pub pw_uid: uid_t, pub pw_gid: gid_t, pub pw_gecos: *mut c_char, pub pw_dir: *mut c_char, pub pw_shell: *mut c_char, } }
The interesting types here are the string type here is *mut c_char
. This is
a raw mutable pointer to a C character (a character in C is just a byte). In
fact, it’s a raw mutable pointer to a sequence of nonzero characters followed
by a zero character (meaning a byte with value 0, '\0'
rather than the
digit '0'
which has value 48).
A raw pointer in Rust is essentially the same thing as a reference except without the safety guarantees: In particular, a pointer need not point at a valid, initialized object of its type whereas a reference always points to a valid, initialized object.
Just as there are two types of references, &T
, and &mut T
, there are two
types of pointers, *const T
and *mut T
. A const
pointer is used when the
thing it is pointing to will not change (similar to &
) whereas a mut
pointer is used when the thing it is pointing to may be changed (similar to
&mut
).
It’s always safe to create a *const T
from a &T
or a *mut T
from a &mut T
.
Here’s an example.
#![allow(unused)] fn main() { let x: i32 = 10; let x_ptr = &x as *const _; let mut y: i32 = 20; let y_ptr = &mut y as *mut _; println!("Address of x: {x_ptr:?}"); println!("Address of y: {y_ptr:?}"); }
If you click the run button, you’ll see that addresses are just numbers. If you think of your computer’s memory as one giant array of bytes ranging from index 0 up to some n, then a pointer to some value in memory is just the index of the first byte of the value in that big array of bytes. (This isn’t completely true for several reasons. Nevertheless, if you think of a pointer as just the index of the first byte of your value, your mental model of how pointers work will be 95% correct.)
The operation that isn’t safe is dereferencing the pointer. Here’s an example:
#![allow(unused)] fn main() { let mut x: i32 = 10; let x_ref = &mut x; let y = *x_ref; // Dereferencing the reference, always safe. Creates y with value 10. *x_ref = 20; // Dereferencing the reference, always safe. Assigns 20 to x. let x_ptr = x_ref as *mut _; // Dereferencing the pointer is safe in this case because it points to // a valid, initialized `i32`. But dereferencing in general is unsafe because // the pointer need not be valid. // Creates z with value 20. let z = unsafe { *x_ptr }; // Again, dereferencing is safe in this case, but not in general. // Assigns 40 to x. unsafe { *x_ptr = 40; } println!("x = {x}\ny = {y}\nz = {z}"); }
Click the Run button.
The getpwuid_r()
function is quite complicated. The Rust binding to the C function is the following.
pub unsafe extern "C" fn getpwuid_r(
uid: uid_t,
pwd: *mut passwd,
buf: *mut c_char,
buflen: size_t,
result: *mut *mut passwd
) -> c_int
The parameters match the parameters for the C function and are as follows.
uid
— The user ID of the user to look up;pwd
— A (mutable) pointer to apasswd
struct that will be filled out bygetpwuid_r()
;buf
- A buffer to hold the contents of string data (see below);buflen
— The length of thebuf
buffer; andresult
— On success,*result
will set topwd
and on failure tostd::ptr::null_mut()
, a null pointer.
The passwd
structure contains pointers to string data. The getpwuid_r()
function needs to put that string data somewhere. It uses the provided buf
as a place to store the strings.
Your task
Your task is to create a new user
module and implement a fn username(uid: u32) -> String
function that calls getpwuid_r()
to retrieve the user name
of the user corresponding to uid
.
Start by creating a new file src/user.rs
and add the user
module to lib.rs
with
pub mod user;
Then add
use std::ffi::CStr;
use std::ptr;
use libc;
pub fn username(uid: u32) -> String {
unimplemented!()
}
to user.rs
.
It is time to implement username()
. Implementing it is a little tricky. You’re going to need to
- Create a
passwd
local variable with all of the fields set to either 0 or nulllet mut pwd = libc::passwd { pw_name: ptr::null_mut(), // fill out the rest of the fields };
- Create a
Vec<libc::c_char>
for thebuf
argumentlet mut buf: Vec<libc::c_char> = vec![0; 1024];
- Create a pointer to a
libc::passwd
namedresult
that will be set bygetpwuid_r()
to the address ofpwd
on success.let mut result: *mut libc::passwd = ptr::null_mut();
- Call
getpwuid_r()
, passing the appropriate arguments.let ret = unsafe { libc::getpwuid_r( uid, &mut pwd as *mut _, buf.as_mut_ptr(), buf.len(), &mut result as *mut _, ) };
- If
ret
islibc::ERANGE
, then the provided buffer wasn’t large enough to hold the string data. Resizebuf
(something likebuf.resize(buf.len() * 2, 0)
) and callgetpwuid_r()
again. Steps 4 and 5 should be in a loop and and you should break out of the loop ifret
is any other value. - If
ret
is any nonzero value (other thanlibc::ERANGE
which you handled in step 5) or ifresult.is_null()
, then either thegetpwuid_r()
call failed or there was no user with that UID. In either case, convert the UID to a String (uid.to_string()
) and return it. - At this point, everything has succeeded and we just need to convert
from a C string into a Rust
String
and return it. For this, we can use a&CStr
reference which behaves like a&str
for a C string.// UNSAFE: This is safe because ret is 0 and result is not null and // therefore pwd was filled out. Additionally, pwd.pw_name is a valid, // NUL-terminated C-string. let username: &CStr = unsafe { CStr::from_ptr(pwd.pw_name) }; // Convert CStr into a String and return it. username.to_string_lossy().into_owned()
Finally, modify the main()
function in whoami.rs
to call
user::username(uid)
with the UID that results from libc::geteuid()
. You’ll
want to add use process::user;
to whoami.rs
to be able to call functions
in the user
module.
If all has worked correctly, running cargo run --bin whoami
should print out your username.
$ cargo run --quiet --bin whoami
steve