erg_common/
pathutil.rs

1use std::borrow::Borrow;
2use std::ffi::OsStr;
3use std::fmt;
4use std::ops::Deref;
5use std::path::{Component, Path, PathBuf};
6
7use crate::consts::PYTHON_MODE;
8use crate::env::erg_pkgs_path;
9use crate::traits::Immutable;
10use crate::vfs::VFS;
11use crate::{cheap_canonicalize_path, normalize_path, Str};
12
13/// Guaranteed equivalence path.
14///
15/// `PathBuf` may give false equivalence decisions in non-case-sensitive file systems.
16/// Use this for dictionary keys, etc.
17/// See also: `els::util::NormalizedUrl`
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
19pub struct NormalizedPathBuf(PathBuf);
20
21impl Immutable for NormalizedPathBuf {}
22
23impl fmt::Display for NormalizedPathBuf {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(f, "{}", self.display())
26    }
27}
28
29impl<P: Into<PathBuf>> From<P> for NormalizedPathBuf {
30    fn from(path: P) -> Self {
31        NormalizedPathBuf::new(path.into())
32    }
33}
34
35impl AsRef<Path> for NormalizedPathBuf {
36    fn as_ref(&self) -> &Path {
37        self.0.as_path()
38    }
39}
40
41impl Borrow<PathBuf> for NormalizedPathBuf {
42    fn borrow(&self) -> &PathBuf {
43        &self.0
44    }
45}
46
47impl Borrow<Path> for NormalizedPathBuf {
48    fn borrow(&self) -> &Path {
49        self.0.as_path()
50    }
51}
52
53impl Deref for NormalizedPathBuf {
54    type Target = Path;
55
56    fn deref(&self) -> &Self::Target {
57        self.0.as_path()
58    }
59}
60
61impl NormalizedPathBuf {
62    pub fn new(path: PathBuf) -> Self {
63        NormalizedPathBuf(normalize_path(cheap_canonicalize_path(&path)))
64    }
65
66    pub fn as_path(&self) -> &Path {
67        self.0.as_path()
68    }
69
70    pub fn to_path_buf(&self) -> PathBuf {
71        self.0.clone()
72    }
73
74    pub fn try_read(&self) -> std::io::Result<String> {
75        VFS.read(self)
76    }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80pub enum DirKind {
81    ErgModule,
82    PyModule,
83    Other,
84    NotDir,
85}
86
87impl From<&Path> for DirKind {
88    fn from(path: &Path) -> Self {
89        let Ok(dir) = path.read_dir() else {
90            return DirKind::NotDir;
91        };
92        for ent in dir {
93            let Ok(ent) = ent else {
94                continue;
95            };
96            if ent.path().file_name() == Some(OsStr::new("__init__.er")) {
97                return DirKind::ErgModule;
98            } else if ent.path().file_name() == Some(OsStr::new("__init__.py")) {
99                return DirKind::PyModule;
100            }
101        }
102        DirKind::Other
103    }
104}
105
106impl DirKind {
107    pub const fn is_erg_module(&self) -> bool {
108        matches!(self, DirKind::ErgModule)
109    }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
113pub enum FileKind {
114    InitEr,
115    InitPy,
116    Er,
117    Py,
118    Other,
119    NotFile,
120}
121
122impl From<&Path> for FileKind {
123    fn from(path: &Path) -> Self {
124        if path.is_file() {
125            match path.file_name() {
126                Some(name) if name == OsStr::new("__init__.er") => FileKind::InitEr,
127                Some(name) if name == OsStr::new("__init__.py") => FileKind::InitPy,
128                Some(name) if name.to_string_lossy().ends_with(".er") => FileKind::Er,
129                Some(name) if name.to_string_lossy().ends_with(".py") => FileKind::Py,
130                _ => FileKind::Other,
131            }
132        } else {
133            FileKind::NotFile
134        }
135    }
136}
137
138impl FileKind {
139    pub const fn is_init_er(&self) -> bool {
140        matches!(self, FileKind::InitEr)
141    }
142    pub const fn is_simple_erg_file(&self) -> bool {
143        matches!(self, FileKind::Er)
144    }
145}
146
147pub fn is_cur_dir<P: AsRef<Path>>(path: P) -> bool {
148    path.as_ref().components().next() == Some(Component::CurDir)
149}
150
151/// ```
152/// # use std::path::{PathBuf};
153/// # use erg_common::pathutil::add_postfix_foreach;
154/// let path = PathBuf::from("erg");
155/// let path = add_postfix_foreach(path, ".d");
156/// assert_eq!(path, PathBuf::from("erg.d"));
157/// let path = PathBuf::from("erg/foo/bar");
158/// let path = add_postfix_foreach(path, ".d");
159/// assert_eq!(path, PathBuf::from("erg.d/foo.d/bar.d"));
160/// ```
161pub fn add_postfix_foreach<P: AsRef<Path>, Q: AsRef<Path>>(path: P, postfix: Q) -> PathBuf {
162    let mut result = PathBuf::new();
163    for c in path.as_ref().components() {
164        match c {
165            Component::Prefix(_) => result.push(c),
166            Component::RootDir => result.push(c),
167            Component::CurDir => result.push(c),
168            Component::ParentDir => result.push(c),
169            Component::Normal(os_str) => {
170                let mut os_string = os_str.to_os_string();
171                os_string.push(postfix.as_ref().as_os_str());
172                result.push(PathBuf::from(os_string));
173            }
174        }
175    }
176    result
177}
178
179pub fn remove_postfix_foreach<P: AsRef<Path>>(path: P, extension: &str) -> PathBuf {
180    let mut result = PathBuf::new();
181    for c in path.as_ref().components() {
182        match c {
183            Component::Prefix(_) => result.push(c),
184            Component::RootDir => result.push(c),
185            Component::CurDir => result.push(c),
186            Component::ParentDir => result.push(c),
187            Component::Normal(os_str) => {
188                let string = os_str.to_string_lossy();
189                result.push(string.trim_end_matches(extension));
190            }
191        }
192    }
193    result
194}
195
196/// cutout the extension from the path, and let file name be the directory name.
197/// ```
198/// # use std::path::{PathBuf};
199/// # use erg_common::pathutil::remove_postfix;
200/// let path = PathBuf::from("erg.d.er");
201/// let path = remove_postfix(path, ".er");
202/// assert_eq!(path, PathBuf::from("erg.d"));
203/// let path = PathBuf::from("erg.d/foo.d/bar.d");
204/// let path = remove_postfix(path, ".d");
205/// assert_eq!(path, PathBuf::from("erg.d/foo.d/bar"));
206pub fn remove_postfix<P: AsRef<Path>>(path: P, extension: &str) -> PathBuf {
207    let string = path.as_ref().to_string_lossy();
208    PathBuf::from(string.trim_end_matches(extension))
209}
210
211///
212/// ```
213/// # use std::path::{PathBuf};
214/// # use erg_common::pathutil::squash;
215/// let path = PathBuf::from("erg/../foo");
216/// let path = squash(path);
217/// assert_eq!(path, PathBuf::from("foo"));
218/// let path = PathBuf::from("erg/./foo");
219/// let path = squash(path);
220/// assert_eq!(path, PathBuf::from("erg/foo"));
221/// ```
222pub fn squash(path: PathBuf) -> PathBuf {
223    let mut result = PathBuf::new();
224    for c in path.components() {
225        match c {
226            Component::Prefix(_) => result.push(c),
227            Component::RootDir => result.push(c),
228            Component::CurDir => {}
229            Component::ParentDir => {
230                result.pop();
231            }
232            Component::Normal(os_str) => {
233                result.push(os_str);
234            }
235        }
236    }
237    result
238}
239
240pub fn remove_verbatim(path: &Path) -> String {
241    path.to_string_lossy().replace("\\\\?\\", "")
242}
243
244/// e.g.
245/// ```txt
246/// http.d/client.d.er -> http.client
247/// $ERG_PATH/pkgs/certified/torch/1.0.0/src/lib.d.er -> torch
248/// $ERG_PATH/pkgs/certified/torch/1.0.0/src/random.d.er -> torch/random
249/// /users/foo/torch/src/lib.d.er -> torch
250/// foo/__pycache__/__init__.d.er -> foo
251/// math.d.er -> math
252/// foo.py -> foo
253/// ```
254/// FIXME: split by `.` instead of `/`
255pub fn mod_name(path: &Path) -> Str {
256    let path = match path.strip_prefix(erg_pkgs_path()) {
257        Ok(path) => {
258            // <namespace>/<mod_root>/<version>/src/<sub>
259            let mod_root = path
260                .components()
261                .nth(1)
262                .unwrap()
263                .as_os_str()
264                .to_string_lossy();
265            let sub = path
266                .components()
267                .skip(4)
268                .map(|c| {
269                    c.as_os_str()
270                        .to_string_lossy()
271                        .trim_end_matches("lib.d.er")
272                        .trim_end_matches(".d.er")
273                        .trim_end_matches(".d")
274                        .trim_end_matches(".py")
275                        .to_string()
276                })
277                .collect::<Vec<_>>()
278                .join("/");
279            return Str::rc(format!("{mod_root}/{sub}").trim_end_matches('/'));
280        }
281        // using local or git path
282        Err(_) if path.display().to_string().split("/src/").count() > 1 => {
283            // <mod_root>/src/<sub>
284            let path = path.display().to_string();
285            let mod_root = path
286                .split("/src/")
287                .next()
288                .unwrap()
289                .split('/')
290                .next_back()
291                .unwrap();
292            let sub = path
293                .split("/src/")
294                .nth(1)
295                .unwrap()
296                .split('/')
297                .map(|c| {
298                    c.trim_end_matches("lib.d.er")
299                        .trim_end_matches(".d.er")
300                        .trim_end_matches(".d")
301                        .trim_end_matches(".py")
302                        .to_string()
303                })
304                .collect::<Vec<_>>()
305                .join("/");
306            return Str::rc(format!("{mod_root}/{sub}").trim_end_matches('/'));
307        }
308        Err(_) => path,
309    };
310    let mut name = path
311        .file_name()
312        .unwrap()
313        .to_string_lossy()
314        .trim_end_matches(".d.er")
315        .trim_end_matches(".py")
316        .to_string();
317    let mut parents = path.components().rev().skip(1);
318    while let Some(parent) = parents.next() {
319        let parent = parent.as_os_str().to_string_lossy();
320        if parent == "__pycache__" {
321            if name == "__init__" {
322                let p = parents
323                    .next()
324                    .unwrap()
325                    .as_os_str()
326                    .to_string_lossy()
327                    .trim_end_matches(".d")
328                    .to_string();
329                name = p;
330            }
331            break;
332        } else if parent.ends_with(".d") {
333            let p = parent.trim_end_matches(".d").to_string();
334            if name == "__init__" {
335                name = p;
336            } else {
337                name = p + "." + &name;
338            }
339        } else {
340            break;
341        }
342    }
343    Str::from(name)
344}
345
346fn erg_project_entry_dir_of(path: &Path) -> Option<PathBuf> {
347    if path.is_dir() && path.join("package.er").exists() {
348        if path.join("src").exists() {
349            return Some(path.join("src"));
350        } else {
351            return Some(path.to_path_buf());
352        }
353    }
354    let mut path = path.to_path_buf();
355    while let Some(parent) = path.parent() {
356        if parent.join("package.er").exists() {
357            if parent.join("src").exists() {
358                return Some(parent.join("src"));
359            } else {
360                return Some(parent.to_path_buf());
361            }
362        }
363        path = parent.to_path_buf();
364    }
365    None
366}
367
368fn py_project_entry_dir_of(path: &Path) -> Option<PathBuf> {
369    let dir_name = path.file_name()?;
370    if path.is_dir() && path.join("pyproject.toml").exists() {
371        if path.join(dir_name).exists() {
372            return Some(path.join(dir_name));
373        } else if path.join("src").join(dir_name).exists() {
374            return Some(path.join("src").join(dir_name));
375        }
376    }
377    let mut path = path.to_path_buf();
378    while let Some(parent) = path.parent() {
379        let dir_name = parent.file_name()?;
380        if parent.join("pyproject.toml").exists() {
381            if parent.join(dir_name).exists() {
382                return Some(parent.join(dir_name));
383            } else if parent.join("src").join(dir_name).exists() {
384                return Some(parent.join("src").join(dir_name));
385            }
386        }
387        path = parent.to_path_buf();
388    }
389    None
390}
391
392pub fn project_entry_dir_of(path: &Path) -> Option<PathBuf> {
393    if PYTHON_MODE {
394        py_project_entry_dir_of(path)
395    } else {
396        erg_project_entry_dir_of(path)
397    }
398}
399
400fn erg_project_entry_file_of(path: &Path) -> Option<PathBuf> {
401    let entry = erg_project_entry_dir_of(path)?;
402    if entry.join("lib.er").exists() {
403        Some(entry.join("lib.er"))
404    } else if entry.join("main.er").exists() {
405        Some(entry.join("main.er"))
406    } else if entry.join("lib.d.er").exists() {
407        Some(entry.join("lib.d.er"))
408    } else {
409        None
410    }
411}
412
413fn py_project_entry_file_of(path: &Path) -> Option<PathBuf> {
414    let entry = py_project_entry_dir_of(path)?;
415    if entry.join("__init__.py").exists() {
416        Some(entry.join("__init__.py"))
417    } else {
418        None
419    }
420}
421
422pub fn project_entry_file_of(path: &Path) -> Option<PathBuf> {
423    if PYTHON_MODE {
424        py_project_entry_file_of(path)
425    } else {
426        erg_project_entry_file_of(path)
427    }
428}