From 93b2b5c51275e77c69da269c4e51e20c6cb10c58 Mon Sep 17 00:00:00 2001 From: atagen Date: Mon, 17 Nov 2025 09:17:43 +1100 Subject: [PATCH] working --- Cargo.lock | 81 +++++++++++++++++++++++++- Cargo.toml | 1 + flake.nix | 14 +++++ src/actions.rs | 147 ++++++++++++++--------------------------------- src/cli/parse.rs | 2 +- src/core.rs | 10 +++- src/envs.rs | 45 +++++++++++++++ src/main.rs | 1 + src/types.rs | 68 ++++++++++++++++++++-- 9 files changed, 255 insertions(+), 114 deletions(-) create mode 100644 src/envs.rs diff --git a/Cargo.lock b/Cargo.lock index 61b1bed..7d22f0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + [[package]] name = "byteorder" version = "1.5.0" @@ -104,6 +110,7 @@ dependencies = [ "bincode", "clap", "microxdg", + "rusqlite", "serde", "serde_json", "sled", @@ -185,6 +192,24 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fs2" version = "0.4.3" @@ -204,6 +229,24 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + [[package]] name = "heck" version = "0.5.0" @@ -237,6 +280,16 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.13" @@ -296,6 +349,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "proc-macro2" version = "1.0.97" @@ -320,7 +379,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.9.4", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", ] [[package]] @@ -435,6 +508,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "virtue" version = "0.0.18" diff --git a/Cargo.toml b/Cargo.toml index b032084..f2cd26d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ anyhow = "1.0.99" bincode = "2.0.1" clap = { version = "4.5.45", features = ["derive"] } microxdg = "0.2.0" +rusqlite = "0.37.0" serde = { version = "1.0.226", features = ["derive"] } serde_json = "1.0.145" sled = "0.34.7" diff --git a/flake.nix b/flake.nix index ce9d71d..f965e92 100644 --- a/flake.nix +++ b/flake.nix @@ -23,8 +23,21 @@ rust-analyzer rustfmt clippy + just ; }; + shellHook = + let + justfile = '' + build: + nix build --offline + fresh: + nix build + ''; + in + '' + echo "${justfile}" > justfile + ''; }; }); packages = forAllSystems (pkgs: { @@ -38,6 +51,7 @@ src = ./.; cargoLock.lockFile = ./Cargo.lock; RUSTFLAGS = "-C prefer-dynamic=yes"; + buildInputs = [ pkgs.sqlite ]; }); }); }; diff --git a/src/actions.rs b/src/actions.rs index 6c4b5e2..45d0e9e 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,6 +1,5 @@ -use crate::types::{CadeActions, Env}; +use crate::types::{CadeActions, Env, EnvSet, InnerHook}; use anyhow::{Context, Result, anyhow, bail}; -use serde_json::Value; use std::{ collections::HashMap, ffi::OsString, @@ -8,7 +7,6 @@ use std::{ io::{BufRead, Read}, path::{Path, PathBuf}, }; -type RawEnv = Vec<(String, Vec)>; #[repr(u8)] #[derive(bincode::Encode, bincode::Decode, PartialEq, Debug)] @@ -37,11 +35,10 @@ pub fn load_flake(path: &Path, output: Option) -> Result { .output() .with_context(|| format!("loading flake at {}", path.display()))? .stdout; - let json: Value = serde_json::from_slice(&output).context("parsing output as json")?; - parse_json(json) + parse_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() { @@ -59,11 +56,10 @@ pub fn load_shell(path: &Path, filename: String) -> Result { .output() .with_context(|| format!("loading shell at {}", path.display()))? .stdout; - let json: Value = serde_json::from_slice(&output).context("parsing output as json")?; - parse_json(json) + EnvSet::from_json(output)? } -pub fn load_env(path: &Path, filename: String) -> Result { +pub fn load_env(path: &Path, filename: String) -> Result { let mut p = path.to_path_buf(); if filename.is_empty() { p.push(".env"); @@ -77,7 +73,7 @@ pub fn load_env(path: &Path, filename: String) -> Result { parse_envs(buf) } -pub fn call(path: &Path, argv: Vec) -> Result { +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()); @@ -94,117 +90,54 @@ pub fn call(path: &Path, argv: Vec) -> Result { parse_envs(text) } -fn parse_envs(text: String) -> Result { - let mut envs = Vec::new(); - for line in text.lines() { - let split: Vec<&str> = line.split('=').collect(); - let parse = match split.len() { - 3 => { - let key = split[0]; - let values = &split[2].split(":").to_owned().collect(); - } - 2 => { - // TODO - } - _ => { - return Err(anyhow!("parsing variable from {text}")); - } - // 1 => Clear(split[0].to_string()), - // _ => Add(split[0].to_string(), split[1..].concat().to_string()), - }; - envs.push(parse); - } - Ok(envs) -} +// TODO cache results by hash/storepath and read from db so we don't +// need to eat eval every time -fn parse_json(json: Value) -> Result { - 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(vars) - } else { - Err(anyhow!("failed to parse PATH value from JSON output")) - } +fn check_permission(dir: &Path) -> Permission { + // TODO check database for permission + todo!(); } pub fn do_activation() -> 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(); + // TODO finish sqlite impl + let db = rusqlite::Connection::open(dir)?; - if db - .get(cwd.into_encoded_bytes()) - .map(|perm| { - perm.map(|inner| { - let (p, _) = - bincode::decode_from_slice(inner.as_ref(), bincode::config::standard()) - .unwrap_or((Permission::Disallowed, 0)); - p - }) - .unwrap_or(Permission::Disallowed) - }) - .unwrap_or(Permission::Disallowed) - == Permission::Disallowed - { + // 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 mut current_dir = std::env::current_dir().context("determine CWD")?; let base_env = std::env::vars().collect::>(); use crate::core::{read_cade, realise}; let mut cascade = HashMap::new(); cascade.insert( - current_dir.clone(), - read_cade(¤t_dir.join(".cade")).context("reading cade file")?, + cwd.clone(), + read_cade(&cwd.join(".cade")).context("reading cade file")?, ); - while let Some(parent) = current_dir.parent() + // recurse into parent dirs + while let Some(parent) = cwd.parent() && std::fs::exists(parent.join(".cade")) .context("check for .cade file in parent directory")? { - current_dir = parent.to_path_buf(); + cwd = parent.to_path_buf(); cascade.insert( - current_dir.clone(), - read_cade(¤t_dir.join(".cade")).context("reading cade file")?, + cwd.clone(), + read_cade(&cwd.join(".cade")).context("reading cade file")?, ); } @@ -217,24 +150,27 @@ pub fn do_activation() -> Result<()> { for action in env_actions { env.entry(action.name) .and_modify(|iv: &mut Vec| { - iv.extend(action.value); + 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() { - env.entry(bk).and_modify(|inner| inner) if env.get(bk).is_some_and(|inner| inner.contains(bv)) { env.remove(bk); } } } - Hook(hook) => {} + Hook(hook) => match hook.kind { + _ => todo!(), + }, }; } for (k, v) in env { - println!("{k}={v}"); + let value: String = v.into_iter().map(|s| [&s, ":"].concat()).collect(); + println!("{k}={}", &value[..value.len() - 1]); } Ok(()) } @@ -265,6 +201,7 @@ fn ensure_dir() -> Result { 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) } diff --git a/src/cli/parse.rs b/src/cli/parse.rs index 9b93e40..1922626 100644 --- a/src/cli/parse.rs +++ b/src/cli/parse.rs @@ -58,7 +58,7 @@ impl FromStr for Keyword { let mut words = lower[4..].split_whitespace(); let mut p = words.clone().peekable(); use crate::types::HookType::*; - Hook(crate::types::Hook { + Hook(crate::types::InnerHook { kind: match p.peek() { None => return Err(ParseError::TooFewOptions), Some(&"preload") => { diff --git a/src/core.rs b/src/core.rs index 351ea4b..b5e2971 100644 --- a/src/core.rs +++ b/src/core.rs @@ -19,10 +19,13 @@ pub fn read_cade(path: &Path) -> Result> { Ok(accum) } -pub fn realise(cascade: HashMap>) -> Result> { - let mut actions: Vec = Vec::new(); +pub fn realise( + cascade: HashMap>, +) -> Result>> { + let mut actions: HashMap> = HashMap::new(); use crate::actions::*; for (path, layer) in cascade { + let mut layer_actions = Vec::new(); for keyword in layer { use Keyword::*; use Loadable::*; @@ -37,8 +40,9 @@ pub fn realise(cascade: HashMap>) -> Result Ok(CadeActions::Hook(hook)), }?; - actions.push(acts); + layer_actions.push(acts); } + actions.insert(path, layer_actions); } Ok(actions) } diff --git a/src/envs.rs b/src/envs.rs new file mode 100644 index 0000000..7e74959 --- /dev/null +++ b/src/envs.rs @@ -0,0 +1,45 @@ +use std::str::FromStr; + +use crate::types::EnvSet; +use anyhow::{Result, anyhow}; +use serde_json::Value; + +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 { + let mut envs = Vec::new(); + for line in text.lines() { + let split: Vec<&str> = line.split('=').collect(); + match split.len() { + 2 => { + let key = split[0].to_owned(); + let values: Vec = split[1].split(":").map(|s| s.to_owned()).collect(); + envs.push((key, values)); + } + _ => { + return Err(anyhow!("parsing variable from {text}")); + } // 1 => Clear(split[0].to_string()), + // _ => Add(split[0].to_string(), split[1..].concat().to_string()), + }; + } + Ok(envs) + } +} diff --git a/src/main.rs b/src/main.rs index 08bf644..8de7ab2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod actions; mod cli; mod core; +mod envs; mod types; use std::{ diff --git a/src/types.rs b/src/types.rs index 004ce4b..26ac9b6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,27 +1,35 @@ use std::path::PathBuf; +use anyhow::{Context, Result, anyhow}; + #[derive(Debug)] pub enum CadeActions { Purify, Environ(Vec), - Hook(Hook), + Hook(InnerHook), } #[derive(Debug)] pub struct Env { pub name: String, pub value: Vec, + // FIXME wtf, at this level we don't need to know origin.. + // that is for a higher structure pub origin: PathBuf, - pub kind: Loadable, pub layer: u8, } +impl Env { + // upcast envset to env + fn from_set(env: EnvSet, origin: &Path, layer: u8) -> Env {} +} + #[derive(Debug)] pub enum Keyword { Pure, Call(Vec), Load(Loadable), - Hook(Hook), + Hook(InnerHook), } #[derive(Debug)] @@ -41,7 +49,59 @@ pub enum HookType { } #[derive(Debug)] -pub struct Hook { +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")) + } + } +}