use std::{
env,
ffi::OsString,
path::{Path, PathBuf},
};
use crate::error::Result;
const REPO_MARKERS: &[&str] = &[
"config/rtx-pack-logo-placements.json",
"mod/EternalMod.json",
"assets/source/logos",
];
#[derive(Debug, Clone)]
pub struct RepoContext {
root: PathBuf,
}
impl RepoContext {
pub fn discover() -> Self {
let anchor = env::current_exe().unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR")));
Self {
root: resolve_repo_root(anchor),
}
}
pub fn from_anchor(anchor: impl AsRef<Path>) -> Self {
Self {
root: resolve_repo_root(anchor),
}
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn repo_path(&self, path: impl AsRef<Path>) -> PathBuf {
let candidate = expand_home(path.as_ref());
if candidate.is_absolute() {
candidate
} else {
self.root.join(candidate)
}
}
pub fn normalize_path(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
normalize_path(&self.repo_path(path))
}
}
pub fn resolve_repo_root(anchor: impl AsRef<Path>) -> PathBuf {
let anchor = anchor.as_ref();
let search_root = if anchor.is_dir() {
anchor.to_path_buf()
} else {
anchor.parent().unwrap_or(anchor).to_path_buf()
};
let mut current = Some(search_root.as_path());
while let Some(candidate) = current {
if is_repo_root(candidate) {
return candidate.to_path_buf();
}
current = candidate.parent();
}
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")))
}
pub fn normalize_path(path: impl AsRef<Path>) -> Result<PathBuf> {
let path = expand_home(path.as_ref());
if path.exists() {
let canonical = path.canonicalize().unwrap_or(path);
return Ok(strip_windows_verbatim_prefix(canonical));
}
if path.is_absolute() {
return Ok(strip_windows_verbatim_prefix(path));
}
Ok(strip_windows_verbatim_prefix(env::current_dir()?.join(path)))
}
pub fn is_repo_root(path: &Path) -> bool {
REPO_MARKERS.iter().all(|marker| path.join(marker).exists())
}
pub fn is_within(path: impl AsRef<Path>, root: impl AsRef<Path>) -> bool {
let path_ref = path.as_ref();
let root_ref = root.as_ref();
let path = normalize_path(path_ref).unwrap_or_else(|_| path_ref.to_path_buf());
let root = normalize_path(root_ref).unwrap_or_else(|_| root_ref.to_path_buf());
path.starts_with(root)
}
pub fn expand_home(path: &Path) -> PathBuf {
let mut components = path.components();
match components.next() {
Some(std::path::Component::Normal(first)) if first == "~" => {
if let Some(home) = home_dir() {
let mut expanded = home;
for component in components {
expanded.push(component.as_os_str());
}
expanded
} else {
path.to_path_buf()
}
}
_ => path.to_path_buf(),
}
}
pub fn home_dir() -> Option<PathBuf> {
env::var_os("USERPROFILE")
.or_else(|| env::var_os("HOME"))
.map(PathBuf::from)
}
pub fn file_name_string(path: &Path) -> Option<String> {
path.file_name()
.map(OsString::from)
.map(|value| value.to_string_lossy().to_string())
}
fn strip_windows_verbatim_prefix(path: PathBuf) -> PathBuf {
let value = path.display().to_string();
if let Some(stripped) = value.strip_prefix(r"\\?\") {
PathBuf::from(stripped)
} else {
path
}
}