perf(fs): don't canonicalize path when opening file if --allow-all is passed (#28716)

Fixes #28702.

Super artificial benchmark:

```ts
const perf = performance;

async function asyncOpen() {
  const start = perf.now();
  for (let i = 0; i < 100_000; i++) {
    const file = await Deno.open("./foo.txt");
    file.close();
  }
  const end = perf.now();
  console.log(end - start);
}

function syncOpen() {
  const start = perf.now();
  for (let i = 0; i < 100_000; i++) {
    const file = Deno.openSync("./foo.txt");
    file.close();
  }
  const end = perf.now();
  console.log(end - start);
}

if (Deno.args[0]?.trim() === "async") {
  await asyncOpen();
} else {
  syncOpen();
}
```

Results (average of 10 for each):

```
deno sync               1785.59
deno-this-pr sync       491.69
deno async              1839.71
deno-this-pr async      528.78
```

---------

Co-authored-by: David Sherret <dsherret@users.noreply.github.com>
This commit is contained in:
Nathan Whitaker 2025-04-29 16:16:24 -07:00 committed by GitHub
parent 1a171f10df
commit c22d17824b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 205 additions and 134 deletions

View file

@ -11,6 +11,7 @@ use std::path::PathBuf;
pub use deno_io::fs::FsError;
use deno_permissions::PermissionCheckError;
pub use interface::CheckedPath;
pub use crate::interface::AccessCheckCb;
pub use crate::interface::AccessCheckFn;
@ -31,12 +32,12 @@ pub use crate::sync::MaybeSync;
pub trait FsPermissions {
fn check_open<'a>(
&mut self,
resolved: bool,
read: bool,
write: bool,
path: &'a Path,
path: Cow<'a, Path>,
api_name: &str,
) -> Result<std::borrow::Cow<'a, Path>, FsError>;
get_path: &'a dyn GetPath,
) -> Result<CheckedPath<'a>, FsError>;
#[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"]
fn check_read(
&mut self,
@ -46,7 +47,7 @@ pub trait FsPermissions {
#[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"]
fn check_read_path<'a>(
&mut self,
path: &'a Path,
path: Cow<'a, Path>,
api_name: &str,
) -> Result<Cow<'a, Path>, PermissionCheckError>;
fn check_read_all(
@ -68,7 +69,7 @@ pub trait FsPermissions {
#[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"]
fn check_write_path<'a>(
&mut self,
path: &'a Path,
path: Cow<'a, Path>,
api_name: &str,
) -> Result<Cow<'a, Path>, PermissionCheckError>;
#[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"]
@ -90,56 +91,69 @@ pub trait FsPermissions {
fn check<'a>(
&mut self,
resolved: bool,
open_options: &OpenOptions,
path: &'a Path,
path: Cow<'a, Path>,
api_name: &str,
) -> Result<std::borrow::Cow<'a, Path>, FsError> {
get_path: &'a dyn GetPath,
) -> Result<CheckedPath<'a>, FsError> {
self.check_open(
resolved,
open_options.read,
open_options.write || open_options.append,
path,
api_name,
get_path,
)
}
fn allows_all(&self) -> bool {
false
}
}
impl FsPermissions for deno_permissions::PermissionsContainer {
fn check_open<'a>(
&mut self,
resolved: bool,
read: bool,
write: bool,
path: &'a Path,
path: Cow<'a, Path>,
api_name: &str,
) -> Result<Cow<'a, Path>, FsError> {
if resolved {
self
.check_special_file(path, api_name)
.map_err(FsError::NotCapable)?;
return Ok(Cow::Borrowed(path));
get_path: &'a dyn GetPath,
) -> Result<CheckedPath<'a>, FsError> {
if self.allows_all() {
return Ok(CheckedPath::Unresolved(path));
}
let (needs_canonicalize, path) = get_path.normalized(path)?;
// If somehow read or write aren't specified, use read
let read = read || !write;
let mut path: Cow<'a, Path> = Cow::Borrowed(path);
if read {
let resolved_path = FsPermissions::check_read_path(self, &path, api_name)
.map_err(|_| FsError::NotCapable("read"))?;
if let Cow::Owned(resolved_path) = resolved_path {
path = Cow::Owned(resolved_path);
}
let path = if read {
FsPermissions::check_read_path(self, path, api_name)
.map_err(|_| FsError::NotCapable("read"))?
} else {
path
};
let path = if write {
FsPermissions::check_write_path(self, path.clone(), api_name)
.map_err(|_| FsError::NotCapable("write"))?
} else {
path
};
let resolved_path = if needs_canonicalize {
Cow::Owned(get_path.resolved(&path)?)
} else {
path
};
self
.check_special_file(&resolved_path, api_name)
.map_err(FsError::NotCapable)?;
if needs_canonicalize {
Ok(CheckedPath::Resolved(resolved_path))
} else {
Ok(CheckedPath::Unresolved(resolved_path))
}
if write {
let resolved_path =
FsPermissions::check_write_path(self, &path, api_name)
.map_err(|_| FsError::NotCapable("write"))?;
if let Cow::Owned(resolved_path) = resolved_path {
path = Cow::Owned(resolved_path);
}
}
Ok(path)
}
fn check_read(
@ -152,7 +166,7 @@ impl FsPermissions for deno_permissions::PermissionsContainer {
fn check_read_path<'a>(
&mut self,
path: &'a Path,
path: Cow<'a, Path>,
api_name: &str,
) -> Result<Cow<'a, Path>, PermissionCheckError> {
deno_permissions::PermissionsContainer::check_read_path(
@ -182,7 +196,7 @@ impl FsPermissions for deno_permissions::PermissionsContainer {
fn check_write_path<'a>(
&mut self,
path: &'a Path,
path: Cow<'a, Path>,
api_name: &str,
) -> Result<Cow<'a, Path>, PermissionCheckError> {
deno_permissions::PermissionsContainer::check_write_path(
@ -224,6 +238,10 @@ impl FsPermissions for deno_permissions::PermissionsContainer {
) -> Result<(), PermissionCheckError> {
deno_permissions::PermissionsContainer::check_write_all(self, api_name)
}
fn allows_all(&self) -> bool {
self.allows_all()
}
}
pub const UNSTABLE_FEATURE_NAME: &str = "fs";
@ -305,3 +323,11 @@ deno_core::extension!(deno_fs,
state.put(options.fs);
},
);
pub trait GetPath {
fn normalized<'a>(
&self,
path: Cow<'a, Path>,
) -> Result<(bool, Cow<'a, Path>), FsError>;
fn resolved(&self, path: &Path) -> Result<PathBuf, FsError>;
}