rqjs_ext/modules/
path.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use std::{
4    path::{Component, Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR},
5    slice::Iter,
6};
7
8use rquickjs::{
9    function::Opt,
10    module::{Declarations, Exports, ModuleDef},
11    prelude::{Func, Rest},
12    Ctx, Object, Result,
13};
14
15use crate::modules::module::export_default;
16
17pub struct PathModule;
18
19#[cfg(windows)]
20const DELIMITER: char = ';';
21#[cfg(not(windows))]
22const DELIMITER: char = ':';
23
24#[cfg(windows)]
25pub const CURRENT_DIR_STR: &str = ".\\";
26#[cfg(not(windows))]
27pub const CURRENT_DIR_STR: &str = "./";
28
29pub fn dirname(path: String) -> String {
30    if path == MAIN_SEPARATOR_STR {
31        return path;
32    }
33    let path = path.strip_suffix(MAIN_SEPARATOR).unwrap_or(&path);
34    match path.rfind(MAIN_SEPARATOR) {
35        Some(idx) => {
36            let parent = &path[..idx];
37            if parent.is_empty() {
38                MAIN_SEPARATOR_STR
39            } else {
40                parent
41            }
42        }
43        None => ".",
44    }
45    .to_string()
46}
47
48fn name_extname(path: &str) -> (&str, &str) {
49    let path = path.strip_suffix(MAIN_SEPARATOR).unwrap_or(path);
50    let path = match path.rfind(MAIN_SEPARATOR) {
51        Some(idx) => &path[idx + 1..],
52        None => path,
53    };
54    if path.starts_with('.') {
55        return (path, "");
56    }
57    match path.rfind('.') {
58        Some(idx) => path.split_at(idx),
59        None => (path, ""),
60    }
61}
62
63fn basename(path: String, suffix: Opt<String>) -> String {
64    if path == MAIN_SEPARATOR_STR {
65        return path;
66    }
67    if path.is_empty() {
68        return String::from(".");
69    }
70    let (base, ext) = name_extname(&path);
71    let name = format!("{}{}", base, ext);
72    if let Some(suffix) = suffix.0 {
73        name.strip_suffix(&suffix).unwrap_or(&name)
74    } else {
75        &name
76    }
77    .to_string()
78}
79
80fn extname(path: String) -> String {
81    let (_, ext) = name_extname(&path);
82    ext.to_string()
83}
84
85fn format(obj: Object) -> String {
86    let dir: String = obj.get("dir").unwrap_or_default();
87    let root: String = obj.get("root").unwrap_or_default();
88    let base: String = obj.get("base").unwrap_or_default();
89    let name: String = obj.get("name").unwrap_or_default();
90    let ext: String = obj.get("ext").unwrap_or_default();
91
92    let mut path = String::new();
93    if !dir.is_empty() {
94        path.push_str(&dir);
95        if !dir.ends_with(MAIN_SEPARATOR) {
96            path.push(MAIN_SEPARATOR);
97        }
98    } else if !root.is_empty() {
99        path.push_str(&root);
100        if !root.ends_with(MAIN_SEPARATOR) {
101            path.push(MAIN_SEPARATOR);
102        }
103    }
104    if !base.is_empty() {
105        path.push_str(&base);
106    } else {
107        path.push_str(&name);
108        if !ext.is_empty() {
109            if !ext.starts_with('.') {
110                path.push('.');
111            }
112            path.push_str(&ext);
113        }
114    }
115    path
116}
117
118fn parse(ctx: Ctx, path_str: String) -> Result<Object> {
119    let obj = Object::new(ctx)?;
120    let path = Path::new(&path_str);
121    let parent = path
122        .parent()
123        .map(|p| p.to_str().unwrap())
124        .unwrap_or_default();
125    let filename = path
126        .file_name()
127        .map(|n| n.to_str().unwrap())
128        .unwrap_or_default();
129
130    let (name, extension) = name_extname(filename);
131
132    let root = path
133        .components()
134        .next()
135        .and_then(|c| match c {
136            Component::Prefix(prefix) => prefix.as_os_str().to_str(),
137            Component::RootDir => c.as_os_str().to_str(),
138            _ => Some(""),
139        })
140        .unwrap_or_default();
141
142    obj.set("root", root)?;
143    obj.set("dir", parent)?;
144    obj.set("base", format!("{}{}", name, extension))?;
145    obj.set("ext", extension)?;
146    obj.set("name", name)?;
147
148    Ok(obj)
149}
150
151fn join(parts: Rest<String>) -> String {
152    join_path(parts.0)
153}
154
155pub fn join_path(parts: Vec<String>) -> String {
156    let mut result = PathBuf::new();
157    let mut empty = true;
158    for part in parts.iter() {
159        if part.starts_with(MAIN_SEPARATOR) && empty {
160            result.push(MAIN_SEPARATOR_STR);
161            empty = false;
162        }
163        for sub_part in part.split(MAIN_SEPARATOR) {
164            if !sub_part.is_empty() {
165                if sub_part.starts_with("..") {
166                    empty = false;
167                    result.pop();
168                } else {
169                    result.push(sub_part.strip_prefix('.').unwrap_or(sub_part));
170                    empty = false;
171                }
172            }
173        }
174    }
175    remove_trailing_slash(result)
176}
177
178fn remove_trailing_slash(result: PathBuf) -> String {
179    let path = result.to_string_lossy().to_string();
180    path.strip_suffix(MAIN_SEPARATOR)
181        .unwrap_or(&path)
182        .to_string()
183}
184
185fn resolve(path: Rest<String>) -> String {
186    resolve_path(path.iter())
187}
188
189pub fn resolve_path(iter: Iter<'_, String>) -> String {
190    let mut dir = std::env::current_dir().unwrap();
191    for part in iter {
192        let p = part.strip_prefix(CURRENT_DIR_STR).unwrap_or(part);
193        if p.starts_with(MAIN_SEPARATOR) {
194            dir = PathBuf::from(p);
195        } else {
196            for sub_part in p.split(MAIN_SEPARATOR) {
197                if sub_part.starts_with("..") {
198                    dir.pop();
199                } else {
200                    dir.push(sub_part.strip_prefix('.').unwrap_or(sub_part))
201                }
202            }
203        }
204    }
205
206    remove_trailing_slash(dir)
207}
208
209fn normalize(path: String) -> String {
210    let path = PathBuf::from(path);
211    let parts = path
212        .components()
213        .map(|c| c.as_os_str().to_string_lossy().to_string())
214        .collect::<Vec<_>>();
215
216    join_path(parts)
217}
218
219pub fn is_absolute(path: String) -> bool {
220    PathBuf::from(path).is_absolute()
221}
222
223impl ModuleDef for PathModule {
224    fn declare(declare: &Declarations<'_>) -> Result<()> {
225        declare.declare("basename")?;
226        declare.declare("dirname")?;
227        declare.declare("extname")?;
228        declare.declare("format")?;
229        declare.declare("parse")?;
230        declare.declare("join")?;
231        declare.declare("resolve")?;
232        declare.declare("normalize")?;
233        declare.declare("isAbsolute")?;
234        declare.declare("delimiter")?;
235
236        declare.declare("default")?;
237        Ok(())
238    }
239
240    fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> {
241        export_default(ctx, exports, |default| {
242            default.set("dirname", Func::from(dirname))?;
243            default.set("basename", Func::from(basename))?;
244            default.set("extname", Func::from(extname))?;
245            default.set("format", Func::from(format))?;
246            default.set("parse", Func::from(parse))?;
247            default.set("join", Func::from(join))?;
248            default.set("resolve", Func::from(resolve))?;
249            default.set("normalize", Func::from(normalize))?;
250            default.set("isAbsolute", Func::from(is_absolute))?;
251            default.prop("delimiter", DELIMITER.to_string())?;
252            Ok(())
253        })
254    }
255}