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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
//! What:
//! Which rules files the scanner loads,
//! and which candidates it is given.
//! Why:
//! The scanner library filters nothing;
//! the standalone scanner and the TypeScript
//! policy applied these choices around it.
//! They are restated here once,
//! as pure
//! functions:
//! the rules-file precedence (the configured `rulesFile` entries, then the
//! `FORBIDDEN_STRINGS_RULES` list, else
//! `forbidden-strings.local.txt` in the repository root) and the paths that hold
//! the rules themselves,
//! which would otherwise match their own content.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // const rules = rulesSource(config.rulesFile, process.env.FORBIDDEN_STRINGS_RULES, repositoryRoot);
//! // const eligible = candidates.filter(candidate => isScannable(candidate, rulesCandidatePaths(rules, repositoryRoot)));
//! ```
/// Import the candidate being judged and its change kinds.
use crateCandidateChange;
use crateCandidate;
/// Import the configured rules files.
use crateRulesFileEntry;
/// Import the rules selection the adapter loads.
use crateRulesSource;
/// Import the scanner's rules-list entry and the one parser of the variable's list grammar.
use ;
/// `OsStr` is borrowed operating-system text of raw OS bytes (sibling `str` must be UTF-8).
use OsStr;
/// What:
/// `Component` is one piece of a path:
/// the root,
/// `.`,
/// `..`,
/// or a name.
/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw OS bytes.
/// Why:
/// A configured rules path is resolved name by name,
/// without touching the filesystem.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// import { resolve, relative } from 'node:path';
/// ```
use ;
/// What:
/// The environment variable naming the runtime rules list.
/// `&str` is a borrowed string compiled into the program.
/// Why:
/// It is the standalone scanner's own variable,
/// so one setting serves both.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const RULES_VARIABLE = 'FORBIDDEN_STRINGS_RULES';
/// ```
pub const RULES_VARIABLE: &str = "FORBIDDEN_STRINGS_RULES";
/// The rules file used when the variable is unset,
/// relative to the repository root.
pub const DEFAULT_RULES_FILE: &str = "forbidden-strings.local.txt";
/// What:
/// Repository paths never given to the scanner because they hold rule sources.
/// `&[&[u8]]` is a borrowed list of borrowed byte strings.
/// Why:
/// These files contain the patterns themselves and would match their own rules.
/// The list is the TypeScript policy's,
/// unchanged;
/// pathnames are compared as
/// bytes because candidate pathnames are bytes.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const SCANNER_SELF_MATCH_PATHS = new Set(['package/forbidden-strings/cli/data/builtin-rules.txt', ...]);
/// ```
pub const SCANNER_SELF_MATCH_PATHS: & = &;
/// What:
/// Select the rules files:
/// the configured `rulesFile` entries,
/// else the variable's list,
/// else
/// the default file,
/// each relative to the repository root.
/// `Option<&[RulesFileEntry]>` is "the
/// configured entries or nothing";
/// `Option<&OsStr>` is "the variable's value or nothing";
/// `Result<..>` fails only for a variable that is not a valid rules list.
/// Why:
/// The user decided on 2026-10-05 that configuration names the file first and the
/// variable second.
/// The spawned scanner ran with the repository root as its working
/// directory,
/// so a relative setting meant "relative to the root".
/// In-process there
/// is no such directory change,
/// so the root is joined explicitly;
/// `.join(..)` keeps
/// an absolute variable value as it is,
/// and a configured name is relative by
/// construction (`config_rules_file.rs`).
/// A configured name or a set variable makes
/// a missing file an error unless its entry is optional;
/// only the default file may be missing.
/// The variable is parsed with the scanner's own rules-list parser, so both tools read one grammar.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function rulesSource(rulesFiles: RulesFileEntry[] | undefined, variable: string | undefined, repositoryRoot: string): RulesSource; // throws RulesListError
/// ```
/// What:
/// The pathnames the rules files would carry as candidates,
/// for each listed file that lies inside the repository.
/// `Vec<Vec<u8>>` is an owned list of owned pathname byte strings.
/// Why:
/// Any tracked rules file would match its own patterns,
/// so every one is left out of the scan,
/// not only the first.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const rulesCandidatePaths = (rules: RulesSource, root: string) => rules.entries.flatMap(e => rulesCandidatePath(e.path, root) ?? []);
/// ```
/// What:
/// Resolve `.` and `..` in a path by its names alone.
/// `PathBuf::new()` is an empty owned path;
/// `.push(..)` appends one piece and
/// `.pop()` removes the last one.
/// Why:
/// The rules file need not exist,
/// so the filesystem cannot be asked;
/// this is the
/// same textual resolution the TypeScript policy applied.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const normalized = path.resolve(configured);
/// ```
/// What:
/// Turn a repository-relative native path into Git's pathname bytes:
/// its names joined by `/`.
/// `Vec<u8>` is an owned byte list (sibling `String` would require UTF-8).
/// Why:
/// Git stores `/` between names on every system.
/// Joining the names here,
/// instead
/// of copying the path's own text,
/// gives the same bytes whatever separator the
/// operating system uses,
/// with one implementation for all of them.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const gitPath = relativePath.split(path.sep).join('/');
/// ```
/// What:
/// The rules file's pathname as a candidate would carry it,
/// when it lies inside the repository.
/// `Option<Vec<u8>>` is "pathname bytes or nothing".
/// Why:
/// A rules file tracked in the repository would match its own patterns,
/// so its
/// candidate is not scanned.
/// A rules file outside the repository can never be a
/// candidate,
/// which is "nothing".
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function rulesCandidatePath(rulesPath: string, repositoryRoot: string): string | undefined;
/// ```
/// What:
/// Whether a candidate is given to the scanner.
/// `Option<&[u8]>` is "the rules file's pathname bytes or nothing".
/// Why:
/// A deleted path has no content and leaves the tree;
/// the rule-source files and
/// the rules file itself would match their own patterns.
/// Every other candidate
/// is scanned,
/// whatever its mode.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function isScannable(candidate: Candidate, rulesPath?: string): boolean;
/// ```
/// Selection controls stay out of the release executable.