Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- - Shell commands start with a cleaner environment, the file watcher stays responsive under event bursts, wide characters render correctly in terminal sessions, and logs redact secrets more reliably.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"targeted_mechanism": "none",
"kind": "harness",
"slice_class": "non_ranking",
"fixtures": [
"harness-goldens"
]
}
41 changes: 39 additions & 2 deletions crates/aft/src/bash_permissions/arity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,53 @@
//! command for reusable permission patterns.

pub fn prefix(tokens: &[String]) -> Vec<String> {
for len in (1..=tokens.len()).rev() {
prefix_with_lookup(tokens, arity)
}

fn prefix_with_lookup(
tokens: &[String],
mut lookup: impl FnMut(&str) -> Option<usize>,
) -> Vec<String> {
// Lookup keys contain at most two words; returned arities can be longer.
for len in (1..=tokens.len().min(2)).rev() {
let candidate = tokens[..len].join(" ");
if let Some(arity) = arity(&candidate) {
if let Some(arity) = lookup(&candidate) {
return tokens[..tokens.len().min(arity)].to_vec();
}
}

tokens.first().cloned().into_iter().collect()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn lookup_work_is_bounded_by_key_words_not_returned_arity() {
let mut tokens = vec![
"docker".to_string(),
"compose".to_string(),
"up".to_string(),
];
tokens.extend((0..1000).map(|i| format!("arg{i}")));
let mut candidates = Vec::new();
let result = prefix_with_lookup(&tokens, |candidate| {
candidates.push(candidate.to_string());
arity(candidate)
});
assert_eq!(result, ["docker", "compose", "up"]);
assert_eq!(
candidates.len(),
1,
"only the longest table key should be tried"
);
assert_eq!(candidates, ["docker compose"]);
assert_eq!(prefix(&["custom".into(), "arg".into()]), ["custom"]);
assert!(prefix(&[]).is_empty());
}
}

fn arity(command: &str) -> Option<usize> {
Some(match command {
"cat" => 1,
Expand Down
39 changes: 35 additions & 4 deletions crates/aft/src/bash_rewrite/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use serde::{Deserialize, Serialize};

use crate::bash_rewrite::catalog::ControlRole;
use crate::bash_rewrite::rules::{
CatAppendRule, CatRule, FindRule, GrepRule, HeadRule, LsRule, RgRule, SedRule, TailRule,
CatAppendRule, CatRule, FindRule, GrepRule, HeadRule, LsRule, ParsedRewriteRule, RgRule,
SedRule, TailRule,
};
use crate::bash_rewrite::RewriteRule;
use crate::context::AppContext;
use crate::protocol::Response;

Expand Down Expand Up @@ -130,7 +130,8 @@ pub fn dispatch_for_request(
return None;
}

let rules: [&dyn RewriteRule; 9] = [
let parsed = crate::bash_rewrite::parser::parse(command);
let rules: [&dyn ParsedRewriteRule; 9] = [
&GrepRule,
&RgRule,
&FindRule,
Expand All @@ -143,7 +144,7 @@ pub fn dispatch_for_request(
];

for rule in rules {
let decision = rule.decide(command, request_id, session_id, ctx);
let decision = rule.decide_parsed(parsed.as_ref(), command, request_id, session_id, ctx);
match decision {
crate::bash_rewrite::RewriteDecision::Accept(mut request) => {
// The footer appended to a rewritten command names the caller's
Expand Down Expand Up @@ -200,6 +201,36 @@ mod tests {
use super::*;
use crate::bash_rewrite::catalog::ControlRole;

#[test]
fn dispatch_parses_once_even_when_every_rule_declines() {
let root = tempfile::tempdir().unwrap();
let ctx = AppContext::new(
crate::context::default_language_provider_factory(),
crate::config::Config {
project_root: Some(root.path().to_path_buf()),
storage_dir: Some(root.path().join("storage")),
experimental_bash_rewrite: true,
..crate::config::Config::default()
},
);
for command in ["printf hi", "printf $(date)"] {
crate::bash_rewrite::parser::take_parse_calls();
assert!(dispatch_for_request(
command,
"parse-once",
None,
&crate::host_tool_names::HostToolNames::default(),
&ctx
)
.is_none());
assert_eq!(
crate::bash_rewrite::parser::take_parse_calls(),
1,
"{command}"
);
}
}

#[test]
fn route_records_are_request_correlated_and_replaceable() {
record_native(
Expand Down
12 changes: 12 additions & 0 deletions crates/aft/src/bash_rewrite/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub struct ParsedCommand {
}

pub fn parse(command: &str) -> Option<ParsedCommand> {
#[cfg(test)]
PARSE_CALLS.with(|calls| calls.set(calls.get() + 1));
let command = command.trim();
if command.is_empty() {
return None;
Expand All @@ -21,6 +23,16 @@ pub fn parse(command: &str) -> Option<ParsedCommand> {
Some(parsed)
}

#[cfg(test)]
thread_local! {
static PARSE_CALLS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}

#[cfg(test)]
pub(super) fn take_parse_calls() -> usize {
PARSE_CALLS.with(|calls| calls.replace(0))
}

fn split_heredoc(command: &str) -> Option<(&str, Option<String>)> {
let Some(op_start) = find_heredoc_operator(command)? else {
return Some((command, None));
Expand Down
Loading
Loading