use crate::types::{CadeActions, Env, EnvSet, InnerHook}; use anyhow::{Context, Result, anyhow, bail}; use std::{ collections::HashMap, ffi::OsString, fmt::{Debug, Display}, io::{BufRead, Read}, path::{Path, PathBuf}, }; #[repr(u8)] #[derive(bincode::Encode, bincode::Decode, PartialEq, Debug)] pub enum Permission { Allowed, Disallowed, } impl Display for Permission { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Debug::fmt(self, f) } } pub fn load_flake(path: &Path, output: Option) -> Result { let mut nix_cmd = String::from("nix print-dev-env --json"); if let Some(flake_output) = output { nix_cmd.push_str(&[" ", &flake_output, " "].concat()); } let mut proc = std::process::Command::new("sh"); // proc.env_clear(); proc.arg("-c"); proc.arg(nix_cmd); proc.current_dir(path); let output = proc .output() .with_context(|| format!("loading flake at {}", path.display()))? .stdout; parse_json(output) } pub fn load_shell(path: &Path, filename: String) -> Result { let mut nix_cmd = String::from("nix print-dev-env --json "); // let mut nix_cmd = String::from("nix-shell "); if !filename.is_empty() { nix_cmd.push_str(&["-F ", &filename].concat()); } else { nix_cmd.push_str(&["-F ./shell.nix"].concat()); } // nix_cmd.push_str("--pure --command env --null"); let mut proc = std::process::Command::new("sh"); proc.env_clear(); proc.arg("-c"); proc.arg(nix_cmd); proc.current_dir(path); let output = proc .output() .with_context(|| format!("loading shell at {}", path.display()))? .stdout; EnvSet::from_json(output)? } pub fn load_env(path: &Path, filename: String) -> Result { let mut p = path.to_path_buf(); if filename.is_empty() { p.push(".env"); } else { p.push(filename); } let mut file = std::fs::File::open(p) .with_context(|| format!("opening env file at {}", path.display()))?; let mut buf = String::new(); file.read_to_string(&mut buf).context("reading env file")?; parse_envs(buf) } pub fn call(path: &Path, argv: Vec) -> Result { let mut it = argv.iter(); // safety: already checked at parsing let mut process = std::process::Command::new(it.next().unwrap()); process.current_dir(path); process.args(it); let output = process .output() .with_context(|| format!("running process {}", argv.concat()))? .stdout; let text = String::from_utf8(output) .with_context(|| format!("converting call {} output to text", argv.concat()))? .replace(' ', "\0"); parse_envs(text) } // TODO cache results by hash/storepath and read from db so we don't // need to eat eval every time fn check_permission(dir: &Path) -> Permission { // TODO check database for permission todo!(); } pub fn do_activation() -> Result<()> { let dir = ensure_dir()?; // TODO finish sqlite impl let db = rusqlite::Connection::open(dir)?; // let db = sled::open(dir).context("open database")?; let mut cwd = std::env::current_dir().context("determine CWD")?; let cwd_str = cwd .clone() .into_os_string() .into_string() .map_err(|_| anyhow!("cwd has invalid unicode"))?; let permission = db.query_one( "SELECT Permission FROM WorkingPaths WHERE Path=(:path)", &[(":path", &cwd_str)], |row| Ok(row.get(0).unwrap_or(false)), )?; if !permission { bail!("cade is not permitted to operate here; use 'cade allow'."); } let base_env = std::env::vars().collect::>(); use crate::core::{read_cade, realise}; let mut cascade = HashMap::new(); cascade.insert( cwd.clone(), read_cade(&cwd.join(".cade")).context("reading cade file")?, ); // recurse into parent dirs while let Some(parent) = cwd.parent() && std::fs::exists(parent.join(".cade")) .context("check for .cade file in parent directory")? { cwd = parent.to_path_buf(); cascade.insert( cwd.clone(), read_cade(&cwd.join(".cade")).context("reading cade file")?, ); } let mut env = HashMap::new(); let adjustments = realise(cascade)?; use CadeActions::*; for action in adjustments { match action { Environ(env_actions) => { for action in env_actions { env.entry(action.name) .and_modify(|iv: &mut Vec| { iv.extend(action.value.clone()); }) .or_insert(action.value); } } Purify => { // FIXME this is a dumb way to purify an env for (bk, bv) in base_env.iter() { if env.get(bk).is_some_and(|inner| inner.contains(bv)) { env.remove(bk); } } } Hook(hook) => match hook.kind { _ => todo!(), }, }; } for (k, v) in env { let value: String = v.into_iter().map(|s| [&s, ":"].concat()).collect(); println!("{k}={}", &value[..value.len() - 1]); } Ok(()) } pub fn set_permission(permission: Permission) -> Result<()> { let dir = ensure_dir()?; let db = sled::open(dir).context("open database")?; let cwd = std::env::current_dir() .context("determine CWD")? .into_os_string(); let encoded_permission = bincode::encode_to_vec(&permission, bincode::config::standard()) .context("encode permission value")?; let _ = db .insert(cwd.into_encoded_bytes(), encoded_permission) .context("update permissions database")?; eprintln!( "cade is now {} here.", permission.to_string().to_lowercase() ); Ok(()) } fn ensure_dir() -> Result { let xdg = microxdg::Xdg::new().context("establish XDG paths")?; let mut path = xdg.state().context("find xdg state dir")?; path.push("cade"); if !std::fs::exists(&path).is_ok_and(|v| v) { std::fs::create_dir(&path).context("create cade's state path")?; } // FIXME should this be appended after ? we only guarantee the dir here .. path.push("permissions.db"); Ok(path) }