state
This commit is contained in:
parent
b2e0936d4a
commit
757020689c
10 changed files with 537 additions and 344 deletions
270
src/actions.rs
Normal file
270
src/actions.rs
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
use crate::types::{CadeActions, Env};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::OsString,
|
||||
fmt::{Debug, Display},
|
||||
io::{BufRead, Read},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
type RawEnv = Vec<(String, Vec<String>)>;
|
||||
|
||||
#[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<String>) -> Result<RawEnv> {
|
||||
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;
|
||||
let json: Value = serde_json::from_slice(&output).context("parsing output as json")?;
|
||||
parse_json(json)
|
||||
}
|
||||
|
||||
pub fn load_shell(path: &Path, filename: String) -> Result<CadeActions> {
|
||||
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;
|
||||
let json: Value = serde_json::from_slice(&output).context("parsing output as json")?;
|
||||
parse_json(json)
|
||||
}
|
||||
|
||||
pub fn load_env(path: &Path, filename: String) -> Result<CadeActions> {
|
||||
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<String>) -> Result<CadeActions> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn parse_envs(text: String) -> Result<RawEnv> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn parse_json(json: Value) -> Result<RawEnv> {
|
||||
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::<Vec<String>>(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.context("collecting env vars")?;
|
||||
Ok(vars)
|
||||
} else {
|
||||
Err(anyhow!("failed to parse PATH value from JSON output"))
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
{
|
||||
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::<HashMap<String, String>>();
|
||||
|
||||
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")?,
|
||||
);
|
||||
|
||||
while let Some(parent) = current_dir.parent()
|
||||
&& std::fs::exists(parent.join(".cade"))
|
||||
.context("check for .cade file in parent directory")?
|
||||
{
|
||||
current_dir = parent.to_path_buf();
|
||||
cascade.insert(
|
||||
current_dir.clone(),
|
||||
read_cade(¤t_dir.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<String>| {
|
||||
iv.extend(action.value);
|
||||
})
|
||||
.or_insert(action.value);
|
||||
}
|
||||
}
|
||||
Purify => {
|
||||
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) => {}
|
||||
};
|
||||
}
|
||||
for (k, v) in env {
|
||||
println!("{k}={v}");
|
||||
}
|
||||
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<PathBuf> {
|
||||
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")?;
|
||||
}
|
||||
path.push("permissions.db");
|
||||
Ok(path)
|
||||
}
|
||||
14
src/cli/clap.rs
Normal file
14
src/cli/clap.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
#[derive(Subcommand)]
|
||||
pub enum CliAction {
|
||||
Enter,
|
||||
Allow,
|
||||
Disallow,
|
||||
Edit,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub action: CliAction,
|
||||
}
|
||||
2
src/cli/mod.rs
Normal file
2
src/cli/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod clap;
|
||||
pub mod parse;
|
||||
98
src/cli/parse.rs
Normal file
98
src/cli/parse.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use crate::Debug;
|
||||
use crate::types::*;
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ParseError {
|
||||
InvalidKeyword,
|
||||
UnknownLoadable,
|
||||
TooManyOptions,
|
||||
TooFewOptions,
|
||||
}
|
||||
|
||||
impl Display for ParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Keyword {
|
||||
type Err = ParseError;
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let lower = s.to_lowercase();
|
||||
use crate::types::Keyword::*;
|
||||
if s.len() >= 4 {
|
||||
let res = match &lower.get(..4) {
|
||||
Some("pure") => Pure,
|
||||
Some("call") => {
|
||||
let target: Vec<String> = lower[4..]
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_owned())
|
||||
.collect();
|
||||
if target.is_empty() {
|
||||
return Err(ParseError::TooFewOptions);
|
||||
}
|
||||
Call(target)
|
||||
}
|
||||
Some("load") => {
|
||||
let mut words = lower[4..].split_whitespace();
|
||||
if words.clone().count() > 2 {
|
||||
return Err(ParseError::TooManyOptions);
|
||||
} else {
|
||||
match words.next() {
|
||||
None => Load(Loadable::Default),
|
||||
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(_) => return Err(ParseError::UnknownLoadable),
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("hook") => {
|
||||
let mut words = lower[4..].split_whitespace();
|
||||
let mut p = words.clone().peekable();
|
||||
use crate::types::HookType::*;
|
||||
Hook(crate::types::Hook {
|
||||
kind: match p.peek() {
|
||||
None => return Err(ParseError::TooFewOptions),
|
||||
Some(&"preload") => {
|
||||
words.next();
|
||||
LoadPre
|
||||
}
|
||||
Some(&"load") => {
|
||||
words.next();
|
||||
LoadPost
|
||||
}
|
||||
Some(&"preunload") => {
|
||||
words.next();
|
||||
UnloadPre
|
||||
}
|
||||
Some(&"unload") => {
|
||||
words.next();
|
||||
UnloadPost
|
||||
}
|
||||
Some(_) => LoadPost,
|
||||
},
|
||||
content: words.map(|s| s.to_owned()).collect(),
|
||||
})
|
||||
}
|
||||
Some(n) => {
|
||||
eprintln!("found invalid command: {n}");
|
||||
return Err(ParseError::InvalidKeyword);
|
||||
}
|
||||
None => {
|
||||
return Err(ParseError::InvalidKeyword);
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(ParseError::InvalidKeyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/core.rs
Normal file
44
src/core.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use crate::types::{CadeActions, Keyword, Loadable};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::BufRead,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub fn read_cade(path: &Path) -> Result<Vec<Keyword>> {
|
||||
let contents = std::fs::read(path).context("reading cade file")?;
|
||||
let mut accum = Vec::new();
|
||||
let mut lines = contents.lines();
|
||||
while let Some(Ok(line)) = lines.next() {
|
||||
let instruction: Keyword = line
|
||||
.parse()
|
||||
.map_err(|e| anyhow!("while parsing cade file at {}: {e:?}", path.display()))?;
|
||||
accum.push(instruction);
|
||||
}
|
||||
Ok(accum)
|
||||
}
|
||||
|
||||
pub fn realise(cascade: HashMap<PathBuf, Vec<Keyword>>) -> Result<Vec<CadeActions>> {
|
||||
let mut actions: Vec<CadeActions> = Vec::new();
|
||||
use crate::actions::*;
|
||||
for (path, layer) in cascade {
|
||||
for keyword in layer {
|
||||
use Keyword::*;
|
||||
use Loadable::*;
|
||||
let acts = match keyword {
|
||||
Pure => Ok(CadeActions::Purify),
|
||||
Call(argv) => call(&path, argv).context("calling process"),
|
||||
Load(loadable) => match loadable {
|
||||
Default => load_flake(&path, None).context("loading flake"),
|
||||
Flake(output) => load_flake(&path, Some(output)),
|
||||
Shell(filename) => load_shell(&path, filename).context("loading shell"),
|
||||
Env(filename) => load_env(&path, filename).context("loading env file"),
|
||||
},
|
||||
Hook(hook) => Ok(CadeActions::Hook(hook)),
|
||||
}?;
|
||||
actions.push(acts);
|
||||
}
|
||||
}
|
||||
Ok(actions)
|
||||
}
|
||||
351
src/main.rs
351
src/main.rs
|
|
@ -1,355 +1,26 @@
|
|||
mod actions;
|
||||
mod cli;
|
||||
mod core;
|
||||
mod types;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::OsString,
|
||||
fmt::{Debug, Display},
|
||||
io::{BufRead, Read},
|
||||
os::unix::process::CommandExt,
|
||||
path::{Path, PathBuf},
|
||||
str::{FromStr, SplitWhitespace},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(bincode::Encode, bincode::Decode, PartialEq, Debug)]
|
||||
enum Permission {
|
||||
Allowed,
|
||||
Disallowed,
|
||||
}
|
||||
impl Display for Permission {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Action {
|
||||
Activate,
|
||||
Allow,
|
||||
Disallow,
|
||||
Edit,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
action: Action,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Loadable {
|
||||
Default,
|
||||
Flake(String),
|
||||
Shell(String),
|
||||
Env(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Keyword {
|
||||
Pure,
|
||||
Call(Vec<String>),
|
||||
Load(Loadable),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ParseError {
|
||||
InvalidKeyword,
|
||||
UnknownLoadable,
|
||||
TooManyOptions,
|
||||
TooFewOptions,
|
||||
}
|
||||
impl Display for ParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Keyword {
|
||||
type Err = ParseError;
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let lower = s.to_lowercase();
|
||||
use Keyword::*;
|
||||
if s.len() >= 4 {
|
||||
let res = match &lower.get(..4) {
|
||||
Some("pure") => Pure,
|
||||
Some("call") => {
|
||||
let target: Vec<String> = lower[4..]
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_owned())
|
||||
.collect();
|
||||
if target.is_empty() {
|
||||
return Err(ParseError::TooFewOptions);
|
||||
}
|
||||
Call(target)
|
||||
}
|
||||
Some("load") => {
|
||||
let mut words = lower[4..].split_whitespace();
|
||||
if words.clone().count() > 2 {
|
||||
return Err(ParseError::TooManyOptions);
|
||||
} else {
|
||||
match words.next() {
|
||||
None => Load(Loadable::Default),
|
||||
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(_) => return Err(ParseError::UnknownLoadable),
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(n) => {
|
||||
eprintln!("found invalid command {n}, dying..");
|
||||
return Err(ParseError::InvalidKeyword);
|
||||
}
|
||||
None => {
|
||||
return Err(ParseError::InvalidKeyword);
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(ParseError::InvalidKeyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_cade(path: &Path) -> Result<Vec<Keyword>> {
|
||||
let contents = std::fs::read(path).context("reading cade file")?;
|
||||
let mut accum = Vec::new();
|
||||
let mut lines = contents.lines();
|
||||
while let Some(Ok(line)) = lines.next() {
|
||||
let instruction: Keyword = line
|
||||
.parse()
|
||||
.map_err(|e| anyhow!("while parsing cade file at {}: {e:?}", path.display()))?;
|
||||
accum.push(instruction);
|
||||
}
|
||||
Ok(accum)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum EnvAction {
|
||||
Add(String, String),
|
||||
Clear(String),
|
||||
Purify,
|
||||
}
|
||||
|
||||
fn load_flake(path: &Path, output: Option<String>) -> Result<Vec<EnvAction>> {
|
||||
let mut nix_cmd = String::from("nix develop ");
|
||||
if let Some(flake_output) = output {
|
||||
nix_cmd.push_str(&[&flake_output, " "].concat());
|
||||
}
|
||||
nix_cmd.push_str("-ic 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 flake at {}", path.display()))?
|
||||
.stdout;
|
||||
let text = String::from_utf8(output).context("converting output to text")?;
|
||||
parse_envs(text)
|
||||
}
|
||||
|
||||
fn load_shell(path: &Path, filename: String) -> Result<Vec<EnvAction>> {
|
||||
let mut nix_cmd = String::from("nix-shell ");
|
||||
if filename.is_empty() {
|
||||
nix_cmd.push_str(&[&filename, " "].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;
|
||||
let text = String::from_utf8(output).context("converting output to text")?;
|
||||
parse_envs(text)
|
||||
}
|
||||
|
||||
fn load_env(path: &Path, filename: String) -> Result<Vec<EnvAction>> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn call(path: &Path, argv: Vec<String>) -> Result<Vec<EnvAction>> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn parse_envs(text: String) -> Result<Vec<EnvAction>> {
|
||||
let mut envs = Vec::new();
|
||||
use EnvAction::*;
|
||||
for line in text.split('\0') {
|
||||
let split: Vec<&str> = line.split('=').collect();
|
||||
let parse = match split.len() {
|
||||
0 => {
|
||||
return Err(anyhow!("processing returned env var {text}"));
|
||||
}
|
||||
1 => Clear(split[0].to_string()),
|
||||
_ => Add(split[0].to_string(), split[1..].concat().to_string()),
|
||||
};
|
||||
envs.push(parse);
|
||||
}
|
||||
Ok(envs)
|
||||
}
|
||||
|
||||
fn interpret(cascade: HashMap<PathBuf, Vec<Keyword>>) -> Result<Vec<EnvAction>> {
|
||||
let mut actions = Vec::new();
|
||||
for (path, layer) in cascade {
|
||||
for keyword in layer {
|
||||
use EnvAction::*;
|
||||
use Keyword::*;
|
||||
use Loadable::*;
|
||||
let acts = match keyword {
|
||||
Pure => Ok(Vec::from([Purify])),
|
||||
Call(argv) => call(&path, argv).context("calling process"),
|
||||
Load(loadable) => match loadable {
|
||||
Default => load_flake(&path, None).context("loading flake"),
|
||||
Flake(output) => load_flake(&path, Some(output)),
|
||||
Shell(filename) => load_shell(&path, filename).context("loading shell"),
|
||||
Env(filename) => load_env(&path, filename).context("loading env file"),
|
||||
},
|
||||
}?;
|
||||
actions.extend(acts);
|
||||
}
|
||||
}
|
||||
Ok(actions)
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
{
|
||||
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::<HashMap<String, String>>();
|
||||
|
||||
let mut cascade = HashMap::new();
|
||||
cascade.insert(
|
||||
current_dir.clone(),
|
||||
read_cade(¤t_dir.join(".cade")).context("reading cade file")?,
|
||||
);
|
||||
|
||||
while let Some(parent) = current_dir.parent()
|
||||
&& std::fs::exists(parent.join(".cade"))
|
||||
.context("check for .cade file in parent directory")?
|
||||
{
|
||||
current_dir = parent.to_path_buf();
|
||||
cascade.insert(
|
||||
current_dir.clone(),
|
||||
read_cade(¤t_dir.join(".cade")).context("reading cade file")?,
|
||||
);
|
||||
}
|
||||
|
||||
let mut env = HashMap::new();
|
||||
let adjustments = interpret(cascade)?;
|
||||
use EnvAction::*;
|
||||
for action in adjustments {
|
||||
let _ = match action {
|
||||
Add(k, v) => env.insert(k, v),
|
||||
Clear(k) => env.remove(&k),
|
||||
Purify => {
|
||||
for (bk, bv) in base_env.iter() {
|
||||
if env.get(bk).is_some_and(|inner| inner == bv) {
|
||||
env.remove(bk);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
for (k, v) in env {
|
||||
println!("{k}={v}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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<PathBuf> {
|
||||
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")?;
|
||||
}
|
||||
path.push("permissions.db");
|
||||
Ok(path)
|
||||
}
|
||||
use clap::Parser;
|
||||
|
||||
fn try_main() -> Result<()> {
|
||||
let args = Cli::parse();
|
||||
use Action::*;
|
||||
let args = cli::clap::Cli::parse();
|
||||
use crate::actions::*;
|
||||
use cli::clap::CliAction::*;
|
||||
match args.action {
|
||||
// TODO recursively ascend until we hit a top level .cade, then activate downwards
|
||||
Activate => do_activation().context("activate cade environment")?,
|
||||
// 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)?,
|
||||
Edit => {
|
||||
|
|
|
|||
47
src/types.rs
Normal file
47
src/types.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CadeActions {
|
||||
Purify,
|
||||
Environ(Vec<Env>),
|
||||
Hook(Hook),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Env {
|
||||
pub name: String,
|
||||
pub value: Vec<String>,
|
||||
pub origin: PathBuf,
|
||||
pub kind: Loadable,
|
||||
pub layer: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Keyword {
|
||||
Pure,
|
||||
Call(Vec<String>),
|
||||
Load(Loadable),
|
||||
Hook(Hook),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Loadable {
|
||||
Default,
|
||||
Flake(String),
|
||||
Shell(String),
|
||||
Env(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HookType {
|
||||
LoadPre,
|
||||
LoadPost,
|
||||
UnloadPre,
|
||||
UnloadPost,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Hook {
|
||||
pub content: Vec<String>,
|
||||
pub kind: HookType,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue