Skip to content
This repository was archived by the owner on Oct 1, 2025. It is now read-only.

Commit 7110a09

Browse files
committed
feat: add support for sudo-rs
1 parent 48867b5 commit 7110a09

File tree

8 files changed

+278
-50
lines changed

8 files changed

+278
-50
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
`oxidizr` is a command-line utility for managing system experiments that replace traditional Unix utilities with modern Rust-based alternatives on Ubuntu systems.
44

5+
It currently supports the following experiments:
6+
7+
- [uutils coreutils](https://github.com/uutils/coreutils)
8+
- [uutils findutils](https://github.com/uutils/findutils)
9+
- [uutils diffutils](https://github.com/uutils/diffutils)
10+
- [sudo-rs](https://github.com/trifectatechfoundation/sudo-rs)
11+
512
## Installation
613

714
> [!WARNING]
@@ -43,7 +50,7 @@ Options:
4350
-e, --experiments <EXPERIMENTS>...
4451
Select experiments to enable or disable
4552

46-
[default: coreutils findutils diffutils]
53+
[default: coreutils findutils diffutils sudo-rs]
4754

4855
-h, --help
4956
Print help (see a summary with '-h')

src/experiments/mod.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,69 @@
1+
mod sudors;
12
mod uutils;
23
use crate::utils::Worker;
34
use anyhow::Result;
45
use std::path::PathBuf;
6+
pub use sudors::SudoRsExperiment;
7+
use tracing::warn;
58
pub use uutils::UutilsExperiment;
69

710
pub enum Experiment<'a> {
811
Uutils(UutilsExperiment<'a>),
12+
SudoRs(SudoRsExperiment<'a>),
913
}
1014

1115
impl Experiment<'_> {
1216
pub fn name(&self) -> String {
1317
match self {
1418
Experiment::Uutils(uutils) => uutils.name(),
19+
Experiment::SudoRs(sudors) => sudors.name(),
1520
}
1621
}
1722

1823
pub fn enable(&self) -> Result<()> {
24+
if !self.check_compatible() {
25+
warn!(
26+
"Skipping '{}'. Minimum supported release is {}.",
27+
self.name(),
28+
self.first_supported_release()
29+
);
30+
return Ok(());
31+
}
1932
match self {
20-
Experiment::Uutils(uutils) => uutils.enable(),
33+
Experiment::Uutils(e) => e.enable(),
34+
Experiment::SudoRs(e) => e.enable(),
2135
}
2236
}
2337

2438
pub fn disable(&self) -> Result<()> {
39+
if !self.check_installed() {
40+
warn!("'{}' not enabled, skipping restore", self.name());
41+
return Ok(());
42+
}
43+
match self {
44+
Experiment::Uutils(e) => e.disable(),
45+
Experiment::SudoRs(e) => e.disable(),
46+
}
47+
}
48+
49+
pub fn check_compatible(&self) -> bool {
50+
match self {
51+
Experiment::Uutils(e) => e.check_compatible(),
52+
Experiment::SudoRs(e) => e.check_compatible(),
53+
}
54+
}
55+
56+
pub fn first_supported_release(&self) -> &str {
57+
match self {
58+
Experiment::Uutils(e) => e.first_supported_release(),
59+
Experiment::SudoRs(e) => e.first_supported_release(),
60+
}
61+
}
62+
63+
pub fn check_installed(&self) -> bool {
2564
match self {
26-
Experiment::Uutils(uutils) => uutils.disable(),
65+
Experiment::Uutils(e) => e.check_installed(),
66+
Experiment::SudoRs(e) => e.check_installed(),
2767
}
2868
}
2969
}
@@ -54,5 +94,6 @@ pub fn all_experiments<'a>(system: &'a impl Worker) -> Vec<Experiment<'a>> {
5494
None,
5595
PathBuf::from("/usr/lib/cargo/bin/findutils"),
5696
)),
97+
Experiment::SudoRs(SudoRsExperiment::<'a>::new(system)),
5798
]
5899
}

