pub mod editorconfig;
pub mod external;
pub mod selection;
pub mod text;
pub mod write;
use std::cell::Cell;
use std::io;
use crate::bootstrap::{Bootstrap, Limits};
use crate::paths::ProjectPath;
use crate::report::{Code, Diagnostic};
use crate::tree::ReadTree;
pub use selection::{Selected, Universe, select};
pub struct Budget {
limits: Limits,
remaining: Cell<u64>,
}
impl Budget {
#[must_use]
pub fn new(limits: &Limits) -> Self {
Self {
limits: *limits,
remaining: Cell::new(limits.hygiene_bytes),
}
}
#[must_use]
pub fn limits(&self) -> &Limits {
&self.limits
}
pub fn read(&self, tree: &dyn ReadTree, path: &ProjectPath) -> Bounded {
let remaining = self.remaining.get();
let budget_bound = remaining <= self.limits.file_bytes;
let limit = self.limits.file_bytes.min(remaining);
let (pulled, over) = match tree.read_bounded(path, limit) {
Ok(read) => read,
Err(error) => return Bounded::Unreadable(error),
};
if let Err(message) = self.charge(path.as_str(), pulled.len() as u64) {
return Bounded::Exhausted(message);
}
match (over, budget_bound) {
(false, _) => Bounded::Within(pulled),
(true, true) => Bounded::Exhausted(self.exhausted(path.as_str())),
(true, false) => Bounded::OverFileLimit,
}
}
pub fn charge(&self, what: &str, bytes: u64) -> Result<(), String> {
match self.remaining.get().checked_sub(bytes) {
Some(left) => {
self.remaining.set(left);
Ok(())
}
None => Err(self.exhausted(what)),
}
}
fn exhausted(&self, what: &str) -> String {
format!(
"hygiene inputs exceed `limits.hygiene_bytes` = {} while reading `{what}`",
self.limits.hygiene_bytes
)
}
}
#[derive(Debug)]
pub enum Bounded {
Within(Vec<u8>),
OverFileLimit,
Exhausted(String),
Unreadable(io::Error),
}
#[derive(Debug)]
pub struct Loaded {
pub selected: Selected,
pub bytes: Vec<u8>,
}
pub fn load(
tree: &dyn ReadTree,
selected: Vec<Selected>,
budget: &Budget,
diagnostics: &mut Vec<Diagnostic>,
) -> Result<Vec<Loaded>, String> {
let limits = budget.limits();
let mut loaded = Vec::new();
for entry in selected {
let path = entry.path.as_str();
match budget.read(tree, &entry.path) {
Bounded::Within(bytes) => loaded.push(Loaded {
selected: entry,
bytes,
}),
Bounded::OverFileLimit => diagnostics.push(Diagnostic::new(
Code::FileUnreadable,
path,
over_limit(tree, &entry.path, "file", limits.file_bytes),
)),
Bounded::Exhausted(message) => return Err(message),
Bounded::Unreadable(error) => diagnostics.push(Diagnostic::new(
Code::FileUnreadable,
path,
format!("cannot read file: {error}"),
)),
}
}
Ok(loaded)
}
pub fn over_limit(tree: &dyn ReadTree, path: &ProjectPath, what: &str, limit: u64) -> String {
match tree.file_len(path) {
Ok(len) if len > limit => {
format!("{what} is {len} bytes, above `limits.file_bytes` = {limit}")
}
_ => format!("{what} exceeds `limits.file_bytes` = {limit}"),
}
}
pub fn check_formatters(
tree: &dyn ReadTree,
loaded: &[Loaded],
decodable: &[bool],
bootstrap: &Bootstrap,
budget: &Budget,
authorized: bool,
diagnostics: &mut Vec<Diagnostic>,
) -> Result<(), String> {
let assigned: Vec<&Loaded> = loaded
.iter()
.zip(decodable)
.filter(|(file, decodable)| file.selected.formatter.is_some() && **decodable)
.map(|(file, _)| file)
.collect();
if assigned.is_empty() {
return Ok(());
}
if !authorized {
return Err(format!(
"bearout.toml declares formatters ({}), which run as trusted host programs; pass --allow-formatters (library: `Options::allow_formatters`) to run them",
bootstrap
.formatters
.iter()
.map(|formatter| format!("`{}`", formatter.name))
.collect::<Vec<_>>()
.join(", ")
));
}
let mut workdirs: Vec<Option<external::Workdir>> = Vec::new();
workdirs.resize_with(bootstrap.formatters.len(), || None);
for file in assigned {
let index = file.selected.formatter.expect("assigned");
let formatter = &bootstrap.formatters[index];
if workdirs[index].is_none() {
workdirs[index] = Some(external::Workdir::prepare(tree, formatter, budget)?);
}
let workdir = workdirs[index].as_ref().expect("prepared");
match external::run(formatter, workdir, &file.selected.path, &file.bytes) {
Ok(output) if output == file.bytes => {}
Ok(_) => diagnostics.push(Diagnostic::new(
Code::FormatDifference,
file.selected.path.as_str(),
format!(
"file differs from the output of formatter `{}`; run `bearout format`",
formatter.name
),
)),
Err(external::Failure::Start(detail)) => {
return Err(format!(
"formatter `{}` cannot start: {detail}",
formatter.name
));
}
Err(failure) => diagnostics.push(Diagnostic::new(
Code::FormatterFailed,
file.selected.path.as_str(),
format!("formatter `{}` {failure}", formatter.name),
)),
}
}
Ok(())
}
pub fn check_text(
tree: &dyn ReadTree,
loaded: &[Loaded],
budget: &Budget,
diagnostics: &mut Vec<Diagnostic>,
) -> Result<Vec<bool>, String> {
let resolver = editorconfig::Resolver::new(tree, budget);
let mut decodable = Vec::with_capacity(loaded.len());
for file in loaded {
match resolver.properties(&file.selected.path) {
Ok(effective) => {
let found = text::check(
file.selected.path.as_str(),
&file.bytes,
file.selected.binary,
effective,
);
decodable.push(!found.iter().any(|d| d.code == Code::Encoding));
diagnostics.extend(found);
}
Err(problems) => {
decodable.push(false);
diagnostics.extend(problems);
}
}
}
diagnostics.extend(resolver.take_diagnostics());
resolver.fatal()?;
Ok(decodable)
}
#[cfg(test)]
mod tests {
use std::io;
use std::sync::Arc;
use super::*;
struct Lying {
content: Vec<u8>,
}
impl ReadTree for Lying {
fn read(&self, _: &ProjectPath) -> io::Result<Vec<u8>> {
Ok(self.content.clone())
}
fn read_bounded(&self, _: &ProjectPath, limit: u64) -> io::Result<(Vec<u8>, bool)> {
let probe = usize::try_from(limit)
.unwrap_or(usize::MAX)
.saturating_add(1);
let pulled = self.content[..self.content.len().min(probe)].to_vec();
let over = pulled.len() > usize::try_from(limit).unwrap_or(usize::MAX);
Ok((pulled, over))
}
fn file_len(&self, _: &ProjectPath) -> io::Result<u64> {
Ok(5)
}
fn is_file(&self, _: &ProjectPath) -> bool {
true
}
fn is_dir(&self, path: &ProjectPath) -> bool {
path.as_str().is_empty()
}
fn exists(&self, _: &ProjectPath) -> bool {
true
}
fn symlink_component(&self, _: &ProjectPath) -> io::Result<Option<ProjectPath>> {
Ok(None)
}
fn walk(&self, _: &ProjectPath) -> io::Result<Vec<ProjectPath>> {
Ok(Vec::new())
}
fn subtree(&self, _: &ProjectPath) -> io::Result<Arc<dyn ReadTree>> {
Err(io::Error::other("no subtrees"))
}
}
fn selected(path: &str) -> Selected {
Selected {
path: ProjectPath::parse(path).unwrap(),
binary: None,
formatter: None,
}
}
#[test]
fn limits_apply_to_the_bytes_actually_read_not_the_reported_length() {
let tree = Lying {
content: vec![b'x'; 1000],
};
let limits = Limits {
file_bytes: 100,
..Limits::default()
};
let budget = Budget::new(&limits);
let mut diagnostics = Vec::new();
let loaded = load(&tree, vec![selected("a.txt")], &budget, &mut diagnostics).unwrap();
assert!(
loaded.is_empty(),
"the file is over the limit despite claiming 5 bytes"
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].code, Code::FileUnreadable);
assert!(
diagnostics[0]
.message
.contains("exceeds `limits.file_bytes` = 100"),
"{}",
diagnostics[0].message
);
let budget_after = Budget::new(&limits);
let mut diagnostics = Vec::new();
load(
&tree,
vec![selected("a.txt")],
&budget_after,
&mut diagnostics,
)
.unwrap();
assert_eq!(
budget_after.remaining.get(),
limits.hygiene_bytes - 101,
"the overflow probe counts toward the budget"
);
let limits = Limits {
file_bytes: 2_000,
hygiene_bytes: 1_500,
..Limits::default()
};
let budget = Budget::new(&limits);
let mut diagnostics = Vec::new();
let error = load(
&tree,
vec![selected("a.txt"), selected("b.txt")],
&budget,
&mut diagnostics,
)
.unwrap_err();
assert!(
error.contains("`limits.hygiene_bytes` = 1500 while reading `b.txt`"),
"{error}"
);
}
}