diff --git a/Cargo.lock b/Cargo.lock index b89f8b4a..9389edf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,14 +147,14 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] name = "binrw" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f36b7cb3ab9ff6a2858650d8dc360e783a5d14dc29594db48c56a3c233cc265" +checksum = "7d4bca59c20d6f40c2cc0802afbe1e788b89096f61bdf7aeea6bf00f10c2909b" dependencies = [ "array-init", "binrw_derive", @@ -163,9 +163,9 @@ dependencies = [ [[package]] name = "binrw_derive" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ea7a8c5c8eeffffac6d54d172444e15beffac6f817fac714460a9a9aa88da3" +checksum = "d8ba42866ce5bced2645bfa15e97eef2c62d2bdb530510538de8dd3d04efff3c" dependencies = [ "either", "owo-colors", @@ -217,7 +217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" dependencies = [ "memchr", - "regex-automata 0.4.7", + "regex-automata 0.4.8", "serde", ] @@ -229,22 +229,22 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] @@ -255,9 +255,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.1.22" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "shlex", ] @@ -308,9 +308,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -318,9 +318,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstyle", "clap_lex", @@ -335,7 +335,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] @@ -456,7 +456,7 @@ checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] @@ -689,6 +689,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "heck" version = "0.5.0" @@ -737,12 +743,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.0", ] [[package]] @@ -788,9 +794,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -896,9 +902,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libcrypt-rs" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "39b3d7c612f85d15ec030b01eb7e6e89cee1dfe04ce9ba7c09f01958f54489ac" [[package]] name = "libloading" @@ -951,7 +963,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "m4-test-manager" -version = "0.1.0" +version = "0.2.1" dependencies = [ "clap", ] @@ -1047,9 +1059,9 @@ dependencies = [ [[package]] name = "notify-debouncer-full" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f5dab59c348b9b50cf7f261960a20e389feb2713636399cd9082cd4b536154" +checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e" dependencies = [ "crossbeam-channel", "file-id", @@ -1120,7 +1132,7 @@ checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "crc32fast", "flate2", - "hashbrown", + "hashbrown 0.14.5", "indexmap", "memchr", "ruzstd", @@ -1128,9 +1140,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "option-ext" @@ -1169,9 +1181,9 @@ dependencies = [ [[package]] name = "pest" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", "thiserror", @@ -1180,9 +1192,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" dependencies = [ "pest", "pest_generator", @@ -1190,22 +1202,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] name = "pest_meta" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" dependencies = [ "once_cell", "pest", @@ -1491,6 +1503,7 @@ dependencies = [ "clap", "gettext-rs", "libc", + "libcrypt-rs", "plib", "syslog", "thiserror", @@ -1524,19 +1537,19 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "910d41a655dac3b764f1ade94821093d3610248694320cd072303a8eedcf221d" dependencies = [ "proc-macro2", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -1555,7 +1568,7 @@ dependencies = [ "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "rusty-fork", "tempfile", "unarray", @@ -1637,9 +1650,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -1657,14 +1670,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -1678,13 +1691,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -1701,9 +1714,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-hash" @@ -1726,9 +1739,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rusty-fork" @@ -1789,22 +1802,22 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] @@ -1887,7 +1900,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] @@ -1903,9 +1916,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" dependencies = [ "proc-macro2", "quote", @@ -1941,9 +1954,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -1993,27 +2006,27 @@ checksum = "5999e24eaa32083191ba4e425deb75cdf25efefabe5aaccb7446dd0d4122a3f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] @@ -2130,9 +2143,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uname" @@ -2222,9 +2235,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -2233,24 +2246,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2258,22 +2271,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "winapi" @@ -2345,7 +2358,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] @@ -2356,7 +2369,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] [[package]] @@ -2534,5 +2547,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.82", ] diff --git a/README.md b/README.md index 7393579d..ff2f7cee 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Because it is a FAQ, the major differences between this project and uutils are: - [x] gencat (i18n) - [x] grep - [x] head + - [x] iconv (i18n) - [x] join - [x] link - [x] ls @@ -127,12 +128,14 @@ Because it is a FAQ, the major differences between this project and uutils are: - [x] logname - [x] mesg - [x] mkfifo + - [x] newgrp - [x] nice - [x] nohup - [x] pathchk - [x] pwd - [x] renice - [x] sleep + - [x] talk - [x] tee - [x] touch - [x] tty @@ -190,12 +193,11 @@ Because it is a FAQ, the major differences between this project and uutils are: ### i18n category - [ ] gettext (i18n) - - [ ] iconv (i18n) (status: in progress) - - [ ] locale (i18n) + - [ ] locale (i18n) -- status: in progress - [ ] localedef (i18n) - [ ] msgfmt (i18n) - [ ] ngettext (i18n) - - [ ] xgettext (i18n) + - [ ] xgettext (i18n) -- status: in progress ### UUCP category - [ ] uucp (UUCP) @@ -213,12 +215,10 @@ Because it is a FAQ, the major differences between this project and uutils are: - [ ] make (status: in progress) - [ ] man (status: in progress) - [ ] more - - [ ] newgrp - [ ] patch (status: in progress) - [ ] pax - [ ] sed - - [ ] sh -- Volunteer starting point at https://github.com/rustcoreutils/posixutils-rs/tree/shell - - [ ] talk (status: in progress) + - [ ] sh (status: in progress) ## Installation diff --git a/m4/test-manager/Cargo.toml b/m4/test-manager/Cargo.toml index eaf1fc74..a66e083e 100644 --- a/m4/test-manager/Cargo.toml +++ b/m4/test-manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "m4-test-manager" -version = "0.1.0" +version = "0.2.1" edition.workspace = true rust-version.workspace = true diff --git a/users/Cargo.toml b/users/Cargo.toml index 67b62ab2..5debb689 100644 --- a/users/Cargo.toml +++ b/users/Cargo.toml @@ -14,12 +14,14 @@ gettext-rs.workspace = true libc.workspace = true chrono.workspace = true syslog = "6.1" +libcrypt-rs = "0.1" thiserror = "1.0" binrw = "0.14" [lints] workspace = true + [[bin]] name = "id" path = "./id.rs" @@ -48,6 +50,12 @@ path = "./tty.rs" name = "write" path = "./write.rs" +[[bin]] + +name = "newgrp" +path = "./newgrp.rs" + [[bin]] name = "talk" path = "./talk.rs" + diff --git a/users/newgrp.rs b/users/newgrp.rs new file mode 100644 index 00000000..4e640f71 --- /dev/null +++ b/users/newgrp.rs @@ -0,0 +1,820 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use clap::{error::ErrorKind, Parser}; +use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; +use libc::{ + getgid, getgrnam, getgroups, getlogin, getpwnam, getpwuid, getuid, gid_t, passwd, setegid, + setgid, setgroups, setuid, uid_t, +}; + +#[cfg(target_os = "linux")] +use libc::{ECHO, ECHONL, TCSANOW}; +#[cfg(target_os = "linux")] +use libcrypt_rs::Crypt; +use plib::{group::Group, PROJECT_NAME}; + +use std::{ + env, + ffi::{CStr, CString}, + io, + process::{self, Command}, +}; + +#[cfg(target_os = "linux")] +use std::{ + fs::File, + io::{BufRead, BufReader}, + os::unix::io::AsRawFd, +}; + +#[cfg(target_os = "linux")] +const GROUPSHADOW_PATH: &str = "/etc/gshadow"; + +#[cfg(target_os = "linux")] +const MAX_GROUPS: usize = libc::KERN_NGROUPS_MAX as usize; + +#[cfg(target_os = "macos")] +const MAX_GROUPS: usize = 16; + +#[derive(Parser)] +#[command(version, about = "newgrp — change to a new group")] +struct Args { + #[arg( + short = 'l', + help = "Change the environment to what would be expected if the user actually logged in again (letter 'l')." + )] + login: bool, + + #[arg( + value_name = "GROUP", + required = true, + help = "Specifies the group ID or group name. This is a positional argument that must be provided." + )] + group: String, +} + +/// Changes the effective group ID and updates the supplementary group list based on the specified group. +/// +/// This function first retrieves the current user's password entry to get the user's supplementary groups. +/// It then checks the specified group against these groups and performs the necessary updates to the effective +/// group ID and supplementary group list. +/// +/// # Parameters +/// +/// * `args`: The command-line arguments containing the target group information. +/// +/// # Returns +/// Returns `Ok(())` if the operation is successful, or an error message if it fails. +fn newgrp(args: Args) -> Result<(), io::Error> { + let groups = plib::group::load(); + + // Retrieve current user information + let pwd = get_password().or_else(|_| { + Err(io::Error::new( + io::ErrorKind::NotFound, + "Could not retrieve current user information.", + )) + })?; + let user_name = unsafe { CStr::from_ptr(pwd.pw_name) } + .to_str() + .unwrap_or_else(|_| "???"); + + if args.login { + set_login_environment(&user_name)?; + } + + let group_identifier = args.group.trim(); + + // Find the matching group + let group = find_matching_group(group_identifier, &groups).ok_or_else(|| { + eprintln!("newgrp: GROUP '{}' does not exist.", group_identifier); + io::Error::new(io::ErrorKind::NotFound, "Group not found.") + })?; + + let current_gid = unsafe { getgid() }; + let current_group_gids: Vec = groups.iter().map(|g| g.gid).collect(); + + // Check if the user is already a member of the target group + if group.gid == current_gid { + eprintln!( + "newgrp: You are already in group '{}'. Trying to change GID", + group_identifier + ); + if unsafe { getuid() } == 0 { + change_gid_and_uid(group.gid, group_identifier)?; + } + return Ok(()); + } + + // Check permissions for the user to join the specified group + check_perms(&group, pwd)?; + + // Check if the effective GID is in the supplementary list + let effective_gid_in_supplementary = current_group_gids.contains(¤t_gid); + let new_gid_in_supplementary = current_group_gids.contains(&group.gid); + + // Logic for changing the effective GID based on the requirements + if effective_gid_in_supplementary { + // The effective GID is in the supplementary list + if new_gid_in_supplementary { + // New GID is also in the supplementary list; change the effective GID + change_effective_gid_and_uid(group.gid, group_identifier)?; + logger(&user_name, group.gid); + } else { + // New GID is not in the supplementary list; add it if possible + add_gid_to_groups(group.gid); + change_effective_gid_and_uid(group.gid, group_identifier)?; + logger(&user_name, group.gid); + } + } else { + // The effective GID is not in the supplementary list + if new_gid_in_supplementary { + // New GID is in the supplementary list; delete it + remove_gid_from_groups(group.gid); + } else { + // Neither the old nor new GID is in the supplementary list; add the old GID + add_gid_to_groups(current_gid); + } + change_gid_and_uid(group.gid, group_identifier)?; + logger(&user_name, group.gid); + } + + Ok(()) +} + +/// Adds a GID to the supplementary group list of the process if there is room. +/// +/// This function modifies the supplementary group list to include the specified GID. +/// If there is no room, it prints an error message. +/// +/// # Parameters +/// +/// * `gid`: The group ID to add to the supplementary group list. +fn add_gid_to_groups(gid: gid_t) { + let mut supplementary_groups = get_current_supplementary_groups(); + if supplementary_groups.len() < MAX_GROUPS { + supplementary_groups.push(gid); + + #[cfg(target_os = "macos")] + let supplementary_groups_len = supplementary_groups.len() as i32; + #[cfg(target_os = "linux")] + let supplementary_groups_len = supplementary_groups.len(); + + unsafe { + setgroups(supplementary_groups_len, supplementary_groups.as_ptr()); + } + } else { + eprintln!("Error: No room to add GID {}", gid); + } +} + +/// Removes the current GID from the supplementary group list. +/// +/// This function modifies the supplementary group list to remove the specified GID. +/// +/// # Parameters +/// +/// * `gid`: The group ID to remove from the supplementary group list. +fn remove_gid_from_groups(gid: gid_t) { + let mut supplementary_groups = get_current_supplementary_groups(); + if let Some(pos) = supplementary_groups.iter().position(|&x| x == gid) { + supplementary_groups.remove(pos); + + #[cfg(target_os = "macos")] + let supplementary_groups_len = supplementary_groups.len() as i32; + #[cfg(target_os = "linux")] + let supplementary_groups_len = supplementary_groups.len(); + + unsafe { + setgroups(supplementary_groups_len, supplementary_groups.as_ptr()); + } + } +} + +/// Retrieves the current supplementary groups for the process. +/// +/// This function obtains the current list of supplementary groups for the calling process. +/// +/// # Returns +/// Returns a vector of GIDs representing the current supplementary groups. +fn get_current_supplementary_groups() -> Vec { + let mut groups = vec![0; MAX_GROUPS]; + + #[cfg(target_os = "macos")] + let max_groups: i32 = 16; + #[cfg(target_os = "linux")] + let max_groups = libc::KERN_NGROUPS_MAX; + + let num_groups = unsafe { libc::getgroups(max_groups, groups.as_mut_ptr()) }; + groups.truncate(num_groups as usize); + groups +} + +/// Changes the effective user ID to the specified UID. +/// +/// # Arguments +/// +/// * `uid` - The new user ID to set. +/// +/// # Returns +/// +/// Returns `Ok(())` if the UID was successfully changed. If an error occurs, it returns +/// an `Err` containing the last OS error. +/// +fn change_uid(uid: uid_t) -> Result<(), io::Error> { + if unsafe { setuid(uid) } != 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Retrieves the current supplementary group IDs for the calling process. +/// +/// # Returns +/// +/// Returns a vector of group IDs (`Vec`) if successful. If an error occurs, +/// it returns an `Err` containing the last OS error. +fn get_current_supplementary_gids() -> Result, io::Error> { + let mut supplementary_gids: [gid_t; 32] = [0; 32]; + let num_groups = unsafe { + getgroups( + supplementary_gids.len() as i32, + supplementary_gids.as_mut_ptr(), + ) + }; + + // Check if getgroups was successful + if num_groups < 0 { + return Err(io::Error::last_os_error()); + } + + Ok(supplementary_gids[..num_groups as usize].to_vec()) +} + +/// Sets the supplementary group IDs for the calling process. +/// +/// # Arguments +/// +/// * `gids` - A slice of group IDs to set as supplementary groups. +/// +/// # Returns +/// +/// Returns `Ok(())` if the supplementary GIDs were successfully set. If an error occurs, +/// it returns an `Err` containing the last OS error. +fn set_supplementary_gids(gids: &[gid_t]) -> Result<(), io::Error> { + #[cfg(target_os = "macos")] + let gids_len = gids.len() as i32; + + #[cfg(target_os = "linux")] + let gids_len = gids.len() as usize; + + if unsafe { setgroups(gids_len, gids.as_ptr()) } != 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Changes the effective group ID and user ID of the calling process to the specified group +/// and current user. +/// +/// # Arguments +/// +/// * `gid` - The group ID to set as the new effective group ID. +/// * `group_name` - The name of the group to be set (for logging or error messages). +/// +/// # Returns +/// +/// Returns `Ok(())` if the group and user IDs were successfully changed. If an error occurs, +/// it returns an `Err` containing the last OS error. +fn change_gid_and_uid(gid: gid_t, group_name: &str) -> Result<(), io::Error> { + // Create a C string for the group name + let c_group_name = CString::new(group_name) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "CString::new failed"))?; + + // Attempt to retrieve the group entry + let gr_entry = unsafe { getgrnam(c_group_name.as_ptr()) }; + if gr_entry.is_null() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("Group '{}' does not exist.", group_name), + )); + } + + // Attempt to set the GID + if unsafe { setgid(gid) } != 0 { + return Err(io::Error::last_os_error()); + } + + // Retrieve the current user UID + let current_uid: uid_t = unsafe { getuid() }; + + // Get the current supplementary GIDs + let mut supplementary_gids = get_current_supplementary_gids()?; + + // Add the new GID if it is not already present + if !supplementary_gids.contains(&gid) { + supplementary_gids.push(gid); + } + + // Set the supplementary groups + set_supplementary_gids(&supplementary_gids)?; + + // Attempt to set the UID to the current UID + change_uid(current_uid)?; + + Ok(()) +} + +/// Changes the effective group ID and user ID of the calling process to the specified group +/// while maintaining the current supplementary group memberships. +/// +/// # Arguments +/// +/// * `gid` - The group ID to set as the new effective group ID. +/// * `group_name` - The name of the group to be set (for logging or error messages). +/// +/// # Returns +/// +/// Returns `Ok(())` if the group and user IDs were successfully changed. If an error occurs, +/// it returns an `Err` containing the last OS error. +fn change_effective_gid_and_uid(gid: gid_t, group_name: &str) -> Result<(), io::Error> { + // Create a C string for the group name + let c_group_name = CString::new(group_name) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "CString::new failed"))?; + + // Attempt to retrieve the group entry + let gr_entry = unsafe { getgrnam(c_group_name.as_ptr()) }; + if gr_entry.is_null() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("Group '{}' does not exist.", group_name), + )); + } + + // Attempt to set the GID + if unsafe { setegid(gid) } != 0 { + return Err(io::Error::last_os_error()); + } + + // Retrieve the current user UID + let current_uid: uid_t = unsafe { getuid() }; + + // Get the current supplementary GIDs + let current_supplementary_gids = get_current_supplementary_gids()?; + + // Set the supplementary groups (no need to add new GID) + set_supplementary_gids(¤t_supplementary_gids)?; + + // Attempt to set the UID to the current UID + change_uid(current_uid)?; + + Ok(()) +} + +/// Retrieves the password entry for the current user based on the login name +/// or user ID (UID). +/// +/// This function attempts to obtain the login name of the current user using +/// the `getlogin` system call. If successful, it checks for a password entry +/// corresponding to that login name. If the login name is empty or if no +/// matching password entry is found, the function falls back to using the +/// UID to retrieve the password entry. +/// +/// # Returns +/// Returns an `Option`, where: +/// - `Some(passwd)`: Represents the password entry if found. +/// - `None`: If the login name cannot be retrieved, if there is no matching +/// password entry for the login name or UID, or if an error occurs. +/// +fn get_password() -> Result { + unsafe { + // Get the login name and handle potential null pointer + let login_ptr = getlogin(); + if login_ptr.is_null() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "Login name not found.", + )); + } + + let login_name = match CStr::from_ptr(login_ptr).to_str() { + Ok(name) => name, + Err(_) => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Failed to convert login name to valid UTF-8.", + )); + } + }; + + let ruid = getuid(); + + // Attempt to get the password entry by login name + if !login_name.is_empty() { + if let Ok(c_login_name) = CString::new(login_name) { + let pw = getpwnam(c_login_name.as_ptr()); + + // Check if pw is not null and the UID matches + if !pw.is_null() && (*pw).pw_uid == ruid { + return Ok(*pw); + } + } + } + + // Fall back to getting the password entry by UID + let pw_by_uid = getpwuid(ruid); + if !pw_by_uid.is_null() { + return Ok(*pw_by_uid); + } + + // If no password entry is found, return an error + Err(io::Error::new( + io::ErrorKind::NotFound, + format!( + "Unable to retrieve password entry for login '{}' or UID '{}'.", + login_name, ruid + ), + )) + } +} + +/// Finds a matching group in a vector of groups by name or GID. +/// +/// This function takes a string identifier that can represent either a group name +/// or a group ID (GID). It searches through the provided vector of `Group` instances +/// to find a match. If a match is found, a clone of the matching `Group` is returned. +/// +/// # Parameters +/// +/// * `group_identifier`: A string slice that can be a group name or a numeric GID. +/// * `groups`: A vector of `Group` instances to search through. +/// +/// # Returns +/// +/// This function returns an `Option`. If a matching group is found, +/// it returns `Some(Group)`, otherwise it returns `None`. +fn find_matching_group(group_identifier: &str, groups: &Vec) -> Option { + // Helper closure to clone and return the group + let clone_group = |group: &Group| { + Some(Group { + gid: group.gid, + name: group.name.clone(), + members: group.members.clone(), + passwd: group.passwd.clone(), + }) + }; + + // Check if the identifier is a number (GID) + if let Ok(gid) = group_identifier.parse::() { + // Find the matching group by GID + if let Some(group) = groups.iter().find(|group| group.gid == gid) { + return clone_group(group); + } + } + + // Otherwise, treat it as a group name and find the matching group + if let Some(group) = groups.iter().find(|group| group.name == group_identifier) { + return clone_group(group); + } + + None +} + +/// Logs a message indicating that a user has switched to a different group. +/// +/// This function retrieves the current login name and terminal device name (tty) +/// and logs a message to standard error, formatted to include the user's name, +/// their login name, the tty they are using, and the group ID they switched to. +/// +/// # Parameters +/// +/// * `name`: A string slice representing the name of the user who is switching groups. +/// * `gid`: An unsigned 32-bit integer representing the group ID the user is switching to. +/// +fn logger(name: &str, gid: u32) { + // Get the current login name + let loginname = plib::curuser::login_name(); + + // Get the current tty device name + let tty = plib::curuser::tty(); + + // Log the message + eprintln!( + "user '{}' (login '{}' on {}) switched to group with id '{}'", + name, loginname, tty, gid + ); +} + +/// Checks permissions for accessing a specified group based on the provided user credentials. +/// +/// This function determines whether the user associated with the given password structure +/// has permission to access the specified group. If a password is required, it prompts the +/// user for the password and verifies it against the group's password. +/// +/// # Arguments +/// +/// * `group` - A reference to the `Group` struct representing the group whose permissions are being checked. +/// * `password` - A `passwd` struct containing the user's password information. +/// +/// # Returns +/// +/// Returns `Ok(group.gid)` if the user has the necessary permissions to access the group. If a password is required +/// and does not match, it returns an `Err` with `io::ErrorKind::PermissionDenied`. + +fn check_perms(group: &Group, password: passwd) -> Result { + let pw_name = unsafe { + CStr::from_ptr(password.pw_name) + .to_string_lossy() + .into_owned() + }; + + // Determine if a password is needed based on group membership and GID + let mut need_password = + group.gid != password.pw_gid && group.members.iter().all(|member| member != &pw_name); + + // Convert C-style strings (char pointers) to Rust &CStr and check for empty passwords + unsafe { + let user_password = CStr::from_ptr(password.pw_passwd).to_bytes(); + + if user_password.is_empty() && !group.passwd.is_empty() { + need_password = true; + } + } + + // Check for permissions if necessary + unsafe { + if getuid() != 0 { + if need_password { + #[cfg(target_os = "linux")] + { + let password_input = read_password().unwrap_or_default(); + let shadow_password = get_shadow_password(&group.name)?; + let hashed_input = pw_encrypt(&password_input, Some(&shadow_password))?; + + if hashed_input == shadow_password { + // Return GID if password matches + return Ok(group.gid); + } else { + eprintln!("Error: Incorrect password for group '{}'.", group.name); + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "Incorrect password for group.", + )); + } + } + } + #[cfg(target_os = "macos")] + { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "Try to use root.", + )); + } + } + } + + // Return the group GID if no password check is required or if it passed + Ok(group.gid) +} + +/// Retrieves the shadow password for a specified group from the group shadow file. +/// +/// This function searches through the group shadow file defined by `GROUPSHADOW_PATH` +/// to find the entry corresponding to the provided `group_name`. If found, it extracts +/// and returns the shadow password associated with that group. +/// +/// # Parameters +/// +/// - `group_name`: A string slice that holds the name of the group whose shadow password +/// is to be retrieved. +/// +/// # Returns +/// +/// - `Result`: On success, returns the shadow password as a `String`. +/// If the group is not found or if an I/O error occurs, it returns an `io::Error`. +/// +#[cfg(target_os = "linux")] +fn get_shadow_password(group_name: &str) -> Result { + let file = File::open(GROUPSHADOW_PATH)?; + let reader = BufReader::new(file); + + for line in reader.lines() { + let line_content = line?; + + if let Some(colon_pos) = line_content.find(':') { + let group = &line_content[..colon_pos]; + if group == group_name { + let remaining = &line_content[colon_pos + 1..]; + if let Some(next_colon_pos) = remaining.find(':') { + let password = &remaining[..next_colon_pos]; + return Ok(password.to_string()); + } + } + } + } + + Ok(String::new()) +} + +/// Extracts the salt from a given full hash string in the format used by cryptographic hash functions. +/// +/// # Parameters +/// - `full_hash`: A string slice representing the full hash, which typically includes the algorithm identifier, +/// the salt, and the hashed password. +/// +/// # Returns +/// - `Some(String)`: The extracted salt if the hash string has the expected format with at least four parts. +/// - `None`: If the hash string does not have the correct format or does not contain enough parts to extract the salt. +#[cfg(target_os = "linux")] +fn extract_salt(full_hash: &str) -> Option { + let parts: Vec<&str> = full_hash.split('$').collect(); + + if parts.len() >= 4 { + Some(format!("${}${}${}", parts[1], parts[2], parts[3])) + } else { + None + } +} + +/// Encrypts a clear text password using the salt extracted from a shadow password entry. +/// +/// # Parameters +/// - `clear`: A string slice representing the clear text password to be encrypted. +/// - `shadow_password`: An optional string slice containing the full shadow password hash +/// from which the salt will be extracted. +/// +/// # Returns +/// - `Some(String)`: The encrypted password if the encryption process is successful. +/// - `None`: If the salt extraction or encryption fails. +#[cfg(target_os = "linux")] +fn pw_encrypt(clear: &str, shadow_password: Option<&str>) -> Result { + let mut engine = Crypt::new(); + + // Extract the salt from the shadow password, returning an error if extraction fails + let salt = match shadow_password.and_then(extract_salt) { + Some(salt) if !salt.contains("!*") => salt, // Ensure the salt is not blocked + _ => { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "Shadow password is blocked or salt extraction failed.", + )); + } + }; + + // Attempt to set the salt in the engine + if engine.set_salt(salt).is_err() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Salt generation failed.", + )); + } + + // Attempt to encrypt the clear text password + if engine.encrypt(clear.to_string()).is_err() { + return Err(io::Error::new(io::ErrorKind::Other, "Encryption failed.")); + } + + Ok(engine.encrypted) +} +/// Reads a password from the terminal without echoing the input. +/// +/// This function opens the terminal (`/dev/tty`), modifies the terminal settings to hide +/// the user's input while typing (except for newlines), prompts the user for a password, +/// reads the input, and restores the original terminal settings before returning the password. +/// +/// # Returns +/// +/// On success, returns the user's password as a `String`. The password is trimmed to remove +/// any trailing newline characters. On failure, returns an `io::Result` with the error encountered. +/// +/// # Errors +/// +/// This function may return an `io::Error` in the following cases: +/// - Failure to open `/dev/tty` for reading from the terminal. +/// - Failure to read the current terminal attributes. +/// - Failure to set the modified terminal attributes (i.e., disabling input echo). +/// - Failure to read the password input. +/// - Failure to restore the original terminal attributes after reading the password. +#[cfg(target_os = "linux")] +fn read_password() -> io::Result { + // Open the terminal (tty) and get its file descriptor + let tty = File::open("/dev/tty")?; + let fd = tty.as_raw_fd(); + let mut reader = BufReader::new(tty); + + // Print password prompt without a newline + eprint!("Password: "); + + // Get the current terminal settings + let mut term_orig = std::mem::MaybeUninit::uninit(); + let term_orig = unsafe { + libc::tcgetattr(fd, term_orig.as_mut_ptr()); + term_orig.assume_init() + }; + + // Modify terminal settings to hide user input (except newline) + let mut term_modified = term_orig; + term_modified.c_lflag &= !ECHO; // Disable echo + term_modified.c_lflag |= ECHONL; // Keep newline + + // Apply the modified terminal settings + let set_result = unsafe { libc::tcsetattr(fd, TCSANOW, &term_modified) }; + if set_result != 0 { + return Err(io::Error::last_os_error()); + } + + // Read the password + let mut password = String::new(); + reader.read_line(&mut password)?; + + // Restore the original terminal settings + let restore_result = unsafe { libc::tcsetattr(fd, TCSANOW, &term_orig) }; + if restore_result != 0 { + return Err(io::Error::last_os_error()); + } + + Ok(password.trim_end().to_string()) +} + +/// Set the environment variables as if the user has logged in and execute their shell. +/// +/// # Arguments +/// +/// * `user` - A string slice that holds the username whose environment needs to be set. +/// +/// # Returns +/// +/// * `Result<(), io::Error>` - Returns `Ok(())` on success, or an `io::Error` if something goes wrong. +/// +fn set_login_environment(user: &str) -> Result<(), io::Error> { + // Get the user's shell from the password entry + let pwd = get_password().or_else(|_| { + Err(io::Error::new( + io::ErrorKind::NotFound, + "Could not retrieve user information.", + )) + })?; + + let user_shell = unsafe { CStr::from_ptr(pwd.pw_shell) } + .to_str() + .unwrap_or("/bin/sh"); + + // Set the necessary environment variables + env::set_var("USER", user); + env::set_var("HOME", unsafe { + CStr::from_ptr(pwd.pw_dir).to_str().unwrap_or("") + }); + env::set_var("SHELL", user_shell); + + let status = Command::new(user_shell) + .env("USER", user) + .env("HOME", unsafe { + CStr::from_ptr(pwd.pw_dir).to_str().unwrap_or("") + }) + .env("SHELL", user_shell) + .status()?; + + if !status.success() { + eprintln!("Failed to start shell: {}", user_shell); + return Err(io::Error::new( + io::ErrorKind::Other, + "Shell execution failed", + )); + } + + Ok(()) +} + +fn main() -> Result<(), Box> { + setlocale(LocaleCategory::LcAll, ""); + textdomain(PROJECT_NAME)?; + bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + let mut exit_code = 0; + + let args = Args::try_parse().unwrap_or_else(|err| { + if err.kind() == ErrorKind::DisplayHelp || err.kind() == ErrorKind::DisplayVersion { + // Print help or version message + eprintln!("{}", err); + } else { + // Print custom error message + eprintln!("Error parsing arguments: {}", err); + } + + // Exit with a non-zero status code + std::process::exit(1); + }); + + if let Err(err) = newgrp(args) { + exit_code = 1; + eprint!("{}", err); + } + + process::exit(exit_code) +}