src/experiments/sudors.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
use crate::utils::Worker;
2+
use anyhow::Result;
3+
use std::path::{Path, PathBuf};
4+
use tracing::info;
5+
use which::which;
6+
7+
const PACKAGE: &str = "sudo-rs";
8+
const FIRST_SUPPORTED_RELEASE: &str = "24.04";
9+
10+
/// An experiment to install and configure sudo-rs as a replacement for sudo.
11+
pub struct SudoRsExperiment<'a> {
12+
system: &'a dyn Worker,
13+
}
14+
15+
impl<'a> SudoRsExperiment<'a> {
16+
/// Create a new SudoRsExperiment.
17+
pub fn new(system: &'a dyn Worker) -> Self {
18+
Self { system }
19+
}
20+
21+
/// Check if the system is compatible with the experiment.
22+
pub fn check_compatible(&self) -> bool {
23+
self.system.distribution().release.as_str() >= FIRST_SUPPORTED_RELEASE
24+
}
25+
26+
/// Reports the first supported release for the experiment.
27+
pub fn first_supported_release(&self) -> &str {
28+
FIRST_SUPPORTED_RELEASE
29+
}
30+
31+
/// Check if the package is installed.
32+
pub fn check_installed(&self) -> bool {
33+
self.system.check_installed(PACKAGE).unwrap_or(false)
34+
}
35+
36+
/// Report the name of the experiment.
37+
pub fn name(&self) -> String {
38+
String::from("sudo-rs")
39+
}
40+
41+
/// Enable the experiment by installing and configuring the package.
42+
pub fn enable(&self) -> Result<()> {
43+
info!("Installing and configuring {}", PACKAGE);
44+
self.system.install_package(PACKAGE)?;
45+
46+
for f in Self::sudors_files() {
47+
let filename = f.file_name().unwrap();
48+
let existing = match which(filename) {
49+
Ok(path) => path,
50+
Err(_) => Path::new("/usr/bin").join(filename),
51+
};
52+
self.system.replace_file_with_symlink(f, existing)?;
53+
}
54+
55+
Ok(())
56+
}
57+
58+
/// Disable the experiment by removing the package and restoring the original files.
59+
pub fn disable(&self) -> Result<()> {
60+
info!("Removing {}", PACKAGE);
61+
self.system.remove_package(PACKAGE)?;
62+
63+
for f in Self::sudors_files() {
64+
let filename = f.file_name().unwrap();
65+
let existing = match which(filename) {
66+
Ok(path) => path,
67+
Err(_) => Path::new("/usr/bin").join(filename),
68+
};
69+
self.system.restore_file(existing.clone())?;
70+
}
71+
72+
Ok(())
73+
}
74+
75+
/// List of files from the package to replace system equivalents with.
76+
fn sudors_files() -> Vec<PathBuf> {
77+
vec![
78+
PathBuf::from("/usr/lib/cargo/bin/su"),
79+
PathBuf::from("/usr/lib/cargo/bin/sudo"),
80+
PathBuf::from("/usr/lib/cargo/bin/visudo"),
81+
]
82+
}
83+
}
84+
85+
#[cfg(test)]
86+
mod tests {
87+
use super::*;
88+
use crate::utils::{Distribution, MockSystem};
89+
90+
#[test]
91+
fn test_sudors_incompatible_distribution() {
92+
let runner = incompatible_runner();
93+
let coreutils = sudors_fixture(&runner);
94+
assert!(!coreutils.check_compatible());
95+
}
96+
97+
#[test]
98+
fn test_sudors_install_success() {
99+
let runner = sudors_compatible_runner();
100+
let sudors = sudors_fixture(&runner);
101+
102+
assert!(sudors.enable().is_ok());
103+
104+
let commands = runner.commands.clone().into_inner();
105+
assert_eq!(commands, &["apt-get install -y sudo-rs"]);
106+
107+
let backed_up_files = runner.backed_up_files.clone().into_inner();
108+
let expected = ["/usr/bin/sudo", "/usr/bin/su", "/usr/sbin/visudo"];
109+
110+
assert_eq!(backed_up_files.len(), 3);
111+
for f in backed_up_files.iter() {
112+
assert!(expected.contains(&f.as_str()));
113+
}
114+
115+
let created_symlinks = runner.created_symlinks.clone().into_inner();
116+
let expected = [
117+
("/usr/lib/cargo/bin/su", "/usr/bin/su"),
118+
("/usr/lib/cargo/bin/sudo", "/usr/bin/sudo"),
119+
("/usr/lib/cargo/bin/visudo", "/usr/sbin/visudo"),
120+
];
121+
122+
assert_eq!(created_symlinks.len(), 3);
123+
for (from, to) in created_symlinks.iter() {
124+
assert!(expected.contains(&(from.as_str(), to.as_str())));
125+
}
126+
127+
assert_eq!(runner.restored_files.clone().into_inner().len(), 0);
128+
}
129+
130+
#[test]
131+
fn test_sudors_restore() {
132+
let runner = sudors_compatible_runner();
133+
runner.mock_install_package("sudo-rs");
134+
135+
let sudors = sudors_fixture(&runner);
136+
assert!(sudors.disable().is_ok());
137+
138+
assert_eq!(runner.created_symlinks.clone().into_inner().len(), 0);
139+
assert_eq!(runner.backed_up_files.clone().into_inner().len(), 0);
140+
141+
let commands = runner.commands.clone().into_inner();
142+
assert_eq!(commands.len(), 1);
143+
assert!(commands.contains(&"apt-get remove -y sudo-rs".to_string()));
144+
145+
let restored_files = runner.restored_files.clone().into_inner();
146+
let expected = ["/usr/bin/sudo", "/usr/bin/su", "/usr/sbin/visudo"];
147+
148+
assert_eq!(restored_files.len(), 3);
149+
for f in restored_files.iter() {
150+
assert!(expected.contains(&f.as_str()));
151+
}
152+
}
153+
154+
fn sudors_fixture(system: &MockSystem) -> SudoRsExperiment {
155+
SudoRsExperiment::new(system)
156+
}
157+
158+
fn sudors_compatible_runner() -> MockSystem {
159+
let runner = MockSystem::default();
160+
runner.mock_files(vec![
161+
("/usr/lib/cargo/bin/sudo", ""),
162+
("/usr/lib/cargo/bin/su", ""),
163+
("/usr/lib/cargo/bin/visudo", ""),
164+
("/usr/bin/sudo", ""),
165+
("/usr/bin/su", ""),
166+
("/usr/sbin/visudo", ""),
167+
]);
168+
runner
169+
}
170+
171+
fn incompatible_runner() -> MockSystem {
172+
MockSystem::new(Distribution {
173+
id: "Ubuntu".to_string(),
174+
release: "20.04".to_string(),
175+
})
176+
}
177+
}

