use crate::{Error, FileInfo, Fingerprint};
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
pub fn inspect_file(path: &Path) -> Result<FileInfo, Error> {
scan_file(path, None)
}
pub fn fingerprint_file(path: &Path) -> Result<Fingerprint, Error> {
Ok(inspect_file(path)?.fingerprint)
}
pub fn fingerprint_through_line(path: &Path, line: u64) -> Result<Fingerprint, Error> {
Ok(scan_file(path, Some(line))?.fingerprint)
}
fn scan_file(path: &Path, through_line: Option<u64>) -> Result<FileInfo, Error> {
scan_reader(open_regular_file(path)?, through_line)
}
pub(super) fn open_regular_file(path: &Path) -> Result<File, Error> {
if !std::fs::metadata(path)?.is_file() {
return Err(Error::NotRegularFile);
}
Ok(File::open(path)?)
}
fn scan_reader(mut reader: impl Read, through_line: Option<u64>) -> Result<FileInfo, Error> {
let mut hasher = blake3::Hasher::new();
let mut byte_len = 0_u64;
let mut lf_count = 0_u64;
let mut ends_with_lf = false;
let mut buffer = [0_u8; 8192];
if through_line != Some(0) {
loop {
let read = match reader.read(&mut buffer) {
Ok(read) => read,
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error.into()),
};
if read == 0 {
break;
}
let mut included = read;
let mut reached_line = false;
for (index, &byte) in buffer[..read].iter().enumerate() {
if byte == b'\n' {
lf_count = lf_count.checked_add(1).ok_or(Error::FileTooLarge)?;
if through_line == Some(lf_count) {
included = index + 1;
reached_line = true;
break;
}
}
}
let bytes = &buffer[..included];
hasher.update(bytes);
byte_len = byte_len
.checked_add(u64::try_from(included).map_err(|_| Error::FileTooLarge)?)
.ok_or(Error::FileTooLarge)?;
ends_with_lf = bytes.last() == Some(&b'\n');
if reached_line {
break;
}
}
}
let line_count = lf_count
.checked_add(u64::from(byte_len > 0 && !ends_with_lf))
.ok_or(Error::FileTooLarge)?;
Ok(FileInfo {
byte_len,
line_count,
fingerprint: Fingerprint(*hasher.finalize().as_bytes()),
})
}
#[cfg(test)]
mod tests;