From bb52ed774e00994e0408d091df0b9c524816496d Mon Sep 17 00:00:00 2001 From: Sion Kang <31057849+Yaminyam@users.noreply.github.com> Date: Thu, 4 Aug 2022 15:44:40 +0900 Subject: [PATCH] Feat: grp module (#3967) --- Cargo.lock | 1 + Lib/test/test_grp.py | 109 ++++++++++++++++++++++++++++++++++++ Lib/test/test_subprocess.py | 4 ++ stdlib/Cargo.toml | 1 + stdlib/src/grp.rs | 97 ++++++++++++++++++++++++++++++++ stdlib/src/lib.rs | 23 +++++--- 6 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 Lib/test/test_grp.py create mode 100644 stdlib/src/grp.rs diff --git a/Cargo.lock b/Cargo.lock index efc4dd341..1137c15b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1750,6 +1750,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "page_size", + "parking_lot", "paste", "puruspe", "rand", diff --git a/Lib/test/test_grp.py b/Lib/test/test_grp.py new file mode 100644 index 000000000..378b61d2d --- /dev/null +++ b/Lib/test/test_grp.py @@ -0,0 +1,109 @@ +"""Test script for the grp module.""" + +import unittest +from test.support import import_helper + + +grp = import_helper.import_module('grp') +@unittest.skipUnless(hasattr(grp, 'getgrall'), 'Does not have getgrall()') +class GroupDatabaseTestCase(unittest.TestCase): + + def check_value(self, value): + # check that a grp tuple has the entries and + # attributes promised by the docs + self.assertEqual(len(value), 4) + self.assertEqual(value[0], value.gr_name) + self.assertIsInstance(value.gr_name, str) + self.assertEqual(value[1], value.gr_passwd) + self.assertIsInstance(value.gr_passwd, str) + self.assertEqual(value[2], value.gr_gid) + self.assertIsInstance(value.gr_gid, int) + self.assertEqual(value[3], value.gr_mem) + self.assertIsInstance(value.gr_mem, list) + + def test_values(self): + entries = grp.getgrall() + + for e in entries: + self.check_value(e) + + def test_values_extended(self): + entries = grp.getgrall() + if len(entries) > 1000: # Huge group file (NIS?) -- skip the rest + self.skipTest('huge group file, extended test skipped') + + for e in entries: + e2 = grp.getgrgid(e.gr_gid) + self.check_value(e2) + self.assertEqual(e2.gr_gid, e.gr_gid) + name = e.gr_name + if name.startswith('+') or name.startswith('-'): + # NIS-related entry + continue + e2 = grp.getgrnam(name) + self.check_value(e2) + # There are instances where getgrall() returns group names in + # lowercase while getgrgid() returns proper casing. + # Discovered on Ubuntu 5.04 (custom). + self.assertEqual(e2.gr_name.lower(), name.lower()) + + def test_errors(self): + self.assertRaises(TypeError, grp.getgrgid) + self.assertRaises(TypeError, grp.getgrnam) + self.assertRaises(TypeError, grp.getgrall, 42) + # embedded null character + self.assertRaises(ValueError, grp.getgrnam, 'a\x00b') + + # try to get some errors + bynames = {} + bygids = {} + for (n, p, g, mem) in grp.getgrall(): + if not n or n == '+': + continue # skip NIS entries etc. + bynames[n] = g + bygids[g] = n + + allnames = list(bynames.keys()) + namei = 0 + fakename = allnames[namei] + while fakename in bynames: + chars = list(fakename) + for i in range(len(chars)): + if chars[i] == 'z': + chars[i] = 'A' + break + elif chars[i] == 'Z': + continue + else: + chars[i] = chr(ord(chars[i]) + 1) + break + else: + namei = namei + 1 + try: + fakename = allnames[namei] + except IndexError: + # should never happen... if so, just forget it + break + fakename = ''.join(chars) + + self.assertRaises(KeyError, grp.getgrnam, fakename) + + # Choose a non-existent gid. + fakegid = 4127 + while fakegid in bygids: + fakegid = (fakegid * 3) % 0x10000 + + self.assertRaises(KeyError, grp.getgrgid, fakegid) + + def test_noninteger_gid(self): + entries = grp.getgrall() + if not entries: + self.skipTest('no groups') + # Choose an existent gid. + gid = entries[0][2] + self.assertRaises(TypeError, grp.getgrgid, float(gid)) + self.assertRaises(TypeError, grp.getgrgid, str(gid)) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index bbcb2734a..55f4f04da 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -1999,6 +1999,8 @@ class POSIXProcessTestCase(BaseTestCase): with self.assertRaises(ValueError): subprocess.check_call(ZERO_RETURN_CMD, user=65535) + # TODO: RUSTPYTHON, observed gids do not match expected gids + @unittest.expectedFailure @unittest.skipUnless(hasattr(os, 'setregid'), 'no setregid() on platform') def test_group(self): gid = os.getegid() @@ -2046,6 +2048,8 @@ class POSIXProcessTestCase(BaseTestCase): with self.assertRaises(ValueError): subprocess.check_call(ZERO_RETURN_CMD, group=65535) + # TODO: RUSTPYTHON, observed gids do not match expected gids + @unittest.expectedFailure @unittest.skipUnless(hasattr(os, 'setgroups'), 'no setgroups() on platform') def test_extra_groups(self): gid = os.getegid() diff --git a/stdlib/Cargo.toml b/stdlib/Cargo.toml index 1713f437c..90bab2a8c 100644 --- a/stdlib/Cargo.toml +++ b/stdlib/Cargo.toml @@ -63,6 +63,7 @@ ahash = "0.7.6" libz-sys = { version = "1.1.5", optional = true } num_enum = "0.5.7" ascii = "1.0.0" +parking_lot = "0.12.0" # mmap [target.'cfg(all(unix, not(target_arch = "wasm32")))'.dependencies] diff --git a/stdlib/src/grp.rs b/stdlib/src/grp.rs new file mode 100644 index 000000000..3ccf63adf --- /dev/null +++ b/stdlib/src/grp.rs @@ -0,0 +1,97 @@ +pub(crate) use grp::make_module; + +#[pymodule] +mod grp { + use crate::vm::{ + builtins::{PyIntRef, PyListRef, PyStrRef}, + convert::{IntoPyException, ToPyObject}, + exceptions, + types::PyStructSequence, + PyObjectRef, PyResult, VirtualMachine, + }; + use nix::unistd; + use std::ptr::NonNull; + + #[pyattr] + #[pyclass(module = "grp", name = "struct_group")] + #[derive(PyStructSequence)] + struct Group { + gr_name: String, + gr_passwd: String, + gr_gid: u32, + gr_mem: PyListRef, + } + #[pyimpl(with(PyStructSequence))] + impl Group { + fn from_unistd_group(group: unistd::Group, vm: &VirtualMachine) -> Self { + let cstr_lossy = |s: std::ffi::CString| { + s.into_string() + .unwrap_or_else(|e| e.into_cstring().to_string_lossy().into_owned()) + }; + Group { + gr_name: group.name, + gr_passwd: cstr_lossy(group.passwd), + gr_gid: group.gid.as_raw(), + gr_mem: vm + .ctx + .new_list(group.mem.iter().map(|s| s.to_pyobject(vm)).collect()), + } + } + } + + #[pyfunction] + fn getgrgid(gid: PyIntRef, vm: &VirtualMachine) -> PyResult { + let gr_gid = gid.as_bigint(); + let gid = libc::gid_t::try_from(gr_gid) + .map(unistd::Gid::from_raw) + .ok(); + let group = gid + .map(unistd::Group::from_gid) + .transpose() + .map_err(|err| err.into_pyexception(vm))? + .flatten(); + let group = group.ok_or_else(|| { + vm.new_key_error( + vm.ctx + .new_str(format!("getgrgid: group id {} not found", gr_gid)) + .into(), + ) + })?; + Ok(Group::from_unistd_group(group, vm)) + } + + #[pyfunction] + fn getgrnam(name: PyStrRef, vm: &VirtualMachine) -> PyResult { + let gr_name = name.as_str(); + if gr_name.contains('\0') { + return Err(exceptions::cstring_error(vm)); + } + let group = unistd::Group::from_name(gr_name).map_err(|err| err.into_pyexception(vm))?; + let group = group.ok_or_else(|| { + vm.new_key_error( + vm.ctx + .new_str(format!("getgrnam: group name {} not found", gr_name)) + .into(), + ) + })?; + Ok(Group::from_unistd_group(group, vm)) + } + + #[pyfunction] + fn getgrall(vm: &VirtualMachine) -> PyResult> { + // setgrent, getgrent, etc are not thread safe. Could use fgetgrent_r, but this is easier + static GETGRALL: parking_lot::Mutex<()> = parking_lot::const_mutex(()); + let _guard = GETGRALL.lock(); + let mut list = Vec::new(); + + unsafe { libc::setgrent() }; + while let Some(ptr) = NonNull::new(unsafe { libc::getgrent() }) { + let group = unistd::Group::from(unsafe { ptr.as_ref() }); + let group = Group::from_unistd_group(group, vm).to_pyobject(vm); + list.push(group); + } + unsafe { libc::endgrent() }; + + Ok(list) + } +} diff --git a/stdlib/src/lib.rs b/stdlib/src/lib.rs index aa59353fd..98cffde6f 100644 --- a/stdlib/src/lib.rs +++ b/stdlib/src/lib.rs @@ -41,6 +41,8 @@ mod multiprocessing; #[cfg(unix)] mod posixsubprocess; // libc is missing constants on redox +#[cfg(all(unix, not(any(target_os = "android", target_os = "redox"))))] +mod grp; #[cfg(all(unix, not(target_os = "redox")))] mod resource; #[cfg(target_os = "macos")] @@ -118,20 +120,25 @@ pub fn get_module_inits() -> impl Iterator, StdlibInit { "_ssl" => ssl::make_module, } - #[cfg(all(unix, not(target_os = "redox"), not(target_os = "ios")))] - { - "termios" => termios::make_module, - } - #[cfg(all(unix, not(target_os = "redox")))] - { - "resource" => resource::make_module, - } + // Unix-only #[cfg(unix)] { "_posixsubprocess" => posixsubprocess::make_module, "syslog" => syslog::make_module, "mmap" => mmap::make_module, } + #[cfg(all(unix, not(target_os = "redox")))] + { + "resource" => resource::make_module, + } + #[cfg(all(unix, not(any(target_os = "ios", target_os = "redox"))))] + { + "termios" => termios::make_module, + } + #[cfg(all(unix, not(any(target_os = "android", target_os = "redox"))))] + { + "grp" => grp::make_module, + } #[cfg(target_os = "macos")] { "_scproxy" => scproxy::make_module,