1use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11
12use std::char::REPLACEMENT_CHARACTER;
13
14const UNICODE_BIDI_CHARS: &[char] = &[
15 '\u{202A}', '\u{202B}', '\u{202D}', '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{202C}',
16 '\u{2069}',
17];
18
19#[derive(Builder, Debug, Default, Clone, Copy)]
24#[builder(field(private))]
25pub struct RejectBiDi {
26 #[builder(default = "false")]
31 allow: bool,
32}
33
34impl RejectBiDi {
35 pub fn builder() -> RejectBiDiBuilder {
37 Default::default()
38 }
39}
40
41impl ContentCheck for RejectBiDi {
42 fn name(&self) -> &str {
43 "reject-bidi"
44 }
45
46 fn check(
47 &self,
48 ctx: &CheckGitContext,
49 content: &dyn Content,
50 ) -> Result<CheckResult, Box<dyn Error>> {
51 let mut result = CheckResult::new();
52
53 for diff in content.diffs() {
54 match diff.status {
55 StatusChange::Added | StatusChange::Modified(_) => (),
56 _ => continue,
57 }
58
59 let diff_attr = ctx.check_attr("diff", diff.name.as_path())?;
60 if let AttributeState::Unset = diff_attr {
61 continue;
63 }
64
65 let patch = match content.path_diff(&diff.name) {
66 Ok(s) => s,
67 Err(err) => {
68 result.add_alert(
69 format!(
70 "{}failed to get the diff for file `{}`: {err}.",
71 commit_prefix(content),
72 diff.name,
73 ),
74 true,
75 );
76 continue;
77 },
78 };
79
80 for line in patch.lines().filter(|line| line.starts_with('+')) {
81 let line_bidi_free: String = line
82 .chars()
83 .map(|c| {
84 if UNICODE_BIDI_CHARS.contains(&c) {
85 REPLACEMENT_CHARACTER
86 } else {
87 c
88 }
89 })
90 .collect();
91 if line_bidi_free != line {
92 let safe_line = line_bidi_free[1..]
93 .replace('\\', "\\\\")
94 .replace('`', "\\`");
95 if self.allow {
96 result.add_warning(format!(
97 "{}Unicode bidirectional control character(s) added in `{}`: `{safe_line}`.",
98 commit_prefix_str(content, "needs checked;"),
99 diff.name,
100 ));
101 } else {
102 result.add_error(format!(
103 "{}Unicode bidirectional control character(s) added in `{}`: `{safe_line}`.",
104 commit_prefix_str(content, "not allowed;"),
105 diff.name,
106 ));
107 }
108 }
109 }
110 }
111
112 Ok(result)
113 }
114}
115
116#[cfg(feature = "config")]
117pub(crate) mod config {
118 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
119 use serde::Deserialize;
120 #[cfg(test)]
121 use serde_json::json;
122
123 use crate::RejectBiDi;
124
125 #[derive(Deserialize, Debug)]
141 pub struct RejectBiDiConfig {
142 #[serde(default)]
143 allow: bool,
144 }
145
146 impl IntoCheck for RejectBiDiConfig {
147 type Check = RejectBiDi;
148
149 fn into_check(self) -> Self::Check {
150 let mut builder = RejectBiDi::builder();
151
152 builder.allow(self.allow);
153
154 builder
155 .build()
156 .expect("configuration mismatch for `RejectBiDi`")
157 }
158 }
159
160 register_checks! {
161 RejectBiDiConfig {
162 "reject_bidi" => CommitCheckConfig,
163 "reject_bidi/topic" => TopicCheckConfig,
164 },
165 }
166
167 #[test]
168 fn test_reject_bidi_config_empty() {
169 let json = json!({});
170 let check: RejectBiDiConfig = serde_json::from_value(json).unwrap();
171
172 assert!(!check.allow);
173
174 let check = check.into_check();
175
176 assert!(!check.allow);
177 }
178
179 #[test]
180 fn test_reject_bidi_config_all_fields() {
181 let json = json!({
182 "allow": true,
183 });
184 let check: RejectBiDiConfig = serde_json::from_value(json).unwrap();
185
186 assert!(check.allow);
187
188 let check = check.into_check();
189
190 assert!(check.allow);
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use git_checks_core::{Check, TopicCheck};
197
198 use crate::test::*;
199 use crate::RejectBiDi;
200
201 const BAD_TOPIC: &str = "678c1deeade619d52c5b0990bb05af79017f2787";
202 const DELETE_TOPIC: &str = "0a5b9308fcedec797d70ba78bb1b92a7b7943828";
203 const FIX_TOPIC: &str = "0a5b9308fcedec797d70ba78bb1b92a7b7943828";
204
205 #[test]
206 fn test_reject_bidi_builder_default() {
207 assert!(RejectBiDi::builder().build().is_ok());
208 }
209
210 #[test]
211 fn test_reject_bidi_name_commit() {
212 let check = RejectBiDi::default();
213 assert_eq!(Check::name(&check), "reject-bidi");
214 }
215
216 #[test]
217 fn test_reject_bidi_name_topic() {
218 let check = RejectBiDi::default();
219 assert_eq!(TopicCheck::name(&check), "reject-bidi");
220 }
221
222 #[test]
223 fn test_reject_bidi() {
224 let check = RejectBiDi::default();
225 let result = run_check("test_reject_bidi", BAD_TOPIC, check);
226 test_result_errors(result, &[
227 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
228 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
229 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
230 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
231 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
232 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
233 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
234 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
235 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
236 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
237 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
238 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
239 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
240 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
241 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
242 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
243 ]);
244 }
245
246 #[test]
247 fn test_reject_bidi_allow() {
248 let check = RejectBiDi::builder().allow(true).build().unwrap();
249 let result = run_check("test_reject_bidi_allow", BAD_TOPIC, check);
250 test_result_warnings(result, &[
251 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
252 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
253 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
254 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
255 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
256 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
257 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
258 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
259 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
260 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
261 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
262 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
263 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
264 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
265 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
266 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
267 ]);
268 }
269
270 #[test]
271 fn test_reject_bidi_topic() {
272 let check = RejectBiDi::default();
273 let result = run_topic_check("test_reject_bidi_topic", BAD_TOPIC, check);
274 test_result_errors(
275 result,
276 &[
277 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
278 \u{fffd}`.",
279 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
280 \u{fffd}`.",
281 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
282 \u{fffd}`.",
283 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
284 \u{fffd}`.",
285 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
286 \u{fffd}`.",
287 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
288 \u{fffd}`.",
289 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
290 \u{fffd}`.",
291 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
292 \u{fffd}`.",
293 ],
294 );
295 }
296
297 #[test]
298 fn test_reject_bidi_topic_allow() {
299 let check = RejectBiDi::builder().allow(true).build().unwrap();
300 let result = run_topic_check("test_reject_bidi_topic_allow", BAD_TOPIC, check);
301 test_result_warnings(
302 result,
303 &[
304 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
305 \u{fffd}`.",
306 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
307 \u{fffd}`.",
308 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
309 \u{fffd}`.",
310 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
311 \u{fffd}`.",
312 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
313 \u{fffd}`.",
314 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
315 \u{fffd}`.",
316 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
317 \u{fffd}`.",
318 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
319 \u{fffd}`.",
320 ],
321 );
322 }
323
324 #[test]
325 fn test_reject_bidi_delete_file() {
326 let check = RejectBiDi::default();
327 let conf = make_check_conf(&check);
328
329 let result = test_check_base(
330 "test_reject_bidi_delete_file",
331 DELETE_TOPIC,
332 BAD_TOPIC,
333 &conf,
334 );
335 test_result_ok(result);
336 }
337
338 #[test]
339 fn test_reject_bidi_delete_file_topic() {
340 let check = RejectBiDi::default();
341 let result = run_topic_check("test_reject_bidi_delete_file_topic", DELETE_TOPIC, check);
342 test_result_ok(result);
343 }
344
345 #[test]
346 fn test_reject_bidi_topic_fixed() {
347 let check = RejectBiDi::default();
348 run_topic_check_ok("test_reject_bidi_topic_fixed", FIX_TOPIC, check);
349 }
350}