1pub mod bwrap;
4pub mod host;
5
6use crate::config::{Binding, Config, trim_slashes};
7use host::Host;
8use std::ffi::{CString, OsString};
9use std::os::unix::ffi::OsStrExt;
10
11#[derive(Debug)]
13pub enum CliError {
14 Exit { code: i32, text: String },
16 Usage(String),
18}
19
20const VERSION: &str = env!("CARGO_PKG_VERSION");
21
22const USAGE: &str = "\
23Usage:
24 proot [option] ... [command]
25
26Regular options:
27 -r *path*, --rootfs=*path* Use *path* as the new guest root file-system, default is /.
28 -b *path*, --bind=*path* Make the content of *path* accessible in the guest rootfs (host:guest).
29 -w *path*, --cwd=*path* Set the initial working directory to *path*.
30 --kill-on-exit Kill all processes on command exit (always on; accepted for compatibility).
31 -v *value*, --verbose=*value* Set the level of debug information to *value*.
32 -V, --version Print version, copyright, license and contact, then exit.
33 -h, --help Print the version and the command-line usage, then exit.
34
35Extension options:
36 -k *string*, --kernel-release=*string* Make current kernel appear as kernel release *string*.
37 -0, --root-id Make current user appear as \"root\": ids read as 0, ownership changes succeed.
38 -l, --link2symlink Accepted and ignored.
39
40Alias options:
41 -R *path* Alias: -r *path* + a couple of recommended -b.
42 -S *path* Alias: -0 -r *path* + a couple of recommended -b.
43
44Environment: PROOT_THREADS (default 4) and PROOT_SPIN_US (default 200) tune the supervisor pool;
45PROOT_NO_PTRACE=1 forces the mode used where ptrace is filtered out; PROOT_USERNS=1 forces, and
46PROOT_USERNS=0 rules out, the user namespace used where Yama's ptrace_scope is 2; where neither
47works the guest-stub mode is the fallback (x86_64 and aarch64 hosts), which PROOT_GUEST_STUB=0
48rules out and PROOT_GUEST_STUB=force takes anywhere.
49";
50
51const RECOMMENDED_R: &[&str] = &[
53 "/etc/host.conf",
54 "/etc/hosts",
55 "/etc/hosts.equiv",
56 "/etc/mtab",
57 "/etc/netgroup",
58 "/etc/networks",
59 "/etc/passwd",
60 "/etc/group",
61 "/etc/nsswitch.conf",
62 "/etc/resolv.conf",
63 "/etc/localtime",
64 "/dev/",
65 "/sys/",
66 "/proc/",
67 "/tmp/",
68 "/run/",
69 "/var/run/dbus/system_bus_socket",
70];
71const RECOMMENDED_S: &[&str] = &[
72 "/etc/host.conf",
73 "/etc/hosts",
74 "/etc/nsswitch.conf",
75 "/etc/resolv.conf",
76 "/dev/",
77 "/sys/",
78 "/proc/",
79 "/tmp/",
80 "/run/shm",
81];
82
83pub fn parse(args: Vec<OsString>, host: &dyn Host) -> Result<Config, CliError> {
99 let mut cfg = Config::default();
100 let mut it = args.into_iter().peekable();
101 let mut cwd_set = false;
102 while let Some(a) = it.peek().cloned() {
103 let s = a.as_bytes();
104 if s.is_empty() || s[0] != b'-' || s == b"-" {
105 break;
106 }
107 it.next();
108 if s == b"--" {
109 break;
110 }
111 let (opt, inline): (Vec<u8>, Option<Vec<u8>>) = match s.iter().position(|&c| c == b'=') {
113 Some(i) if s.starts_with(b"--") => (s[..i].to_vec(), Some(s[i + 1..].to_vec())),
114 _ => (s.to_vec(), None),
115 };
116 let value =
117 |it: &mut std::iter::Peekable<std::vec::IntoIter<OsString>>| -> Result<Vec<u8>, CliError> {
118 if let Some(v) = inline.clone() {
119 return Ok(v);
120 }
121 it.next().map(|v| v.as_bytes().to_vec()).ok_or_else(|| {
122 CliError::Usage(format!("missing value for {}", String::from_utf8_lossy(&opt)))
123 })
124 };
125 match &opt[..] {
126 b"-r" | b"--rootfs" => cfg.root = host.host_path(&value(&mut it)?),
127 b"-R" | b"-S" => {
128 cfg.root = host.host_path(&value(&mut it)?);
129 let list = if opt == b"-R" {
130 RECOMMENDED_R
131 } else {
132 RECOMMENDED_S
133 };
134 for p in list {
135 if host.exists(p.as_bytes()) {
136 cfg.bindings.push(bind(host, p.as_bytes(), None));
137 }
138 }
139 if let Some(home) = host.var(b"HOME") {
140 cfg.bindings.push(bind(host, &home, None));
141 }
142 if opt == b"-S" {
143 cfg.fake_root = true;
144 }
145 }
146 b"-b" | b"--bind" | b"-m" | b"--mount" => {
147 let v = value(&mut it)?;
148 match v.iter().position(|&c| c == b':') {
149 Some(i) => cfg.bindings.push(bind(host, &v[..i], Some(&v[i + 1..]))),
150 None => cfg.bindings.push(bind(host, &v, None)),
151 }
152 }
153 b"-w" | b"--cwd" | b"--pwd" => {
154 cfg.cwd = value(&mut it)?;
155 cwd_set = true;
156 }
157 b"--kill-on-exit" => cfg.kill_on_exit = true,
158 b"-v" | b"--verbose" => {
159 let v = value(&mut it)?;
160 cfg.verbose = String::from_utf8_lossy(&v)
161 .parse()
162 .map_err(|_| CliError::Usage("bad verbosity".into()))?;
163 }
164 b"-k" | b"--kernel-release" => cfg.kernel_release = Some(value(&mut it)?),
165 b"-0" | b"--root-id" => cfg.fake_root = true,
166 b"-l" | b"--link2symlink" | b"-i" | b"--change-id" | b"-p" | b"--port" => {
167 if matches!(&opt[..], b"-i" | b"--change-id" | b"-p" | b"--port") {
168 value(&mut it)?;
169 }
170 eprintln!(
171 "proot: option {} is accepted but not implemented yet",
172 String::from_utf8_lossy(&opt)
173 );
174 }
175 b"-q" | b"--qemu" => return Err(CliError::Usage("qemu support is not implemented".into())),
176 b"-V" | b"--version" => {
177 return Err(CliError::Exit {
178 code: 0,
179 text: format!(
180 "proot-next {VERSION}\nA Rust reimplementation of PRoot (seccomp user notification + ptrace).\n"
181 ),
182 });
183 }
184 b"-h" | b"--help" | b"--usage" => {
185 return Err(CliError::Exit {
186 code: 0,
187 text: format!("proot-next {VERSION}\n\n{USAGE}"),
188 });
189 }
190 _ => {
191 return Err(CliError::Usage(format!(
192 "unknown option {}",
193 String::from_utf8_lossy(&opt)
194 )));
195 }
196 }
197 }
198 cfg.command = it
199 .map(|a| CString::new(a.as_bytes()).unwrap_or_default())
200 .collect();
201 if cfg.command.is_empty() {
202 let shell = host.var(b"SHELL").unwrap_or_else(|| b"/bin/sh".to_vec());
203 cfg.command = vec![CString::new(shell).unwrap_or_default()];
204 }
205 if !cwd_set {
206 cfg.cwd = host.cwd();
208 cfg.cwd_default = true;
209 }
210 Ok(cfg)
211}
212
213fn bind(host: &dyn Host, path: &[u8], guest: Option<&[u8]>) -> Binding {
215 let path = trim_slashes(path.to_vec());
216 let guest = trim_slashes(guest.map(|g| g.to_vec()).unwrap_or_else(|| path.clone()));
217 Binding {
218 host: trim_slashes(host.host_path(&path)),
219 guest,
220 read_only: false,
221 }
222}