Implemented flags and extra safety for zoltraak

This commit is contained in:
2025-03-24 20:25:38 -04:00
parent 9ace3a177d
commit 0be4de0ffe
16 changed files with 3276 additions and 322 deletions

View File

@@ -1,6 +1,6 @@
use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock}; use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock};
use crate::{getopt::{get_opts_from_tokens, Opt, OptSet}, jobs::JobBldr, libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::IoStack}; use crate::{getopt::{get_opts_from_tokens, Opt, OptSet}, jobs::JobBldr, libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}};
use super::setup_builtin; use super::setup_builtin;
@@ -15,28 +15,93 @@ pub const ZOLTRAAK_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
].into() ].into()
}); });
bitflags! {
#[derive(Clone,Copy,Debug,PartialEq,Eq)]
struct ZoltFlags: u32 {
const DRY = 0b000001;
const CONFIRM = 0b000010;
const NO_PRESERVE_ROOT = 0b000100;
const RECURSIVE = 0b001000;
const FORCE = 0b010000;
const VERBOSE = 0b100000;
}
}
/// Annihilate a file /// Annihilate a file
/// ///
/// This command works similarly to 'rm', but behaves more destructively. /// This command works similarly to 'rm', but behaves more destructively.
/// The file given as an argument is completely destroyed. The command works by shredding all of the data contained in the file, before truncating the length of the file to 0 to ensure that not even any metadata remains. /// The file given as an argument is completely destroyed. The command works by shredding all of the data contained in the file, before truncating the length of the file to 0 to ensure that not even any metadata remains.
pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments, argv } = node.class else { let NdRule::Command { assignments: _, argv } = node.class else {
unreachable!() unreachable!()
}; };
let mut flags = ZoltFlags::empty();
let (argv,opts) = get_opts_from_tokens(argv); let (argv,opts) = get_opts_from_tokens(argv);
for opt in opts {
if !ZOLTRAAK_OPTS.contains(&opt) {
return Err(
ShErr::simple(
ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{opt}'")
)
)
}
match opt {
Opt::Long(flag) => {
match flag.as_str() {
"no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT,
"confirm" => flags |= ZoltFlags::CONFIRM,
"dry-run" => flags |= ZoltFlags::DRY,
_ => unreachable!()
}
}
Opt::Short(flag) => {
match flag {
'r' => flags |= ZoltFlags::RECURSIVE,
'f' => flags |= ZoltFlags::FORCE,
_ => unreachable!()
}
}
}
}
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let mut io_frame = io_frame.unwrap();
io_frame.redirect()?;
for (arg,span) in argv { for (arg,span) in argv {
annihilate(&arg, false).blame(span)?; if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
return Err(
ShErr::simple(
ShErrKind::ExecFail,
"zoltraak: Attempted to destroy root directory '/'"
)
.with_note(
Note::new("If you really want to do this, you can use the --no-preserve-root flag")
.with_sub_notes(vec![
"Example: 'zoltraak --no-preserve-root /'"
])
)
)
}
if let Err(e) = annihilate(&arg, flags).blame(span) {
io_frame.restore()?;
return Err(e.into());
}
} }
io_frame.restore()?;
Ok(()) Ok(())
} }
fn annihilate(path: &str, allow_dirs: bool) -> ShResult<()> { fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
let path_buf = PathBuf::from(path); let path_buf = PathBuf::from(path);
let is_recursive = flags.contains(ZoltFlags::RECURSIVE);
let is_verbose = flags.contains(ZoltFlags::VERBOSE);
const BLOCK_SIZE: u64 = 4096; const BLOCK_SIZE: u64 = 4096;
@@ -74,10 +139,14 @@ fn annihilate(path: &str, allow_dirs: bool) -> ShResult<()> {
file.set_len(0)?; file.set_len(0)?;
mem::drop(file); mem::drop(file);
fs::remove_file(path)?; fs::remove_file(path)?;
if is_verbose {
let stderr = borrow_fd(STDERR_FILENO);
write(stderr, format!("removed file '{path}'").as_bytes())?;
}
} else if path_buf.is_dir() { } else if path_buf.is_dir() {
if allow_dirs { if is_recursive {
annihilate_recursive(path)?; // scary annihilate_recursive(path, flags)?; // scary
} else { } else {
return Err( return Err(
ShErr::simple( ShErr::simple(
@@ -97,19 +166,24 @@ fn annihilate(path: &str, allow_dirs: bool) -> ShResult<()> {
Ok(()) Ok(())
} }
fn annihilate_recursive(dir: &str) -> ShResult<()> { fn annihilate_recursive(dir: &str, flags: ZoltFlags) -> ShResult<()> {
let dir_path = PathBuf::from(dir); let dir_path = PathBuf::from(dir);
let is_verbose = flags.contains(ZoltFlags::VERBOSE);
for dir_entry in fs::read_dir(&dir_path)? { for dir_entry in fs::read_dir(&dir_path)? {
let entry = dir_entry?.path(); let entry = dir_entry?.path();
let file = entry.to_str().unwrap(); let file = entry.to_str().unwrap();
if entry.is_file() { if entry.is_file() {
annihilate(file, true)?; annihilate(file, flags)?;
} else if entry.is_dir() { } else if entry.is_dir() {
annihilate_recursive(file)?; annihilate_recursive(file, flags)?;
} }
} }
fs::remove_dir(dir)?; fs::remove_dir(dir)?;
if is_verbose {
let stderr = borrow_fd(STDERR_FILENO);
write(stderr, format!("removed directory '{dir}'").as_bytes())?;
}
Ok(()) Ok(())
} }

View File

@@ -1,6 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use crate::{exec_input, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Span, Tk, TkFlags, TkRule}, Redir, RedirType}, prelude::*, procio::{IoBuf, IoFrame, IoMode}, state::{read_logic, read_vars, write_meta}}; use crate::{exec_input, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Span, Tk, TkFlags, TkRule}, Redir, RedirType}, prelude::*, procio::{IoBuf, IoFrame, IoMode}, state::{read_logic, read_vars, write_meta, LogTab}};
/// Variable substitution marker /// Variable substitution marker
pub const VAR_SUB: char = '\u{fdd0}'; pub const VAR_SUB: char = '\u{fdd0}';
@@ -602,7 +602,7 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
} }
/// Expand aliases in the given input string /// Expand aliases in the given input string
pub fn expand_aliases(input: String, mut already_expanded: HashSet<String>) -> String { pub fn expand_aliases(input: String, mut already_expanded: HashSet<String>, log_tab: &LogTab) -> String {
let mut result = input.clone(); let mut result = input.clone();
let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect();
let mut expanded_this_iter: Vec<String> = vec![]; let mut expanded_this_iter: Vec<String> = vec![];
@@ -611,12 +611,13 @@ pub fn expand_aliases(input: String, mut already_expanded: HashSet<String>) -> S
let Ok(tk) = token_result else { continue }; let Ok(tk) = token_result else { continue };
if !tk.flags.contains(TkFlags::IS_CMD) { continue } if !tk.flags.contains(TkFlags::IS_CMD) { continue }
if tk.flags.contains(TkFlags::KEYWORD) { continue }
let raw_tk = tk.span.as_str().to_string(); let raw_tk = tk.span.as_str().to_string();
if already_expanded.contains(&raw_tk) { continue } if already_expanded.contains(&raw_tk) { continue }
if let Some(alias) = read_logic(|l| l.get_alias(&raw_tk)) { if let Some(alias) = log_tab.get_alias(&raw_tk) {
result.replace_range(tk.span.range(), &alias); result.replace_range(tk.span.range(), &alias);
expanded_this_iter.push(raw_tk); expanded_this_iter.push(raw_tk);
} }
@@ -626,6 +627,6 @@ pub fn expand_aliases(input: String, mut already_expanded: HashSet<String>) -> S
return result return result
} else { } else {
already_expanded.extend(expanded_this_iter.into_iter()); already_expanded.extend(expanded_this_iter.into_iter());
return expand_aliases(result, already_expanded) return expand_aliases(result, already_expanded, log_tab)
} }
} }

