use crate::{
cancellation::{CancellationFlag, Outcome},
format::CodePath,
};
use ignore::{WalkBuilder, overrides::OverrideBuilder};
use std::{
collections::{HashMap, HashSet},
ffi::OsString,
fs,
path::{Path, PathBuf},
};
pub fn wiki_tree_walker(wiki_directory: &Path) -> Result<WalkBuilder, ignore::Error> {
let mut overrides = OverrideBuilder::new(wiki_directory);
overrides
.add("!.git/")
.expect("The static .git override should be valid.")
.add("!.hg/")
.expect("The static .hg override should be valid.");
let overrides = overrides.build()?;
let mut walker_builder = WalkBuilder::new(wiki_directory);
walker_builder
.current_dir(wiki_directory)
.follow_links(true)
.hidden(false)
.parents(false)
.require_git(false)
.overrides(overrides);
Ok(walker_builder)
}
#[derive(Debug, Eq, PartialEq)]
pub enum Visibility {
Visible,
Empty,
Ignored,
}
pub fn visibility(
wiki_directory: &Path,
path: &Path,
cancellation: &CancellationFlag,
) -> Outcome<Result<Visibility, ignore::Error>> {
let target = wiki_directory.join(path);
let mut walker_builder = match wiki_tree_walker(wiki_directory) {
Ok(walker_builder) => walker_builder,
Err(error) => return Outcome::Completed(Err(error)),
};
walker_builder.filter_entry({
let target = target.clone();
move |entry| target.starts_with(entry.path()) || entry.path().starts_with(&target)
});
let mut reached = false;
for entry in walker_builder.build().flatten() {
if cancellation.is_cancelled() {
return Outcome::Cancelled;
}
if entry.path() == target {
reached = true;
}
if entry.path().starts_with(&target) && entry.file_type().is_some_and(|t| t.is_file()) {
return Outcome::Completed(Ok(Visibility::Visible));
}
}
Outcome::Completed(Ok(if reached {
Visibility::Empty
} else {
Visibility::Ignored
}))
}
pub type DirectoryListings = HashMap<PathBuf, Option<HashSet<OsString>>>;
pub fn check_spelling(
wiki_directory: &Path,
path: &Path,
listings: &mut DirectoryListings,
) -> (PathBuf, Option<String>) {
let mut written = PathBuf::new();
let mut spelled = PathBuf::new();
let mut misspelled = false;
let mut unmatched = None;
for component in path.components() {
let name = component.as_os_str();
written.push(name);
let directory = wiki_directory.join(&spelled);
let names = listings.entry(directory.clone()).or_insert_with(|| {
fs::read_dir(&directory).ok().map(|entries| {
entries
.flatten()
.map(|entry| fs::DirEntry::file_name(&entry))
.collect()
})
});
let Some(names) = names else {
spelled.push(name);
continue;
};
if names.contains(name) {
spelled.push(name);
continue;
}
let identity = entry_identity(&directory.join(name));
let mut candidates = names
.iter()
.filter(|candidate| {
identity.is_some() && entry_identity(&directory.join(candidate)) == identity
})
.collect::<Vec<_>>();
candidates.sort();
let actual = candidates.first().map(|candidate| (*candidate).clone());
misspelled = true;
if actual.is_none() && unmatched.is_none() {
unmatched = Some(written.clone());
}
spelled.push(actual.as_deref().unwrap_or(name));
}
let message = match unmatched {
Some(unmatched) => Some(format!(
"{} doesn't match the spelling of any name on disk.",
unmatched.code_path(),
)),
None => misspelled.then(|| {
format!(
"{} is spelled {} on disk.",
path.code_path(),
spelled.code_path(),
)
}),
};
(spelled, message)
}
#[cfg(unix)]
pub fn entry_identity(path: &Path) -> Option<(u64, u64)> {
use std::os::unix::fs::MetadataExt;
fs::symlink_metadata(path)
.ok()
.map(|metadata| (metadata.dev(), metadata.ino()))
}
#[cfg(not(unix))]
pub fn entry_identity(path: &Path) -> Option<PathBuf> {
fs::canonicalize(path).ok()
}