1use std::collections::VecDeque;
2use std::io::Result as IoResult;
3use std::path::{Component, Path, PathBuf};
4use std::result::Result as StdResult;
5use std::{env, fs};
6
7use crate::error::Result;
8use crate::function::Function;
9use crate::state::Lua;
10
11use super::{NavigateError, Require};
12
13#[derive(Default, Debug)]
15pub struct FsRequirer {
16 abs_path: PathBuf,
18 rel_path: PathBuf,
20 resolved_path: Option<PathBuf>,
23}
24
25impl FsRequirer {
26 const CHUNK_PREFIX: &str = "@";
29
30 const FILE_EXTENSIONS: &[&str] = &["luau", "lua"];
32
33 const LUAURC_CONFIG_FILENAME: &str = ".luaurc";
35
36 const LUAU_CONFIG_FILENAME: &str = ".config.luau";
38
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 fn normalize_chunk_name(chunk_name: &str) -> &str {
45 if let Some((path, line)) = chunk_name.rsplit_once(':')
46 && line.parse::<u32>().is_ok()
47 {
48 return path;
49 }
50 chunk_name
51 }
52
53 fn normalize_path(path: &Path) -> PathBuf {
55 let mut components = VecDeque::new();
56
57 for comp in path.components() {
58 match comp {
59 Component::Prefix(..) | Component::RootDir => {
60 components.push_back(comp);
61 }
62 Component::CurDir => {}
63 Component::ParentDir => {
64 if matches!(components.back(), None | Some(Component::ParentDir)) {
65 components.push_back(Component::ParentDir);
66 } else if matches!(components.back(), Some(Component::Normal(..))) {
67 components.pop_back();
68 }
69 }
70 Component::Normal(..) => components.push_back(comp),
71 }
72 }
73
74 if matches!(components.front(), None | Some(Component::Normal(..))) {
75 components.push_front(Component::CurDir);
76 }
77
78 components.into_iter().collect()
80 }
81
82 fn resolve_module(path: &Path) -> StdResult<Option<PathBuf>, NavigateError> {
86 let mut found_path = None;
87
88 if path.components().next_back() != Some(Component::Normal("init".as_ref())) {
89 let current_ext = (path.extension().and_then(|s| s.to_str()))
90 .map(|s| format!("{s}."))
91 .unwrap_or_default();
92 for ext in Self::FILE_EXTENSIONS {
93 let candidate = path.with_extension(format!("{current_ext}{ext}"));
94 if candidate.is_file() && found_path.replace(candidate).is_some() {
95 return Err(NavigateError::Ambiguous);
96 }
97 }
98 }
99 if path.is_dir() {
100 for component in Self::FILE_EXTENSIONS.iter().map(|ext| format!("init.{ext}")) {
101 let candidate = path.join(component);
102 if candidate.is_file() && found_path.replace(candidate).is_some() {
103 return Err(NavigateError::Ambiguous);
104 }
105 }
106
107 if found_path.is_none() {
108 return Ok(None);
110 }
111 }
112
113 Ok(Some(found_path.ok_or(NavigateError::NotFound)?))
114 }
115}
116
117impl Require for FsRequirer {
118 fn is_require_allowed(&self, chunk_name: &str) -> bool {
119 chunk_name.starts_with(Self::CHUNK_PREFIX)
120 }
121
122 fn reset(&mut self, chunk_name: &str) -> StdResult<(), NavigateError> {
123 if !chunk_name.starts_with(Self::CHUNK_PREFIX) {
124 return Err(NavigateError::NotFound);
125 }
126 let chunk_name = Self::normalize_chunk_name(&chunk_name[1..]);
127 let chunk_path = Self::normalize_path(chunk_name.as_ref());
128
129 if chunk_path.extension() == Some("rs".as_ref()) {
130 let chunk_filename = chunk_path.file_name().unwrap();
132 let cwd = env::current_dir().map_err(|_| NavigateError::NotFound)?;
133 self.abs_path = Self::normalize_path(&cwd.join(chunk_filename));
134 self.rel_path = ([Component::CurDir, Component::Normal(chunk_filename)].into_iter()).collect();
135 self.resolved_path = None;
136
137 return Ok(());
138 }
139
140 let abs_path = if chunk_path.is_absolute() {
141 chunk_path.clone()
142 } else {
143 let cwd = env::current_dir().map_err(|_| NavigateError::NotFound)?;
144 Self::normalize_path(&cwd.join(&chunk_path))
145 };
146 let resolved_path = match Self::resolve_module(&abs_path) {
148 Err(NavigateError::NotFound) if abs_path.is_file() => Some(abs_path.clone()),
149 result => result?,
150 };
151 self.abs_path = abs_path;
152 self.rel_path = chunk_path;
153 self.resolved_path = resolved_path;
154
155 Ok(())
156 }
157
158 fn jump_to_alias(&mut self, path: &str) -> StdResult<(), NavigateError> {
159 let path = Self::normalize_path(path.as_ref());
160 let resolved_path = Self::resolve_module(&path)?;
161
162 self.abs_path = path.clone();
163 self.rel_path = path;
164 self.resolved_path = resolved_path;
165
166 Ok(())
167 }
168
169 fn to_parent(&mut self) -> StdResult<(), NavigateError> {
170 let mut abs_path = self.abs_path.clone();
171 if !abs_path.pop() {
172 return Err(NavigateError::NotFound);
177 }
178 let mut rel_parent = self.rel_path.clone();
179 rel_parent.pop();
180 let resolved_path = Self::resolve_module(&abs_path)?;
181
182 self.abs_path = abs_path;
183 self.rel_path = Self::normalize_path(&rel_parent);
184 self.resolved_path = resolved_path;
185
186 Ok(())
187 }
188
189 fn to_child(&mut self, name: &str) -> StdResult<(), NavigateError> {
190 let abs_path = self.abs_path.join(name);
191 let rel_path = self.rel_path.join(name);
192 let resolved_path = Self::resolve_module(&abs_path)?;
193
194 self.abs_path = abs_path;
195 self.rel_path = rel_path;
196 self.resolved_path = resolved_path;
197
198 Ok(())
199 }
200
201 fn has_module(&self) -> bool {
202 (self.resolved_path.as_deref())
203 .map(Path::is_file)
204 .unwrap_or(false)
205 }
206
207 fn cache_key(&self) -> String {
208 self.resolved_path.as_deref().unwrap().display().to_string()
209 }
210
211 fn has_config(&self) -> bool {
212 self.abs_path.is_dir()
213 && (self.abs_path.join(Self::LUAURC_CONFIG_FILENAME).is_file()
214 || self.abs_path.join(Self::LUAU_CONFIG_FILENAME).is_file())
215 }
216
217 fn config(&self) -> IoResult<Vec<u8>> {
218 let path = self.abs_path.join(Self::LUAURC_CONFIG_FILENAME);
219 if path.is_file() {
220 return fs::read(path);
221 }
222 fs::read(self.abs_path.join(Self::LUAU_CONFIG_FILENAME))
223 }
224
225 fn loader(&self, lua: &Lua) -> Result<Function> {
226 let name = format!("@{}", self.rel_path.display());
227 lua.load(self.resolved_path.as_deref().unwrap())
228 .set_name(name)
229 .into_function()
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use std::path::Path;
236
237 use super::FsRequirer;
238
239 #[test]
240 fn test_path_normalize() {
241 for (input, expected) in [
242 ("", "./"),
244 (".", "./"),
245 ("a/relative/path", "./a/relative/path"),
246 ("./remove/extraneous/symbols/", "./remove/extraneous/symbols"),
248 ("./remove/extraneous//symbols", "./remove/extraneous/symbols"),
249 ("./remove/extraneous/symbols/.", "./remove/extraneous/symbols"),
250 ("./remove/extraneous/./symbols", "./remove/extraneous/symbols"),
251 ("../remove/extraneous/symbols/", "../remove/extraneous/symbols"),
252 ("../remove/extraneous//symbols", "../remove/extraneous/symbols"),
253 ("../remove/extraneous/symbols/.", "../remove/extraneous/symbols"),
254 ("../remove/extraneous/./symbols", "../remove/extraneous/symbols"),
255 ("/remove/extraneous/symbols/", "/remove/extraneous/symbols"),
256 ("/remove/extraneous//symbols", "/remove/extraneous/symbols"),
257 ("/remove/extraneous/symbols/.", "/remove/extraneous/symbols"),
258 ("/remove/extraneous/./symbols", "/remove/extraneous/symbols"),
259 ("./remove/me/..", "./remove"),
261 ("./remove/me/../", "./remove"),
262 ("../remove/me/..", "../remove"),
263 ("../remove/me/../", "../remove"),
264 ("/remove/me/..", "/remove"),
265 ("/remove/me/../", "/remove"),
266 ("./..", "../"),
267 ("./../", "../"),
268 ("../..", "../../"),
269 ("../../", "../../"),
270 ("/../", "/"),
272 ] {
273 let path = FsRequirer::normalize_path(input.as_ref());
274 assert_eq!(
275 &path,
276 expected.as_ref() as &Path,
277 "wrong normalization for {input}"
278 );
279 }
280 }
281}