1use anyhow::{Context as _, Result};
2use log::debug;
3use std::{
4 fs::{File, OpenOptions},
5 io::{BufReader, Read, Seek, SeekFrom, Write},
6 path::Path,
7};
8
9pub fn lint_files<I, P>(files: I, fix: bool) -> Result<bool>
10where
11 I: IntoIterator<Item = P>,
12 P: AsRef<Path>,
13{
14 let mut ret = true;
15 for f in files {
16 debug!("checking {}", f.as_ref().to_string_lossy());
17 let passed = lint(&mut BufReader::new(File::open(&f)?))?;
18 if !passed {
19 println!(
20 "{}: no newline at end of file",
21 f.as_ref().to_string_lossy()
22 );
23 if fix {
24 let mut file = OpenOptions::new().append(true).open(&f)?;
25 file.write(b"\n").with_context(|| {
26 format!(
27 "failed to append newline to {}",
28 f.as_ref().to_string_lossy()
29 )
30 })?;
31 }
32 }
33 ret &= passed;
34 }
35
36 Ok(ret)
37}
38
39pub fn lint(reader: &mut (impl Read + Seek)) -> Result<bool> {
40 if is_binary(reader)? {
41 debug!("binary file skipped");
42 return Ok(true);
43 }
44
45 let n = reader.seek(SeekFrom::End(0))?;
46 if n == 0 {
47 debug!("empty file skipped");
48 return Ok(true);
49 }
50
51 reader.seek(SeekFrom::End(-1))?;
52 let eof = reader.bytes().next().transpose()?;
53 Ok(eof == Some(b'\n'))
54}
55
56fn is_binary(file: &mut impl Read) -> Result<bool> {
58 const FIRST_FEW_BYTES: usize = 8000;
59 let mut head = vec![0; FIRST_FEW_BYTES];
60 let n = file.read(&mut head)?;
61 Ok(head[..n].contains(&0))
62}
63
64#[cfg(test)]
65mod tests {
66 use super::{is_binary, lint};
67 use std::io::Cursor;
68
69 #[test]
70 fn empty() {
71 assert!(lint(&mut Cursor::new("".as_bytes())).unwrap());
72 }
73
74 #[test]
75 fn valid_eof() {
76 assert!(lint(&mut Cursor::new("text\n".as_bytes())).unwrap());
77 }
78
79 #[test]
80 fn invalid_eof() {
81 assert!(!lint(&mut Cursor::new("text".as_bytes())).unwrap());
82 }
83
84 #[test]
85 fn text_is_not_binary() {
86 assert!(!is_binary(&mut "text".as_bytes()).unwrap());
87 }
88
89 #[test]
90 fn null_is_binary() {
91 assert!(is_binary(&mut [0, 1].as_slice()).unwrap());
92 }
93}