use super::VFS;
use crate::{
CollapseOptions, NormalizedPath, SourceContributionReport, SourceKind, SourceMeta,
normalize_host_path,
paths::{key_to_path_buf_bytes, key_to_path_buf_lossy, key_to_string_lossy},
};
use ahash::{AHashMap, AHashSet};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct VfsProviderRecord {
pub source_index: usize,
pub source: SourceMeta,
pub key: PathBuf,
pub original_path: PathBuf,
pub resolved_path: String,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct ExplainReport {
pub key: PathBuf,
pub winner: VfsProviderRecord,
pub overridden: Vec<VfsProviderRecord>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct DuplicateEntry {
pub key: PathBuf,
pub providers: Vec<VfsProviderRecord>,
pub winner_index: usize,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct DuplicateReport {
pub entries: Vec<DuplicateEntry>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct ArchiveInfo {
pub source_index: usize,
pub path: PathBuf,
pub entry_count: usize,
pub winning_entry_count: usize,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct ArchiveEntry {
pub key: PathBuf,
pub archive_path: PathBuf,
pub original_path: PathBuf,
pub wins: bool,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serialize", serde(rename_all = "snake_case"))]
pub enum MaterializationAction {
Hardlink {
key: PathBuf,
source: PathBuf,
dest: PathBuf,
},
Symlink {
key: PathBuf,
source: PathBuf,
dest: PathBuf,
},
Copy {
key: PathBuf,
source: PathBuf,
dest: PathBuf,
},
ExtractArchive {
key: PathBuf,
archive: PathBuf,
dest: PathBuf,
},
SkipArchiveFile {
key: PathBuf,
archive: PathBuf,
},
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serialize", serde(rename_all = "snake_case"))]
pub enum MaterializationIssue {
MissingLooseSource {
key: PathBuf,
source: PathBuf,
},
FileDirectoryConflict {
key: PathBuf,
dest: PathBuf,
},
UnsafeDestination {
key: PathBuf,
dest: PathBuf,
},
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct MaterializationPlan {
pub actions: Vec<MaterializationAction>,
pub issues: Vec<MaterializationIssue>,
}
impl VFS {
fn provider_record_from_entry(
key: &NormalizedPath,
entry: &super::ProviderEntry,
) -> VfsProviderRecord {
let key_path = key_to_path_buf_lossy(key);
let source = entry.provider.source.clone();
let original_path = VFS::provider_original_path(&source, key, &entry.provider.file);
let resolved_path = if source.kind == SourceKind::Archive {
format!("{}::{}", source.path.display(), original_path.display())
} else {
source.path.join(&original_path).display().to_string()
};
VfsProviderRecord {
source_index: entry.source_index,
source,
key: key_path,
original_path,
resolved_path,
}
}
fn provider_records_for_key(&self, key: &NormalizedPath) -> Vec<VfsProviderRecord> {
self.providers.get(key).map_or_else(Vec::new, |entries| {
entries
.iter()
.map(|entry| Self::provider_record_from_entry(key, entry))
.collect()
})
}
#[must_use]
pub fn provider_records_for<K: crate::VfsKeyInput + ?Sized>(
&self,
path: &K,
) -> Vec<VfsProviderRecord> {
let key = path.to_vfs_key();
self.provider_records_for_key(&key)
}
#[must_use]
pub fn explain<K: crate::VfsKeyInput + ?Sized>(&self, path: &K) -> Option<ExplainReport> {
let key = path.to_vfs_key();
let mut providers = self.provider_records_for_key(&key);
let winner = providers.pop()?;
Some(ExplainReport {
key: key_to_path_buf_lossy(&key),
winner,
overridden: providers,
})
}
#[must_use]
pub fn duplicates(&self) -> DuplicateReport {
self.duplicates_matching_key(|_| true)
}
pub fn duplicates_matching_regex(
&self,
pattern: &str,
) -> std::result::Result<DuplicateReport, regex::Error> {
let re = regex::RegexBuilder::new(pattern)
.case_insensitive(true)
.build()?;
Ok(self.duplicates_matching_key(|key| re.is_match(&key_to_string_lossy(key))))
}
fn duplicates_matching_key(
&self,
matches_key: impl Fn(&NormalizedPath) -> bool,
) -> DuplicateReport {
let mut entries: Vec<_> = self
.providers
.keys()
.filter(|key| matches_key(key))
.cloned()
.filter_map(|key| {
let providers = self.provider_records_for_key(&key);
(providers.len() > 1).then(|| DuplicateEntry {
key: key_to_path_buf_lossy(&key),
winner_index: providers.len() - 1,
providers,
})
})
.collect();
entries.sort_by(|a, b| a.key.cmp(&b.key));
DuplicateReport { entries }
}
#[must_use]
pub fn archives(&self) -> Vec<ArchiveInfo> {
let mut counts: AHashMap<usize, (usize, usize)> = AHashMap::new();
for providers in self.providers.values() {
let Some(winner_index) = providers.len().checked_sub(1) else {
continue;
};
for (provider_index, entry) in providers
.iter()
.enumerate()
.filter(|(_, entry)| entry.provider.source.kind == SourceKind::Archive)
{
let counts = counts.entry(entry.source_index).or_default();
counts.0 += 1;
if provider_index == winner_index {
counts.1 += 1;
}
}
}
let mut archives: Vec<_> = self
.sources
.iter()
.enumerate()
.filter(|(_, source)| source.kind == SourceKind::Archive)
.map(|(source_index, source)| {
let (entry_count, winning_entry_count) =
counts.get(&source_index).copied().unwrap_or_default();
ArchiveInfo {
source_index,
path: source.path.clone(),
entry_count,
winning_entry_count,
}
})
.collect();
archives.sort_by(|a, b| a.path.cmp(&b.path));
archives
}
#[must_use]
pub fn archive_entries(&self, archive: impl AsRef<Path>) -> Vec<ArchiveEntry> {
let archive = normalize_host_path(archive.as_ref()).into_owned();
let matching_sources: AHashSet<_> = self
.sources
.iter()
.enumerate()
.filter_map(|(source_index, source)| {
(source.kind == SourceKind::Archive
&& normalize_host_path(&source.path).as_ref() == archive.as_path())
.then_some(source_index)
})
.collect();
let mut entries = Vec::new();
for (key, providers) in &self.providers {
let Some(winner_index) = providers.len().checked_sub(1) else {
continue;
};
for (provider_index, entry) in providers
.iter()
.enumerate()
.filter(|(_, entry)| matching_sources.contains(&entry.source_index))
{
let original_path =
VFS::provider_original_path(&entry.provider.source, key, &entry.provider.file);
entries.push(ArchiveEntry {
key: key_to_path_buf_lossy(key),
archive_path: entry.provider.source.path.clone(),
original_path,
wins: provider_index == winner_index,
});
}
}
entries.sort_by(|a, b| {
a.key
.cmp(&b.key)
.then_with(|| a.archive_path.cmp(&b.archive_path))
.then_with(|| a.original_path.cmp(&b.original_path))
.then_with(|| a.wins.cmp(&b.wins))
});
entries
}
#[must_use]
pub fn files_from_archive(&self, archive: impl AsRef<Path>) -> Vec<PathBuf> {
self.archive_entries(archive)
.into_iter()
.map(|entry| entry.key)
.collect()
}
#[must_use]
pub fn source_contributions(&self) -> SourceContributionReport {
self.layer_index().source_contributions()
}
#[must_use]
pub fn materialization_plan(
&self,
dest: impl AsRef<Path>,
opts: &CollapseOptions,
) -> MaterializationPlan {
let dest = dest.as_ref();
let mut actions = Vec::new();
let mut issues = Vec::new();
let mut keys: Vec<_> = self.file_map.keys().cloned().collect();
keys.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
for key in keys {
let file = &self.file_map[&key];
let Some(key_path) = key_to_path_buf_bytes(&key) else {
let display_key = key_to_path_buf_lossy(&key);
issues.push(MaterializationIssue::UnsafeDestination {
key: display_key.clone(),
dest: dest.join(display_key),
});
continue;
};
let target = dest.join(&key_path);
if Self::ensure_output_parent_safe(dest, &target).is_err() {
issues.push(MaterializationIssue::UnsafeDestination {
key: key_path.clone(),
dest: target.clone(),
});
continue;
}
if file.is_loose() {
if !file.path().exists() {
issues.push(MaterializationIssue::MissingLooseSource {
key: key_path.clone(),
source: file.path().to_path_buf(),
});
continue;
}
if opts.extract_archives && super::VFS::is_archive_file(file) {
actions.push(MaterializationAction::SkipArchiveFile {
key: key_path.clone(),
archive: file.path().to_path_buf(),
});
} else if opts.use_symlinks {
actions.push(MaterializationAction::Symlink {
key: key_path.clone(),
source: file.path().to_path_buf(),
dest: target,
});
} else {
actions.push(MaterializationAction::Hardlink {
key: key_path.clone(),
source: file.path().to_path_buf(),
dest: target,
});
}
} else if opts.extract_archives {
actions.push(MaterializationAction::ExtractArchive {
key: key_path.clone(),
archive: PathBuf::from(file.parent_archive_path().unwrap_or_default()),
dest: target,
});
} else {
actions.push(MaterializationAction::SkipArchiveFile {
key: key_path,
archive: PathBuf::from(file.parent_archive_path().unwrap_or_default()),
});
}
}
MaterializationPlan { actions, issues }
}
}