lune_utils/path/
std.rs

1/*!
2    Utilities for working with Rust standard library paths.
3*/
4
5use std::{
6    env::{current_dir, current_exe},
7    ffi::OsStr,
8    path::{Component, MAIN_SEPARATOR, Path, PathBuf},
9    sync::{Arc, LazyLock},
10};
11
12use path_clean::PathClean;
13
14static CWD: LazyLock<Arc<Path>> = LazyLock::new(create_cwd);
15static EXE: LazyLock<Arc<Path>> = LazyLock::new(create_exe);
16
17fn create_cwd() -> Arc<Path> {
18    let mut cwd = current_dir()
19        .expect("failed to find current working directory")
20        .to_str()
21        .expect("current working directory is not valid UTF-8")
22        .to_string();
23    if !cwd.ends_with(MAIN_SEPARATOR) {
24        cwd.push(MAIN_SEPARATOR);
25    }
26    dunce::canonicalize(cwd)
27        .expect("failed to canonicalize current working directory")
28        .into()
29}
30
31fn create_exe() -> Arc<Path> {
32    let exe = current_exe()
33        .expect("failed to find current executable")
34        .to_str()
35        .expect("current executable path is not valid UTF-8")
36        .to_string();
37    dunce::canonicalize(exe)
38        .expect("failed to canonicalize current executable path")
39        .into()
40}
41
42/**
43    Gets the current working directory as an absolute path.
44
45    This absolute path is canonicalized and does not contain any `.` or `..`
46    components, and it is also in a friendly (non-UNC) format.
47
48    This path is also guaranteed to:
49
50    - Be valid UTF-8.
51    - End with the platform's main path separator.
52*/
53#[must_use]
54pub fn get_current_dir() -> Arc<Path> {
55    Arc::clone(&CWD)
56}
57
58/**
59    Gets the path to the current executable as an absolute path.
60
61    This absolute path is canonicalized and does not contain any `.` or `..`
62    components, and it is also in a friendly (non-UNC) format.
63
64    This path is also guaranteed to:
65
66    - Be valid UTF-8.
67*/
68#[must_use]
69pub fn get_current_exe() -> Arc<Path> {
70    Arc::clone(&EXE)
71}
72
73/**
74    Cleans a path.
75
76    See the [`path_clean`] crate for more information on what cleaning a path does.
77*/
78#[must_use]
79pub fn clean_path(path: impl AsRef<Path>) -> PathBuf {
80    path.as_ref().clean()
81}
82
83/**
84    Makes a path absolute, if it is relative, and then cleans it.
85
86    Relative paths are resolved against the current working directory.
87
88    See the [`path_clean`] crate for more information on what cleaning a path does.
89*/
90#[must_use]
91pub fn clean_path_and_make_absolute(path: impl AsRef<Path>) -> PathBuf {
92    let path = path.as_ref();
93    if path.is_relative() {
94        CWD.join(path).clean()
95    } else {
96        path.clean()
97    }
98}
99
100/**
101    Appends the given extension to the path.
102
103    Does not replace or modify any existing extension(s).
104*/
105#[must_use]
106pub fn append_extension(path: impl AsRef<Path>, ext: impl AsRef<OsStr>) -> PathBuf {
107    let path = path.as_ref();
108    match path.extension() {
109        None => path.with_extension(ext),
110        Some(curr_ext) => {
111            let mut new_ext = curr_ext.to_os_string();
112            new_ext.push(".");
113            new_ext.push(ext);
114            path.with_extension(new_ext)
115        }
116    }
117}
118
119/**
120    Normalizes the given relative path.
121
122    This will clean the path, removing any redundant components,
123    and ensure that it has a leading "current dir" (`./`) component.
124*/
125#[must_use]
126pub fn relative_path_normalize(path: impl AsRef<Path>) -> PathBuf {
127    let path = clean_path(path);
128
129    let mut it = path.components().peekable();
130    if it.peek().is_none_or(|c| matches!(c, Component::Normal(..))) {
131        std::iter::once(Component::CurDir).chain(it).collect()
132    } else {
133        path
134    }
135}
136
137/**
138    Pops the relative path up to the parent directory, pushing "parent dir" (`..`)
139    components to the front of the path when it no longer has any normal components.
140
141    This means that unlike [`PathBuf::pop`], this function may be called an arbitrary
142    number of times, and represent parent folders without first canonicalizing paths.
143*/
144pub fn relative_path_parent(rel: &mut PathBuf) {
145    if rel.as_os_str() == Component::CurDir.as_os_str() {
146        *rel = PathBuf::from(Component::ParentDir.as_os_str());
147    } else if rel.components().all(|c| c == Component::ParentDir) {
148        rel.push(Component::ParentDir.as_os_str());
149    } else {
150        rel.pop();
151    }
152}