unrspack_resolver/
file_system.rs1use 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
10pub trait FileSystem: Send + Sync {
12 fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
18
19 fn read_to_string(&self, path: &Path) -> io::Result<String>;
30
31 fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
41
42 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
53
54 fn read_link(&self, path: &Path) -> io::Result<PathBuf>;
60
61 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
72}
73
74#[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
131pub 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 pub fn read_to_string(path: &Path) -> io::Result<String> {
156 let bytes = std::fs::read(path)?;
158 if simdutf8::basic::from_utf8(&bytes).is_err() {
159 return Err(io::Error::new(
161 io::ErrorKind::InvalidData,
162 "stream did not contain valid UTF-8",
163 ));
164 }
165 Ok(unsafe { String::from_utf8_unchecked(bytes) })
167 }
168
169 #[inline]
173 pub fn metadata(path: &Path) -> io::Result<FileMetadata> {
174 fs::metadata(path).map(FileMetadata::from)
175 }
176
177 #[inline]
181 pub fn symlink_metadata(path: &Path) -> io::Result<FileMetadata> {
182 fs::symlink_metadata(path).map(FileMetadata::from)
183 }
184
185 #[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 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 if simdutf8::basic::from_utf8(&bytes).is_err() {
220 return Err(io::Error::new(
222 io::ErrorKind::InvalidData,
223 "stream did not contain valid UTF-8",
224 ));
225 }
226 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 {
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}