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 `{}`: {}.",
71 commit_prefix(content),
72 diff.name,
73 err,
74 ),
75 true,
76 );
77 continue;
78 },
79 };
80
81 for line in patch.lines().filter(|line| line.starts_with('+')) {
82 let line_bidi_free: String = line
83 .chars()
84 .map(|c| {
85 if UNICODE_BIDI_CHARS.contains(&c) {
86 REPLACEMENT_CHARACTER
87 } else {
88 c
89 }
90 })
91 .collect();
92 if line_bidi_free != line {
93 let safe_line = line_bidi_free[1..]
94 .replace('\\', "\\\\")
95 .replace('`', "\\`");
96 if self.allow {
97 result.add_warning(format!(
98 "{}Unicode bidirectional control character(s) added in `{}`: `{}`.",
99 commit_prefix_str(content, "needs checked;"),
100 diff.name,
101 safe_line,
102 ));
103 } else {
104 result.add_error(format!(
105 "{}Unicode bidirectional control character(s) added in `{}`: `{}`.",
106 commit_prefix_str(content, "not allowed;"),
107 diff.name,
108 safe_line,
109 ));
110 }
111 }
112 }
113 }
114
115 Ok(result)
116 }
117}
118
119#[cfg(feature = "config")]
120pub(crate) mod config {
121 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
122 use serde::Deserialize;
123 #[cfg(test)]
124 use serde_json::json;
125
126 use crate::RejectBiDi;
127
128 #[derive(Deserialize, Debug)]
144 pub struct RejectBiDiConfig {
145 #[serde(default)]
146 allow: bool,
147 }
148
149 impl IntoCheck for RejectBiDiConfig {
150 type Check = RejectBiDi;
151
152 fn into_check(self) -> Self::Check {
153 let mut builder = RejectBiDi::builder();
154
155 builder.allow(self.allow);
156
157 builder
158 .build()
159 .expect("configuration mismatch for `RejectBiDi`")
160 }
161 }
162
163 register_checks! {
164 RejectBiDiConfig {
165 "reject_bidi" => CommitCheckConfig,
166 "reject_bidi/topic" => TopicCheckConfig,
167 },
168 }
169
170 #[test]
171 fn test_reject_bidi_config_empty() {
172 let json = json!({});
173 let check: RejectBiDiConfig = serde_json::from_value(json).unwrap();
174
175 assert!(!check.allow);
176
177 let check = check.into_check();
178
179 assert!(!check.allow);
180 }
181
182 #[test]
183 fn test_reject_bidi_config_all_fields() {
184 let json = json!({
185 "allow": true,
186 });
187 let check: RejectBiDiConfig = serde_json::from_value(json).unwrap();
188
189 assert!(check.allow);
190
191 let check = check.into_check();
192
193 assert!(check.allow);
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use git_checks_core::{Check, TopicCheck};
200
201 use crate::test::*;
202 use crate::RejectBiDi;
203
204 const BAD_TOPIC: &str = "678c1deeade619d52c5b0990bb05af79017f2787";
205 const DELETE_TOPIC: &str = "0a5b9308fcedec797d70ba78bb1b92a7b7943828";
206 const FIX_TOPIC: &str = "0a5b9308fcedec797d70ba78bb1b92a7b7943828";
207
208 #[test]
209 fn test_reject_bidi_builder_default() {
210 assert!(RejectBiDi::builder().build().is_ok());
211 }
212
213 #[test]
214 fn test_reject_bidi_name_commit() {
215 let check = RejectBiDi::default();
216 assert_eq!(Check::name(&check), "reject-bidi");
217 }
218
219 #[test]
220 fn test_reject_bidi_name_topic() {
221 let check = RejectBiDi::default();
222 assert_eq!(TopicCheck::name(&check), "reject-bidi");
223 }
224
225 #[test]
226 fn test_reject_bidi() {
227 let check = RejectBiDi::default();
228 let result = run_check("test_reject_bidi", BAD_TOPIC, check);
229 test_result_errors(result, &[
230 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
231 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
232 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
233 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
234 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
235 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
236 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
237 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
238 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
239 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
240 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
241 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
242 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
243 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
244 "commit 678c1deeade619d52c5b0990bb05af79017f2787 not allowed; Unicode bidirectional \
245 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
246 ]);
247 }
248
249 #[test]
250 fn test_reject_bidi_allow() {
251 let check = RejectBiDi::builder().allow(true).build().unwrap();
252 let result = run_check("test_reject_bidi_allow", BAD_TOPIC, check);
253 test_result_warnings(result, &[
254 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
255 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
256 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
257 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
258 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
259 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
260 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
261 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
262 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
263 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
264 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
265 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
266 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
267 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
268 "commit 678c1deeade619d52c5b0990bb05af79017f2787 needs checked; Unicode bidirectional \
269 control character(s) added in `has-bidi`: `bidi character: \u{fffd}`.",
270 ]);
271 }
272
273 #[test]
274 fn test_reject_bidi_topic() {
275 let check = RejectBiDi::default();
276 let result = run_topic_check("test_reject_bidi_topic", BAD_TOPIC, check);
277 test_result_errors(
278 result,
279 &[
280 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
281 \u{fffd}`.",
282 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
283 \u{fffd}`.",
284 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
285 \u{fffd}`.",
286 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
287 \u{fffd}`.",
288 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
289 \u{fffd}`.",
290 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
291 \u{fffd}`.",
292 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
293 \u{fffd}`.",
294 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
295 \u{fffd}`.",
296 ],
297 );
298 }
299
300 #[test]
301 fn test_reject_bidi_topic_allow() {
302 let check = RejectBiDi::builder().allow(true).build().unwrap();
303 let result = run_topic_check("test_reject_bidi_topic_allow", BAD_TOPIC, check);
304 test_result_warnings(
305 result,
306 &[
307 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
308 \u{fffd}`.",
309 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
310 \u{fffd}`.",
311 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
312 \u{fffd}`.",
313 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
314 \u{fffd}`.",
315 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
316 \u{fffd}`.",
317 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
318 \u{fffd}`.",
319 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
320 \u{fffd}`.",
321 "Unicode bidirectional control character(s) added in `has-bidi`: `bidi character: \
322 \u{fffd}`.",
323 ],
324 );
325 }
326
327 #[test]
328 fn test_reject_bidi_delete_file() {
329 let check = RejectBiDi::default();
330 let conf = make_check_conf(&check);
331
332 let result = test_check_base(
333 "test_reject_bidi_delete_file",
334 DELETE_TOPIC,
335 BAD_TOPIC,
336 &conf,
337 );
338 test_result_ok(result);
339 }
340
341 #[test]
342 fn test_reject_bidi_delete_file_topic() {
343 let check = RejectBiDi::default();
344 let result = run_topic_check("test_reject_bidi_delete_file_topic", DELETE_TOPIC, check);
345 test_result_ok(result);
346 }
347
348 #[test]
349 fn test_reject_bidi_topic_fixed() {
350 let check = RejectBiDi::default();
351 run_topic_check_ok("test_reject_bidi_topic_fixed", FIX_TOPIC, check);
352 }
353}