1use std::fs;
2use std::os::unix::fs::PermissionsExt;
3use std::path::{Path, PathBuf};
4use std::process::ExitCode;
5
6use crate::cli::ShimCommand;
7use crate::error::SboxError;
8
9const SHIM_TARGETS: &[&str] = &[
14 "npm", "npx", "pnpm", "yarn", "bun", "uv", "pip", "pip3", "poetry", "cargo", "composer",
16 "node", "python3", "python", "go",
18];
19
20pub fn execute(command: &ShimCommand) -> Result<ExitCode, SboxError> {
21 let shim_dir = resolve_shim_dir(command)?;
22
23 if !command.dry_run {
24 fs::create_dir_all(&shim_dir).map_err(|source| SboxError::InitWrite {
25 path: shim_dir.clone(),
26 source,
27 })?;
28 }
29
30 let mut created = 0usize;
31 let mut skipped = 0usize;
32
33 for &name in SHIM_TARGETS {
34 let dest = shim_dir.join(name);
35
36 if dest.exists() && !command.force && !command.dry_run {
37 println!(
38 "skip {} (already exists; use --force to overwrite)",
39 dest.display()
40 );
41 skipped += 1;
42 continue;
43 }
44
45 let real_binary = find_real_binary(name, &shim_dir);
46 let script = render_shim(name, real_binary.as_deref());
47
48 if command.dry_run {
49 match &real_binary {
50 Some(p) => println!("would create {} -> {}", dest.display(), p.display()),
51 None => println!("would create {} (real binary not found)", dest.display()),
52 }
53 created += 1;
54 continue;
55 }
56
57 fs::write(&dest, &script).map_err(|source| SboxError::InitWrite {
58 path: dest.clone(),
59 source,
60 })?;
61
62 let mut perms = fs::metadata(&dest)
63 .map_err(|source| SboxError::InitWrite {
64 path: dest.clone(),
65 source,
66 })?
67 .permissions();
68 perms.set_mode(0o755);
69 fs::set_permissions(&dest, perms).map_err(|source| SboxError::InitWrite {
70 path: dest.clone(),
71 source,
72 })?;
73
74 match &real_binary {
75 Some(p) => println!("created {} -> {}", dest.display(), p.display()),
76 None => println!(
77 "created {} (real binary not found at shim time)",
78 dest.display()
79 ),
80 }
81 created += 1;
82 }
83
84 if !command.dry_run {
85 println!();
86 if created > 0 {
87 println!(
88 "Add {} to your PATH before the real package manager binaries:",
89 shim_dir.display()
90 );
91 println!();
92 println!(" export PATH=\"{}:$PATH\"", shim_dir.display());
93 println!();
94 println!("Then restart your shell or run: source ~/.bashrc");
95 }
96 if skipped > 0 {
97 println!("({skipped} skipped — use --force to overwrite)");
98 }
99 }
100
101 Ok(ExitCode::SUCCESS)
102}
103
104fn resolve_shim_dir(command: &ShimCommand) -> Result<PathBuf, SboxError> {
105 if let Some(dir) = &command.dir {
106 let abs = if dir.is_absolute() {
107 dir.clone()
108 } else {
109 std::env::current_dir()
110 .map_err(|source| SboxError::CurrentDirectory { source })?
111 .join(dir)
112 };
113 return Ok(abs);
114 }
115
116 if let Some(home) = std::env::var_os("HOME") {
118 return Ok(PathBuf::from(home).join(".local/bin"));
119 }
120
121 std::env::current_dir().map_err(|source| SboxError::CurrentDirectory { source })
123}
124
125fn find_real_binary(name: &str, exclude_dir: &Path) -> Option<PathBuf> {
127 let path_os = std::env::var_os("PATH")?;
128 for dir in std::env::split_paths(&path_os) {
129 if dir == exclude_dir {
130 continue;
131 }
132 let candidate = dir.join(name);
133 if is_executable_file(&candidate) {
134 return Some(candidate);
135 }
136 }
137 None
138}
139
140fn is_executable_file(path: &Path) -> bool {
141 path.metadata()
142 .map(|m| m.is_file() && (m.permissions().mode() & 0o111) != 0)
143 .unwrap_or(false)
144}
145
146fn render_shim(name: &str, real_binary: Option<&Path>) -> String {
152 let fallback = match real_binary {
153 Some(path) => format!(
154 "printf 'sbox: no sbox.yaml found — running {name} unsandboxed\\n' >&2\nexec {path} \"$@\"",
155 path = path.display()
156 ),
157 None => format!(
158 "printf 'sbox shim: {name}: real binary not found; reinstall or run `sbox shim` again\\n' >&2\nexit 127"
159 ),
160 };
161
162 format!(
165 "#!/bin/sh\n\
166 # sbox shim: {name}\n\
167 # Generated by `sbox shim`. Re-run `sbox shim --dir DIR` to regenerate.\n\
168 _sbox_d=\"$PWD\"\n\
169 while true; do\n\
170 \x20 if [ -f \"$_sbox_d/sbox.yaml\" ]; then\n\
171 \x20 exec sbox run -- {name} \"$@\"\n\
172 \x20 fi\n\
173 \x20 [ \"$_sbox_d\" = \"/\" ] && break\n\
174 \x20 _sbox_d=\"${{_sbox_d%/*}}\"\n\
175 \x20 [ -z \"$_sbox_d\" ] && _sbox_d=\"/\"\n\
176 done\n\
177 {fallback}\n"
178 )
179}
180
181#[cfg(test)]
182mod tests {
183 use super::render_shim;
184
185 #[test]
186 fn shim_contains_sbox_run_delegation() {
187 let script = render_shim("npm", Some(std::path::Path::new("/usr/bin/npm")));
188 assert!(script.contains("exec sbox run -- npm \"$@\""));
189 assert!(script.contains("sbox.yaml"));
190 assert!(script.contains("exec /usr/bin/npm \"$@\""));
191 assert!(script.contains("running npm unsandboxed"));
192 }
193
194 #[test]
195 fn shim_fallback_when_real_binary_missing() {
196 let script = render_shim("npm", None);
197 assert!(script.contains("real binary not found"));
198 assert!(script.contains("exit 127"));
199 }
200
201 #[test]
202 fn shim_walks_to_root() {
203 let script = render_shim("uv", Some(std::path::Path::new("/usr/local/bin/uv")));
204 assert!(script.contains("_sbox_d%/*"));
206 assert!(script.contains("[ \"$_sbox_d\" = \"/\" ] && break"));
208 }
209}