mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
feat(runtime): Allow embedders to perform additional access checks on file open (#23208)
Embedders may have special requirements around file opening, so we add a new `check_open` permission check that is called as part of the file open process.
This commit is contained in:
parent
365e1f48f7
commit
472a370640
17 changed files with 476 additions and 121 deletions
|
@ -112,7 +112,7 @@ impl CjsCodeAnalyzer for CliCjsCodeAnalyzer {
|
||||||
Some(source) => source,
|
Some(source) => source,
|
||||||
None => self
|
None => self
|
||||||
.fs
|
.fs
|
||||||
.read_text_file_sync(&specifier.to_file_path().unwrap())?,
|
.read_text_file_sync(&specifier.to_file_path().unwrap(), None)?,
|
||||||
};
|
};
|
||||||
let analysis = self.inner_cjs_analysis(specifier, &source)?;
|
let analysis = self.inner_cjs_analysis(specifier, &source)?;
|
||||||
match analysis {
|
match analysis {
|
||||||
|
|
|
@ -305,7 +305,7 @@ impl NpmModuleLoader {
|
||||||
let file_path = specifier.to_file_path().unwrap();
|
let file_path = specifier.to_file_path().unwrap();
|
||||||
let code = self
|
let code = self
|
||||||
.fs
|
.fs
|
||||||
.read_text_file_sync(&file_path)
|
.read_text_file_sync(&file_path, None)
|
||||||
.map_err(AnyError::from)
|
.map_err(AnyError::from)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
if file_path.is_dir() {
|
if file_path.is_dir() {
|
||||||
|
|
|
@ -5,6 +5,7 @@ use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use deno_runtime::deno_fs::AccessCheckCb;
|
||||||
use deno_runtime::deno_fs::FileSystem;
|
use deno_runtime::deno_fs::FileSystem;
|
||||||
use deno_runtime::deno_fs::FsDirEntry;
|
use deno_runtime::deno_fs::FsDirEntry;
|
||||||
use deno_runtime::deno_fs::FsFileType;
|
use deno_runtime::deno_fs::FsFileType;
|
||||||
|
@ -47,6 +48,7 @@ impl DenoCompileFileSystem {
|
||||||
create_new: false,
|
create_new: false,
|
||||||
mode: None,
|
mode: None,
|
||||||
},
|
},
|
||||||
|
None,
|
||||||
&old_file_bytes,
|
&old_file_bytes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -75,22 +77,24 @@ impl FileSystem for DenoCompileFileSystem {
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb>,
|
||||||
) -> FsResult<Rc<dyn File>> {
|
) -> FsResult<Rc<dyn File>> {
|
||||||
if self.0.is_path_within(path) {
|
if self.0.is_path_within(path) {
|
||||||
Ok(self.0.open_file(path)?)
|
Ok(self.0.open_file(path)?)
|
||||||
} else {
|
} else {
|
||||||
RealFs.open_sync(path, options)
|
RealFs.open_sync(path, options, access_check)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn open_async(
|
async fn open_async<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
) -> FsResult<Rc<dyn File>> {
|
) -> FsResult<Rc<dyn File>> {
|
||||||
if self.0.is_path_within(&path) {
|
if self.0.is_path_within(&path) {
|
||||||
Ok(self.0.open_file(&path)?)
|
Ok(self.0.open_file(&path)?)
|
||||||
} else {
|
} else {
|
||||||
RealFs.open_async(path, options).await
|
RealFs.open_async(path, options, access_check).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,7 @@ impl GitIgnoreTree {
|
||||||
});
|
});
|
||||||
let current = self
|
let current = self
|
||||||
.fs
|
.fs
|
||||||
.read_text_file_sync(&dir_path.join(".gitignore"))
|
.read_text_file_sync(&dir_path.join(".gitignore"), None)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|text| {
|
.and_then(|text| {
|
||||||
let mut builder = ignore::gitignore::GitignoreBuilder::new(dir_path);
|
let mut builder = ignore::gitignore::GitignoreBuilder::new(dir_path);
|
||||||
|
|
|
@ -19,6 +19,7 @@ use deno_io::fs::FsError;
|
||||||
use deno_io::fs::FsResult;
|
use deno_io::fs::FsResult;
|
||||||
use deno_io::fs::FsStat;
|
use deno_io::fs::FsStat;
|
||||||
|
|
||||||
|
use crate::interface::AccessCheckCb;
|
||||||
use crate::interface::FsDirEntry;
|
use crate::interface::FsDirEntry;
|
||||||
use crate::interface::FsFileType;
|
use crate::interface::FsFileType;
|
||||||
use crate::FileSystem;
|
use crate::FileSystem;
|
||||||
|
@ -48,6 +49,7 @@ impl InMemoryFs {
|
||||||
.write_file_sync(
|
.write_file_sync(
|
||||||
&path,
|
&path,
|
||||||
OpenOptions::write(true, false, false, None),
|
OpenOptions::write(true, false, false, None),
|
||||||
|
None,
|
||||||
&text.into_bytes(),
|
&text.into_bytes(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -82,15 +84,17 @@ impl FileSystem for InMemoryFs {
|
||||||
&self,
|
&self,
|
||||||
_path: &Path,
|
_path: &Path,
|
||||||
_options: OpenOptions,
|
_options: OpenOptions,
|
||||||
|
_access_check: Option<AccessCheckCb>,
|
||||||
) -> FsResult<Rc<dyn File>> {
|
) -> FsResult<Rc<dyn File>> {
|
||||||
Err(FsError::NotSupported)
|
Err(FsError::NotSupported)
|
||||||
}
|
}
|
||||||
async fn open_async(
|
async fn open_async<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
) -> FsResult<Rc<dyn File>> {
|
) -> FsResult<Rc<dyn File>> {
|
||||||
self.open_sync(&path, options)
|
self.open_sync(&path, options, access_check)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mkdir_sync(
|
fn mkdir_sync(
|
||||||
|
@ -350,6 +354,7 @@ impl FileSystem for InMemoryFs {
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
_access_check: Option<AccessCheckCb>,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> FsResult<()> {
|
) -> FsResult<()> {
|
||||||
let path = normalize_path(path);
|
let path = normalize_path(path);
|
||||||
|
@ -397,16 +402,21 @@ impl FileSystem for InMemoryFs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_file_async(
|
async fn write_file_async<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
) -> FsResult<()> {
|
) -> FsResult<()> {
|
||||||
self.write_file_sync(&path, options, &data)
|
self.write_file_sync(&path, options, access_check, &data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_file_sync(&self, path: &Path) -> FsResult<Vec<u8>> {
|
fn read_file_sync(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
_access_check: Option<AccessCheckCb>,
|
||||||
|
) -> FsResult<Vec<u8>> {
|
||||||
let entry = self.get_entry(path);
|
let entry = self.get_entry(path);
|
||||||
match entry {
|
match entry {
|
||||||
Some(entry) => match &*entry {
|
Some(entry) => match &*entry {
|
||||||
|
@ -419,7 +429,11 @@ impl FileSystem for InMemoryFs {
|
||||||
None => Err(FsError::Io(Error::new(ErrorKind::NotFound, "Not found"))),
|
None => Err(FsError::Io(Error::new(ErrorKind::NotFound, "Not found"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn read_file_async(&self, path: PathBuf) -> FsResult<Vec<u8>> {
|
async fn read_file_async<'a>(
|
||||||
self.read_file_sync(&path)
|
&'a self,
|
||||||
|
path: PathBuf,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
|
) -> FsResult<Vec<u8>> {
|
||||||
|
self.read_file_sync(&path, access_check)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,25 @@ pub struct FsDirEntry {
|
||||||
#[allow(clippy::disallowed_types)]
|
#[allow(clippy::disallowed_types)]
|
||||||
pub type FileSystemRc = crate::sync::MaybeArc<dyn FileSystem>;
|
pub type FileSystemRc = crate::sync::MaybeArc<dyn FileSystem>;
|
||||||
|
|
||||||
|
pub trait AccessCheckFn:
|
||||||
|
for<'a> FnMut(
|
||||||
|
bool,
|
||||||
|
&'a Path,
|
||||||
|
&'a OpenOptions,
|
||||||
|
) -> FsResult<std::borrow::Cow<'a, Path>>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
impl<T> AccessCheckFn for T where
|
||||||
|
T: for<'a> FnMut(
|
||||||
|
bool,
|
||||||
|
&'a Path,
|
||||||
|
&'a OpenOptions,
|
||||||
|
) -> FsResult<std::borrow::Cow<'a, Path>>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type AccessCheckCb<'a> = &'a mut (dyn AccessCheckFn + 'a);
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
|
pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
|
||||||
fn cwd(&self) -> FsResult<PathBuf>;
|
fn cwd(&self) -> FsResult<PathBuf>;
|
||||||
|
@ -91,11 +110,13 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb>,
|
||||||
) -> FsResult<Rc<dyn File>>;
|
) -> FsResult<Rc<dyn File>>;
|
||||||
async fn open_async(
|
async fn open_async<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
) -> FsResult<Rc<dyn File>>;
|
) -> FsResult<Rc<dyn File>>;
|
||||||
|
|
||||||
fn mkdir_sync(&self, path: &Path, recursive: bool, mode: u32)
|
fn mkdir_sync(&self, path: &Path, recursive: bool, mode: u32)
|
||||||
|
@ -202,22 +223,24 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb>,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> FsResult<()> {
|
) -> FsResult<()> {
|
||||||
let file = self.open_sync(path, options)?;
|
let file = self.open_sync(path, options, access_check)?;
|
||||||
if let Some(mode) = options.mode {
|
if let Some(mode) = options.mode {
|
||||||
file.clone().chmod_sync(mode)?;
|
file.clone().chmod_sync(mode)?;
|
||||||
}
|
}
|
||||||
file.write_all_sync(data)?;
|
file.write_all_sync(data)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn write_file_async(
|
async fn write_file_async<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
) -> FsResult<()> {
|
) -> FsResult<()> {
|
||||||
let file = self.open_async(path, options).await?;
|
let file = self.open_async(path, options, access_check).await?;
|
||||||
if let Some(mode) = options.mode {
|
if let Some(mode) = options.mode {
|
||||||
file.clone().chmod_async(mode).await?;
|
file.clone().chmod_async(mode).await?;
|
||||||
}
|
}
|
||||||
|
@ -225,15 +248,23 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_file_sync(&self, path: &Path) -> FsResult<Vec<u8>> {
|
fn read_file_sync(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
access_check: Option<AccessCheckCb>,
|
||||||
|
) -> FsResult<Vec<u8>> {
|
||||||
let options = OpenOptions::read();
|
let options = OpenOptions::read();
|
||||||
let file = self.open_sync(path, options)?;
|
let file = self.open_sync(path, options, access_check)?;
|
||||||
let buf = file.read_all_sync()?;
|
let buf = file.read_all_sync()?;
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
async fn read_file_async(&self, path: PathBuf) -> FsResult<Vec<u8>> {
|
async fn read_file_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
path: PathBuf,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
|
) -> FsResult<Vec<u8>> {
|
||||||
let options = OpenOptions::read();
|
let options = OpenOptions::read();
|
||||||
let file = self.open_async(path, options).await?;
|
let file = self.open_async(path, options, access_check).await?;
|
||||||
let buf = file.read_all_async().await?;
|
let buf = file.read_all_async().await?;
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
@ -253,14 +284,22 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
|
||||||
self.stat_sync(path).is_ok()
|
self.stat_sync(path).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_text_file_sync(&self, path: &Path) -> FsResult<String> {
|
fn read_text_file_sync(
|
||||||
let buf = self.read_file_sync(path)?;
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
access_check: Option<AccessCheckCb>,
|
||||||
|
) -> FsResult<String> {
|
||||||
|
let buf = self.read_file_sync(path, access_check)?;
|
||||||
String::from_utf8(buf).map_err(|err| {
|
String::from_utf8(buf).map_err(|err| {
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, err).into()
|
std::io::Error::new(std::io::ErrorKind::InvalidData, err).into()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
async fn read_text_file_async(&self, path: PathBuf) -> FsResult<String> {
|
async fn read_text_file_async<'a>(
|
||||||
let buf = self.read_file_async(path).await?;
|
&'a self,
|
||||||
|
path: PathBuf,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
|
) -> FsResult<String> {
|
||||||
|
let buf = self.read_file_async(path, access_check).await?;
|
||||||
String::from_utf8(buf).map_err(|err| {
|
String::from_utf8(buf).map_err(|err| {
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, err).into()
|
std::io::Error::new(std::io::ErrorKind::InvalidData, err).into()
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,6 +7,8 @@ mod std_fs;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
|
||||||
pub use crate::in_memory_fs::InMemoryFs;
|
pub use crate::in_memory_fs::InMemoryFs;
|
||||||
|
pub use crate::interface::AccessCheckCb;
|
||||||
|
pub use crate::interface::AccessCheckFn;
|
||||||
pub use crate::interface::FileSystem;
|
pub use crate::interface::FileSystem;
|
||||||
pub use crate::interface::FileSystemRc;
|
pub use crate::interface::FileSystemRc;
|
||||||
pub use crate::interface::FsDirEntry;
|
pub use crate::interface::FsDirEntry;
|
||||||
|
@ -20,9 +22,18 @@ use crate::ops::*;
|
||||||
|
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
|
use deno_io::fs::FsError;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub trait FsPermissions {
|
pub trait FsPermissions: Send + Sync {
|
||||||
|
fn check_open<'a>(
|
||||||
|
&mut self,
|
||||||
|
resolved: bool,
|
||||||
|
read: bool,
|
||||||
|
write: bool,
|
||||||
|
path: &'a Path,
|
||||||
|
api_name: &str,
|
||||||
|
) -> Result<std::borrow::Cow<'a, Path>, FsError>;
|
||||||
fn check_read(&mut self, path: &Path, api_name: &str)
|
fn check_read(&mut self, path: &Path, api_name: &str)
|
||||||
-> Result<(), AnyError>;
|
-> Result<(), AnyError>;
|
||||||
fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError>;
|
fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError>;
|
||||||
|
@ -50,19 +61,20 @@ pub trait FsPermissions {
|
||||||
api_name: &str,
|
api_name: &str,
|
||||||
) -> Result<(), AnyError>;
|
) -> Result<(), AnyError>;
|
||||||
|
|
||||||
fn check(
|
fn check<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
resolved: bool,
|
||||||
open_options: &OpenOptions,
|
open_options: &OpenOptions,
|
||||||
path: &Path,
|
path: &'a Path,
|
||||||
api_name: &str,
|
api_name: &str,
|
||||||
) -> Result<(), AnyError> {
|
) -> Result<std::borrow::Cow<'a, Path>, FsError> {
|
||||||
if open_options.read {
|
self.check_open(
|
||||||
self.check_read(path, api_name)?;
|
resolved,
|
||||||
}
|
open_options.read,
|
||||||
if open_options.write || open_options.append {
|
open_options.write || open_options.append,
|
||||||
self.check_write(path, api_name)?;
|
path,
|
||||||
}
|
api_name,
|
||||||
Ok(())
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
153
ext/fs/ops.rs
153
ext/fs/ops.rs
|
@ -28,12 +28,55 @@ use rand::Rng;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::check_unstable;
|
use crate::check_unstable;
|
||||||
|
use crate::interface::AccessCheckFn;
|
||||||
use crate::interface::FileSystemRc;
|
use crate::interface::FileSystemRc;
|
||||||
use crate::interface::FsDirEntry;
|
use crate::interface::FsDirEntry;
|
||||||
use crate::interface::FsFileType;
|
use crate::interface::FsFileType;
|
||||||
use crate::FsPermissions;
|
use crate::FsPermissions;
|
||||||
use crate::OpenOptions;
|
use crate::OpenOptions;
|
||||||
|
|
||||||
|
fn sync_permission_check<'a, P: FsPermissions + 'static>(
|
||||||
|
permissions: &'a mut P,
|
||||||
|
api_name: &'static str,
|
||||||
|
) -> impl AccessCheckFn + 'a {
|
||||||
|
move |resolved, path, options| {
|
||||||
|
permissions.check(resolved, options, path, api_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn async_permission_check<P: FsPermissions + 'static>(
|
||||||
|
state: Rc<RefCell<OpState>>,
|
||||||
|
api_name: &'static str,
|
||||||
|
) -> impl AccessCheckFn {
|
||||||
|
move |resolved, path, options| {
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
let permissions = state.borrow_mut::<P>();
|
||||||
|
permissions.check(resolved, options, path, api_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_permission_error(
|
||||||
|
operation: &'static str,
|
||||||
|
error: FsError,
|
||||||
|
path: &Path,
|
||||||
|
) -> AnyError {
|
||||||
|
match error {
|
||||||
|
FsError::PermissionDenied(err) => {
|
||||||
|
let path = format!("{path:?}");
|
||||||
|
let (path, truncated) = if path.len() > 1024 {
|
||||||
|
(&path[0..1024], "...(truncated)")
|
||||||
|
} else {
|
||||||
|
(path.as_str(), "")
|
||||||
|
};
|
||||||
|
custom_error("PermissionDenied", format!("Requires {err} access to {path}{truncated}, run again with the --allow-{err} flag"))
|
||||||
|
}
|
||||||
|
err => Err::<(), _>(err)
|
||||||
|
.context_path(operation, path)
|
||||||
|
.err()
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[op2]
|
#[op2]
|
||||||
#[string]
|
#[string]
|
||||||
pub fn op_fs_cwd<P>(state: &mut OpState) -> Result<String, AnyError>
|
pub fn op_fs_cwd<P>(state: &mut OpState) -> Result<String, AnyError>
|
||||||
|
@ -89,12 +132,14 @@ where
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
|
|
||||||
let options = options.unwrap_or_else(OpenOptions::read);
|
let options = options.unwrap_or_else(OpenOptions::read);
|
||||||
let permissions = state.borrow_mut::<P>();
|
|
||||||
permissions.check(&options, &path, "Deno.openSync()")?;
|
|
||||||
|
|
||||||
let fs = state.borrow::<FileSystemRc>();
|
|
||||||
let file = fs.open_sync(&path, options).context_path("open", &path)?;
|
|
||||||
|
|
||||||
|
let fs = state.borrow::<FileSystemRc>().clone();
|
||||||
|
let mut access_check =
|
||||||
|
sync_permission_check::<P>(state.borrow_mut(), "Deno.openSync()");
|
||||||
|
let file = fs
|
||||||
|
.open_sync(&path, options, Some(&mut access_check))
|
||||||
|
.map_err(|error| map_permission_error("open", error, &path))?;
|
||||||
|
drop(access_check);
|
||||||
let rid = state
|
let rid = state
|
||||||
.resource_table
|
.resource_table
|
||||||
.add(FileResource::new(file, "fsFile".to_string()));
|
.add(FileResource::new(file, "fsFile".to_string()));
|
||||||
|
@ -114,16 +159,13 @@ where
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
|
|
||||||
let options = options.unwrap_or_else(OpenOptions::read);
|
let options = options.unwrap_or_else(OpenOptions::read);
|
||||||
let fs = {
|
let mut access_check =
|
||||||
let mut state = state.borrow_mut();
|
async_permission_check::<P>(state.clone(), "Deno.open()");
|
||||||
let permissions = state.borrow_mut::<P>();
|
let fs = state.borrow().borrow::<FileSystemRc>().clone();
|
||||||
permissions.check(&options, &path, "Deno.open()")?;
|
|
||||||
state.borrow::<FileSystemRc>().clone()
|
|
||||||
};
|
|
||||||
let file = fs
|
let file = fs
|
||||||
.open_async(path.clone(), options)
|
.open_async(path.clone(), options, Some(&mut access_check))
|
||||||
.await
|
.await
|
||||||
.context_path("open", &path)?;
|
.map_err(|error| map_permission_error("open", error, &path))?;
|
||||||
|
|
||||||
let rid = state
|
let rid = state
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
|
@ -961,11 +1003,10 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
const MAX_TRIES: u32 = 10;
|
const MAX_TRIES: u32 = 10;
|
||||||
for _ in 0..MAX_TRIES {
|
for _ in 0..MAX_TRIES {
|
||||||
let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
|
let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
|
||||||
match fs.open_sync(&path, open_opts) {
|
match fs.open_sync(&path, open_opts, None) {
|
||||||
Ok(_) => return path_into_string(path.into_os_string()),
|
Ok(_) => return path_into_string(path.into_os_string()),
|
||||||
Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
|
Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1007,7 +1048,7 @@ where
|
||||||
const MAX_TRIES: u32 = 10;
|
const MAX_TRIES: u32 = 10;
|
||||||
for _ in 0..MAX_TRIES {
|
for _ in 0..MAX_TRIES {
|
||||||
let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
|
let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
|
||||||
match fs.clone().open_async(path.clone(), open_opts).await {
|
match fs.clone().open_async(path.clone(), open_opts, None).await {
|
||||||
Ok(_) => return path_into_string(path.into_os_string()),
|
Ok(_) => return path_into_string(path.into_os_string()),
|
||||||
Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
|
Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1150,14 +1191,13 @@ where
|
||||||
{
|
{
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
|
|
||||||
let permissions = state.borrow_mut::<P>();
|
|
||||||
let options = OpenOptions::write(create, append, create_new, mode);
|
let options = OpenOptions::write(create, append, create_new, mode);
|
||||||
permissions.check(&options, &path, "Deno.writeFileSync()")?;
|
let fs = state.borrow::<FileSystemRc>().clone();
|
||||||
|
let mut access_check =
|
||||||
|
sync_permission_check::<P>(state.borrow_mut(), "Deno.writeFileSync()");
|
||||||
|
|
||||||
let fs = state.borrow::<FileSystemRc>();
|
fs.write_file_sync(&path, options, Some(&mut access_check), &data)
|
||||||
|
.map_err(|error| map_permission_error("writefile", error, &path))?;
|
||||||
fs.write_file_sync(&path, options, &data)
|
|
||||||
.context_path("writefile", &path)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1181,16 +1221,21 @@ where
|
||||||
|
|
||||||
let options = OpenOptions::write(create, append, create_new, mode);
|
let options = OpenOptions::write(create, append, create_new, mode);
|
||||||
|
|
||||||
|
let mut access_check =
|
||||||
|
async_permission_check::<P>(state.clone(), "Deno.writeFile()");
|
||||||
let (fs, cancel_handle) = {
|
let (fs, cancel_handle) = {
|
||||||
let mut state = state.borrow_mut();
|
let state = state.borrow_mut();
|
||||||
let permissions = state.borrow_mut::<P>();
|
|
||||||
permissions.check(&options, &path, "Deno.writeFile()")?;
|
|
||||||
let cancel_handle = cancel_rid
|
let cancel_handle = cancel_rid
|
||||||
.and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
|
.and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
|
||||||
(state.borrow::<FileSystemRc>().clone(), cancel_handle)
|
(state.borrow::<FileSystemRc>().clone(), cancel_handle)
|
||||||
};
|
};
|
||||||
|
|
||||||
let fut = fs.write_file_async(path.clone(), options, data.to_vec());
|
let fut = fs.write_file_async(
|
||||||
|
path.clone(),
|
||||||
|
options,
|
||||||
|
Some(&mut access_check),
|
||||||
|
data.to_vec(),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(cancel_handle) = cancel_handle {
|
if let Some(cancel_handle) = cancel_handle {
|
||||||
let res = fut.or_cancel(cancel_handle).await;
|
let res = fut.or_cancel(cancel_handle).await;
|
||||||
|
@ -1201,9 +1246,11 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
res?.context_path("writefile", &path)?;
|
res?.map_err(|error| map_permission_error("writefile", error, &path))?;
|
||||||
} else {
|
} else {
|
||||||
fut.await.context_path("writefile", &path)?;
|
fut
|
||||||
|
.await
|
||||||
|
.map_err(|error| map_permission_error("writefile", error, &path))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1220,11 +1267,12 @@ where
|
||||||
{
|
{
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
|
|
||||||
let permissions = state.borrow_mut::<P>();
|
let fs = state.borrow::<FileSystemRc>().clone();
|
||||||
permissions.check_read(&path, "Deno.readFileSync()")?;
|
let mut access_check =
|
||||||
|
sync_permission_check::<P>(state.borrow_mut(), "Deno.readFileSync()");
|
||||||
let fs = state.borrow::<FileSystemRc>();
|
let buf = fs
|
||||||
let buf = fs.read_file_sync(&path).context_path("readfile", &path)?;
|
.read_file_sync(&path, Some(&mut access_check))
|
||||||
|
.map_err(|error| map_permission_error("readfile", error, &path))?;
|
||||||
|
|
||||||
Ok(buf.into())
|
Ok(buf.into())
|
||||||
}
|
}
|
||||||
|
@ -1241,16 +1289,16 @@ where
|
||||||
{
|
{
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
|
|
||||||
|
let mut access_check =
|
||||||
|
async_permission_check::<P>(state.clone(), "Deno.readFile()");
|
||||||
let (fs, cancel_handle) = {
|
let (fs, cancel_handle) = {
|
||||||
let mut state = state.borrow_mut();
|
let state = state.borrow();
|
||||||
let permissions = state.borrow_mut::<P>();
|
|
||||||
permissions.check_read(&path, "Deno.readFile()")?;
|
|
||||||
let cancel_handle = cancel_rid
|
let cancel_handle = cancel_rid
|
||||||
.and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
|
.and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
|
||||||
(state.borrow::<FileSystemRc>().clone(), cancel_handle)
|
(state.borrow::<FileSystemRc>().clone(), cancel_handle)
|
||||||
};
|
};
|
||||||
|
|
||||||
let fut = fs.read_file_async(path.clone());
|
let fut = fs.read_file_async(path.clone(), Some(&mut access_check));
|
||||||
|
|
||||||
let buf = if let Some(cancel_handle) = cancel_handle {
|
let buf = if let Some(cancel_handle) = cancel_handle {
|
||||||
let res = fut.or_cancel(cancel_handle).await;
|
let res = fut.or_cancel(cancel_handle).await;
|
||||||
|
@ -1261,9 +1309,11 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
res?.context_path("readfile", &path)?
|
res?.map_err(|error| map_permission_error("readfile", error, &path))?
|
||||||
} else {
|
} else {
|
||||||
fut.await.context_path("readfile", &path)?
|
fut
|
||||||
|
.await
|
||||||
|
.map_err(|error| map_permission_error("readfile", error, &path))?
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(buf.into())
|
Ok(buf.into())
|
||||||
|
@ -1280,11 +1330,12 @@ where
|
||||||
{
|
{
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
|
|
||||||
let permissions = state.borrow_mut::<P>();
|
let fs = state.borrow::<FileSystemRc>().clone();
|
||||||
permissions.check_read(&path, "Deno.readFileSync()")?;
|
let mut access_check =
|
||||||
|
sync_permission_check::<P>(state.borrow_mut(), "Deno.readFileSync()");
|
||||||
let fs = state.borrow::<FileSystemRc>();
|
let buf = fs
|
||||||
let buf = fs.read_file_sync(&path).context_path("readfile", &path)?;
|
.read_file_sync(&path, Some(&mut access_check))
|
||||||
|
.map_err(|error| map_permission_error("readfile", error, &path))?;
|
||||||
|
|
||||||
Ok(string_from_utf8_lossy(buf))
|
Ok(string_from_utf8_lossy(buf))
|
||||||
}
|
}
|
||||||
|
@ -1301,16 +1352,16 @@ where
|
||||||
{
|
{
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
|
|
||||||
|
let mut access_check =
|
||||||
|
async_permission_check::<P>(state.clone(), "Deno.readFile()");
|
||||||
let (fs, cancel_handle) = {
|
let (fs, cancel_handle) = {
|
||||||
let mut state = state.borrow_mut();
|
let state = state.borrow_mut();
|
||||||
let permissions = state.borrow_mut::<P>();
|
|
||||||
permissions.check_read(&path, "Deno.readFile()")?;
|
|
||||||
let cancel_handle = cancel_rid
|
let cancel_handle = cancel_rid
|
||||||
.and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
|
.and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
|
||||||
(state.borrow::<FileSystemRc>().clone(), cancel_handle)
|
(state.borrow::<FileSystemRc>().clone(), cancel_handle)
|
||||||
};
|
};
|
||||||
|
|
||||||
let fut = fs.read_file_async(path.clone());
|
let fut = fs.read_file_async(path.clone(), Some(&mut access_check));
|
||||||
|
|
||||||
let buf = if let Some(cancel_handle) = cancel_handle {
|
let buf = if let Some(cancel_handle) = cancel_handle {
|
||||||
let res = fut.or_cancel(cancel_handle).await;
|
let res = fut.or_cancel(cancel_handle).await;
|
||||||
|
@ -1321,9 +1372,11 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
res?.context_path("readfile", &path)?
|
res?.map_err(|error| map_permission_error("readfile", error, &path))?
|
||||||
} else {
|
} else {
|
||||||
fut.await.context_path("readfile", &path)?
|
fut
|
||||||
|
.await
|
||||||
|
.map_err(|error| map_permission_error("readfile", error, &path))?
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(string_from_utf8_lossy(buf))
|
Ok(string_from_utf8_lossy(buf))
|
||||||
|
|
128
ext/fs/std_fs.rs
128
ext/fs/std_fs.rs
|
@ -2,27 +2,29 @@
|
||||||
|
|
||||||
#![allow(clippy::disallowed_methods)]
|
#![allow(clippy::disallowed_methods)]
|
||||||
|
|
||||||
|
use std::env::current_dir;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::io::Read;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use deno_core::normalize_path;
|
||||||
use deno_core::unsync::spawn_blocking;
|
use deno_core::unsync::spawn_blocking;
|
||||||
use deno_io::fs::File;
|
use deno_io::fs::File;
|
||||||
|
use deno_io::fs::FsError;
|
||||||
use deno_io::fs::FsResult;
|
use deno_io::fs::FsResult;
|
||||||
use deno_io::fs::FsStat;
|
use deno_io::fs::FsStat;
|
||||||
use deno_io::StdFileResourceInner;
|
use deno_io::StdFileResourceInner;
|
||||||
|
|
||||||
|
use crate::interface::AccessCheckCb;
|
||||||
use crate::interface::FsDirEntry;
|
use crate::interface::FsDirEntry;
|
||||||
use crate::interface::FsFileType;
|
use crate::interface::FsFileType;
|
||||||
use crate::FileSystem;
|
use crate::FileSystem;
|
||||||
use crate::OpenOptions;
|
use crate::OpenOptions;
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
use deno_io::fs::FsError;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RealFs;
|
pub struct RealFs;
|
||||||
|
|
||||||
|
@ -80,18 +82,18 @@ impl FileSystem for RealFs {
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb>,
|
||||||
) -> FsResult<Rc<dyn File>> {
|
) -> FsResult<Rc<dyn File>> {
|
||||||
let opts = open_options(options);
|
let std_file = open_with_access_check(options, path, access_check)?;
|
||||||
let std_file = opts.open(path)?;
|
|
||||||
Ok(Rc::new(StdFileResourceInner::file(std_file)))
|
Ok(Rc::new(StdFileResourceInner::file(std_file)))
|
||||||
}
|
}
|
||||||
async fn open_async(
|
async fn open_async<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
) -> FsResult<Rc<dyn File>> {
|
) -> FsResult<Rc<dyn File>> {
|
||||||
let opts = open_options(options);
|
let std_file = open_with_access_check(options, &path, access_check)?;
|
||||||
let std_file = spawn_blocking(move || opts.open(path)).await??;
|
|
||||||
Ok(Rc::new(StdFileResourceInner::file(std_file)))
|
Ok(Rc::new(StdFileResourceInner::file(std_file)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,10 +278,10 @@ impl FileSystem for RealFs {
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb>,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> FsResult<()> {
|
) -> FsResult<()> {
|
||||||
let opts = open_options(options);
|
let mut file = open_with_access_check(options, path, access_check)?;
|
||||||
let mut file = opts.open(path)?;
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(mode) = options.mode {
|
if let Some(mode) = options.mode {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
@ -289,15 +291,15 @@ impl FileSystem for RealFs {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_file_async(
|
async fn write_file_async<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
options: OpenOptions,
|
options: OpenOptions,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
) -> FsResult<()> {
|
) -> FsResult<()> {
|
||||||
|
let mut file = open_with_access_check(options, &path, access_check)?;
|
||||||
spawn_blocking(move || {
|
spawn_blocking(move || {
|
||||||
let opts = open_options(options);
|
|
||||||
let mut file = opts.open(path)?;
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(mode) = options.mode {
|
if let Some(mode) = options.mode {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
@ -309,11 +311,41 @@ impl FileSystem for RealFs {
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_file_sync(&self, path: &Path) -> FsResult<Vec<u8>> {
|
fn read_file_sync(
|
||||||
fs::read(path).map_err(Into::into)
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
access_check: Option<AccessCheckCb>,
|
||||||
|
) -> FsResult<Vec<u8>> {
|
||||||
|
let mut file = open_with_access_check(
|
||||||
|
OpenOptions {
|
||||||
|
read: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
path,
|
||||||
|
access_check,
|
||||||
|
)?;
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
file.read_to_end(&mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
}
|
}
|
||||||
async fn read_file_async(&self, path: PathBuf) -> FsResult<Vec<u8>> {
|
async fn read_file_async<'a>(
|
||||||
spawn_blocking(move || fs::read(path))
|
&'a self,
|
||||||
|
path: PathBuf,
|
||||||
|
access_check: Option<AccessCheckCb<'a>>,
|
||||||
|
) -> FsResult<Vec<u8>> {
|
||||||
|
let mut file = open_with_access_check(
|
||||||
|
OpenOptions {
|
||||||
|
read: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&path,
|
||||||
|
access_check,
|
||||||
|
)?;
|
||||||
|
spawn_blocking(move || {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
file.read_to_end(&mut buf)?;
|
||||||
|
Ok::<_, FsError>(buf)
|
||||||
|
})
|
||||||
.await?
|
.await?
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
@ -410,7 +442,6 @@ fn copy_file(from: &Path, to: &Path) -> FsResult<()> {
|
||||||
use libc::stat;
|
use libc::stat;
|
||||||
use libc::unlink;
|
use libc::unlink;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::io::Read;
|
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
@ -845,3 +876,60 @@ fn open_options(options: OpenOptions) -> fs::OpenOptions {
|
||||||
open_options.create_new(options.create_new);
|
open_options.create_new(options.create_new);
|
||||||
open_options
|
open_options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn open_with_access_check(
|
||||||
|
options: OpenOptions,
|
||||||
|
path: &Path,
|
||||||
|
access_check: Option<AccessCheckCb>,
|
||||||
|
) -> FsResult<std::fs::File> {
|
||||||
|
if let Some(access_check) = access_check {
|
||||||
|
let path = if path.is_absolute() {
|
||||||
|
normalize_path(path)
|
||||||
|
} else {
|
||||||
|
let cwd = current_dir()?;
|
||||||
|
normalize_path(cwd.join(path))
|
||||||
|
};
|
||||||
|
(*access_check)(false, &path, &options)?;
|
||||||
|
// On Linux, /proc may contain magic links that we don't want to resolve
|
||||||
|
let needs_canonicalization =
|
||||||
|
!cfg!(target_os = "linux") || path.starts_with("/proc");
|
||||||
|
let path = if needs_canonicalization {
|
||||||
|
match path.canonicalize() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(_) => {
|
||||||
|
if let (Some(parent), Some(filename)) =
|
||||||
|
(path.parent(), path.file_name())
|
||||||
|
{
|
||||||
|
parent.canonicalize()?.join(filename)
|
||||||
|
} else {
|
||||||
|
return Err(std::io::ErrorKind::NotFound.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
(*access_check)(true, &path, &options)?;
|
||||||
|
|
||||||
|
// For windows
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut opts: fs::OpenOptions = open_options(options);
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
// Don't follow symlinks on open -- we must always pass fully-resolved files
|
||||||
|
// with the exception of /proc/ which is too special, and /dev/std* which might point to
|
||||||
|
// proc.
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
if needs_canonicalization {
|
||||||
|
opts.custom_flags(libc::O_NOFOLLOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(opts.open(&path)?)
|
||||||
|
} else {
|
||||||
|
let opts = open_options(options);
|
||||||
|
Ok(opts.open(path)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
15
ext/io/fs.rs
15
ext/io/fs.rs
|
@ -6,6 +6,7 @@ use std::rc::Rc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use std::time::UNIX_EPOCH;
|
use std::time::UNIX_EPOCH;
|
||||||
|
|
||||||
|
use deno_core::error::custom_error;
|
||||||
use deno_core::error::not_supported;
|
use deno_core::error::not_supported;
|
||||||
use deno_core::error::resource_unavailable;
|
use deno_core::error::resource_unavailable;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
|
@ -21,6 +22,7 @@ pub enum FsError {
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
FileBusy,
|
FileBusy,
|
||||||
NotSupported,
|
NotSupported,
|
||||||
|
PermissionDenied(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FsError {
|
impl FsError {
|
||||||
|
@ -29,6 +31,7 @@ impl FsError {
|
||||||
Self::Io(err) => err.kind(),
|
Self::Io(err) => err.kind(),
|
||||||
Self::FileBusy => io::ErrorKind::Other,
|
Self::FileBusy => io::ErrorKind::Other,
|
||||||
Self::NotSupported => io::ErrorKind::Other,
|
Self::NotSupported => io::ErrorKind::Other,
|
||||||
|
Self::PermissionDenied(_) => io::ErrorKind::PermissionDenied,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +40,9 @@ impl FsError {
|
||||||
FsError::Io(err) => err,
|
FsError::Io(err) => err,
|
||||||
FsError::FileBusy => io::Error::new(self.kind(), "file busy"),
|
FsError::FileBusy => io::Error::new(self.kind(), "file busy"),
|
||||||
FsError::NotSupported => io::Error::new(self.kind(), "not supported"),
|
FsError::NotSupported => io::Error::new(self.kind(), "not supported"),
|
||||||
|
FsError::PermissionDenied(err) => {
|
||||||
|
io::Error::new(self.kind(), format!("requires {err} access"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,12 +53,21 @@ impl From<io::Error> for FsError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<io::ErrorKind> for FsError {
|
||||||
|
fn from(err: io::ErrorKind) -> Self {
|
||||||
|
Self::Io(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<FsError> for AnyError {
|
impl From<FsError> for AnyError {
|
||||||
fn from(err: FsError) -> Self {
|
fn from(err: FsError) -> Self {
|
||||||
match err {
|
match err {
|
||||||
FsError::Io(err) => AnyError::from(err),
|
FsError::Io(err) => AnyError::from(err),
|
||||||
FsError::FileBusy => resource_unavailable(),
|
FsError::FileBusy => resource_unavailable(),
|
||||||
FsError::NotSupported => not_supported(),
|
FsError::NotSupported => not_supported(),
|
||||||
|
FsError::PermissionDenied(err) => {
|
||||||
|
custom_error("PermissionDenied", format!("permission denied: {err}"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -452,7 +452,7 @@ where
|
||||||
let file_path = PathBuf::from(file_path);
|
let file_path = PathBuf::from(file_path);
|
||||||
ensure_read_permission::<P>(state, &file_path)?;
|
ensure_read_permission::<P>(state, &file_path)?;
|
||||||
let fs = state.borrow::<FileSystemRc>();
|
let fs = state.borrow::<FileSystemRc>();
|
||||||
Ok(fs.read_text_file_sync(&file_path)?)
|
Ok(fs.read_text_file_sync(&file_path, None)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op2]
|
#[op2]
|
||||||
|
|
|
@ -82,7 +82,7 @@ impl PackageJson {
|
||||||
return Ok(CACHE.with(|cache| cache.borrow()[&path].clone()));
|
return Ok(CACHE.with(|cache| cache.borrow()[&path].clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let source = match fs.read_text_file_sync(&path) {
|
let source = match fs.read_text_file_sync(&path, None) {
|
||||||
Ok(source) => source,
|
Ok(source) => source,
|
||||||
Err(err) if err.kind() == ErrorKind::NotFound => {
|
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||||
return Ok(Rc::new(PackageJson::empty(path)));
|
return Ok(Rc::new(PackageJson::empty(path)));
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::url::Url;
|
use deno_core::url::Url;
|
||||||
|
pub use deno_io::fs::FsError;
|
||||||
pub use deno_permissions::create_child_permissions;
|
pub use deno_permissions::create_child_permissions;
|
||||||
pub use deno_permissions::parse_sys_kind;
|
pub use deno_permissions::parse_sys_kind;
|
||||||
pub use deno_permissions::set_prompt_callbacks;
|
pub use deno_permissions::set_prompt_callbacks;
|
||||||
|
@ -142,6 +144,34 @@ impl deno_websocket::WebSocketPermissions for PermissionsContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl deno_fs::FsPermissions for PermissionsContainer {
|
impl deno_fs::FsPermissions for PermissionsContainer {
|
||||||
|
fn check_open<'a>(
|
||||||
|
&mut self,
|
||||||
|
resolved: bool,
|
||||||
|
read: bool,
|
||||||
|
write: bool,
|
||||||
|
path: &'a Path,
|
||||||
|
api_name: &str,
|
||||||
|
) -> Result<Cow<'a, Path>, FsError> {
|
||||||
|
if resolved {
|
||||||
|
self.check_special_file(path, api_name).map_err(|_| {
|
||||||
|
std::io::Error::from(std::io::ErrorKind::PermissionDenied)
|
||||||
|
})?;
|
||||||
|
return Ok(Cow::Borrowed(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If somehow read or write aren't specified, use read
|
||||||
|
let read = read || !write;
|
||||||
|
if read {
|
||||||
|
deno_fs::FsPermissions::check_read(self, path, api_name)
|
||||||
|
.map_err(|_| FsError::PermissionDenied("read"))?;
|
||||||
|
}
|
||||||
|
if write {
|
||||||
|
deno_fs::FsPermissions::check_write(self, path, api_name)
|
||||||
|
.map_err(|_| FsError::PermissionDenied("write"))?;
|
||||||
|
}
|
||||||
|
Ok(Cow::Borrowed(path))
|
||||||
|
}
|
||||||
|
|
||||||
fn check_read(
|
fn check_read(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
|
|
@ -21,6 +21,7 @@ use fqdn::FQDN;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
@ -1641,6 +1642,91 @@ impl PermissionsContainer {
|
||||||
self.0.lock().env.check_all()
|
self.0.lock().env.check_all()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn check_sys_all(&mut self) -> Result<(), AnyError> {
|
||||||
|
self.0.lock().sys.check_all()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn check_ffi_all(&mut self) -> Result<(), AnyError> {
|
||||||
|
self.0.lock().ffi.check_all()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This checks to see if the allow-all flag was passed, not whether all
|
||||||
|
/// permissions are enabled!
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn check_was_allow_all_flag_passed(&mut self) -> Result<(), AnyError> {
|
||||||
|
self.0.lock().all.check()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks special file access, returning the failed permission type if
|
||||||
|
/// not successful.
|
||||||
|
pub fn check_special_file(
|
||||||
|
&mut self,
|
||||||
|
path: &Path,
|
||||||
|
_api_name: &str,
|
||||||
|
) -> Result<(), &'static str> {
|
||||||
|
let error_all = |_| "all";
|
||||||
|
|
||||||
|
// Safe files with no major additional side-effects. While there's a small risk of someone
|
||||||
|
// draining system entropy by just reading one of these files constantly, that's not really
|
||||||
|
// something we worry about as they already have --allow-read to /dev.
|
||||||
|
if cfg!(unix)
|
||||||
|
&& (path == OsStr::new("/dev/random")
|
||||||
|
|| path == OsStr::new("/dev/urandom")
|
||||||
|
|| path == OsStr::new("/dev/zero")
|
||||||
|
|| path == OsStr::new("/dev/null"))
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
if path.starts_with("/dev")
|
||||||
|
|| path.starts_with("/proc")
|
||||||
|
|| path.starts_with("/sys")
|
||||||
|
{
|
||||||
|
if path.ends_with("/environ") {
|
||||||
|
self.check_env_all().map_err(|_| "env")?;
|
||||||
|
} else {
|
||||||
|
self.check_was_allow_all_flag_passed().map_err(error_all)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path.starts_with("/etc") {
|
||||||
|
self.check_was_allow_all_flag_passed().map_err(error_all)?;
|
||||||
|
}
|
||||||
|
} else if cfg!(unix) {
|
||||||
|
if path.starts_with("/dev") {
|
||||||
|
self.check_was_allow_all_flag_passed().map_err(error_all)?;
|
||||||
|
}
|
||||||
|
if path.starts_with("/etc") {
|
||||||
|
self.check_was_allow_all_flag_passed().map_err(error_all)?;
|
||||||
|
}
|
||||||
|
if path.starts_with("/private/etc") {
|
||||||
|
self.check_was_allow_all_flag_passed().map_err(error_all)?;
|
||||||
|
}
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
fn is_normalized_windows_drive_path(path: &Path) -> bool {
|
||||||
|
let s = path.as_os_str().as_encoded_bytes();
|
||||||
|
// \\?\X:\
|
||||||
|
if s.len() < 7 {
|
||||||
|
false
|
||||||
|
} else if s.starts_with(br#"\\?\"#) {
|
||||||
|
s[4].is_ascii_alphabetic() && s[5] == b':' && s[6] == b'\\'
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a normalized drive path, accept it
|
||||||
|
if !is_normalized_windows_drive_path(path) {
|
||||||
|
self.check_was_allow_all_flag_passed().map_err(error_all)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn check_net_url(
|
pub fn check_net_url(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -2795,7 +2881,6 @@ mod tests {
|
||||||
fn test_check_fail() {
|
fn test_check_fail() {
|
||||||
set_prompter(Box::new(TestPrompter));
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut perms = Permissions::none_with_prompt();
|
let mut perms = Permissions::none_with_prompt();
|
||||||
|
|
||||||
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
|
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
|
||||||
|
|
||||||
prompt_value.set(false);
|
prompt_value.set(false);
|
||||||
|
|
|
@ -10,6 +10,7 @@ use deno_core::snapshot::*;
|
||||||
use deno_core::v8;
|
use deno_core::v8;
|
||||||
use deno_core::Extension;
|
use deno_core::Extension;
|
||||||
use deno_http::DefaultHttpPropertyExtractor;
|
use deno_http::DefaultHttpPropertyExtractor;
|
||||||
|
use deno_io::fs::FsError;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -129,6 +130,17 @@ impl deno_net::NetPermissions for Permissions {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl deno_fs::FsPermissions for Permissions {
|
impl deno_fs::FsPermissions for Permissions {
|
||||||
|
fn check_open<'a>(
|
||||||
|
&mut self,
|
||||||
|
_resolved: bool,
|
||||||
|
_read: bool,
|
||||||
|
_write: bool,
|
||||||
|
_path: &'a Path,
|
||||||
|
_api_name: &str,
|
||||||
|
) -> Result<std::borrow::Cow<'a, Path>, FsError> {
|
||||||
|
unreachable!("snapshotting!")
|
||||||
|
}
|
||||||
|
|
||||||
fn check_read(
|
fn check_read(
|
||||||
&mut self,
|
&mut self,
|
||||||
_path: &Path,
|
_path: &Path,
|
||||||
|
|
|
@ -704,7 +704,7 @@ fn permission_request_long() {
|
||||||
.args_vec(["run", "--quiet", "run/permission_request_long.ts"])
|
.args_vec(["run", "--quiet", "run/permission_request_long.ts"])
|
||||||
.with_pty(|mut console| {
|
.with_pty(|mut console| {
|
||||||
console.expect(concat!(
|
console.expect(concat!(
|
||||||
"❌ Permission prompt length (100017 bytes) was larger than the configured maximum length (10240 bytes): denying request.\r\n",
|
"was larger than the configured maximum length (10240 bytes): denying request.\r\n",
|
||||||
"❌ WARNING: This may indicate that code is trying to bypass or hide permission check requests.\r\n",
|
"❌ WARNING: This may indicate that code is trying to bypass or hide permission check requests.\r\n",
|
||||||
"❌ Run again with --allow-read to bypass this check if this is really what you want to do.\r\n",
|
"❌ Run again with --allow-read to bypass this check if this is really what you want to do.\r\n",
|
||||||
));
|
));
|
||||||
|
@ -4309,7 +4309,10 @@ fn fsfile_set_raw_should_not_panic_on_no_tty() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!output.status.success());
|
assert!(!output.status.success());
|
||||||
let stderr = std::str::from_utf8(&output.stderr).unwrap().trim();
|
let stderr = std::str::from_utf8(&output.stderr).unwrap().trim();
|
||||||
assert!(stderr.contains("BadResource"));
|
assert!(
|
||||||
|
stderr.contains("BadResource"),
|
||||||
|
"stderr did not contain BadResource: {stderr}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -4674,7 +4677,7 @@ fn stdio_streams_are_locked_in_permission_prompt() {
|
||||||
console.write_line(r#"new Worker(URL.createObjectURL(new Blob(["setInterval(() => console.log('**malicious**'), 10)"])), { type: "module" });"#);
|
console.write_line(r#"new Worker(URL.createObjectURL(new Blob(["setInterval(() => console.log('**malicious**'), 10)"])), { type: "module" });"#);
|
||||||
// The worker is now spamming
|
// The worker is now spamming
|
||||||
console.expect(malicious_output);
|
console.expect(malicious_output);
|
||||||
console.write_line(r#"Deno.readTextFileSync('Cargo.toml');"#);
|
console.write_line(r#"Deno.readTextFileSync('../Cargo.toml');"#);
|
||||||
// We will get a permission prompt
|
// We will get a permission prompt
|
||||||
console.expect("Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) > ");
|
console.expect("Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) > ");
|
||||||
// The worker is blocked, so nothing else should get written here
|
// The worker is blocked, so nothing else should get written here
|
||||||
|
@ -4690,7 +4693,7 @@ fn stdio_streams_are_locked_in_permission_prompt() {
|
||||||
console.human_delay();
|
console.human_delay();
|
||||||
console.write_line_raw("y");
|
console.write_line_raw("y");
|
||||||
// We ensure that nothing gets written here between the permission prompt and this text, despire the delay
|
// We ensure that nothing gets written here between the permission prompt and this text, despire the delay
|
||||||
console.expect_raw_next(format!("y{newline}\x1b[4A\x1b[0J✅ Granted read access to \"Cargo.toml\"."));
|
console.expect_raw_next(format!("y{newline}\x1b[4A\x1b[0J✅ Granted read access to \""));
|
||||||
|
|
||||||
// Back to spamming!
|
// Back to spamming!
|
||||||
console.expect(malicious_output);
|
console.expect(malicious_output);
|
||||||
|
|
|
@ -173,7 +173,7 @@ Deno.test(
|
||||||
await Deno.readFile("tests/testdata/assets/");
|
await Deno.readFile("tests/testdata/assets/");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (Deno.build.os === "windows") {
|
if (Deno.build.os === "windows") {
|
||||||
assertEquals(e.code, "ENOENT");
|
assertEquals(e.code, "EPERM");
|
||||||
} else {
|
} else {
|
||||||
assertEquals(e.code, "EISDIR");
|
assertEquals(e.code, "EISDIR");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue