use super::EvalContext;
use crate::action::{Action, AtomicAction};
use crate::expand::Expansion;
use crate::realpath::{PathResolver, physical};
use crate::shell::{ParseOutcome, ShellContext, Unparsed, classify_globs};
use crate::{host, paths};
fn classify_action(
action: &Action,
ctx: &EvalContext,
) -> (ParseOutcome, Vec<(AtomicAction, bool)>) {
let outcome = match action {
Action::Shell { command } => {
return classify_globs(
command,
&ShellContext {
home: &ctx.home,
cwd: &[Some(ctx.cwd.clone())],
},
);
}
Action::ForeignShell { shell, .. } => ParseOutcome::Unparseable {
reason: format!("{shell} commands are not parsed; only POSIX shell is classified"),
atoms: Vec::new(),
asks: false,
},
Action::FsRead { path } => ParseOutcome::Parsed(file_atoms(path, ctx, read).collect()),
Action::FsWrite { path } => ParseOutcome::Parsed(file_atoms(path, ctx, write).collect()),
Action::Patch { writes } if writes.is_empty() => ParseOutcome::Unparseable {
reason: "patch names no files".to_owned(),
atoms: Vec::new(),
asks: false,
},
Action::Patch { writes } => ParseOutcome::Parsed(
writes
.iter()
.flat_map(|p| file_atoms(p, ctx, write))
.collect(),
),
Action::ReadFiles { paths } if paths.is_empty() => ParseOutcome::Unparseable {
reason: "read names no files".to_owned(),
atoms: Vec::new(),
asks: false,
},
Action::ReadFiles { paths } => ParseOutcome::Parsed(
paths
.iter()
.flat_map(|p| file_atoms(p, ctx, read))
.collect(),
),
Action::Net { url } => match host::of_url(url) {
Some(host) => ParseOutcome::Parsed(vec![AtomicAction::Net { host }]),
None => ParseOutcome::Unparseable {
reason: format!("no host in url `{url}`"),
atoms: Vec::new(),
asks: false,
},
},
Action::Fetch { url } => match host::of_url(url) {
Some(host) => ParseOutcome::Parsed(vec![AtomicAction::Fetch { host }]),
None => ParseOutcome::Unparseable {
reason: format!("no host in url `{url}`"),
atoms: Vec::new(),
asks: false,
},
},
Action::WebSearch { query } => ParseOutcome::Parsed(vec![AtomicAction::WebSearch {
query: query.clone(),
}]),
Action::UnknownTool { host, tool } => {
ParseOutcome::Parsed(vec![AtomicAction::UnknownTool {
host: host.clone(),
tool: tool.clone(),
}])
}
Action::McpTool {
name,
reads,
writes,
hosts,
..
} => {
let mut atoms = vec![AtomicAction::McpTool { name: name.clone() }];
atoms.extend(reads.iter().flat_map(|p| file_atoms(p, ctx, read)));
atoms.extend(writes.iter().flat_map(|p| file_atoms(p, ctx, write)));
let mut bad_host = None;
for host in hosts {
match host::of_url(host) {
Some(host) => atoms.push(AtomicAction::Net { host }),
None => bad_host = bad_host.or(Some(host)),
}
}
match bad_host {
Some(host) => ParseOutcome::Unparseable {
reason: format!("no host in mcp argument `{host}`"),
atoms,
asks: false,
},
None => ParseOutcome::Parsed(atoms),
}
}
};
(outcome, Vec::new())
}
pub(super) fn checked_atoms(
action: &Action,
ctx: &EvalContext,
paths: &dyn PathResolver,
) -> (Vec<AtomicAction>, Option<Unparsed>) {
let (outcome, globs) = classify_action(action, ctx);
let (mut atoms, unparsed) = outcome.into_parts();
let mut expansion = Expansion::new(paths);
for (glob, dot_dirs) in &globs {
if let Some(pattern) = glob.subject() {
let pattern = if paths::has_dotdot(pattern) {
physical(pattern, paths)
} else {
pattern.to_owned()
};
let named = expansion.paths(&pattern, *dot_dirs);
atoms.extend(named.into_iter().filter_map(|path| glob.with_path(path)));
}
}
let unexpanded = expansion.overflow().map(|reason| Unparsed {
reason,
asks: false,
});
let atoms = with_environ_reads(with_resolved_paths(atoms, paths));
(atoms, unparsed.or(unexpanded))
}
fn with_environ_reads(atoms: Vec<AtomicAction>) -> Vec<AtomicAction> {
let environ: Vec<AtomicAction> = atoms
.iter()
.filter_map(|atom| match atom {
AtomicAction::FsRead { path } if is_proc_environ(path) => Some(AtomicAction::Shell {
argv: vec!["@proc-environ".to_owned(), path.clone()],
}),
_ => None,
})
.collect();
atoms.into_iter().chain(environ).collect()
}
fn is_proc_environ(path: &str) -> bool {
let parts: Vec<&str> = path.split('/').collect();
let id = |s: &str| !s.is_empty();
match parts.as_slice() {
["", "proc", pid, "environ"] => id(pid),
["", "proc", pid, "task", tid, "environ"] => id(pid) && id(tid),
_ => false,
}
}
fn file_atoms(
raw: &str,
ctx: &EvalContext,
atom: fn(String) -> AtomicAction,
) -> impl Iterator<Item = AtomicAction> {
let project = ctx.project.as_deref();
let collapsed = paths::normalise(raw, &ctx.home, project, &ctx.cwd);
let traversal = paths::traversal(raw, &ctx.home, project, Some(&ctx.cwd));
std::iter::once(atom(collapsed)).chain(traversal.map(atom))
}
fn read(path: String) -> AtomicAction {
AtomicAction::FsRead { path }
}
fn write(path: String) -> AtomicAction {
AtomicAction::FsWrite { path }
}
fn file_path(atom: &AtomicAction) -> Option<&str> {
match atom {
AtomicAction::FsRead { path } | AtomicAction::FsWrite { path } => Some(path),
_ => None,
}
}
fn with_resolved_paths(atoms: Vec<AtomicAction>, paths: &dyn PathResolver) -> Vec<AtomicAction> {
let atoms: Vec<AtomicAction> = atoms
.into_iter()
.filter_map(|atom| match file_path(&atom) {
Some(path) if paths::has_dotdot(path) => atom.with_path(physical(path, paths)),
_ => Some(atom),
})
.collect();
let resolved: Vec<AtomicAction> = atoms
.iter()
.filter_map(|atom| {
let (AtomicAction::FsRead { path } | AtomicAction::FsWrite { path }) = atom else {
return None;
};
atom.with_path(paths.resolve(path).filter(|real| real != path)?)
})
.collect();
atoms.into_iter().chain(resolved).collect()
}