use std::borrow::Cow;
use std::collections::{BTreeSet, VecDeque};
use std::path::{Path, PathBuf};
use crate::resolution::capacity::admits_one_more;
use crate::resolution::identity::index_of;
use super::limits::GoResolutionLimits;
use super::paths;
use super::snapshot_error::GoSnapshotError;
const MANIFEST: &str = "go.mod";
const SOURCE_EXTENSION: &str = ".go";
const RESERVED_DIRECTORIES: &[&str] = &["testdata", "vendor"];
type AdmittedEntries = (Box<[PathBuf]>, Box<[PathBuf]>);
pub(super) struct GoPackageDirectory {
pub(super) canonical: PathBuf,
pub(super) within_module: Box<str>,
pub(super) sources: Box<[PathBuf]>,
}
pub(super) struct EntryBudget {
limits: GoResolutionLimits,
visited: u32,
}
impl EntryBudget {
pub(super) fn new(limits: GoResolutionLimits) -> Self {
Self { limits, visited: 0 }
}
fn spend(&mut self) -> Result<(), GoSnapshotError> {
match admits_one_more(index_of(self.visited), self.limits.max_directory_entries) {
false => Err(GoSnapshotError::DirectoryEntryLimitExceeded {
limit: self.limits.max_directory_entries,
}),
true => {
self.visited = self.visited.saturating_add(1);
Ok(())
}
}
}
}
pub(super) fn package_directories(
root: &Path,
module: &Path,
budget: &mut EntryBudget,
) -> Result<Box<[GoPackageDirectory]>, GoSnapshotError> {
let mut walk = PackageWalk {
root,
module,
budget,
walked: BTreeSet::from([module.to_path_buf()]),
pending: VecDeque::from([module.to_path_buf()]),
found: Vec::new(),
};
while let Some(directory) = walk.pending.pop_front() {
walk.descend(&directory)?;
}
Ok(walk.found.into_boxed_slice())
}
struct PackageWalk<'a> {
root: &'a Path,
module: &'a Path,
budget: &'a mut EntryBudget,
walked: BTreeSet<PathBuf>,
pending: VecDeque<PathBuf>,
found: Vec<GoPackageDirectory>,
}
impl PackageWalk<'_> {
fn descend(&mut self, directory: &Path) -> Result<(), GoSnapshotError> {
let (sources, subdirectories) = self.listing(directory)?;
self.retain(directory, sources)?;
self.pending.extend(subdirectories.into_vec());
Ok(())
}
fn retain(&mut self, directory: &Path, sources: Box<[PathBuf]>) -> Result<(), GoSnapshotError> {
let Some(sources) = (!sources.is_empty()).then_some(sources) else {
return Ok(());
};
self.found.push(GoPackageDirectory {
within_module: paths::relative_text(self.module, directory)?,
canonical: directory.to_path_buf(),
sources,
});
Ok(())
}
fn listing(&mut self, directory: &Path) -> Result<AdmittedEntries, GoSnapshotError> {
let read = std::fs::read_dir(directory)
.map_err(|source| directory_error(self.root, directory, source))?;
let mut entries: Vec<PathBuf> = Vec::new();
for entry in read {
self.budget.spend()?;
let held = entry.map_err(|source| directory_error(self.root, directory, source))?;
entries.push(held.path());
}
entries.sort();
self.admitted(&entries)
}
fn admitted(&mut self, entries: &[PathBuf]) -> Result<AdmittedEntries, GoSnapshotError> {
let mut sources: Vec<PathBuf> = Vec::new();
let mut subdirectories: Vec<PathBuf> = Vec::new();
let named = entries
.iter()
.map(|entry| (entry, entry_name(entry)))
.filter(|(_, name)| !is_excluded_name(name));
for (entry, name) in named {
let held = paths::canonical_in_root(self.root, entry)?;
self.sort_entry(held, &name, (&mut sources, &mut subdirectories));
}
Ok((
sources.into_boxed_slice(),
subdirectories.into_boxed_slice(),
))
}
fn sort_entry(
&mut self,
canonical: Option<PathBuf>,
name: &str,
admitted: (&mut Vec<PathBuf>, &mut Vec<PathBuf>),
) {
let (sources, subdirectories) = admitted;
let Some(canonical) = canonical else {
return;
};
match (canonical.is_dir(), name.ends_with(SOURCE_EXTENSION)) {
(true, _) => subdirectories.extend(self.walkable(canonical)),
(false, true) => sources.push(canonical),
(false, false) => (),
}
}
fn walkable(&mut self, canonical: PathBuf) -> Option<PathBuf> {
match holds_manifest(&canonical) || self.walked.contains(&canonical) {
true => None,
false => {
self.walked.insert(canonical.clone());
Some(canonical)
}
}
}
}
fn is_ignored_name(name: &str) -> bool {
name.starts_with('.') || name.starts_with('_')
}
fn is_excluded_name(name: &str) -> bool {
is_ignored_name(name) || RESERVED_DIRECTORIES.contains(&name)
}
fn holds_manifest(directory: &Path) -> bool {
directory.join(MANIFEST).is_file()
}
fn entry_name(entry: &Path) -> Cow<'_, str> {
entry
.file_name()
.map(std::ffi::OsStr::to_string_lossy)
.unwrap_or(Cow::Borrowed(""))
}
fn directory_error(root: &Path, directory: &Path, source: std::io::Error) -> GoSnapshotError {
GoSnapshotError::DirectoryRead {
path: paths::relative_text(root, directory).unwrap_or_else(|_| paths::path_text(directory)),
source,
}
}