diff --git a/Cargo.lock b/Cargo.lock index 7d22f0f..832f771 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ dependencies = [ "serde", "serde_json", "sled", + "whoami", ] [[package]] @@ -280,6 +281,17 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.9.4", + "libc", + "redox_syscall 0.5.18", +] + [[package]] name = "libsqlite3-sys" version = "0.35.0" @@ -344,7 +356,7 @@ dependencies = [ "cfg-if", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi", ] @@ -382,6 +394,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.9.4", +] + [[package]] name = "rusqlite" version = "0.37.0" @@ -520,6 +541,22 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index f2cd26d..e75887c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ rusqlite = "0.37.0" serde = { version = "1.0.226", features = ["derive"] } serde_json = "1.0.145" sled = "0.34.7" +whoami = { version = "1.6.1", default-features = false } diff --git a/src/actions.rs b/src/actions.rs index 45d0e9e..f39e7b7 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -21,7 +21,7 @@ impl Display for Permission { } } -pub fn load_flake(path: &Path, output: Option) -> Result { +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()); @@ -35,28 +35,24 @@ pub fn load_flake(path: &Path, output: Option) -> Result { .output() .with_context(|| format!("loading flake at {}", path.display()))? .stdout; - parse_json(output) + EnvSet::from_json(&output) } -pub fn load_shell(path: &Path, filename: String) -> Result { +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"); + let mut proc = std::process::Command::new(nix_cmd); 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)? + EnvSet::from_json(&output) } pub fn load_env(path: &Path, filename: String) -> Result { @@ -70,7 +66,7 @@ pub fn load_env(path: &Path, filename: String) -> Result { .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) + EnvSet::from_envs(&buf) } pub fn call(path: &Path, argv: Vec) -> Result { @@ -84,10 +80,10 @@ pub fn call(path: &Path, argv: Vec) -> Result { .with_context(|| format!("running process {}", argv.concat()))? .stdout; + // FIXME locale ? let text = String::from_utf8(output) - .with_context(|| format!("converting call {} output to text", argv.concat()))? - .replace(' ', "\0"); - parse_envs(text) + .with_context(|| format!("converting call {} output to text", argv.concat()))?; + EnvSet::from_envs(&text) } // TODO cache results by hash/storepath and read from db so we don't @@ -98,110 +94,4 @@ fn check_permission(dir: &Path) -> 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) -} +// FIXME TODO make these methods on Cade struct diff --git a/src/cli/parse.rs b/src/cli/parse.rs index 1922626..206f167 100644 --- a/src/cli/parse.rs +++ b/src/cli/parse.rs @@ -44,12 +44,19 @@ impl FromStr for Keyword { Some("shell") => { Load(Loadable::Shell(s.get(11..).unwrap_or("").to_string())) } + Some("flake") => { Load(Loadable::Flake(s.get(11..).unwrap_or("").to_string())) } + Some("env") => { Load(Loadable::Env(s.get(9..).unwrap_or("").to_string())) } + + Some("cass") => { + Load(Loadable::Cass(s.get(10..).unwrap_or("").to_string())) + } + Some(_) => return Err(ParseError::UnknownLoadable), } } diff --git a/src/core.rs b/src/core.rs index b5e2971..b26c28b 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,11 +1,146 @@ use crate::types::{CadeActions, Keyword, Loadable}; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result, anyhow, bail}; +use rusqlite::named_params; use std::{ collections::HashMap, io::BufRead, path::{Path, PathBuf}, }; +pub struct Cade { + db: rusqlite::Connection, + layers: HashMap, + cwd: PathBuf, +} + +impl Cade { + pub fn init() -> anyhow::Result { + let mut db_path = Cade::ensure_dir()?; + db_path.push("cade.db"); + let mut db = rusqlite::Connection::open(db_path)?; + Cade::ensure_db(&mut db)?; + Ok(Self { + db, + layers: HashMap::new(), + cwd: std::env::current_dir().context("determine cwd")?, + }) + } + + fn ensure_db(conn: &mut rusqlite::Connection) -> Result<()> { + conn.execute( + " + CREATE TABLE IF NOT EXISTS WorkingPaths ( + Path TEXT PRIMARY KEY, + Permission INTEGER NOT NULL DEFAULT 0 + ); + ", + [], + ) + .context("create table in database") + .map(|_| ()) + } + + pub fn set_permission(&mut self, permission: bool) -> Result<()> { + self.db.execute( + "INSERT OR REPLACE INTO WorkingPaths (Permission) VALUES ((:cwd), (:perm));", + named_params! { + ":cwd": self.cwd.to_str().context("parse cwd as unicode")?, + ":perm": permission, + }, + )?; + eprintln!( + "cade is now {} here.", + if permission { "allowed" } else { "disallowed" } + ); + Ok(()) + } + + pub fn do_activation(&mut self) -> Result<()> { + let permission = self.db.query_one( + "SELECT Permission FROM WorkingPaths WHERE Path=(:path)", + &[(":path", &self.cwd.to_str().context("parse cwd as unicode")?)], + |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(()) + } + + fn ensure_dir() -> Result { + let mut path = if let Ok(xdg) = microxdg::Xdg::new() + && let Ok(state_dir) = xdg.state() + { + state_dir + } else { + let mut p = PathBuf::new(); + p.push("home"); + p.push(whoami::username()); + p.push(".state"); + p + }; + path.push("cade"); + + if !std::fs::exists(&path).is_ok_and(|v| v) { + std::fs::create_dir(&path).context("create cade state path")?; + } + Ok(path) + } +} + pub fn read_cade(path: &Path) -> Result> { let contents = std::fs::read(path).context("reading cade file")?; let mut accum = Vec::new(); diff --git a/src/envs.rs b/src/envs.rs index 7e74959..2691930 100644 --- a/src/envs.rs +++ b/src/envs.rs @@ -1,30 +1,8 @@ -use std::str::FromStr; - use crate::types::EnvSet; -use anyhow::{Result, anyhow}; -use serde_json::Value; +use anyhow::{Context, Result, anyhow, bail}; -pub enum EnvProviders { - RawEnvs, - NixJson, - Cade, -} -pub enum EnvSetParseErr { - Bad, -} - -impl FromStr for EnvSet { - type Err = EnvSetParseErr; -} - -pub fn parse(text: &str) -> Result {} - -pub trait EnvProvider { - fn parse(text: &str) -> Result; -} - -impl EnvProvider for RawEnvs { - fn parse(text: &str) -> Result { +impl EnvSet { + pub fn from_envs(text: &str) -> Result { let mut envs = Vec::new(); for line in text.lines() { let split: Vec<&str> = line.split('=').collect(); @@ -35,11 +13,57 @@ impl EnvProvider for RawEnvs { envs.push((key, values)); } _ => { - return Err(anyhow!("parsing variable from {text}")); + bail!("parsing variable from {text}") } // 1 => Clear(split[0].to_string()), // _ => Add(split[0].to_string(), split[1..].concat().to_string()), }; } - Ok(envs) + Ok(EnvSet(envs)) + } + pub fn from_json(raw: &[u8]) -> Result { + let json: serde_json::Value = serde_json::from_slice(raw).context("parsing json")?; + if json.is_object() + && let Some(all_vars) = json.get("variables") + { + let vars = all_vars + .as_object() + .map(|inner| { + inner + .iter() + .filter(|(var, _)| match var.as_str() { + "NIX_BUILD_TOP" + | "NIX_BUILD_CORES" + | "NIX_STORE" + | "TEMP" + | "TEMPDIR" + | "TMP" + | "TMPDIR" + | "builder" + | "out" + | "stdenv" + | "system" + | "dontAddDisableDepTrack" + | "outputs" => false, + _ => true, + }) + .filter_map(|var| { + Some((var.0.to_string(), var.1.get("value")?.as_str()?.to_owned())) + }) + .map(|(name, value)| { + ( + name, + value + .split(':') + .map(|s| s.to_string()) + .collect::>(), + ) + }) + .collect() + }) + .context("collecting env vars")?; + Ok(EnvSet(vars)) + } else { + Err(anyhow!("failed to parse values from JSON output")) + } } } diff --git a/src/main.rs b/src/main.rs index 8de7ab2..b4ba3c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,20 +7,21 @@ mod types; use std::{ collections::HashMap, ffi::OsString, - fmt::{Debug, Display}, - io::{BufRead, Read}, path::{Path, PathBuf}, }; use anyhow::{Context, Result, anyhow, bail}; use clap::Parser; +use rusqlite::named_params; + +use crate::types::CadeLayer; fn try_main() -> Result<()> { let args = cli::clap::Cli::parse(); use crate::actions::*; use cli::clap::CliAction::*; + let cade = Cade::init()?; match args.action { - // TODO recursively ascend until we hit a top level .cade, then activate downwards (ie. in reverse) Enter => do_activation().context("activate cade environment")?, Allow => set_permission(Permission::Allowed)?, Disallow => set_permission(Permission::Disallowed)?, diff --git a/src/types.rs b/src/types.rs index 26ac9b6..0f8f164 100644 --- a/src/types.rs +++ b/src/types.rs @@ -9,6 +9,12 @@ pub enum CadeActions { Hook(InnerHook), } +pub struct CadeLayer { + pub envs: EnvSet, + pub origin: PathBuf, + pub layer: u8, +} + #[derive(Debug)] pub struct Env { pub name: String, @@ -16,12 +22,6 @@ pub struct Env { // FIXME wtf, at this level we don't need to know origin.. // that is for a higher structure pub origin: PathBuf, - pub layer: u8, -} - -impl Env { - // upcast envset to env - fn from_set(env: EnvSet, origin: &Path, layer: u8) -> Env {} } #[derive(Debug)] @@ -38,6 +38,7 @@ pub enum Loadable { Flake(String), Shell(String), Env(String), + Cass(String), } #[derive(Debug)] @@ -50,58 +51,8 @@ pub enum HookType { #[derive(Debug)] pub struct InnerHook { - // FIXME should be a pathbuf ? pub content: Vec, pub kind: HookType, } -pub struct EnvSet(Vec<(String, Vec)>); - -impl EnvSet { - pub fn from_json(raw: &[u8]) -> Result { - let json: serde_json::Value = serde_json::from_slice(raw).context("parsing json")?; - if json.is_object() - && let Some(all_vars) = json.get("variables") - { - let vars = all_vars - .as_object() - .map(|inner| { - inner - .iter() - .filter(|(var, _)| match var.as_str() { - "NIX_BUILD_TOP" - | "NIX_BUILD_CORES" - | "NIX_STORE" - | "TEMP" - | "TEMPDIR" - | "TMP" - | "TMPDIR" - | "builder" - | "out" - | "stdenv" - | "system" - | "dontAddDisableDepTrack" - | "outputs" => false, - _ => true, - }) - .filter_map(|var| { - Some((var.0.to_string(), var.1.get("value")?.as_str()?.to_owned())) - }) - .map(|(name, value)| { - ( - name, - value - .split(':') - .map(|s| s.to_string()) - .collect::>(), - ) - }) - .collect() - }) - .context("collecting env vars")?; - Ok(EnvSet(vars)) - } else { - Err(anyhow!("failed to parse values from JSON output")) - } - } -} +pub struct EnvSet(pub Vec<(String, Vec)>);