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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
//! Anchored verifier regexes with whole-chunk-equivalent left context.
use regex::{Regex, RegexBuilder};
use std::sync::{Arc, OnceLock};
/// Lazily compiled anchored copies of a detector regex.
pub(crate) struct AnchoredRegex {
src: Arc<str>,
case_insensitive: bool,
cell: OnceLock<Arc<Regex>>,
left_context_cell: OnceLock<Arc<Regex>>,
}
impl AnchoredRegex {
pub(crate) fn new(src: &str, case_insensitive: bool) -> Self {
Self {
src: Arc::from(src),
case_insensitive,
cell: OnceLock::new(),
left_context_cell: OnceLock::new(),
}
}
/// The `\A`-anchored verifier. FAIL CLOSED in this LazyLock init: see
/// [`AnchoredRegex::compile`]. A build failure panics rather than returning
/// `None`, so consumers can never silently drop this pattern's matches.
pub(crate) fn get(&self) -> &Regex {
self.cell.get_or_init(|| self.compile(r"\A(?:", ")"))
}
/// The left-context anchored verifier (`\A(?s:.)(?:<src>)`). Same
/// fail-closed contract as [`AnchoredRegex::get`].
pub(crate) fn get_with_left_context(&self) -> &Regex {
self.left_context_cell
.get_or_init(|| self.compile(r"\A(?s:.)(?:", ")"))
}
fn compile(&self, prefix: &str, suffix: &str) -> Arc<Regex> {
let anchored = format!("{prefix}{}{suffix}", self.src);
match RegexBuilder::new(&anchored)
.case_insensitive(self.case_insensitive)
.size_limit(crate::types::REGEX_SIZE_LIMIT_BYTES)
.dfa_size_limit(crate::types::regex_dfa_limit())
.crlf(self.case_insensitive)
.build()
{
Ok(rx) => Arc::new(rx),
// Law 10 / fail-closed: the base detector regex ALREADY compiled, so
// wrapping it as `{prefix}<src>{suffix}` failing is a build-invariant
// violation of a HARDCODED transform baked into the binary, never a
// valid runtime condition. The former handling returned `None`, which
// the anchored-scan consumer swallowed into an early `return`, silently
// dropping every match for this pattern (recall loss with no fallback on
// the anchored fast path). A build bug must abort loudly, not degrade
// recall invisibly: panic in the init exactly as the CLAUDE.md Law-10
// guidance for baked-in patterns requires.
Err(error) => panic!(
"keyhog BUILD-INVARIANT VIOLATION: anchored verifier regex failed to compile \
though its base detector regex already compiled. Wrapper `{prefix}…{suffix}` over source \
`{src}` is a compile-time-constant transform, so this can only be a build bug (or a \
size/DFA-limit edge), never valid runtime input. Failing closed instead of silently \
dropping this pattern's matches (Law 10). error={error}",
src = self.src,
),
}
}
}
#[cfg(test)]
mod fail_closed_tests {
use super::AnchoredRegex;
#[test]
fn get_returns_a_working_anchor_at_start() {
// A well-formed detector source compiles into a `\A`-anchored verifier:
// it matches only when the pattern begins at offset 0 of the haystack.
let ar = AnchoredRegex::new("[A-Z]{3}[0-9]{2}", false);
let re = ar.get();
let mut locs = re.capture_locations();
let m = re
.captures_read(&mut locs, "ABC12tail")
.expect("anchored verifier must match a value at the start");
assert_eq!(m.start(), 0);
assert_eq!(m.end(), 5, "matches exactly the 5-char shape ABC12");
// Embedded (not at offset 0): `\A` blocks it (proving the anchor is real).
assert!(
re.captures_read(&mut locs, "xxABC12").is_none(),
"\\A must reject a value that does not start at offset 0"
);
}
#[test]
#[should_panic(expected = "BUILD-INVARIANT VIOLATION")]
fn no_context_compile_failure_panics_fail_closed() {
// A source that makes the hardcoded `\A(?:<src>)` wrapper unbalanced forces
// the anchored build to fail. Per Law 10 that is a build-invariant violation
// of a baked-in transform and MUST abort loudly, the former handling
// returned `None`, which the anchored-scan consumer swallowed into a silent
// early `return`, dropping every match for this pattern.
let ar = AnchoredRegex::new(")unbalanced", false);
ar.get();
}
#[test]
#[should_panic(expected = "BUILD-INVARIANT VIOLATION")]
fn left_context_compile_failure_panics_fail_closed() {
// The left-context variant (`\A(?s:.)(?:<src>)`) fails closed identically:
// no path is allowed to swallow the compile failure into silent recall loss.
let ar = AnchoredRegex::new(")unbalanced", false);
ar.get_with_left_context();
}
// ── CRLF/case-flag parity with the two-branch base compile ──────────────
// The anchored verifier couples BOTH `case_insensitive` and `crlf` to the
// detector's `case_insensitive` bit. This is NOT a copy-paste of crlf<-ci:
// the base detector regex (`LazyRegex::get`) is itself compiled on two
// branches that pair the flags exactly the same way
// ci detector -> `shared_regex` => case_insensitive(true) + crlf(true)
// non-ci -> `Regex::new` => case_insensitive(false) + crlf(false)
// The anchored verifier's whole purpose is whole-chunk-equivalence with that
// base, so it MUST reproduce whichever branch applies. Under crlf(true) the
// dot excludes `\r`; under crlf(false) it matches `\r`. That makes a ci and a
// non-ci verifier legitimately DIVERGE on a CR-bearing haystack, mirroring
// their base regexes. This pins the coupling so a future "crlf should always
// be true" edit, which would silently break case-sensitive-detector parity
// on CRLF input (fails loudly here instead).
#[test]
fn anchored_crlf_and_case_flags_mirror_the_two_branch_base_compile() {
use regex::{Regex, RegexBuilder};
let hay = "A\rB";
// Base branch A, case-sensitive detector: `Regex::new` default, crlf
// false, so the dot matches CR and the whole shape matches.
let base_cs = Regex::new("A.B").expect("base cs regex compiles");
assert!(
base_cs.is_match(hay),
"crlf(false) base: the dot matches CR, so `A.B` matches `A\\rB`"
);
// Base branch B, case-insensitive detector: `shared_regex` flags, crlf
// true, so the dot excludes CR and the shape does NOT match.
let base_ci = RegexBuilder::new("A.B")
.case_insensitive(true)
.crlf(true)
.build()
.expect("base ci regex compiles");
assert!(
!base_ci.is_match(hay),
"crlf(true) base: the dot excludes CR, so `A.B` does NOT match `A\\rB`"
);
// The anchored verifier for each detector kind must AGREE with its own
// base branch, proving the flag coupling reproduces the base, not that
// crlf is uniformly true.
let anch_cs = AnchoredRegex::new("A.B", false);
assert!(
anch_cs.get().is_match(hay),
"case-sensitive anchored verifier mirrors crlf(false): dot matches CR"
);
let anch_ci = AnchoredRegex::new("A.B", true);
assert!(
!anch_ci.get().is_match(hay),
"case-insensitive anchored verifier mirrors crlf(true): dot excludes CR"
);
}
}