src/experiments/uutils.rs

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::utils::Worker;
22
use anyhow::Result;
33
use std::path::{Path, PathBuf};
4-
use tracing::{info, warn};
4+
use tracing::info;
55
use which::which;
66

77
/// An experiment to install and configure a Rust-based replacement for a system utility.
@@ -35,34 +35,28 @@ impl<'a> UutilsExperiment<'a> {
3535
}
3636

3737
/// Check if the system is compatible with the experiment.
38-
fn check_compatible(&self) -> bool {
38+
pub fn check_compatible(&self) -> bool {
3939
self.system.distribution().release >= self.first_supported_release
4040
}
4141

42+
/// Reports the first supported release for the experiment.
43+
pub fn first_supported_release(&self) -> &str {
44+
&self.first_supported_release
45+
}
46+
4247
/// Check if the package is installed.
43-
fn check_installed(&self) -> bool {
48+
pub fn check_installed(&self) -> bool {
4449
self.system.check_installed(&self.package).unwrap_or(false)
4550
}
46-
}
4751

48-
impl UutilsExperiment<'_> {
4952
/// Report the name of the experiment.
5053
pub fn name(&self) -> String {
5154
self.name.clone()
5255
}
5356

5457
/// Enable the experiment by installing and configuring the package.
5558
pub fn enable(&self) -> Result<()> {
56-
if !self.check_compatible() {
57-
warn!(
58-
"Skipping '{}'. Minimum supported release is {}.",
59-
self.package, self.first_supported_release
60-
);
61-
return Ok(());
62-
}
63-
6459
info!("Installing and configuring {}", self.package);
65-
6660
self.system.install_package(&self.package)?;
6761

6862
let files = self.system.list_files(self.bin_directory.clone())?;
@@ -87,12 +81,8 @@ impl UutilsExperiment<'_> {
8781

8882
/// Disable the experiment by removing the package and restoring the original files.
8983
pub fn disable(&self) -> Result<()> {
90-
if !self.check_installed() {
91-
warn!("{} not found, skipping restore", self.package);
92-
return Ok(());
93-
}
94-
9584
info!("Removing {}", self.package);
85+
self.system.remove_package(&self.package)?;
9686

9787
let files = self.system.list_files(self.bin_directory.clone())?;
9888

@@ -105,8 +95,6 @@ impl UutilsExperiment<'_> {
10595
self.system.restore_file(existing)?;
10696
}
10797

108-
self.system.remove_package(&self.package)?;
109-
11098
Ok(())
11199
}
112100
}
@@ -120,14 +108,7 @@ mod tests {
120108
fn test_uutils_incompatible_distribution() {
121109
let runner = incompatible_runner();
122110
let coreutils = coreutils_fixture(&runner);
123-
124111
assert!(!coreutils.check_compatible());
125-
126-
assert!(coreutils.enable().is_ok());
127-
assert_eq!(runner.commands.clone().into_inner().len(), 0);
128-
assert_eq!(runner.created_symlinks.clone().into_inner().len(), 0);
129-
assert_eq!(runner.backed_up_files.clone().into_inner().len(), 0);
130-
assert_eq!(runner.restored_files.clone().into_inner().len(), 0);
131112
}
132113

133114
#[test]
@@ -194,19 +175,6 @@ mod tests {
194175
assert_eq!(runner.restored_files.clone().into_inner().len(), 0);
195176
}
196177

197-
#[test]
198-
fn test_uutils_restore_not_installed() {
199-
let runner = MockSystem::default();
200-
let coreutils = coreutils_fixture(&runner);
201-
202-
assert!(coreutils.disable().is_ok());
203-
204-
assert_eq!(runner.commands.clone().into_inner().len(), 0);
205-
assert_eq!(runner.created_symlinks.clone().into_inner().len(), 0);
206-
assert_eq!(runner.backed_up_files.clone().into_inner().len(), 0);
207-
assert_eq!(runner.restored_files.clone().into_inner().len(), 0);
208-
}
209-
210178
#[test]
211179
fn test_uutils_restore_installed() {
212180
let runner = coreutils_compatible_runner();

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ struct Args {
6161
#[arg(
6262
short,
6363
long,
64-
default_values_t = vec!["coreutils".to_string(), "findutils".to_string(), "diffutils".to_string()],
64+
default_values_t = vec!["coreutils".to_string(), "findutils".to_string(), "diffutils".to_string(), "sudo-rs".to_string()],
6565
global = true,
6666
num_args = 1..,
6767
help = "Select experiments to enable or disable"

0 commit comments

Comments
 (0)