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
//! What:
//! The `rulesFile` option of `security/forbidden-strings`:
//! which file in the
//! repository holds the scanner's private rules,
//! written relative to the
//! repository's top level.
//! Why:
//! The user decided on 2026-10-05 that configuration names the rules file first,
//! then
//! `FORBIDDEN_STRINGS_RULES`,
//! then the default file.
//! A configured name is checked
//! when the configuration is read:
//! it must stay inside the repository by its words
//! alone,
//! so a configuration cannot point the scanner at a file elsewhere on the
//! machine.
//! A name that is missing on disk is not a configuration error;
//! loading
//! the rules reports it when the policy runs.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // const rulesFile = parseRulesFile(option); // throws RulesFileRefusal
//! ```
/// What:
/// Why a `rulesFile` value was refused.
/// `#[derive(...)]` generates copying,
/// debug
/// printing and `==`.
/// Why:
/// The configuration error names the key and explains this one reason.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type RulesFileRefusal = 'empty' | 'absolute' | 'drive' | 'backslash' | 'nul' | 'empty-component' | 'dot-component';
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RulesFileRefusal {
/// The value is empty,
/// so it names no file.
Empty,
/// The value starts with `/`,
/// so it does not start at the repository's top level.
Absolute,
/// The value starts with a drive letter and a colon,
/// which Windows reads as another volume.
Drive,
/// The value holds a backslash,
/// which Windows reads as a separator.
Backslash,
/// The value holds a NUL character,
/// which no file name can hold.
Nul,
/// The value has an empty component:
/// two slashes in a row or a final slash.
EmptyComponent,
/// The value has a `.` or `..` component;
/// `..` could leave the repository.
DotComponent,
}
/// What:
/// The explanation of one refusal,
/// completing the sentence "the value ...".
/// Why:
/// One place owns the words,
/// so every refusal reads the same.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function refusalReason(refusal: RulesFileRefusal): string;
/// ```
pub fn rules_file_refusal_reason(refusal: RulesFileRefusal) -> &'static str {
// `match` names every refusal, so a new one cannot be added without its words.
match refusal {
RulesFileRefusal::Empty => return "is empty",
RulesFileRefusal::Absolute => return "starts with /",
RulesFileRefusal::Drive => return "starts with a drive letter",
RulesFileRefusal::Backslash => return "contains a backslash; use / between directories",
RulesFileRefusal::Nul => return "contains a NUL character",
RulesFileRefusal::EmptyComponent => {
return "has an empty component (two slashes in a row or a trailing slash)";
}
RulesFileRefusal::DotComponent => return "has a . or .. component",
}
}
/// What:
/// Whether one character is `/`,
/// the separator of the option's components.
/// Why:
/// A named predicate for `split`,
/// because the repository bans anonymous functions.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const isSlash = (character: string) => character === '/';
/// ```
fn is_slash(character: char) -> bool {
return character == '/';
}
/// What:
/// Whether `text` starts with an ASCII letter and a colon,
/// as `C:` does.
/// Why:
/// Joining such a value to the repository's top level on Windows would name
/// another volume.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const startsWithDrive = (text: string) => /^[A-Za-z]:/.test(text);
/// ```
fn starts_with_drive(text: &str) -> bool {
let bytes: &[u8] = text.as_bytes();
// `bytes.len() > 1` keeps the two indexed reads inside the text.
return bytes.len() > 1 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':';
}
/// What:
/// Check one `rulesFile` value.
/// `Result<(), RulesFileRefusal>` is "accepted" or the
/// first reason it is not.
/// Why:
/// A value that is relative,
/// uses `/` between non-empty components,
/// and has no `.`
/// or `..` component names a file inside the repository on every platform,
/// whatever
/// the file system later holds.
/// The checks run in a fixed order so the first
/// reason reported is the same on every platform.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function checkRulesFile(value: string): void; // throws RulesFileRefusal
/// ```
pub fn check_rules_file(value: &str) -> Result<(), RulesFileRefusal> {
if value.is_empty() {
return Err(RulesFileRefusal::Empty);
}
if value.starts_with('/') {
return Err(RulesFileRefusal::Absolute);
}
if starts_with_drive(value) {
return Err(RulesFileRefusal::Drive);
}
if value.contains('\\') {
return Err(RulesFileRefusal::Backslash);
}
if value.contains('\0') {
return Err(RulesFileRefusal::Nul);
}
for component in value.split(is_slash) {
if component.is_empty() {
return Err(RulesFileRefusal::EmptyComponent);
}
if component == "." || component == ".." {
return Err(RulesFileRefusal::DotComponent);
}
}
// `Ok(())` is success with no value.
return Ok(());
}
/// Option-value controls stay out of the release executable.
#[cfg(test)]
#[path = "config_rules_file_tests.rs"]
mod tests;