doom-eternal 0.1.0

Rust CLI for the Xylex DOOM Eternal texture and install workflow
Documentation
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
    }
}