1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//! When the credential-print scan must rescan a command (#8771), split out of
//! `credential_print` for the 500-SLOC cap.
//!
//! Why: #8734 re-ran the whole scan whenever a pass bound a new tainted name,
//! so a command binding one (`T=$(gcloud auth print-access-token); …`) was
//! scanned twice and refused at half the size. A rescan matters only when a
//! pass judged some text against the name set from before the binding.
//! What: [`stale_after`] answers that from the stages of one pass; [`MAX_PASSES`]
//! bounds how many passes one scan runs.
//! Test: `credential_print_tests::a_bound_command_keeps_the_one_pass_threshold_8771`,
//! `credential_print_tests::a_late_binding_still_rescans_8771`.
use super::MARK;
use crate::commands::hook_rewrite::is_env_assignment;
/// Passes one scan runs before it refuses as unreadable: each rescan follows
/// one more name bound after text that reads it, and the work budget grows by
/// one [`super::WORK_BUDGET`] per top-level pass, so the whole scan stays
/// linear in the command's length.
pub(super) const MAX_PASSES: usize = 4;
/// Words that run text later than its place in the command — a loop body, a
/// function, a trap or alias string, a coprocess, a prompt hook — so a stage
/// before a binding can still run after it.
const DEFERRING_WORDS: &[&str] = &[
"while",
"until",
"for",
"select",
"function",
"trap",
"alias",
"coproc",
"bind",
"complete",
"fc",
"PROMPT_COMMAND",
];
/// Whether the pass that produced `stages` from `flat` judged text against a
/// name set older than the one it ended with, given `first_bind`, the index
/// of the first stage that bound a new name.
///
/// What: true when `flat` defers text ([`DEFERRING_WORDS`], or a `()`
/// function header), when a lifted substitution sits after the binding — its
/// body was judged before any stage ran, though the shell runs it later — or
/// when the binding stage is more than `NAME=value` words (it was judged
/// before its own binding: `T=$(…) printenv T`) or lifts more than one.
/// Fail-closed: anything else in doubt rescans.
pub(super) fn stale_after(flat: &str, stages: &[(String, bool, bool)], first_bind: usize) -> bool {
let defers = flat.contains("()")
|| flat
.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
.any(|w| DEFERRING_WORDS.contains(&w));
let binder = stages
.get(first_bind)
.map_or("", |(stage, ..)| stage.as_str());
let only_assigns =
shlex::split(binder.trim()).is_some_and(|words| words.iter().all(|w| is_env_assignment(w)));
defers
|| !only_assigns
|| binder.matches(MARK).count() > 1
|| stages
.iter()
.skip(first_bind + 1)
.any(|(stage, ..)| stage.contains(MARK))
}