use super::*;
#[test]
fn heredoc_bodies_cover_a_quoted_delimiter_body() {
let command = "python3 <<'PY'\nprint(len(k) > 3)\nPY";
let bodies = HeredocBodies::scan(command);
let gt = command.find('>').expect("comparison operator");
assert!(bodies.contains(gt), "the script's `>` must be body content");
}
#[test]
fn heredoc_bodies_exclude_the_operator_line() {
let command = "python3 <<'PY' > out.rs\nprint(1)\nPY";
let bodies = HeredocBodies::scan(command);
let gt = command.find('>').expect("redirect");
assert!(!bodies.contains(gt), "the operator line must stay live");
}
#[test]
fn heredoc_bodies_ignore_a_here_string() {
let command = "grep x <<< 'a > b'\necho done > f.rs";
let bodies = HeredocBodies::scan(command);
let redirect = command.rfind('>').expect("redirect");
assert!(!bodies.contains(redirect));
}
#[test]
fn heredoc_bodies_claim_nothing_when_unterminated() {
let unterminated = "python3 <<'PY'\nprint(1)\n";
assert!(!HeredocBodies::scan(unterminated).contains(20));
let shift = "echo $((1 << 3))\necho x > f.rs";
let redirect = shift.rfind('>').expect("redirect");
assert!(!HeredocBodies::scan(shift).contains(redirect));
}
#[test]
fn heredoc_frames_cover_the_terminator_line() {
let command = "cat <<'EOF' > f\nbody\nEOF\nnext";
let bodies = HeredocBodies::scan(command);
let opening_newline = command.find('\n').expect("operator line ends");
assert!(bodies.suppresses_separator(opening_newline));
assert!(!bodies.suppresses_separator(opening_newline - 1));
let terminator = command.find("EOF\nnext").expect("terminator line");
for idx in terminator..terminator + 3 {
assert!(bodies.suppresses_separator(idx));
}
assert!(!bodies.suppresses_separator(terminator + 3));
}
#[test]
fn heredoc_frames_are_empty_for_a_shell_operator_line() {
for command in [
"bash <<'EOF'\nbody\nEOF",
"sudo /bin/sh <<EOF\nbody\nEOF",
"true && zsh <<EOF\nbody\nEOF",
] {
let bodies = HeredocBodies::scan(command);
let newline = command.find('\n').expect("operator line ends");
assert!(
!bodies.suppresses_separator(newline),
"{command} should not be framed"
);
assert!(bodies.contains(newline + 1), "{command} body span");
}
}
#[test]
fn heredoc_bodies_handle_tab_stripped_delimiter() {
let command = "cat <<-EOF\n\ta > b\n\tEOF";
let bodies = HeredocBodies::scan(command);
let gt = command.find('>').expect("arrow");
assert!(bodies.contains(gt));
}
#[test]
fn heredoc_bodies_ignore_quoted_operator() {
let command = "grep -n '<<EOF' f\necho x > g.rs";
let bodies = HeredocBodies::scan(command);
let redirect = command.rfind('>').expect("redirect");
assert!(!bodies.contains(redirect));
}
#[test]
fn splits_a_heredoc_body_out_of_the_argv_text() {
let command = "cat >> verb.rs <<'RSEOF'\nstruct VerbStub {\n}\nRSEOF";
let (argv_text, bodies) = split_heredoc_bodies(command);
assert_eq!(argv_text.len(), command.len(), "byte offsets are preserved");
assert!(argv_text.starts_with("cat >> verb.rs <<'RSEOF'"));
assert!(argv_text.trim_end().ends_with("RSEOF"), "{argv_text:?}");
assert!(!argv_text.contains('{'), "{argv_text:?}");
assert_eq!(bodies, vec!["struct VerbStub {\n}\n".to_string()]);
}
#[test]
fn leaves_a_shell_heredoc_body_in_the_argv_text() {
let command = "bash <<'EOF'\nls .env\nEOF";
let (argv_text, bodies) = split_heredoc_bodies(command);
assert_eq!(argv_text, command);
assert!(bodies.is_empty());
}
#[test]
fn splits_nothing_without_a_heredoc() {
for command in ["awk '{print}' f", "cat <<EOF\nno terminator"] {
let (argv_text, bodies) = split_heredoc_bodies(command);
assert_eq!(argv_text, command);
assert!(bodies.is_empty(), "{command:?}");
}
}
#[test]
fn data_bodies_record_whether_the_delimiter_was_quoted() {
for (command, expands) in [
("cat <<EOF\n$(date)\nEOF", true),
("cat <<'EOF'\n$(date)\nEOF", false),
("cat <<\"EOF\"\n$(date)\nEOF", false),
("cat <<\\EOF\n$(date)\nEOF", false),
("cat <<-E'O'F\n$(date)\n\tEOF", false),
] {
let bodies = data_bodies(command);
assert_eq!(bodies.len(), 1, "{command:?}");
assert_eq!(bodies[0].expands, expands, "{command:?}");
let (start, end) = bodies[0].operator_line;
assert!(command[start..end].starts_with("cat <<"), "{command:?}");
}
}
#[test]
fn heredoc_bodies_record_every_expanding_body_9155() {
for (command, expanding) in [
("cat <<EOF\n$(date)\nEOF", 1),
("bash <<X\necho '$(date)'\nX", 1),
("python3 - <<PY\nprint('$(date)')\nPY", 1),
("bash <<'X'\necho '$(date)'\nX", 0),
("cat <<'EOF'\n$(date)\nEOF", 0),
] {
let bodies = HeredocBodies::scan(command);
assert_eq!(bodies.expanding().len(), expanding, "{command:?}");
}
let command = "bash <<X\necho '$(date)'\nX";
let (start, end) = HeredocBodies::scan(command).expanding()[0];
assert_eq!(&command[start..end], "echo '$(date)'\n");
}
#[test]
fn heredoc_bodies_expand_an_unterminated_unquoted_body_9155() {
for (command, want) in [
("cat <<X\n'$(date)'", vec!["'$(date)'"]),
("cat <<X\n'$(date)'\nX ", vec!["'$(date)'\nX "]),
(
"cat <<A\n$(a)\nA\ncat <<B\n'$(b)'",
vec!["$(a)\n", "'$(b)'"],
),
("cat <<A\n$(a)\nA\ncat <<'B'\n$(b)", vec!["$(a)\n"]),
("cat <<'X'\n'$(date)'", vec![]),
("echo $((1 << 3))\n'$(date)'", vec![]),
] {
let bodies = HeredocBodies::scan(command);
let found: Vec<&str> = bodies
.expanding()
.iter()
.map(|&(s, e)| &command[s..e])
.collect();
assert_eq!(found, want, "{command:?}");
assert!(bodies.data().is_empty(), "{command:?} claims no body");
assert!(
!bodies.contains(command.len() - 1),
"{command:?} stays live"
);
}
}
#[test]
fn heredoc_bodies_refuse_a_delimiter_split_by_a_word_break() {
for command in [
"cat <<'A B'\nx\nA B\necho \"$(rm -rf /)\"\nA",
"cat <<'A>B'\nx\nA>B\nA",
"cat <<'A<B'\nx\nA<B\nA",
"cat <<-'A B'\n\tx\n\tA B\n\tA",
"cat <<'A\tB'\nx\nA\tB\nA",
"cat <<\"A;B\"\nx\nA;B\nA",
"cat <<A\\ B\nx\nA B\nA",
"cat <<A\\\nB\n$(rm -rf /)\nA\nAB",
"cat <<'A\nB'\nx\nA",
"cat <<'A B'\nit's\nA B\nA",
"cat <<'A\nB'\nit's\nA",
"cat <<'A'$(x)\nx\nA$(x)\necho \"$(rm -rf /)\"\nA$",
"cat <<'A'$((1 + 2))\nA$((1 + 2))\necho \"$(rm -rf /)\"\nA$",
"cat <<'A'$[1 + 2]\nx\nA$[1 + 2]\necho \"$(rm -rf /)\"\nA$[1",
"cat <<'A'${x:- }\nA${x:- }\necho \"$(rm -rf /)\"\nA${x:-",
"cat <<'A'`x y`\nx\nA`x y`\necho \"$(rm -rf /)\"\nA`x",
"cat <<'A'<(x)\nx\nA<(x)\necho \"$(rm -rf /)\"\nA",
"cat <<'A'>(x)\nx\nA>(x)\necho \"$(rm -rf /)\"\nA",
"cat <<'A'(x y)\nx\nA(x y)\necho \"$(rm -rf /)\"\nA",
"cat <<'A\\B'\nx\nA\\B\necho \"$(rm -rf /)\"\nAB",
"cat <<\"A\\B\"\nx\nA\\B\necho \"$(rm -rf /)\"\nAB",
"cat <<A\\\\B\nx\nA\\B\necho \"$(rm -rf /)\"\nAB",
"cat <<\"A'B\"\nx\nA'B\necho \"$(rm -rf /)\"\nAB\n: '",
"cat <<A\\'B\nx\nA'B\necho \"$(rm -rf /)\"\nAB\n: '",
"cat <<''\nx\n\necho \"$(rm -rf /)\"",
] {
let bodies = HeredocBodies::scan(command);
assert!(bodies.is_unscannable(), "{command:?}");
assert!(bodies.spans.is_empty(), "{command:?} claims nothing");
assert_eq!(
super::super::unclassifiable_command(command),
Some(HEREDOC_DELIMITER_REASON),
"{command:?}"
);
}
for command in [
"cat <<'EOF'\n$(rm -rf /)\nEOF\necho benign",
"cat <<\"EOF\" > out\nx\nEOF",
"cat <<\\EOF\nx\nEOF",
"cat <<-E'O'F\nx\n\tEOF",
"cat <<'EOF';echo hi\nx\nEOF",
"cat <<'EOF'\nit's\nEOF",
"echo $((1 << 3))",
"python3 <<'PY'\nprint(1)\n",
"cat <<'END_OF-file.1'\nx\nEND_OF-file.1",
"x=$(cat <<'EOF'\n$(rm -rf /)\nEOF\n)",
"(cat <<'EOF')\nx\nEOF",
"cat <<EOF\r\nx\r\nEOF\r",
] {
assert!(
!HeredocBodies::scan(command).is_unscannable(),
"{command:?}"
);
assert_eq!(
super::super::unclassifiable_command(command),
None,
"{command:?}"
);
}
}
#[test]
fn heredoc_delimiter_refusal_reaches_a_wrapped_command() {
let command = "sh -c \"cat <<'A B'\nx\nA B\necho \\$(rm -rf /)\nA\"";
assert!(!HeredocBodies::scan(command).is_unscannable());
assert_eq!(
super::super::unclassifiable_command(command),
Some(HEREDOC_DELIMITER_REASON)
);
}
#[test]
fn heredoc_operators_skip_comments_escapes_and_expansions() {
use OperatorCtx::{Ambiguous, Code};
for (command, want) in [
("cat <<'EOF'\nx\nEOF", vec![(4, Code)]),
("echo hi # <<'EOF'\nx\nEOF", vec![]),
("echo \\<<'EOF'\nx\nEOF", vec![]),
("echo ${x:-<<'EOF'}\nx\nEOF}", vec![]),
("echo '<<EOF'", vec![]),
("echo $((1<<\"2\"))\nx\n2", vec![(9, Ambiguous)]),
("(( x = 1 <<\\2 ))", vec![(9, Ambiguous)]),
("echo $[1<<2]", vec![(8, Ambiguous)]),
("x=$(cat <<'EOF')\nx\nEOF", vec![(8, Ambiguous)]),
("x=`cat <<'EOF' `\nx\nEOF", vec![(7, Ambiguous)]),
("x=$(cat <<'EOF'\nx\nEOF\n)", vec![(8, Code)]),
("echo ${x:-$(cat <<'EOF'\nx\nEOF\n)}", vec![(16, Code)]),
] {
assert_eq!(heredoc_operators(command), want, "{command:?}");
}
}
#[test]
fn heredoc_bodies_skip_an_operator_outside_code() {
for command in [
"echo hi # <<'EOF'\necho \"$(rm -rf /)\"\nEOF",
"echo \\<<'EOF'\necho \"$(rm -rf /)\"\nEOF",
"echo ${x:-<<'EOF'}\necho \"$(rm -rf /)\"\nEOF}",
"echo $((1 << 3))\necho \"$(rm -rf /)\"",
] {
let bodies = HeredocBodies::scan(command);
assert!(!bodies.is_unscannable(), "{command:?}");
assert!(bodies.spans.is_empty(), "{command:?} claims nothing");
}
for command in [
"echo $((1<<\"2\"))\necho \"$(rm -rf /)\"\n2",
"(( x = 1 <<\\2 ))\necho \"$(rm -rf /)\"\n2",
"echo $[1<<2]\necho \"$(rm -rf /)\"\n2",
"x=$(cat <<'EOF')\necho \"$(rm -rf /)\"\nEOF",
"x=`cat <<'EOF' `\necho \"$(rm -rf /)\"\nEOF",
] {
assert!(HeredocBodies::scan(command).is_unscannable(), "{command:?}");
assert_eq!(
super::super::unclassifiable_command(command),
Some(HEREDOC_DELIMITER_REASON),
"{command:?}"
);
}
}
#[test]
fn heredoc_bodies_span_two_heredocs_on_one_line() {
let command = "diff <<A <<B\na > b\nA\nc > d\nB";
let bodies = HeredocBodies::scan(command);
let first = command.find('>').expect("first arrow");
let second = command.rfind('>').expect("second arrow");
assert!(bodies.contains(first), "first body claimed");
assert!(bodies.contains(second), "second body claimed");
}
#[test]
fn heredoc_bodies_read_a_crlf_heredoc_as_the_shell_does() {
let plain = "cat <<'EOF'\r\na > b\r\nEOF\r\necho done\r\n";
let bodies = HeredocBodies::scan(plain);
assert!(!bodies.is_unscannable());
assert!(bodies.contains(plain.find('>').expect("arrow")), "body");
assert!(!bodies.contains(plain.find("echo").expect("echo")), "live");
let trimmed = "cat <<'EOF'\r\na > b\r\nEOF";
assert!(HeredocBodies::scan(trimmed).contains(trimmed.find('>').expect(">")));
let live = "cat <<'EOF'\r\nx\r\nEOF\r\necho \"$(rm -rf /)\"\r\n";
assert!(!HeredocBodies::scan(live).contains(live.find("$(").expect("$(")));
assert_eq!(super::super::unclassifiable_command(live), None);
let decoy = "cat <<'A'\nx\nA\r\ncat <<'X'\nA\necho \"$(rm -rf /)\"\nX";
let bodies = HeredocBodies::scan(decoy);
assert!(bodies.contains(decoy.find("cat <<'X'").expect("inner")));
assert!(!bodies.contains(decoy.find("$(").expect("$(")), "live");
}
#[test]
fn heredoc_bodies_follow_a_continued_operator_line_9180() {
let command = "cat <<'X' \\\n; rm -rf /\nbody > here\nX";
let bodies = HeredocBodies::scan(command);
assert!(!bodies.is_unscannable());
assert!(
!bodies.contains(command.find("rm").expect("rm")),
"joined line is live"
);
assert!(
bodies.contains(command.find('>').expect(">")),
"body claimed"
);
let glued = "bash<<'O'\nbody\nO";
let newline = glued.find('\n').expect("operator line ends");
assert!(!HeredocBodies::scan(glued).suppresses_separator(newline));
}
#[test]
fn heredoc_bodies_refuse_a_continuation_they_cannot_place_9180() {
for command in [
"cat <\\\n<X\nbody\nX",
"cat <<'X' # c \\\nX\nbody\nX",
"cat <<X\na\\\nX\nb\nX",
] {
assert!(HeredocBodies::scan(command).is_unscannable(), "{command:?}");
}
let quoted = "cat <<'X'\na\\\nX\necho done";
let bodies = HeredocBodies::scan(quoted);
assert!(!bodies.is_unscannable());
assert!(!bodies.contains(quoted.find("echo").expect("echo")), "live");
}
#[test]
fn heredoc_bodies_keep_every_body_when_abandoned_9180() {
let command = "bash <<'O'\ncat <<X\nx\nX";
let bodies = HeredocBodies::scan(command);
assert!(bodies.lost_confidence());
assert!(bodies.spans().is_empty());
let [body] = bodies.bodies() else {
panic!("one body expected");
};
assert_eq!(&command[body.span.0..body.span.1], "cat <<X\nx\nX");
let unbalanced = "bash <<'O'\nx\nO\necho \"open";
let bodies = HeredocBodies::scan(unbalanced);
assert!(bodies.lost_confidence() && bodies.spans().is_empty());
assert_eq!(bodies.bodies().len(), 1);
assert!(!HeredocBodies::scan("cat <<'X'\nx\nX").lost_confidence());
}
#[test]
fn heredoc_bodies_scan_a_long_continued_body_in_linear_time_9180() {
let command = format!("cat <<X\n{}b\nX", "a\\\n".repeat(1_000_000));
let (send, recv) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let found = HeredocBodies::scan(&command);
send.send(found.spans().len()).ok();
});
let spans = recv.recv_timeout(std::time::Duration::from_secs(2));
assert_eq!(spans, Ok(1), "no scan within 2 s");
}