use crate::error::{Error, ErrorKind};
use crate::lockfile::{Buffers, Lockfile, Source};
use crate::pipeline::{Plan, auto_chunks};
use crate::scan::MAX_INPUT_LEN;
use crate::utf8;
use memmap2::{Advice, Mmap};
use rayon::prelude::*;
use std::fmt;
use std::fs::File;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
pub const MAP_MIN: usize = 256 * 1024;
pub struct MappedText {
map: Mmap,
}
impl MappedText {
#[inline]
pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(&self.map) }
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum FileError {
Io {
path: PathBuf,
source: io::Error,
},
Parse {
path: PathBuf,
error: Error,
line: usize,
column: usize,
detail: String,
},
}
impl FileError {
#[must_use]
pub fn path(&self) -> &Path {
match self {
Self::Io { path, .. } | Self::Parse { path, .. } => path,
}
}
#[must_use]
pub const fn parse_error(&self) -> Option<Error> {
match self {
Self::Parse { error, .. } => Some(*error),
Self::Io { .. } => None,
}
}
#[cold]
#[inline(never)]
fn io(path: &Path, source: io::Error) -> Self {
Self::Io {
path: path.to_owned(),
source,
}
}
#[cold]
#[inline(never)]
fn parse(path: &Path, error: Error, text: &str) -> Self {
let (line, column) = error.line_col(text);
Self::Parse {
path: path.to_owned(),
error,
line,
column,
detail: error.detail(text),
}
}
}
impl fmt::Display for FileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io { path, source } => write!(f, "{}: {source}", path.display()),
Self::Parse {
path,
line,
column,
detail,
..
} => write!(f, "{}:{line}:{column}: {detail}", path.display()),
}
}
}
impl std::error::Error for FileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
Self::Parse { error, .. } => Some(error),
}
}
}
pub fn parse_file_with(path: &Path, buffers: &mut Buffers) -> Result<Lockfile<'static>, FileError> {
let mut file = File::open(path).map_err(|e| FileError::io(path, e))?;
let meta = file.metadata().map_err(|e| FileError::io(path, e))?;
let len = usize::try_from(meta.len()).unwrap_or(usize::MAX);
if meta.is_file() && len > MAX_INPUT_LEN {
return Err(too_large(path));
}
if meta.is_file()
&& len >= MAP_MIN
&& let Some(mapped) = map_file(&file, path)?
{
return Lockfile::parse_source(Source::Mapped(mapped), buffers, Plan::Auto)
.map_err(|(e, text)| FileError::parse(path, e, text.as_str()));
}
read_file(
&mut file,
path,
buffers,
if meta.is_file() { len } else { 0 },
)
}
fn map_file(file: &File, path: &Path) -> Result<Option<MappedText>, FileError> {
let Ok(map) = (unsafe { Mmap::map(file) }) else {
return Ok(None);
};
if map.len() > MAX_INPUT_LEN {
return Err(too_large(path));
}
prefetch(&map);
match utf8::validate(&map) {
Ok(_) => Ok(Some(MappedText { map })),
Err(e) => Err(invalid_utf8(path, e, &map)),
}
}
#[cold]
fn invalid_utf8(path: &Path, e: Error, bytes: &[u8]) -> FileError {
let valid = bytes
.get(..e.offset())
.and_then(|b| std::str::from_utf8(b).ok());
FileError::parse(path, e, valid.unwrap_or(""))
}
fn prefetch(map: &Mmap) {
if looks_cached(map) {
return;
}
let chunks = auto_chunks(map.len());
if chunks == 1 {
drop(map.advise(Advice::WillNeed));
return;
}
let step = map.len().div_ceil(chunks);
(0..chunks).into_par_iter().for_each(|i| {
let at = (i * step).min(map.len());
let n = step.min(map.len() - at);
#[cfg(target_os = "linux")]
if map.advise_range(Advice::PopulateRead, at, n).is_ok() {
return;
}
drop(map.advise_range(Advice::WillNeed, at, n));
});
}
#[cfg(target_os = "linux")]
const CACHE_SAMPLES: usize = 16;
#[cfg(target_os = "linux")]
pub fn looks_cached(map: &[u8]) -> bool {
const ALIGN: usize = 64 * 1024;
let stride = map.len() / CACHE_SAMPLES;
(0..CACHE_SAMPLES).all(|k| {
let off = (k * stride) / ALIGN * ALIGN;
if off >= map.len() {
return true;
}
let mut v = 0u8;
let r = unsafe { libc::mincore(map.as_ptr().add(off).cast_mut().cast(), 1, &raw mut v) };
r == 0 && v & 1 == 1
})
}
#[cfg(target_os = "macos")]
pub fn looks_cached(_map: &[u8]) -> bool {
false
}
fn read_file(
file: &mut File,
path: &Path,
buffers: &mut Buffers,
size_hint: usize,
) -> Result<Lockfile<'static>, FileError> {
let mut text = std::mem::take(&mut buffers.text);
text.clear();
let _ = text.try_reserve_exact(size_hint.saturating_add(1));
if let Err(e) = read_text(file, path, &mut text) {
text.clear();
buffers.text = text;
return Err(e);
}
let text = unsafe { String::from_utf8_unchecked(text) };
Lockfile::parse_source(Source::Owned(text), buffers, Plan::Auto).map_err(|(e, text)| {
let err = FileError::parse(path, e, text.as_str());
if let Source::Owned(text) = text {
buffers.text = text.into_bytes();
buffers.text.clear();
}
err
})
}
fn read_text(file: &mut File, path: &Path, text: &mut Vec<u8>) -> Result<(), FileError> {
file.read_to_end(text).map_err(|e| FileError::io(path, e))?;
if text.len() > MAX_INPUT_LEN {
return Err(too_large(path));
}
match utf8::validate(text) {
Ok(_) => Ok(()),
Err(e) => Err(invalid_utf8(path, e, text)),
}
}
#[cold]
#[inline(never)]
fn too_large(path: &Path) -> FileError {
FileError::Parse {
path: path.to_owned(),
error: Error::new(ErrorKind::InputTooLarge, 0),
line: 1,
column: 1,
detail: ErrorKind::InputTooLarge.as_str().to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn a_file_just_written_and_read_looks_cached() {
let path = std::env::temp_dir().join(format!("berry-cached-{}.txt", std::process::id()));
let mut f = File::create(&path).unwrap();
let line = "x".repeat(99) + "\n";
for _ in 0..10_000 {
f.write_all(line.as_bytes()).unwrap();
}
drop(f);
let text = std::fs::read(&path).unwrap();
let file = File::open(&path).unwrap();
let map = unsafe { Mmap::map(&file) }.unwrap();
assert_eq!(&map[..], &text[..]);
if cfg!(target_os = "linux") {
assert!(looks_cached(&map));
}
drop(map);
std::fs::remove_file(&path).unwrap();
let _ = looks_cached(&text);
assert_eq!(looks_cached(&[]), cfg!(target_os = "linux"));
let _ = looks_cached(&text[1..2]);
}
}