use crate::{format::CodePath, parser::LinkPath, path_util::relative_path};
use colored::ColoredString;
use std::{
collections::{HashMap, HashSet},
ffi::{OsStr, OsString},
fs, io,
path::{Path, PathBuf},
rc::Rc,
};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DiskPath(PathBuf);
impl DiskPath {
pub fn as_path(&self) -> &Path {
&self.0
}
pub fn is_wiki_directory(&self) -> bool {
self.0.as_os_str().is_empty()
}
pub fn starts_with(&self, other: &DiskPath) -> bool {
self.0.starts_with(&other.0)
}
pub fn parent(&self) -> Option<DiskPath> {
self.0.parent().map(|parent| DiskPath(parent.to_owned()))
}
pub fn file_name(&self) -> Option<&OsStr> {
self.0.file_name()
}
}
impl CodePath for DiskPath {
fn code_path(&self) -> ColoredString {
self.0.code_path()
}
}
#[derive(Debug)]
pub struct SpellingError {
pub message: String,
pub spelled: Option<DiskPath>,
pub reason: Option<Rc<io::Error>>,
}
#[derive(Clone, Debug)]
pub struct WikiDirectory {
path: PathBuf,
wiki_path: DiskPath,
}
impl WikiDirectory {
pub fn new(wiki_path: &Path) -> Self {
let path = wiki_path
.parent()
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
.to_owned();
let relative_wiki_path = relative_path(&path, wiki_path);
let wiki_path =
match check_spelling(&path, relative_wiki_path, &mut DirectoryListings::new()) {
Ok(spelled) => DiskPath(spelled),
Err(SpellingError {
spelled: Some(spelled),
..
}) => spelled,
Err(SpellingError { spelled: None, .. }) => DiskPath(relative_wiki_path.to_owned()),
};
Self { path, wiki_path }
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn wiki_path(&self) -> &DiskPath {
&self.wiki_path
}
pub fn resolve(&self, path: &DiskPath) -> PathBuf {
self.path.join(&path.0)
}
pub fn entry_path(&self, entry: &ignore::DirEntry) -> DiskPath {
DiskPath(
entry
.path()
.strip_prefix(&self.path)
.expect("A walk of the wiki directory should only find entries within it.")
.to_owned(),
)
}
pub fn spell(
&self,
path: &LinkPath,
listings: &mut DirectoryListings,
) -> Result<DiskPath, SpellingError> {
check_spelling(&self.path, path.as_path(), listings).map(DiskPath)
}
pub fn spell_destination(&self, path: &LinkPath) -> Result<DiskPath, SpellingError> {
let path = path.as_path();
let Some(ancestor) = path
.ancestors()
.skip(1)
.find(|ancestor| self.path.join(ancestor).exists())
else {
return Ok(DiskPath(path.to_owned()));
};
let spelled = check_spelling(&self.path, ancestor, &mut DirectoryListings::new())?;
Ok(DiskPath(spelled.join(path.strip_prefix(ancestor).expect(
"An ancestor of a path should be a prefix of it.",
))))
}
}
pub type DirectoryListings = HashMap<PathBuf, Result<HashSet<OsString>, Rc<io::Error>>>;
fn check_spelling(
wiki_directory: &Path,
path: &Path,
listings: &mut DirectoryListings,
) -> Result<PathBuf, SpellingError> {
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 = match listings.entry(directory.clone()).or_insert_with(|| {
fs::read_dir(&directory)
.map(|entries| {
entries
.flatten()
.map(|entry| fs::DirEntry::file_name(&entry))
.collect()
})
.map_err(Rc::new)
}) {
Ok(names) => names,
Err(error) => {
return Err(SpellingError {
message: format!(
"Unable to list {}, so the spelling of {} can't be checked.",
if spelled.as_os_str().is_empty() {
"the wiki directory".to_owned()
} else {
spelled.code_path().to_string()
},
path.code_path(),
),
spelled: None,
reason: Some(error.clone()),
});
}
};
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));
}
match unmatched {
Some(unmatched) => Err(SpellingError {
message: format!(
"{} doesn't match the spelling of any name on disk.",
unmatched.code_path(),
),
spelled: None,
reason: None,
}),
None if misspelled => Err(SpellingError {
message: format!(
"{} is spelled {} on disk.",
path.code_path(),
spelled.code_path(),
),
spelled: Some(DiskPath(spelled)),
reason: None,
}),
None => Ok(spelled),
}
}
#[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()
}