1use crate::args::ReadmeArgs;
2
3use cargo::{CliError, CliResult};
4use regex::Regex;
5use std::fs::File;
6use std::io::{BufRead, BufReader, Error, Write};
7use std::path::{Path, PathBuf};
8
9pub const README_FILENAME: &str = "README.md";
11const CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER: &str =
13 "## Cargo Geiger Safety Report";
14
15pub fn create_or_replace_section_in_readme(
19 readme_args: &ReadmeArgs,
20 scan_output_lines: &[String],
21) -> CliResult {
22 let readme_path_buf =
23 get_readme_path_buf_from_arguments_or_default(readme_args);
24
25 if !readme_path_buf.exists() {
26 eprintln!(
27 "File: {} does not exist. To construct a Cargo Geiger Safety Report section, please first create a README.",
28 readme_path_buf.to_str().unwrap()
29 );
30 return CliResult::Err(CliError::code(1));
31 }
32
33 let mut readme_content =
34 read_file_contents(&readme_path_buf).map_err(|e| {
35 eprintln!(
36 "Failed to read contents from file: {}",
37 readme_path_buf.to_str().unwrap()
38 );
39 anyhow::Error::from(e)
40 })?;
41
42 update_readme_content(readme_args, &mut readme_content, scan_output_lines);
43
44 write_lines_to_file(&readme_content, &readme_path_buf).map_err(|e| {
45 eprintln!(
46 "Failed to write lines to file: {}",
47 readme_path_buf.to_str().unwrap()
48 );
49 anyhow::Error::from(e)
50 })?;
51
52 Ok(())
53}
54
55fn find_start_and_end_lines_of_safety_report_section(
59 readme_args: &ReadmeArgs,
60 readme_content: &[String],
61) -> (i32, i32) {
62 let mut start_line_number = -1;
63 let mut end_line_number = -1;
64
65 let start_line_pattern =
66 construct_regex_expression_for_section_header(readme_args);
67
68 let end_line_pattern = Regex::new("^#+.*").unwrap();
69
70 for (line_number, line) in readme_content.iter().enumerate() {
71 if start_line_pattern.is_match(line) {
72 start_line_number = line_number as i32;
73 continue;
74 }
75
76 if start_line_number != -1 && end_line_pattern.is_match(line) {
77 end_line_number = line_number as i32;
78 break;
79 }
80 }
81
82 (start_line_number, end_line_number)
83}
84
85fn construct_regex_expression_for_section_header(
88 readme_args: &ReadmeArgs,
89) -> Regex {
90 match &readme_args.section_name {
91 Some(section_name) => {
92 let mut regex_string = String::from("^#+\\s");
93 regex_string.push_str(§ion_name.replace(' ', "\\s"));
94 regex_string.push_str("\\s*");
95
96 Regex::new(®ex_string).unwrap()
97 }
98 None => {
99 Regex::new("^#+\\sCargo\\sGeiger\\sSafety\\sReport\\s*").unwrap()
100 }
101 }
102}
103
104fn get_readme_path_buf_from_arguments_or_default(
107 readme_args: &ReadmeArgs,
108) -> PathBuf {
109 match &readme_args.readme_path {
110 Some(readme_path) => readme_path.to_path_buf(),
111 None => {
112 let mut current_dir_path_buf = std::env::current_dir().unwrap();
113 current_dir_path_buf.push(README_FILENAME);
114 current_dir_path_buf
115 }
116 }
117}
118
119fn read_file_contents(path: &Path) -> Result<Vec<String>, Error> {
121 let file = File::open(path)?;
122 let buf_reader = BufReader::new(file);
123
124 Ok(buf_reader
125 .lines()
126 .filter_map(|l| l.ok())
127 .collect::<Vec<String>>())
128}
129
130fn update_readme_content(
134 readme_args: &ReadmeArgs,
135 readme_content: &mut Vec<String>,
136 scan_result: &[String],
137) {
138 let (start_line_number, mut end_line_number) =
139 find_start_and_end_lines_of_safety_report_section(
140 readme_args,
141 readme_content,
142 );
143
144 if start_line_number == -1 {
145 match &readme_args.section_name {
148 Some(section_name) => {
149 let mut section_name_string = String::from("## ");
150 section_name_string.push_str(section_name);
151
152 readme_content.push(section_name_string);
153 }
154 None => {
155 readme_content.push(
156 CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER.to_string(),
157 );
158 }
159 }
160 readme_content.push(String::from("```"));
161 for scan_result_line in scan_result {
162 readme_content.push(scan_result_line.to_string())
163 }
164 readme_content.push(String::from("```"));
165 } else {
166 if end_line_number == -1 {
167 end_line_number = readme_content.len() as i32;
168 }
169
170 for _ in start_line_number + 1..end_line_number {
173 readme_content.remove((start_line_number + 1) as usize);
174 }
175
176 let mut running_scan_line_index = start_line_number + 1;
177
178 readme_content
179 .insert(running_scan_line_index as usize, String::from("```"));
180 running_scan_line_index += 1;
181
182 for scan_result_line in scan_result {
183 readme_content.insert(
184 running_scan_line_index as usize,
185 scan_result_line.to_string(),
186 );
187 running_scan_line_index += 1;
188 }
189
190 readme_content
191 .insert(running_scan_line_index as usize, String::from("```"));
192 }
193}
194
195fn write_lines_to_file(lines: &[String], path: &Path) -> Result<(), Error> {
197 let mut readme_file = File::create(path)?;
198
199 for line in lines {
200 writeln!(readme_file, "{}", line)?
201 }
202
203 Ok(())
204}
205
206#[cfg(test)]
207mod readme_tests {
208 use super::*;
209
210 use rstest::*;
211 use std::io::Write;
212 use tempfile::tempdir;
213
214 #[rstest]
215 fn create_or_replace_section_test_readme_doesnt_exist() {
216 let temp_dir = tempdir().unwrap();
217 let readme_path = temp_dir.path().join("README.md");
218
219 let readme_args = ReadmeArgs {
220 readme_path: Some(readme_path),
221 ..Default::default()
222 };
223
224 let scan_result = vec![];
225
226 let result =
227 create_or_replace_section_in_readme(&readme_args, &scan_result);
228
229 assert!(result.is_err());
230 }
231
232 #[rstest]
233 fn create_or_replace_section_test_readme_doesnt_contain_section() {
234 let temp_dir = tempdir().unwrap();
235 let readme_path = temp_dir.path().join("README.md");
236
237 let readme_args = ReadmeArgs {
238 readme_path: Some(readme_path.clone()),
239 ..Default::default()
240 };
241
242 let mut readme_file = File::create(readme_path.clone()).unwrap();
243 let scan_result = vec![
244 String::from("First safety report line"),
245 String::from("Second safety report line"),
246 String::from("Third safety report line"),
247 ];
248
249 writeln!(
250 readme_file,
251 "# Readme Header\nSome text\nAnother line\n## Another header\nMore text"
252 ).unwrap();
253
254 let result =
255 create_or_replace_section_in_readme(&readme_args, &scan_result);
256
257 assert!(result.is_ok());
258
259 let updated_file_content =
260 BufReader::new(File::open(readme_path).unwrap())
261 .lines()
262 .map(|l| l.unwrap())
263 .collect::<Vec<String>>();
264
265 let expected_readme_content = vec![
266 String::from("# Readme Header"),
267 String::from("Some text"),
268 String::from("Another line"),
269 String::from("## Another header"),
270 String::from("More text"),
271 CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER.to_string(),
272 String::from("```"),
273 String::from("First safety report line"),
274 String::from("Second safety report line"),
275 String::from("Third safety report line"),
276 String::from("```"),
277 ];
278
279 assert_eq!(updated_file_content, expected_readme_content)
280 }
281
282 #[rstest(
283 input_readme_args,
284 expected_regex_expression,
285 case(
286 ReadmeArgs{
287 section_name: None,
288 ..Default::default()
289 },
290 Regex::new("^#+\\sCargo\\sGeiger\\sSafety\\sReport\\s*").unwrap()
291 ),
292 case(
293 ReadmeArgs{
294 section_name: Some(String::from("Test Section Name")),
295 ..Default::default()
296 },
297 Regex::new("^#+\\sTest\\sSection\\sName\\s*").unwrap()
298 )
299 )]
300 fn construct_regex_expression_for_section_header_test(
301 input_readme_args: ReadmeArgs,
302 expected_regex_expression: Regex,
303 ) {
304 let regex_expression =
305 construct_regex_expression_for_section_header(&input_readme_args);
306
307 assert_eq!(
308 regex_expression.as_str(),
309 expected_regex_expression.as_str()
310 );
311 }
312
313 #[rstest(
314 input_readme_content,
315 expected_start_line_number,
316 expected_end_line_number,
317 case(
318 vec![
319 String::from("## Cargo Geiger Safety Report"),
320 String::from("First line"),
321 String::from("Second line")
322 ],
323 0,
324 -1
325 ),
326 case(
327 vec![
328 String::from("# Cargo Geiger Safety Report"),
329 String::from("First line"),
330 String::from("Second line")
331 ],
332 0,
333 -1
334 ),
335 case(
336 vec![
337 String::from("First line"),
338 String::from("## Cargo Geiger Safety Report"),
339 String::from("Second line"),
340 String::from("Third line")
341 ],
342 1,
343 -1
344 ),
345 case(
346 vec![
347 String::from("# Another header"),
348 String::from("First line"),
349 String::from("## Cargo Geiger Safety Report"),
350 String::from("Second line"),
351 String::from("Third line")
352 ],
353 2,
354 -1
355 ),
356 case(
357 vec![
358 String::from("# Another header"),
359 String::from("First line"),
360 String::from("## Cargo Geiger Safety Report"),
361 String::from("Second line"),
362 String::from("Third line"),
363 String::from("# Next header"),
364 String::from("Fourth line")
365 ],
366 2,
367 5
368 )
369 )]
370 fn find_start_and_end_lines_of_safety_report_section_test(
371 input_readme_content: Vec<String>,
372 expected_start_line_number: i32,
373 expected_end_line_number: i32,
374 ) {
375 let readme_args = ReadmeArgs::default();
376
377 let (start_line_number, end_line_number) =
378 find_start_and_end_lines_of_safety_report_section(
379 &readme_args,
380 &input_readme_content,
381 );
382
383 assert_eq!(start_line_number, expected_start_line_number);
384 assert_eq!(end_line_number, expected_end_line_number);
385 }
386
387 #[rstest]
388 fn get_readme_path_buf_from_arguments_or_default_test_none() {
389 let mut path_buf = std::env::current_dir().unwrap();
390 path_buf.push(README_FILENAME);
391
392 let readme_args = ReadmeArgs {
393 readme_path: None,
394 ..Default::default()
395 };
396
397 let readme_path_buf =
398 get_readme_path_buf_from_arguments_or_default(&readme_args);
399
400 assert_eq!(readme_path_buf, path_buf)
401 }
402
403 #[rstest]
404 fn get_readme_path_buf_from_arguments_or_default_test_some() {
405 let path_buf = PathBuf::from("/test/path");
406
407 let readme_args = ReadmeArgs {
408 readme_path: Some(path_buf.clone()),
409 ..Default::default()
410 };
411
412 let readme_path_buf =
413 get_readme_path_buf_from_arguments_or_default(&readme_args);
414
415 assert_eq!(readme_path_buf, path_buf);
416 }
417
418 #[rstest(
419 input_readme_args,
420 expected_section_header,
421 case(
422 ReadmeArgs{
423 section_name: None,
424 ..Default::default()
425 },
426 CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER.to_string()
427 ),
428 case(
429 ReadmeArgs{
430 section_name: Some(String::from("Test Section Name")),
431 ..Default::default()
432 },
433 String::from("## Test Section Name")
434 )
435 )]
436 fn update_readme_content_test_no_safety_report_present(
437 input_readme_args: ReadmeArgs,
438 expected_section_header: String,
439 ) {
440 let mut readme_content = vec![
441 String::from("# readme header"),
442 String::from("line of text"),
443 String::from("another line of text"),
444 String::from("## another header"),
445 ];
446
447 let scan_result = vec![
448 String::from("first line of scan result"),
449 String::from("second line of scan result"),
450 String::from("third line of scan result"),
451 ];
452
453 update_readme_content(
454 &input_readme_args,
455 &mut readme_content,
456 &scan_result,
457 );
458
459 let expected_readme_content = vec![
460 String::from("# readme header"),
461 String::from("line of text"),
462 String::from("another line of text"),
463 String::from("## another header"),
464 expected_section_header,
465 String::from("```"),
466 String::from("first line of scan result"),
467 String::from("second line of scan result"),
468 String::from("third line of scan result"),
469 String::from("```"),
470 ];
471
472 assert_eq!(readme_content, expected_readme_content);
473 }
474
475 #[rstest]
476 fn update_readme_content_test_safety_report_present_in_middle_of_readme() {
477 let readme_args = ReadmeArgs::default();
478
479 let mut readme_content = vec![
480 String::from("# readme header"),
481 String::from("line of text"),
482 String::from("another line of text"),
483 CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER.to_string(),
484 String::from("first line of old scan result"),
485 String::from("second line of old scan result"),
486 String::from("# another header"),
487 String::from("line of text"),
488 ];
489
490 let scan_result = vec![
491 String::from("first line of scan result"),
492 String::from("second line of scan result"),
493 String::from("third line of scan result"),
494 ];
495
496 update_readme_content(&readme_args, &mut readme_content, &scan_result);
497
498 let expected_readme_content = vec![
499 String::from("# readme header"),
500 String::from("line of text"),
501 String::from("another line of text"),
502 CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER.to_string(),
503 String::from("```"),
504 String::from("first line of scan result"),
505 String::from("second line of scan result"),
506 String::from("third line of scan result"),
507 String::from("```"),
508 String::from("# another header"),
509 String::from("line of text"),
510 ];
511
512 assert_eq!(readme_content, expected_readme_content);
513 }
514}