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_to_string(&self, path: &Path) -> io::Result<String>;
fn read_to_string_bypass_system_cache(&self, path: &Path) -> io::Result<String> {
self.read_to_string(path)
}
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>;
}
#[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(target_os = "windows")]
{
let result = crate::windows::symlink_metadata(path)?;
if result.is_symlink {
return fs::metadata(path).map(FileMetadata::from);
}
Ok(result.into())
}
#[cfg(not(target_os = "windows"))]
{
fs::metadata(path).map(FileMetadata::from)
}
}
#[inline]
pub fn symlink_metadata(path: &Path) -> io::Result<FileMetadata> {
#[cfg(target_os = "windows")]
{
Ok(crate::windows::symlink_metadata(path)?.into())
}
#[cfg(not(target_os = "windows"))]
{
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)
}
}
}
}
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_to_string(&self, path: &Path) -> io::Result<String> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.yarn_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => {
self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path)
}
VPath::Virtual(info) => Self::read_to_string(&info.physical_base_path()),
VPath::Native(path) => Self::read_to_string(&path),
}
}
}
}
Self::read_to_string(path)
}
fn read_to_string_bypass_system_cache(&self, path: &Path) -> io::Result<String> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.yarn_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => {
self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path)
}
VPath::Virtual(info) => Self::read_to_string(&info.physical_base_path()),
VPath::Native(path) => Self::read_to_string(&path),
}
}
}
}
#[cfg(target_os = "macos")]
{
use libc::F_NOCACHE;
use std::{io::Read, os::unix::fs::OpenOptionsExt};
let mut fd = fs::OpenOptions::new().read(true).custom_flags(F_NOCACHE).open(path)?;
let meta = fd.metadata()?;
#[allow(clippy::cast_possible_truncation)]
let mut buffer = Vec::with_capacity(meta.len() as usize);
fd.read_to_end(&mut buffer)?;
Self::validate_string(buffer)
}
#[cfg(target_os = "linux")]
{
use std::{io::Read, os::fd::AsRawFd};
let mut fd = fs::OpenOptions::new().read(true).open(path)?;
let _ = unsafe { libc::posix_fadvise(fd.as_raw_fd(), 0, 0, libc::POSIX_FADV_DONTNEED) };
let meta = fd.metadata();
let mut buffer = meta.ok().map_or_else(Vec::new, |meta| {
#[allow(clippy::cast_possible_truncation)]
Vec::with_capacity(meta.len() as usize)
});
fd.read_to_end(&mut buffer)?;
Self::validate_string(buffer)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Self::read_to_string(path)
}
}
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)
}
}
#[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());
}