eoflint/
lib.rs

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
56/// https://git.kernel.org/pub/scm/git/git.git/tree/xdiff-interface.c?h=v2.37.1#n192
57fn 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}