use std::borrow::Cow;
use std::collections::{BTreeSet, VecDeque};
use std::fs::FileType;
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 SOURCE_EXTENSION: &str = ".go";
const RESERVED_DIRECTORIES: &[&str] = &["testdata", "vendor"];
type AdmittedEntries = (Box<[PathBuf]>, Box<[PathBuf]>);
struct ListedEntry {
path: PathBuf,
file_type: FileType,
}
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> {
match sources.is_empty() {
true => Ok(()),
false => {
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<ListedEntry> = Vec::new();
for entry in read {
self.budget.spend()?;
let held = entry.map_err(|source| directory_error(self.root, directory, source))?;
let file_type = held
.file_type()
.map_err(|source| directory_error(self.root, directory, source))?;
entries.push(ListedEntry {
path: held.path(),
file_type,
});
}
entries.sort_by(|left, right| left.path.cmp(&right.path));
self.admitted(&entries)
}
fn admitted(&mut self, entries: &[ListedEntry]) -> 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.path)))
.filter(|(_, name)| !is_excluded_name(name));
for (entry, name) in named {
self.sort_entry(entry, &name, (&mut sources, &mut subdirectories))?;
}
Ok((
sources.into_boxed_slice(),
subdirectories.into_boxed_slice(),
))
}
fn sort_entry(
&mut self,
entry: &ListedEntry,
name: &str,
admitted: (&mut Vec<PathBuf>, &mut Vec<PathBuf>),
) -> Result<(), GoSnapshotError> {
let (sources, subdirectories) = admitted;
match (entry.file_type.is_dir(), entry.file_type.is_file()) {
(true, _) => {
subdirectories.extend(self.walkable(entry.path.clone())?);
Ok(())
}
(false, true) => {
sources.extend(is_source_name(name).then(|| entry.path.clone()));
Ok(())
}
(false, false) => self.sort_link(entry, name, (sources, subdirectories)),
}
}
fn sort_link(
&mut self,
entry: &ListedEntry,
name: &str,
admitted: (&mut Vec<PathBuf>, &mut Vec<PathBuf>),
) -> Result<(), GoSnapshotError> {
let (sources, subdirectories) = admitted;
let Some(canonical) = paths::canonical_in_root(self.root, &entry.path)? else {
return Ok(());
};
match (canonical.is_dir(), is_source_name(name)) {
(true, _) => subdirectories.extend(self.walkable(canonical)?),
(false, true) => sources.push(canonical),
(false, false) => (),
}
Ok(())
}
fn walkable(&mut self, canonical: PathBuf) -> Result<Option<PathBuf>, GoSnapshotError> {
match holds_manifest(self.root, &canonical)? || self.walked.contains(&canonical) {
true => Ok(None),
false => {
self.walked.insert(canonical.clone());
Ok(Some(canonical))
}
}
}
}
fn is_source_name(name: &str) -> bool {
name.ends_with(SOURCE_EXTENSION)
}
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(root: &Path, directory: &Path) -> Result<bool, GoSnapshotError> {
match paths::confined_manifest(root, directory)? {
None => Ok(false),
Some(manifest) if manifest.is_regular() => Ok(true),
Some(manifest) => Err(GoSnapshotError::PathRead {
path: paths::path_text(manifest.path()),
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"go.mod is not a regular 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,
}
}