use std::{
fs, io,
path::{Path, PathBuf},
};
use cfg_if::cfg_if;
#[cfg(feature = "yarn_pnp")]
use pnp::fs::{LruZipCache, VPath, VPathInfo, ZipCache};
use crate::ResolveError;
pub trait FileSystem: Send + Sync {
#[cfg(feature = "yarn_pnp")]
fn new(yarn_pnp: bool) -> Self;
#[cfg(not(feature = "yarn_pnp"))]
fn new() -> Self;
fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
fn read_to_string(&self, path: &Path) -> io::Result<String>;
fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
fn read_link(&self, path: &Path) -> Result<PathBuf, ResolveError>;
fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
}
#[derive(Debug, Clone, Copy)]
pub struct FileMetadata {
pub(crate) is_file: bool,
pub(crate) is_dir: bool,
pub(crate) is_symlink: bool,
}
impl FileMetadata {
#[must_use]
pub const fn new(is_file: bool, is_dir: bool, is_symlink: bool) -> Self {
Self { is_file, is_dir, is_symlink }
}
#[must_use]
pub const fn is_file(self) -> bool {
self.is_file
}
#[must_use]
pub const fn is_dir(self) -> bool {
self.is_dir
}
#[must_use]
pub const fn is_symlink(self) -> bool {
self.is_symlink
}
}
#[cfg(target_os = "windows")]
impl From<crate::windows::SymlinkMetadata> for FileMetadata {
fn from(value: crate::windows::SymlinkMetadata) -> Self {
Self::new(value.is_file, value.is_dir, value.is_symlink)
}
}
#[cfg(feature = "yarn_pnp")]
impl From<pnp::fs::FileType> for FileMetadata {
fn from(value: pnp::fs::FileType) -> Self {
Self::new(value == pnp::fs::FileType::File, value == pnp::fs::FileType::Directory, false)
}
}
impl From<fs::Metadata> for FileMetadata {
fn from(metadata: fs::Metadata) -> Self {
Self::new(metadata.is_file(), metadata.is_dir(), metadata.is_symlink())
}
}
#[cfg(not(feature = "yarn_pnp"))]
pub struct FileSystemOs;
#[cfg(feature = "yarn_pnp")]
pub struct FileSystemOs {
pnp_lru: LruZipCache<Vec<u8>>,
yarn_pnp: bool,
}
impl FileSystemOs {
#[inline]
pub fn validate_string(bytes: Vec<u8>) -> io::Result<String> {
if simdutf8::basic::from_utf8(&bytes).is_err() {
#[cold]
fn invalid_utf8_error() -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8")
}
return Err(invalid_utf8_error());
}
Ok(unsafe { String::from_utf8_unchecked(bytes) })
}
pub fn read_to_string(path: &Path) -> io::Result<String> {
let bytes = std::fs::read(path)?;
Self::validate_string(bytes)
}
#[inline]
pub fn metadata(path: &Path) -> io::Result<FileMetadata> {
cfg_if! {
if #[cfg(target_os = "windows")] {
let result = crate::windows::symlink_metadata(path)?;
if result.is_symlink {
return fs::metadata(path).map(FileMetadata::from);
}
Ok(result.into())
} else if #[cfg(target_os = "linux")] {
use rustix::fs::{AtFlags, CWD, FileType, StatxFlags};
match rustix::fs::statx(CWD, path, AtFlags::STATX_DONT_SYNC, StatxFlags::TYPE) {
Ok(statx) => {
let file_type = FileType::from_raw_mode(statx.stx_mode.into());
Ok(FileMetadata::new(file_type.is_file(), file_type.is_dir(), file_type.is_symlink()))
}
Err(rustix::io::Errno::NOSYS) => {
fs::metadata(path).map(FileMetadata::from)
}
Err(err) => Err(err.into()),
}
} else {
fs::metadata(path).map(FileMetadata::from)
}
}
}
#[inline]
pub fn symlink_metadata(path: &Path) -> io::Result<FileMetadata> {
cfg_if! {
if #[cfg(target_os = "windows")] {
Ok(crate::windows::symlink_metadata(path)?.into())
} else if #[cfg(target_os = "linux")] {
use rustix::fs::{AtFlags, CWD, FileType, StatxFlags};
match rustix::fs::statx(CWD, path, AtFlags::SYMLINK_NOFOLLOW, StatxFlags::TYPE) {
Ok(statx) => {
let file_type = FileType::from_raw_mode(statx.stx_mode.into());
Ok(FileMetadata::new(file_type.is_file(), file_type.is_dir(), file_type.is_symlink()))
}
Err(rustix::io::Errno::NOSYS) => {
fs::symlink_metadata(path).map(FileMetadata::from)
}
Err(err) => Err(err.into()),
}
} else {
fs::symlink_metadata(path).map(FileMetadata::from)
}
}
}
#[inline]
pub fn read_link(path: &Path) -> Result<PathBuf, ResolveError> {
let path = fs::read_link(path)?;
cfg_if! {
if #[cfg(target_os = "windows")] {
crate::windows::strip_windows_prefix(path)
} else {
Ok(path)
}
}
}
#[inline]
pub fn canonicalize(path: &Path) -> io::Result<PathBuf> {
fs::canonicalize(path)
}
}
impl FileSystem for FileSystemOs {
#[cfg(feature = "yarn_pnp")]
fn new(yarn_pnp: bool) -> Self {
Self { pnp_lru: LruZipCache::new(50, pnp::fs::open_zip_via_read_p), yarn_pnp }
}
#[cfg(not(feature = "yarn_pnp"))]
fn new() -> Self {
Self
}
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.yarn_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => {
self.pnp_lru.read(info.physical_base_path(), info.zip_path)
}
VPath::Virtual(info) => fs::read(info.physical_base_path()),
VPath::Native(path) => fs::read(path),
}
}
}
}
fs::read(path)
}
fn read_to_string(&self, path: &Path) -> io::Result<String> {
let bytes = self.read(path)?;
Self::validate_string(bytes)
}
fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.yarn_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => self
.pnp_lru
.file_type(info.physical_base_path(), info.zip_path)
.map(FileMetadata::from),
VPath::Virtual(info) => {
Self::metadata(&info.physical_base_path())
}
VPath::Native(path) => Self::metadata(&path),
}
}
}
}
Self::metadata(path)
}
fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
Self::symlink_metadata(path)
}
fn read_link(&self, path: &Path) -> Result<PathBuf, ResolveError> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.yarn_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => Self::read_link(&info.physical_base_path().join(info.zip_path)),
VPath::Virtual(info) => Self::read_link(&info.physical_base_path()),
VPath::Native(path) => Self::read_link(&path),
}
}
}
}
Self::read_link(path)
}
fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.yarn_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => Self::canonicalize(&info.physical_base_path().join(info.zip_path)),
VPath::Virtual(info) => Self::canonicalize(&info.physical_base_path()),
VPath::Native(path) => Self::canonicalize(&path),
}
}
}
}
Self::canonicalize(path)
}
}
#[test]
fn metadata() {
let meta = FileMetadata { is_file: true, is_dir: true, is_symlink: true };
assert_eq!(
format!("{meta:?}"),
"FileMetadata { is_file: true, is_dir: true, is_symlink: true }"
);
let _ = meta;
}
#[test]
fn file_metadata_getters() {
let file_meta = FileMetadata::new(true, false, false);
assert!(file_meta.is_file());
assert!(!file_meta.is_dir());
assert!(!file_meta.is_symlink());
let dir_meta = FileMetadata::new(false, true, false);
assert!(!dir_meta.is_file());
assert!(dir_meta.is_dir());
assert!(!dir_meta.is_symlink());
let symlink_meta = FileMetadata::new(false, false, true);
assert!(!symlink_meta.is_file());
assert!(!symlink_meta.is_dir());
assert!(symlink_meta.is_symlink());
}