View File

@@ -8,18 +8,18 @@ pub mod state;
pub mod builtin; pub mod builtin;
pub mod jobs; pub mod jobs;
pub mod signal; pub mod signal;
#[cfg(test)]
pub mod tests;
pub mod getopt; pub mod getopt;
pub mod shopt; pub mod shopt;
#[cfg(test)]
pub mod tests;
use std::collections::HashSet; use std::collections::HashSet;
use expand::expand_aliases; use crate::expand::expand_aliases;
use libsh::error::ShResult; use libsh::error::ShResult;
use parse::{execute::Dispatcher, ParsedSrc}; use parse::{execute::Dispatcher, ParsedSrc};
use signal::sig_setup; use signal::sig_setup;
use state::{source_rc, write_meta}; use state::{read_logic, source_rc, write_meta};
use termios::{LocalFlags, Termios}; use termios::{LocalFlags, Termios};
use crate::prelude::*; use crate::prelude::*;
@@ -74,7 +74,8 @@ fn set_termios() {
pub fn exec_input(input: String) -> ShResult<()> { pub fn exec_input(input: String) -> ShResult<()> {
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
let input = expand_aliases(input, HashSet::new()); let log_tab = read_logic(|l| l.clone());
let input = expand_aliases(input, HashSet::new(), &log_tab);
let mut parser = ParsedSrc::new(Arc::new(input)); let mut parser = ParsedSrc::new(Arc::new(input));
parser.parse_src()?; parser.parse_src()?;

View File

View File

@@ -608,6 +608,7 @@ impl ParseStream {
} }
let case_pat_tk = self.next_tk().unwrap(); let case_pat_tk = self.next_tk().unwrap();
node_tks.push(case_pat_tk.clone()); node_tks.push(case_pat_tk.clone());
self.catch_separator(&mut node_tks);
let mut nodes = vec![]; let mut nodes = vec![];
while let Some(node) = self.parse_block(true /* check_pipelines */)? { while let Some(node) = self.parse_block(true /* check_pipelines */)? {

View File

@@ -66,7 +66,6 @@ impl Highlighter for FernReadline {
impl Validator for FernReadline { impl Validator for FernReadline {
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> { fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
return Ok(ValidationResult::Valid(None));
let mut tokens = vec![]; let mut tokens = vec![];
let tk_stream = LexStream::new(Arc::new(ctx.input().to_string()), LexFlags::empty()); let tk_stream = LexStream::new(Arc::new(ctx.input().to_string()), LexFlags::empty());
for tk in tk_stream { for tk in tk_stream {

View File

@@ -79,6 +79,12 @@ impl LogTab {
pub fn get_alias(&self, name: &str) -> Option<String> { pub fn get_alias(&self, name: &str) -> Option<String> {
self.aliases.get(name).cloned() self.aliases.get(name).cloned()
} }
pub fn clear_aliases(&mut self) {
self.aliases.clear()
}
pub fn clear_functions(&mut self) {
self.functions.clear()
}
} }
#[derive(Clone)] #[derive(Clone)]

View File

@@ -1,6 +1,4 @@
use libsh::error::{ShErr, ShErrKind}; use super::*;
use super::super::*;
#[test] #[test]
fn cmd_not_found() { fn cmd_not_found() {
@@ -95,3 +93,30 @@ fn case_no_in() {
let err_fmt = format!("{e}"); let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test]
fn error_with_notes() {
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
.with_note(Note::new("Execution failed for this reason"))
.with_note(Note::new("Here is how to fix it: blah blah blah"));
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}
#[test]
fn error_with_notes_and_sub_notes() {
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
.with_note(Note::new("Execution failed for this reason"))
.with_note(
Note::new("Here is how to fix it:")
.with_sub_notes(vec![
"blah",
"blah",
"blah"
])
);
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}

View File

@@ -1,9 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use expand::{expand_aliases, unescape_str}; use super::*;
use parse::lex::{Tk, TkFlags, TkRule};
use state::{write_logic, write_vars};
use super::super::*;
#[test] #[test]
fn simple_expansion() { fn simple_expansion() {
@@ -32,61 +29,97 @@ fn unescape_string() {
#[test] #[test]
fn expand_alias_simple() { fn expand_alias_simple() {
write_logic(|l| l.insert_alias("foo", "echo foo")); write_logic(|l| {
l.insert_alias("foo", "echo foo");
let input = String::from("foo");
let input = String::from("foo"); let result = expand_aliases(input, HashSet::new(), &l);
assert_eq!(result.as_str(),"echo foo");
let result = expand_aliases(input, HashSet::new()); l.clear_aliases();
assert_eq!(result.as_str(),"echo foo") });
} }
#[test] #[test]
fn expand_alias_in_if() { fn expand_alias_in_if() {
write_logic(|l| l.insert_alias("foo", "echo foo")); write_logic(|l| {
l.insert_alias("foo", "echo foo");
let input = String::from("if foo; then echo bar; fi");
let input = String::from("if foo; then echo bar; fi"); let result = expand_aliases(input, HashSet::new(), &l);
assert_eq!(result.as_str(),"if echo foo; then echo bar; fi");
l.clear_aliases();
});
}
let result = expand_aliases(input, HashSet::new()); #[test]
assert_eq!(result.as_str(),"if echo foo; then echo bar; fi") fn expand_alias_multiline() {
write_logic(|l| {
l.insert_alias("foo", "echo foo");
l.insert_alias("bar", "echo bar");
let input = String::from("
foo
if true; then
bar
fi
");
let expected = String::from("
echo foo
if true; then
echo bar
fi
");
let result = expand_aliases(input, HashSet::new(), &l);
assert_eq!(result,expected)
});
} }
#[test] #[test]
fn expand_multiple_aliases() { fn expand_multiple_aliases() {
write_logic(|l| l.insert_alias("foo", "echo foo")); write_logic(|l| {
write_logic(|l| l.insert_alias("bar", "echo bar")); l.insert_alias("foo", "echo foo");
write_logic(|l| l.insert_alias("biz", "echo biz")); l.insert_alias("bar", "echo bar");
l.insert_alias("biz", "echo biz");
let input = String::from("foo; bar; biz");
let input = String::from("foo; bar; biz"); let result = expand_aliases(input, HashSet::new(), &l);
assert_eq!(result.as_str(),"echo foo; echo bar; echo biz");
let result = expand_aliases(input, HashSet::new()); });
assert_eq!(result.as_str(),"echo foo; echo bar; echo biz")
} }
#[test] #[test]
fn alias_in_arg_position() { fn alias_in_arg_position() {
write_logic(|l| l.insert_alias("foo", "echo foo")); write_logic(|l| {
l.insert_alias("foo", "echo foo");
let input = String::from("echo foo");
let input = String::from("echo foo"); let result = expand_aliases(input.clone(), HashSet::new(), &l);
assert_eq!(input,result);
let result = expand_aliases(input.clone(), HashSet::new()); l.clear_aliases();
assert_eq!(input,result) });
} }
#[test] #[test]
fn expand_recursive_alias() { fn expand_recursive_alias() {
write_logic(|l| l.insert_alias("foo", "echo foo")); write_logic(|l| {
write_logic(|l| l.insert_alias("bar", "foo bar")); l.insert_alias("foo", "echo foo");
l.insert_alias("bar", "foo bar");
let input = String::from("bar"); let input = String::from("bar");
let result = expand_aliases(input, HashSet::new()); let result = expand_aliases(input, HashSet::new(), &l);
assert_eq!(result.as_str(),"echo foo bar") assert_eq!(result.as_str(),"echo foo bar");
});
} }
#[test] #[test]
fn test_infinite_recursive_alias() { fn test_infinite_recursive_alias() {
write_logic(|l| l.insert_alias("foo", "foo bar")); write_logic(|l| {
l.insert_alias("foo", "foo bar");
let input = String::from("foo");
let result = expand_aliases(input, HashSet::new(), &l);
assert_eq!(result.as_str(),"foo bar");
l.clear_aliases();
});
let input = String::from("foo");
let result = expand_aliases(input, HashSet::new());
assert_eq!(result.as_str(),"foo bar")
} }

View File

@@ -1,4 +1,4 @@
use getopt::get_opts_from_tokens; use getopt::{get_opts, get_opts_from_tokens};
use parse::NdRule; use parse::NdRule;
use tests::get_nodes; use tests::get_nodes;

View File

@@ -1,4 +1,4 @@
use super::super::*; use super::*;
#[test] #[test]
fn lex_simple() { fn lex_simple() {
let input = "echo hello world"; let input = "echo hello world";

View File

@@ -1,6 +1,22 @@
use std::rc::Arc; use std::sync::Arc;
pub use super::*;
use crate::libsh::error::{
Note, ShErr, ShErrKind
};
use crate::parse::{
node_operation, Node, NdRule, ParseStream,
lex::{
Tk, TkFlags, TkRule, LexFlags, LexStream
}
};
use crate::expand::{
expand_aliases, unescape_str
};
use crate::state::{
write_logic, write_vars
};
use crate::parse::{lex::{LexFlags, LexStream}, node_operation, Node, ParseStream};
pub mod lexer; pub mod lexer;
pub mod parser; pub mod parser;

View File

@@ -1,6 +1,4 @@
use parse::{node_operation, NdRule, Node}; use super::*;
use super::super::*;
#[test] #[test]
fn parse_simple() { fn parse_simple() {
@@ -170,13 +168,30 @@ esac";
#[test] #[test]
fn parse_case_nested() { fn parse_case_nested() {
let input = "case foo in let input = "case foo in
foo) if true; then foo)
echo foo if true; then
fi while true; do
echo foo
done
fi
;; ;;
bar) if false; then bar)
echo bar if false; then
fi until false; do
case foo in
foo)
if true; then
echo foo
fi
;;
bar)
if false; then
echo foo
fi
;;
esac
done
fi
;; ;;
esac"; esac";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())

View File

@@ -0,0 +1,8 @@
---
source: src/tests/error.rs
expression: err_fmt
---
Execution failed
note: Execution failed for this reason
note: Here is how to fix it: blah blah blah

View File

@@ -0,0 +1,11 @@
---
source: src/tests/error.rs
expression: err_fmt
---
Execution failed
note: Execution failed for this reason
note: Here is how to fix it:
- blah
- blah
- blah

File diff suppressed because it is too large Load Diff