use crate::executable::is_executable as is_executable_candidate;
use std::ffi::{OsStr, OsString};
pub fn env_passthrough() -> bool {
std::env::var("RDSH_PASSTHROUGH").is_ok_and(|v| v == "1")
}
pub fn env_dry() -> bool {
std::env::var("RDSH_DRY_RUN").is_ok_and(|v| v == "1")
}
fn origin_file() -> Option<String> {
let home = crate::inspect::home_dir()?;
let p = format!("{home}/.config/rdsh/origin");
let s = std::fs::read_to_string(p).ok()?;
let s = s.trim().to_string();
if s.is_empty() {
None
} else {
Some(s)
}
}
#[cfg(unix)]
fn warn_if_risky_origin(path: &str, via: &str) {
use std::os::unix::fs::MetadataExt;
let Ok(md) = std::fs::metadata(path) else {
return;
};
let mut why: Vec<&str> = vec![];
if !md.is_file() {
why.push("not a regular file");
}
if md.uid() != unsafe { libc::getuid() } {
why.push("not owned by you");
}
if md.mode() & 0o022 != 0 {
why.push("group/world-writable");
}
if !why.is_empty() {
eprintln!(
"[rdsh] warning: original dsh from {via} ({path}) is {} — check it before delegating",
why.join(" and ")
);
}
}
#[cfg(not(unix))]
fn warn_if_risky_origin(_path: &str, _via: &str) {}
fn is_runnable_original(path: &std::path::Path) -> bool {
if path.extension().is_some_and(|e| e == "js") {
return path.is_file();
}
is_executable_candidate(path)
}
fn is_node_entrypoint(orig: &str) -> bool {
orig.ends_with(".js") || node_shebang(std::path::Path::new(orig))
}
fn node_shebang(path: &std::path::Path) -> bool {
let Some(line) = shebang_line(path) else {
return false;
};
let tokens: Vec<&[u8]> = line
.split(|byte| *byte == b' ' || *byte == b'\t')
.filter(|token| !token.is_empty())
.collect();
let Some(program) = tokens.first().and_then(|token| token.strip_prefix(b"#!")) else {
return false;
};
let is_node = |token: &[u8]| token == b"node" || token.ends_with(b"/node");
let arguments: Vec<&[u8]> = tokens[1..]
.iter()
.copied()
.filter(|token| *token != b"-S")
.collect();
if is_node(program) {
arguments.is_empty()
} else if program == b"env" || program.ends_with(b"/env") {
matches!(arguments.as_slice(), [only] if is_node(only))
} else {
false
}
}
fn shebang_line(path: &std::path::Path) -> Option<Vec<u8>> {
use std::io::Read;
let mut head = [0u8; 256];
let mut file = std::fs::File::open(path).ok()?;
let read = file.read(&mut head).ok()?;
let head = &head[..read];
if !head.starts_with(b"#!") {
return None;
}
let end = head
.iter()
.position(|byte| *byte == b'\n' || *byte == b'\r')
.unwrap_or(head.len());
Some(head[..end].to_vec())
}
pub fn find_original_dsh() -> Option<String> {
for key in ["RDSH_ORIG_BIN", "DSH_ORIG_BIN"] {
if let Ok(p) = std::env::var(key) {
if !p.is_empty() {
warn_if_risky_origin(&p, key);
return Some(p);
}
}
}
if let Some(p) = origin_file() {
if is_runnable_original(std::path::Path::new(&p)) {
warn_if_risky_origin(&p, "origin file");
return Some(p);
}
}
let me = std::env::current_exe().ok();
if let Some(exe) = &me {
if let Some(dir) = exe.parent() {
for name in ["dsh-orig", "dsh.orig", "dsh.real", "dsh-orig.exe"] {
let cand = dir.join(name);
if is_runnable_original(&cand) {
return Some(cand.to_string_lossy().into_owned());
}
}
}
}
if let Ok(path) = std::env::var("PATH") {
for dir in std::env::split_paths(&path) {
#[cfg(target_os = "windows")]
let names = ["dsh.exe", "dsh"];
#[cfg(not(target_os = "windows"))]
let names = ["dsh"];
for n in names {
let cand = dir.join(n);
if !is_executable_candidate(&cand) {
continue;
}
let mut skip = false;
if let (Some(m), Ok(c)) = (&me, std::fs::canonicalize(&cand)) {
if let Ok(m) = std::fs::canonicalize(m) {
skip = m == c;
}
}
if !skip {
return Some(cand.to_string_lossy().into_owned());
}
}
}
}
if let Some(home) = crate::inspect::home_dir() {
for cand in [
format!("{home}/.local/bin/dsh.orig"),
format!("{home}/.local/bin/dsh-orig"),
] {
if is_executable_candidate(std::path::Path::new(&cand)) {
return Some(cand);
}
}
if let Some(tree) = latest_node_tree_for(&format!("{home}/.local/opt")) {
let cand = format!("{tree}/lib/node_modules/@deepseek-ai/dsh/lib/bin.js");
if is_runnable_original(std::path::Path::new(&cand)) {
return Some(cand);
}
}
}
crate::dsh_env::find_tool_fallback("dsh", me.as_deref())
}
fn current_os_arch() -> (&'static str, &'static str) {
let os = if cfg!(target_os = "windows") {
"win"
} else if cfg!(target_os = "macos") {
"darwin"
} else {
"linux"
};
let arch = if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"x64"
};
(os, arch)
}
pub fn parse_node_tree_version(dir_name: &str) -> Option<((u64, u64, u64), String, String)> {
let rest = dir_name.strip_prefix("node-v")?;
let mut parts = rest.split('-');
let ver = parts.next()?;
let nums: Vec<&str> = ver.split('.').collect();
if nums.len() != 3 {
return None;
}
let major: u64 = nums[0].parse().ok()?;
let minor: u64 = nums[1].parse().ok()?;
let patch: u64 = nums[2].parse().ok()?;
let os = parts.next()?.to_string();
let arch = parts.next()?.to_string();
if parts.next().is_some() {
return None;
}
if arch.is_empty() || os.is_empty() {
return None;
}
Some(((major, minor, patch), os, arch))
}
pub fn pick_latest_node_tree<'a>(
names: &'a [String],
want_os: &str,
want_arch: &str,
) -> Option<&'a str> {
let mut best: Option<(&'a str, (u64, u64, u64))> = None;
for n in names {
if let Some((v, os, arch)) = parse_node_tree_version(n) {
if os == want_os && arch == want_arch {
let take = match &best {
None => true,
Some((_, bv)) => v > *bv,
};
if take {
best = Some((n.as_str(), v));
}
}
}
}
best.map(|(n, _)| n)
}
pub fn latest_node_tree_for(opt_dir: &str) -> Option<String> {
let (want_os, want_arch) = current_os_arch();
let entries = std::fs::read_dir(opt_dir).ok()?;
let mut names: Vec<String> = Vec::new();
for e in entries.flatten() {
if e.metadata().is_ok_and(|m| m.is_dir()) {
if let Some(n) = e.file_name().to_str().map(str::to_owned) {
names.push(n);
}
}
}
pick_latest_node_tree(&names, want_os, want_arch).map(|n| format!("{opt_dir}/{n}"))
}
pub fn latest_node_tree() -> Option<String> {
let home = crate::inspect::home_dir()?;
latest_node_tree_for(&format!("{home}/.local/opt"))
}
pub fn find_node_bin() -> Option<String> {
#[cfg(target_os = "windows")]
const NAMES: &[&str] = &["node.exe", "node.cmd", "node.bat", "node"];
#[cfg(not(target_os = "windows"))]
const NAMES: &[&str] = &["node"];
if let Ok(path) = std::env::var("PATH") {
for dir in std::env::split_paths(&path) {
for n in NAMES {
let cand = dir.join(n);
if is_executable_candidate(&cand) {
return Some(cand.to_string_lossy().into_owned());
}
}
}
}
if let Some(tree) = latest_node_tree() {
#[cfg(target_os = "windows")]
let cand = format!("{tree}/bin/node.exe");
#[cfg(not(target_os = "windows"))]
let cand = format!("{tree}/bin/node");
if is_executable_candidate(std::path::Path::new(&cand)) {
return Some(cand);
}
}
let home = crate::inspect::home_dir()?;
crate::dsh_env::latest_nvm_node(std::path::Path::new(&home))
.map(|node| node.to_string_lossy().into_owned())
}
fn node_for_dsh(orig: &str) -> Option<String> {
let entry = std::fs::canonicalize(orig).ok()?;
let (wanted_os, wanted_arch) = current_os_arch();
for ancestor in entry.ancestors().skip(1) {
let Some((_, os, arch)) = ancestor
.file_name()
.and_then(|name| name.to_str())
.and_then(parse_node_tree_version)
else {
continue;
};
if os != wanted_os || arch != wanted_arch {
continue;
}
let node = ancestor
.join("bin")
.join(if cfg!(windows) { "node.exe" } else { "node" });
if is_executable_candidate(&node) {
return Some(node.to_string_lossy().into_owned());
}
#[cfg(windows)]
{
let node = ancestor.join("node.exe");
if is_executable_candidate(&node) {
return Some(node.to_string_lossy().into_owned());
}
}
}
crate::dsh_env::nvm_node_ancestor(&entry).map(|node| node.to_string_lossy().into_owned())
}
pub(crate) fn selected_node(orig: &str) -> Option<String> {
node_for_dsh(orig).or_else(find_node_bin)
}
const WEB_PROFILE: &str = "web";
fn dump_composed_config(orig: &str, profile: &str) -> Option<String> {
let mut cmd = base_cmd(orig, false, true, crate::locale::Locale::English).ok()?;
cmd.arg("--profile")
.arg(profile)
.arg("--dump-config")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null());
let output = cmd.output().ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout).ok()
}
fn write_overlay(body: &str) -> std::io::Result<String> {
let dir = format!("{}/webui", crate::inspect::dsh_home());
std::fs::create_dir_all(&dir)?;
let path = format!("{dir}/gzip-level.patch.yml");
std::fs::write(&path, body)?;
Ok(path)
}
fn gzip_overlay(orig: &str, profile: &str) -> Option<String> {
if profile != WEB_PROFILE {
return None;
}
if !crate::rdsh_config::feature_enabled(
&crate::rdsh_config::load(),
crate::webui_gzip::FEATURE_ID,
) {
return None;
}
let configured = crate::webui_gzip::DEFAULT_COMPRESSION_LEVEL;
let level = crate::webui_gzip::resolve_level(
std::env::var(crate::webui_gzip::ENV_LEVEL).ok().as_deref(),
configured,
);
if !crate::webui_gzip::is_worth_applying(level) {
return None;
}
let composed = dump_composed_config(orig, profile)?;
let overlay = crate::webui_gzip::overlay_for(&composed, level)?;
write_overlay(&overlay).ok()
}
const FRONTEND_ASSETS_REL: &str = "node_modules/@deepseek-ai/dsh-web-frontend/dist/assets";
fn find_frontend_assets_dir(orig: &str) -> Option<std::path::PathBuf> {
if let Ok(explicit) = std::env::var("RDSH_WEB_ASSETS_DIST") {
if !explicit.is_empty() {
let p = std::path::PathBuf::from(&explicit);
return p.is_dir().then_some(p);
}
}
let real = std::fs::canonicalize(orig).unwrap_or_else(|_| std::path::PathBuf::from(orig));
for ancestor in real.ancestors() {
let inside_pkg = ancestor.join(FRONTEND_ASSETS_REL);
if inside_pkg.is_dir() {
return Some(inside_pkg);
}
let under_tree = ancestor
.join("lib")
.join("node_modules")
.join("@deepseek-ai")
.join("dsh")
.join(FRONTEND_ASSETS_REL);
if under_tree.is_dir() {
return Some(under_tree);
}
}
let home = crate::inspect::home_dir()?;
let tree = latest_node_tree_for(&format!("{home}/.local/opt"))?;
let cand = std::path::PathBuf::from(tree)
.join("lib")
.join("node_modules")
.join("@deepseek-ai")
.join("dsh")
.join(FRONTEND_ASSETS_REL);
cand.is_dir().then_some(cand)
}
fn profile_of<T: AsRef<OsStr>>(args: &[T]) -> Option<&str> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
let arg = arg.as_ref();
if arg == "--profile" {
return iter.next().and_then(|value| value.as_ref().to_str());
}
if let Some(value) = arg
.to_str()
.and_then(|value| value.strip_prefix("--profile="))
{
return Some(value);
}
}
None
}
fn apply_web_assets(cmd: &mut std::process::Command, orig: &str, profile: &str) {
if profile != WEB_PROFILE {
return;
}
if std::env::var_os("RDSH_WEB_ASSETS").is_some() {
return;
}
if !crate::rdsh_config::feature_enabled(
&crate::rdsh_config::load(),
crate::webui_assets::FEATURE_ID,
) {
return;
}
let Some(dist) = find_frontend_assets_dir(orig) else {
return;
};
let home = crate::inspect::dsh_home();
let Some(cache) = crate::webui_assets::prepare(std::path::Path::new(&home), &dist) else {
return;
};
cmd.env("RDSH_WEB_ASSETS", cache);
}
fn apply_gzip_overlay(cmd: &mut std::process::Command, orig: &str, profile: &str) -> bool {
let Some(path) = gzip_overlay(orig, profile) else {
return false;
};
cmd.arg("--patch").arg(path);
true
}
fn base_cmd(
orig: &str,
dry: bool,
metadata_only: bool,
locale: crate::locale::Locale,
) -> anyhow::Result<std::process::Command> {
if dry || metadata_only {
let mut command = if is_node_entrypoint(orig) {
let mut command = std::process::Command::new(
selected_node(orig).unwrap_or_else(|| "node".to_string()),
);
command.arg(orig);
command
} else {
std::process::Command::new(orig)
};
apply_privacy_default(&mut command);
apply_web_search_provider(&mut command);
return Ok(command);
}
let bin = selected_node(orig).ok_or_else(|| {
anyhow::anyhow!(match locale {
crate::locale::Locale::English => "RDSH_SECURITY: compatible Node runtime is required",
crate::locale::Locale::Chinese => "RDSH_SECURITY:需要兼容的 Node 运行时",
crate::locale::Locale::Japanese =>
"RDSH_SECURITY: 互換性のある Node ランタイムが必要です",
})
})?;
if !crate::tool_security::sandbox_enabled() {
let mut command = if is_node_entrypoint(orig) {
let mut command = std::process::Command::new(&bin);
if let Some(preload) = crate::opencode_header::import_specifier() {
command.arg("--import").arg(preload);
}
command.arg(orig);
command
} else {
std::process::Command::new(orig)
};
command.env("RDSH_VERSION", env!("CARGO_PKG_VERSION"));
apply_privacy_default(&mut command);
apply_web_search_provider(&mut command);
return Ok(command);
}
let mut command = crate::tool_security::command(orig, &bin, locale)?;
command.env("RDSH_VERSION", env!("CARGO_PKG_VERSION"));
apply_privacy_default(&mut command);
apply_web_search_provider(&mut command);
Ok(command)
}
static WEB_SEARCH_PROVIDER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
pub fn configure_web_search_provider(cfg: &crate::rdsh_config::RdshSettings) {
let _ = WEB_SEARCH_PROVIDER.set(cfg.search.dsh_search_provider.clone());
}
const WEB_SEARCH_PROVIDER_ENV: &str = "DSH_WEB_SEARCH_PROVIDER";
fn provider_env_id<'a>(configured: &'a str, existing: Option<&str>) -> Option<&'a str> {
if existing.is_some_and(|value| !value.is_empty()) {
return None;
}
let id = configured.trim();
if id.is_empty() || id.chars().any(|c| c.is_control() || c.is_whitespace()) {
return None;
}
Some(id)
}
fn apply_web_search_provider(cmd: &mut std::process::Command) {
let configured = WEB_SEARCH_PROVIDER.get().map(String::as_str).unwrap_or("");
let existing = std::env::var(WEB_SEARCH_PROVIDER_ENV).ok();
if let Some(id) = provider_env_id(configured, existing.as_deref()) {
cmd.env(WEB_SEARCH_PROVIDER_ENV, id);
}
}
fn apply_privacy_default(cmd: &mut std::process::Command) {
let existing = std::env::var("DSH_TELEMETRY_DISABLED").ok();
if let Some((key, value)) =
crate::slim::telemetry_env(crate::slim::features().telemetry, existing.as_deref())
{
cmd.env(key, value);
}
}
fn apply_slim(cmd: &mut std::process::Command, slim: bool, locale: crate::locale::Locale) {
if !slim {
return;
}
for (k, v) in crate::slim::slim_env() {
cmd.env(k, v);
}
if !crate::slim::features().node_compile_cache {
return;
}
let hint = std::env::var("RDSH_NODE_COMPILE_CACHE").ok();
let existing = std::env::var("NODE_COMPILE_CACHE").ok();
let default_dir = crate::slim::default_compile_cache_dir().unwrap_or_default();
if let Some(dir) =
crate::slim::resolve_node_compile_cache(hint.as_deref(), existing.as_deref(), &default_dir)
{
let _ = std::fs::create_dir_all(&dir);
if let Some(advice) = crate::slim::compile_cache_advice(&dir, locale) {
eprintln!("[rdsh slim] {advice}");
}
cmd.env("NODE_COMPILE_CACHE", dir);
}
}
fn exec_or_spawn(
mut cmd: std::process::Command,
dry: bool,
locale: crate::locale::Locale,
) -> anyhow::Result<()> {
if dry {
println!(
"[rdsh dry-run] {}: {cmd:?}",
match locale {
crate::locale::Locale::English => "would exec",
crate::locale::Locale::Chinese => "将执行",
crate::locale::Locale::Japanese => "実行予定",
}
);
return Ok(());
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = cmd.exec();
Err(anyhow::anyhow!(
"{}: {err}",
match locale {
crate::locale::Locale::English => "exec failed",
crate::locale::Locale::Chinese => "执行失败",
crate::locale::Locale::Japanese => "実行に失敗しました",
}
))
}
#[cfg(not(unix))]
{
match cmd.status() {
Ok(st) => std::process::exit(st.code().unwrap_or(1)),
Err(e) => Err(anyhow::anyhow!(
"{}: {e}",
match locale {
crate::locale::Locale::English => "spawn failed",
crate::locale::Locale::Chinese => "启动失败",
crate::locale::Locale::Japanese => "起動に失敗しました",
}
)),
}
}
}
pub fn exec_boot(
profile: &str,
from_default: Option<&str>,
patches: &[String],
app_args: &[String],
dry: bool,
slim: bool,
locale: crate::locale::Locale,
) -> anyhow::Result<()> {
let orig =
find_original_dsh().ok_or_else(|| anyhow::anyhow!(original_dsh_not_found(locale)))?;
let mut cmd = base_cmd(&orig, dry, false, locale)?;
if !dry {
crate::auth::pre_boot(true, locale);
}
cmd.arg("--profile").arg(profile);
if let Some(f) = from_default {
cmd.arg("--from-default-profile").arg(f);
}
let overlay = !dry && apply_gzip_overlay(&mut cmd, &orig, profile);
for p in patches {
cmd.arg("--patch").arg(p);
}
cmd.args(app_args);
apply_slim(&mut cmd, slim, locale);
apply_web_assets(&mut cmd, &orig, profile);
if slim {
eprintln!(
"[rdsh] {} '{profile}' {} {orig} ({}: {})",
match locale {
crate::locale::Locale::English => "boot",
crate::locale::Locale::Chinese => "启动",
crate::locale::Locale::Japanese => "起動",
},
match locale {
crate::locale::Locale::English => "via",
crate::locale::Locale::Chinese => "通过",
crate::locale::Locale::Japanese => "経由",
},
match locale {
crate::locale::Locale::English => "slim ON",
crate::locale::Locale::Chinese => "精简环境开启",
crate::locale::Locale::Japanese => "slim 環境オン",
},
crate::slim::describe()
);
}
if overlay {
eprintln!("[rdsh] web ui: gzip overlay applied");
}
exec_or_spawn(cmd, dry, locale)
}
pub fn exec_dump_config(
profile: &str,
from_default: Option<&str>,
kind: crate::dsh_args::DumpKind,
patches: &[String],
dry: bool,
slim: bool,
locale: crate::locale::Locale,
) -> anyhow::Result<()> {
let orig =
find_original_dsh().ok_or_else(|| anyhow::anyhow!(original_dsh_not_found(locale)))?;
let mut cmd = base_cmd(&orig, dry, false, locale)?;
if !dry {
crate::auth::pre_boot(false, locale);
}
cmd.arg("--profile").arg(profile);
if let Some(f) = from_default {
cmd.arg("--from-default-profile").arg(f);
}
for p in patches {
cmd.arg("--patch").arg(p);
}
match kind {
crate::dsh_args::DumpKind::Config => cmd.arg("--dump-config"),
crate::dsh_args::DumpKind::DefaultConfig => cmd.arg("--dump-default-config"),
crate::dsh_args::DumpKind::Schema => cmd.arg("--dump-config-schema"),
};
apply_slim(&mut cmd, slim, locale);
exec_or_spawn(cmd, dry, locale)
}
pub fn exec_raw(args: &[OsString], dry: bool, slim: bool) -> anyhow::Result<()> {
let orig = find_original_dsh().ok_or_else(|| {
anyhow::anyhow!("original dsh not found (set DSH_ORIG_BIN or reinstall with install.sh)")
})?;
let metadata_only = args.len() == 1
&& ["--version", "-V", "--help", "-h"]
.iter()
.any(|flag| args[0] == OsStr::new(flag));
let mut cmd = base_cmd(&orig, dry, metadata_only, crate::locale::Locale::English)?;
if !dry {
crate::auth::pre_boot(true, crate::locale::Locale::English);
}
cmd.args(args);
apply_slim(&mut cmd, slim, crate::locale::Locale::English);
if let Some(profile) = profile_of(args) {
let profile = profile.to_string();
apply_web_assets(&mut cmd, &orig, &profile);
}
exec_or_spawn(cmd, dry, crate::locale::Locale::English)
}
pub fn exec_plugin(
profile: &str,
pnpm_args: &[String],
dry: bool,
slim: bool,
locale: crate::locale::Locale,
) -> anyhow::Result<()> {
let orig =
find_original_dsh().ok_or_else(|| anyhow::anyhow!(original_dsh_not_found(locale)))?;
let mut cmd = base_cmd(&orig, dry, false, locale)?;
if !dry {
crate::auth::pre_boot(false, locale);
}
cmd.arg("plugin").arg("--profile").arg(profile);
cmd.args(pnpm_args);
apply_slim(&mut cmd, slim, locale);
exec_or_spawn(cmd, dry, locale)
}
fn original_dsh_not_found(locale: crate::locale::Locale) -> &'static str {
match locale {
crate::locale::Locale::English => "original dsh not found in PATH (set DSH_ORIG_BIN)",
crate::locale::Locale::Chinese => "未在 PATH 中找到原始 dsh(请设置 DSH_ORIG_BIN)",
crate::locale::Locale::Japanese => {
"PATH に元の dsh が見つかりません(DSH_ORIG_BIN を設定してください)"
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sv(v: &[&str]) -> Vec<String> {
v.iter().map(ToString::to_string).collect()
}
#[test]
fn web_search_provider_is_opt_in_and_never_overrides_the_caller() {
assert_eq!(provider_env_id("", None), None);
assert_eq!(provider_env_id(" ", Some("")), None);
assert_eq!(provider_env_id("local searxng", None), None);
assert_eq!(
provider_env_id("searxng\nDSH_TELEMETRY_DISABLED=1", None),
None
);
assert_eq!(provider_env_id("searxng", Some("exa")), None);
assert_eq!(provider_env_id(" searxng ", Some("")), Some("searxng"));
assert_eq!(
provider_env_id("dsh-web-search-searxng", None),
Some("dsh-web-search-searxng")
);
}
#[test]
fn profile_flag_is_found_in_every_spelling() {
assert_eq!(profile_of(&sv(&["--profile", "web"])), Some("web"));
assert_eq!(profile_of(&sv(&["--profile=web"])), Some("web"));
assert_eq!(
profile_of(&sv(&["--profile", "web", "--", "--port", "1"])),
Some("web")
);
assert_eq!(profile_of(&sv(&["--port", "3080"])), None);
assert_eq!(profile_of(&sv(&["--profile"])), None);
assert_eq!(profile_of::<String>(&[]), None);
assert_eq!(
profile_of(&sv(&["--profile", "tui", "--profile", "web"])),
Some("tui")
);
}
#[test]
fn frontend_assets_are_found_through_a_bin_symlink_layout() {
let tree = std::env::temp_dir().join(format!(
"rdsh-assets-probe-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&tree);
let assets = tree
.join("lib/node_modules/@deepseek-ai/dsh")
.join(FRONTEND_ASSETS_REL);
std::fs::create_dir_all(&assets).unwrap();
std::fs::write(assets.join("index-abc.js"), b"x").unwrap();
let bin = tree.join("bin");
std::fs::create_dir_all(&bin).unwrap();
let dsh = bin.join("dsh");
std::fs::write(&dsh, b"").unwrap();
let expected = std::fs::canonicalize(&assets).unwrap();
let found = find_frontend_assets_dir(&dsh.to_string_lossy());
let found_canonical = found.as_ref().and_then(|p| std::fs::canonicalize(p).ok());
assert_eq!(found_canonical.as_deref(), Some(expected.as_path()));
assert!(find_frontend_assets_dir("/nonexistent/rdsh/bin/dsh").is_none());
let _ = std::fs::remove_dir_all(&tree);
}
#[test]
fn parses_tree_names() {
let p = parse_node_tree_version("node-v24.16.0-linux-x64").unwrap();
assert_eq!(p.0, (24, 16, 0));
assert_eq!(p.1, "linux");
assert_eq!(p.2, "x64");
assert!(parse_node_tree_version("node-v24.16.0").is_none());
assert!(parse_node_tree_version("node-v24-linux-x64").is_none());
assert!(parse_node_tree_version("other-v24.0.0-linux-x64").is_none());
}
fn env_lock() -> crate::test_support::EnvLock {
crate::test_support::env_lock()
}
struct EnvGuard {
saved: Vec<(String, Option<String>)>,
}
impl EnvGuard {
fn set(keys: &[&str]) -> Self {
let saved = keys
.iter()
.map(|k| (k.to_string(), std::env::var(k).ok()))
.collect();
Self { saved }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (k, v) in &self.saved {
match v {
Some(v) => std::env::set_var(k, v),
None => std::env::remove_var(k),
}
}
}
}
#[cfg(unix)]
fn make_exec(path: &std::path::Path) {
use std::os::unix::fs::PermissionsExt;
let mut p = std::fs::metadata(path).unwrap().permissions();
p.set_mode(0o755);
std::fs::set_permissions(path, p).unwrap();
}
#[cfg(unix)]
fn make_non_exec(path: &std::path::Path) {
use std::os::unix::fs::PermissionsExt;
let mut p = std::fs::metadata(path).unwrap().permissions();
p.set_mode(0o644);
std::fs::set_permissions(path, p).unwrap();
}
#[cfg(unix)]
fn make_other_exec_only(path: &std::path::Path) {
use std::os::unix::fs::PermissionsExt;
let mut p = std::fs::metadata(path).unwrap().permissions();
p.set_mode(0o001);
std::fs::set_permissions(path, p).unwrap();
}
#[test]
fn skips_directory_and_non_executable_candidates() {
let root = std::env::temp_dir().join(format!(
"rdsh-passthrough-cand-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&root).unwrap();
let dir_cand = root.join("dsh-dir");
std::fs::create_dir_all(&dir_cand).unwrap();
assert!(!is_executable_candidate(&dir_cand));
assert!(!is_executable_candidate(&root.join("missing")));
let file_cand = root.join("dsh-file");
std::fs::write(&file_cand, b"x").unwrap();
#[cfg(unix)]
{
make_non_exec(&file_cand);
assert!(!is_executable_candidate(&file_cand));
make_exec(&file_cand);
assert!(is_executable_candidate(&file_cand));
if unsafe { libc::geteuid() } != 0 {
make_other_exec_only(&file_cand);
assert!(!is_executable_candidate(&file_cand));
}
}
#[cfg(not(unix))]
{
assert!(is_executable_candidate(&file_cand));
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn path_lookup_skips_non_executable_dsh_and_node() {
let _guard = env_lock();
let env = EnvGuard::set(&[
"PATH",
"RDSH_ORIG_BIN",
"DSH_ORIG_BIN",
"HOME",
"USERPROFILE",
]);
let root = std::env::temp_dir().join(format!(
"rdsh-passthrough-path-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let first = root.join("first");
let second = root.join("second");
let home = root.join("home");
std::fs::create_dir_all(&first).unwrap();
std::fs::create_dir_all(&second).unwrap();
std::fs::create_dir_all(&home).unwrap();
std::env::set_var("HOME", &home);
std::env::set_var("USERPROFILE", &home);
std::env::remove_var("RDSH_ORIG_BIN");
std::env::remove_var("DSH_ORIG_BIN");
let valid_dsh = second.join("dsh");
std::fs::write(&valid_dsh, b"#!/bin/sh\nexit 0\n").unwrap();
let valid_node = second.join("node");
std::fs::write(&valid_node, b"#!/bin/sh\nexit 0\n").unwrap();
#[cfg(unix)]
{
make_exec(&valid_dsh);
make_exec(&valid_node);
}
let sys_path = env.saved[0].1.clone().unwrap_or_default();
let mut parts = vec![first.clone(), second];
parts.extend(std::env::split_paths(&sys_path));
std::env::set_var("PATH", std::env::join_paths(&parts).unwrap());
std::fs::create_dir_all(first.join("dsh")).unwrap();
std::fs::create_dir_all(first.join("node")).unwrap();
assert_eq!(
find_original_dsh(),
Some(valid_dsh.to_string_lossy().into_owned())
);
assert_eq!(
find_node_bin(),
Some(valid_node.to_string_lossy().into_owned())
);
std::fs::remove_dir_all(first.join("dsh")).unwrap();
std::fs::remove_dir_all(first.join("node")).unwrap();
#[cfg(unix)]
{
std::fs::write(first.join("dsh"), b"not executable\n").unwrap();
make_non_exec(&first.join("dsh"));
std::fs::write(first.join("node"), b"not executable\n").unwrap();
make_non_exec(&first.join("node"));
assert_eq!(
find_original_dsh(),
Some(valid_dsh.to_string_lossy().into_owned())
);
assert_eq!(
find_node_bin(),
Some(valid_node.to_string_lossy().into_owned())
);
if unsafe { libc::geteuid() } != 0 {
std::fs::write(first.join("dsh"), b"not executable for owner\n").unwrap();
make_other_exec_only(&first.join("dsh"));
std::fs::write(first.join("node"), b"not executable for owner\n").unwrap();
make_other_exec_only(&first.join("node"));
assert_eq!(
find_original_dsh(),
Some(valid_dsh.to_string_lossy().into_owned())
);
assert_eq!(
find_node_bin(),
Some(valid_node.to_string_lossy().into_owned())
);
}
}
#[cfg(unix)]
{
std::env::set_var("RDSH_ORIG_BIN", first.join("dsh"));
assert_eq!(
find_original_dsh(),
Some(first.join("dsh").to_string_lossy().into_owned())
);
std::env::remove_var("RDSH_ORIG_BIN");
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn skips_foreign_trees_and_picks_latest() {
let names = sv(&[
"node-v20.11.0-linux-x64",
"node-v24.16.0-darwin-arm64",
"node-v22.1.0-linux-x64",
"node-v24.16.0-linux-x64",
"node-v24.16.0-linux-arm64",
"node-v24.16.0-win-x64",
]);
assert_eq!(
pick_latest_node_tree(&names, "linux", "x64"),
Some("node-v24.16.0-linux-x64")
);
assert_eq!(
pick_latest_node_tree(&names, "darwin", "arm64"),
Some("node-v24.16.0-darwin-arm64")
);
assert_eq!(pick_latest_node_tree(&names, "win", "arm64"), None);
}
#[test]
fn delegated_js_boots_receive_the_preload_as_a_file_url() {
let _lock = env_lock();
if crate::tool_security::sandbox_enabled() {
return; }
let dir = std::env::temp_dir().join(format!(
"rdsh-base-cmd-{}-{}",
std::process::id(),
crate::local_http::random_token().unwrap()
));
std::fs::create_dir(&dir).unwrap();
let fixture = dir.join("dsh.js");
std::fs::write(&fixture, "// fixture\n").unwrap();
let orig = fixture.to_string_lossy().into_owned();
let command = base_cmd(&orig, false, false, crate::locale::Locale::English);
std::fs::remove_dir_all(&dir).unwrap();
let Ok(command) = command else {
return; };
let args: Vec<String> = command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect();
if crate::opencode_header::import_specifier().is_none() {
assert_eq!(args, vec![orig]); return;
}
assert_eq!(args.len(), 3, "unexpected delegated argv: {args:?}");
assert_eq!(args[0], "--import");
assert!(
args[1].starts_with("file:///") && args[1].ends_with("opencode-session-header.mjs"),
"the preload must be a file: URL, got {}",
args[1]
);
assert_eq!(args[2], orig);
}
#[test]
fn node_entrypoints_are_detected_from_the_shebang_not_the_name() {
let dir = std::env::temp_dir().join(format!(
"rdsh-node-entrypoint-{}-{}",
std::process::id(),
crate::local_http::random_token().unwrap()
));
std::fs::create_dir(&dir).unwrap();
let cases = [
("dsh-orig", "#!/usr/bin/env node\n", true),
("node-bin", "#!/usr/bin/node\n", true),
("env-s-node", "#!/usr/bin/env -S node\n", true),
(
"env-s-flags",
"#!/usr/bin/env -S node --no-warnings\n",
false,
),
("shell-wrapper", "#!/bin/sh\nexec node \"$@\"\n", false),
("binary", "\u{7f}ELF\n", false),
];
for (name, body, expected) in cases {
let path = dir.join(name);
std::fs::write(&path, body).unwrap();
assert_eq!(node_shebang(&path), expected, "shebang: {name}");
assert_eq!(
is_node_entrypoint(&path.to_string_lossy()),
expected || name.ends_with(".js"),
"entrypoint: {name}"
);
}
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn a_symlinked_backup_is_still_a_node_entrypoint() {
use std::os::unix::fs::symlink;
let dir = std::env::temp_dir().join(format!(
"rdsh-node-entrypoint-link-{}-{}",
std::process::id(),
crate::local_http::random_token().unwrap()
));
std::fs::create_dir(&dir).unwrap();
let target = dir.join("bin.js");
std::fs::write(&target, "#!/usr/bin/env node\n// fixture\n").unwrap();
let link = dir.join("dsh-orig");
symlink(&target, &link).unwrap();
assert!(node_shebang(&link));
assert!(is_node_entrypoint(&link.to_string_lossy()));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn delegated_extensionless_node_backups_receive_the_preload() {
let _lock = env_lock();
if crate::tool_security::sandbox_enabled() {
return; }
let dir = std::env::temp_dir().join(format!(
"rdsh-dsh-orig-{}-{}",
std::process::id(),
crate::local_http::random_token().unwrap()
));
std::fs::create_dir(&dir).unwrap();
let fixture = dir.join("dsh-orig");
std::fs::write(&fixture, "#!/usr/bin/env node\n// fixture\n").unwrap();
let orig = fixture.to_string_lossy().into_owned();
let command = base_cmd(&orig, false, false, crate::locale::Locale::English);
std::fs::remove_dir_all(&dir).unwrap();
let Ok(command) = command else {
return; };
let args: Vec<String> = command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect();
if crate::opencode_header::import_specifier().is_none() {
assert_eq!(args, vec![orig]); return;
}
assert_eq!(args.len(), 3, "unexpected delegated argv: {args:?}");
assert_eq!(args[0], "--import");
assert!(
args[1].starts_with("file:///") && args[1].ends_with("opencode-session-header.mjs"),
"the preload must be a file: URL, got {}",
args[1]
);
assert_eq!(args[2], orig);
}
#[test]
fn delegated_shell_wrappers_keep_their_direct_exec() {
if crate::tool_security::sandbox_enabled() {
return; }
let dir = std::env::temp_dir().join(format!(
"rdsh-shell-wrapper-{}-{}",
std::process::id(),
crate::local_http::random_token().unwrap()
));
std::fs::create_dir(&dir).unwrap();
let fixture = dir.join("dsh-orig");
std::fs::write(&fixture, "#!/bin/sh\nexec node \"$@\"\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&fixture, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let orig = fixture.to_string_lossy().into_owned();
let command = base_cmd(&orig, false, false, crate::locale::Locale::English);
std::fs::remove_dir_all(&dir).unwrap();
let Ok(command) = command else {
return; };
assert_eq!(
command.get_program().to_string_lossy(),
orig.as_str(),
"a shell wrapper must keep its direct exec"
);
let args: Vec<String> = command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect();
assert!(args.is_empty(), "unexpected delegated argv: {args:?}");
}
#[test]
fn delegated_runs_disable_upstream_telemetry_by_default() {
let _lock = env_lock();
let _env = EnvGuard::set(&["DSH_TELEMETRY_DISABLED", "RDSH_TELEMETRY"]);
std::env::remove_var("DSH_TELEMETRY_DISABLED");
std::env::remove_var("RDSH_TELEMETRY");
let dir = std::env::temp_dir().join(format!(
"rdsh-telemetry-{}-{}",
std::process::id(),
crate::local_http::random_token().unwrap()
));
std::fs::create_dir(&dir).unwrap();
let fixture = dir.join("dsh-orig");
std::fs::write(&fixture, "#!/bin/sh\nexit 0\n").unwrap();
#[cfg(unix)]
make_exec(&fixture);
let orig = fixture.to_string_lossy().into_owned();
let Ok(command) = base_cmd(&orig, false, false, crate::locale::Locale::English) else {
std::fs::remove_dir_all(&dir).unwrap();
return; };
let telemetry = command
.get_envs()
.find(|(key, _)| *key == "DSH_TELEMETRY_DISABLED")
.and_then(|(_, value)| value)
.map(|value| value.to_string_lossy().into_owned());
assert_eq!(telemetry.as_deref(), Some("1"));
std::env::set_var("DSH_TELEMETRY_DISABLED", "0");
let Ok(command) = base_cmd(&orig, false, false, crate::locale::Locale::English) else {
std::fs::remove_dir_all(&dir).unwrap();
return;
};
assert!(
command
.get_envs()
.all(|(key, _)| key != "DSH_TELEMETRY_DISABLED"),
"an explicit DSH_TELEMETRY_DISABLED must not be overwritten"
);
std::fs::remove_dir_all(&dir).unwrap();
}
}