unrspack_resolver/
file_system.rs

1use std::{
2    fs, io,
3    path::{Path, PathBuf},
4};
5
6use cfg_if::cfg_if;
7#[cfg(feature = "yarn_pnp")]
8use pnp::fs::{LruZipCache, VPath, VPathInfo, ZipCache};
9
10/// File System abstraction used for `ResolverGeneric`
11pub trait FileSystem: Send + Sync {
12    /// See [std::fs::read]
13    ///
14    /// # Errors
15    ///
16    /// See [std::fs::read]
17    fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
18
19    /// See [std::fs::read_to_string]
20    ///
21    /// # Errors
22    ///
23    /// * See [std::fs::read_to_string]
24    /// ## Warning
25    /// Use `&Path` instead of a generic `P: AsRef<Path>` here,
26    /// because object safety requirements, it is especially useful, when
27    /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric<Fs>` in
28    /// napi env.
29    fn read_to_string(&self, path: &Path) -> io::Result<String>;
30
31    /// See [std::fs::metadata]
32    ///
33    /// # Errors
34    /// See [std::fs::metadata]
35    /// ## Warning
36    /// Use `&Path` instead of a generic `P: AsRef<Path>` here,
37    /// because object safety requirements, it is especially useful, when
38    /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric<Fs>` in
39    /// napi env.
40    fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
41
42    /// See [std::fs::symlink_metadata]
43    ///
44    /// # Errors
45    ///
46    /// See [std::fs::symlink_metadata]
47    /// ## Warning
48    /// Use `&Path` instead of a generic `P: AsRef<Path>` here,
49    /// because object safety requirements, it is especially useful, when
50    /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric<Fs>` in
51    /// napi env.
52    fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
53
54    /// Returns the resolution of a symbolic link.
55    ///
56    /// # Errors
57    ///
58    /// See [std::fs::read_link]
59    fn read_link(&self, path: &Path) -> io::Result<PathBuf>;
60
61    /// See [std::fs::canonicalize]
62    ///
63    /// # Errors
64    ///
65    /// See [std::fs::read_link]
66    /// ## Warning
67    /// Use `&Path` instead of a generic `P: AsRef<Path>` here,
68    /// because object safety requirements, it is especially useful, when
69    /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric<Fs>` in
70    /// napi env.
71    fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
72}
73
74/// Metadata information about a file
75#[derive(Debug, Clone, Copy)]
76pub struct FileMetadata {
77    pub(crate) is_file: bool,
78    pub(crate) is_dir: bool,
79    pub(crate) is_symlink: bool,
80}
81
82impl FileMetadata {
83    #[must_use]
84    pub const fn new(is_file: bool, is_dir: bool, is_symlink: bool) -> Self {
85        Self { is_file, is_dir, is_symlink }
86    }
87
88    #[must_use]
89    pub const fn is_file(self) -> bool {
90        self.is_file
91    }
92
93    #[must_use]
94    pub const fn is_dir(self) -> bool {
95        self.is_dir
96    }
97
98    #[must_use]
99    pub const fn is_symlink(self) -> bool {
100        self.is_symlink
101    }
102}
103
104#[cfg(feature = "yarn_pnp")]
105impl From<pnp::fs::FileType> for FileMetadata {
106    fn from(value: pnp::fs::FileType) -> Self {
107        Self::new(value == pnp::fs::FileType::File, value == pnp::fs::FileType::Directory, false)
108    }
109}
110
111impl From<fs::Metadata> for FileMetadata {
112    fn from(metadata: fs::Metadata) -> Self {
113        Self::new(metadata.is_file(), metadata.is_dir(), metadata.is_symlink())
114    }
115}
116
117pub struct FileSystemOptions {
118    #[cfg(feature = "yarn_pnp")]
119    pub enable_pnp: bool,
120}
121
122impl Default for FileSystemOptions {
123    fn default() -> Self {
124        Self {
125            #[cfg(feature = "yarn_pnp")]
126            enable_pnp: true,
127        }
128    }
129}
130
131/// Operating System
132pub struct FileSystemOs {
133    options: FileSystemOptions,
134    #[cfg(feature = "yarn_pnp")]
135    pnp_lru: LruZipCache<Vec<u8>>,
136}
137
138#[cfg(not(feature = "yarn_pnp"))]
139pub struct FileSystemOs;
140
141impl Default for FileSystemOs {
142    fn default() -> Self {
143        Self {
144            options: FileSystemOptions::default(),
145            #[cfg(feature = "yarn_pnp")]
146            pnp_lru: LruZipCache::new(50, pnp::fs::open_zip_via_read_p),
147        }
148    }
149}
150
151impl FileSystemOs {
152    /// # Errors
153    ///
154    /// See [std::fs::read_to_string]
155    pub fn read_to_string(path: &Path) -> io::Result<String> {
156        // `simdutf8` is faster than `std::str::from_utf8` which `fs::read_to_string` uses internally
157        let bytes = std::fs::read(path)?;
158        if simdutf8::basic::from_utf8(&bytes).is_err() {
159            // Same error as `fs::read_to_string` produces (`io::Error::INVALID_UTF8`)
160            return Err(io::Error::new(
161                io::ErrorKind::InvalidData,
162                "stream did not contain valid UTF-8",
163            ));
164        }
165        // SAFETY: `simdutf8` has ensured it's a valid UTF-8 string
166        Ok(unsafe { String::from_utf8_unchecked(bytes) })
167    }
168
169    /// # Errors
170    ///
171    /// See [std::fs::metadata]
172    #[inline]
173    pub fn metadata(path: &Path) -> io::Result<FileMetadata> {
174        fs::metadata(path).map(FileMetadata::from)
175    }
176
177    /// # Errors
178    ///
179    /// See [std::fs::symlink_metadata]
180    #[inline]
181    pub fn symlink_metadata(path: &Path) -> io::Result<FileMetadata> {
182        fs::symlink_metadata(path).map(FileMetadata::from)
183    }
184
185    /// # Errors
186    ///
187    /// See [std::fs::read_link]
188    #[inline]
189    pub fn read_link(path: &Path) -> io::Result<PathBuf> {
190        let path = fs::read_link(path)?;
191        cfg_if! {
192            if #[cfg(windows)] {
193                Ok(Self::strip_windows_prefix(path))
194            } else {
195                Ok(path)
196            }
197        }
198    }
199
200    pub fn strip_windows_prefix<P: AsRef<Path>>(path: P) -> PathBuf {
201        const UNC_PATH_PREFIX: &[u8] = b"\\\\?\\UNC\\";
202        const LONG_PATH_PREFIX: &[u8] = b"\\\\?\\";
203        let path_bytes = path.as_ref().as_os_str().as_encoded_bytes();
204        path_bytes
205            .strip_prefix(UNC_PATH_PREFIX)
206            .or_else(|| path_bytes.strip_prefix(LONG_PATH_PREFIX))
207            .map_or_else(
208                || path.as_ref().to_path_buf(),
209                |p| {
210                    // SAFETY: `as_encoded_bytes` ensures `p` is valid path bytes
211                    unsafe { PathBuf::from(std::ffi::OsStr::from_encoded_bytes_unchecked(p)) }
212                },
213            )
214    }
215}
216
217fn buffer_to_string(bytes: Vec<u8>) -> io::Result<String> {
218    // `simdutf8` is faster than `std::str::from_utf8` which `fs::read_to_string` uses internally
219    if simdutf8::basic::from_utf8(&bytes).is_err() {
220        // Same error as `fs::read_to_string` produces (`io::Error::INVALID_UTF8`)
221        return Err(io::Error::new(
222            io::ErrorKind::InvalidData,
223            "stream did not contain valid UTF-8",
224        ));
225    }
226    // SAFETY: `simdutf8` has ensured it's a valid UTF-8 string
227    Ok(unsafe { String::from_utf8_unchecked(bytes) })
228}
229
230impl FileSystem for FileSystemOs {
231    fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
232        cfg_if! {
233          if #[cfg(feature = "yarn_pnp")] {
234            if self.options.enable_pnp {
235                return match VPath::from(path)? {
236                    VPath::Zip(info) => self.pnp_lru.read(info.physical_base_path(), info.zip_path),
237                    VPath::Virtual(info) => std::fs::read(info.physical_base_path()),
238                    VPath::Native(path) => std::fs::read(&path),
239                }
240            }
241        }}
242
243        std::fs::read(path)
244    }
245
246    fn read_to_string(&self, path: &Path) -> io::Result<String> {
247        let buffer = self.read(path)?;
248        buffer_to_string(buffer)
249    }
250
251    fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
252        cfg_if! {
253            if #[cfg(feature = "yarn_pnp")] {
254                match VPath::from(path)? {
255                    VPath::Zip(info) => self
256                        .pnp_lru
257                        .file_type(info.physical_base_path(), info.zip_path)
258                        .map(FileMetadata::from),
259                    VPath::Virtual(info) => {
260                        Self::metadata(&info.physical_base_path())
261                    }
262                    VPath::Native(path) => Self::metadata(&path),
263                }
264            } else {
265                Self::metadata(path)}
266        }
267    }
268
269    fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
270        Self::symlink_metadata(path)
271    }
272
273    fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
274        cfg_if! {
275            if #[cfg(feature = "yarn_pnp")] {
276                match VPath::from(path)? {
277                    VPath::Zip(info) => Self::read_link(&info.physical_base_path().join(info.zip_path)),
278                    VPath::Virtual(info) => Self::read_link(&info.physical_base_path()),
279                    VPath::Native(path) => Self::read_link(&path),
280                }
281            } else {
282                Self::read_link(path)
283            }
284        }
285    }
286
287    fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
288        cfg_if! {
289            if #[cfg(feature = "yarn_pnp")] {
290                if self.options.enable_pnp {
291                    return match VPath::from(path)? {
292                        VPath::Zip(info) => {
293                            dunce::canonicalize(info.physical_base_path().join(info.zip_path))
294                        }
295                        VPath::Virtual(info) => dunce::canonicalize(info.physical_base_path()),
296                        VPath::Native(path) => dunce::canonicalize(path),
297                    }
298                }
299            }
300        }
301
302        cfg_if! {
303            if #[cfg(not(target_os = "wasi"))]{
304                dunce::canonicalize(path)
305            } else {
306                use std::path::Component;
307                let mut path_buf = path.to_path_buf();
308                loop {
309                    let link = fs::read_link(&path_buf)?;
310                    path_buf.pop();
311                    for component in link.components() {
312                        match component {
313                            Component::ParentDir => {
314                                path_buf.pop();
315                            }
316                            Component::Normal(seg) => {
317                                #[cfg(target_family = "wasm")]
318                                // Need to trim the extra \0 introduces by https://github.com/nodejs/uvwasi/issues/262
319                                {
320                                    path_buf.push(seg.to_string_lossy().trim_end_matches('\0'));
321                                }
322                                #[cfg(not(target_family = "wasm"))]
323                                {
324                                    path_buf.push(seg);
325                                }
326                            }
327                            Component::RootDir => {
328                                path_buf = PathBuf::from("/");
329                            }
330                            Component::CurDir | Component::Prefix(_) => {}
331                        }
332                    }
333                    if !fs::symlink_metadata(&path_buf)?.is_symlink() {
334                        break;
335                    }
336                }
337                Ok(path_buf)
338            }
339        }
340    }
341}
342
343#[test]
344fn metadata() {
345    let meta = FileMetadata { is_file: true, is_dir: true, is_symlink: true };
346    assert_eq!(
347        format!("{meta:?}"),
348        "FileMetadata { is_file: true, is_dir: true, is_symlink: true }"
349    );
350    let _ = meta;
351}