1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-28 16:20:57 -05:00

fix(ext/fs): make errors in tempfile creation clearer (#22498)

When using a prefix or suffix containing an invalid filename character,
it's not entirely clear where the errors come from. We make these errors
more consistent across platforms.

In addition, all permission prompts for tempfile and tempdir were
printing the same API name.

We also take the opportunity to make the tempfile random space larger by
2x (using a base32-encoded u64 rather than a hex-encoded u32).
This commit is contained in:
Matt Mastracci 2024-02-21 13:11:04 -07:00 committed by GitHub
parent a2c1cc5a1a
commit 76ebf567e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 91 additions and 25 deletions

1
Cargo.lock generated
View file

@ -1402,6 +1402,7 @@ name = "deno_fs"
version = "0.48.0" version = "0.48.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base32",
"deno_core", "deno_core",
"deno_io", "deno_io",
"filetime", "filetime",

View file

@ -85,6 +85,7 @@ deno_webstorage = { version = "0.133.0", path = "./ext/webstorage" }
aes = "=0.8.3" aes = "=0.8.3"
anyhow = "1.0.57" anyhow = "1.0.57"
async-trait = "0.1.73" async-trait = "0.1.73"
base32 = "=0.4.0"
base64 = "0.21.4" base64 = "0.21.4"
bencher = "0.1" bencher = "0.1"
brotli = "3.3.4" brotli = "3.3.4"

View file

@ -80,7 +80,7 @@ eszip = "=0.64.0"
napi_sym.workspace = true napi_sym.workspace = true
async-trait.workspace = true async-trait.workspace = true
base32 = "=0.4.0" base32.workspace = true
base64.workspace = true base64.workspace = true
bincode = "=1.3.3" bincode = "=1.3.3"
bytes.workspace = true bytes.workspace = true

View file

@ -18,6 +18,7 @@ sync_fs = []
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
base32.workspace = true
deno_core.workspace = true deno_core.workspace = true
deno_io.workspace = true deno_io.workspace = true
filetime.workspace = true filetime.workspace = true

View file

@ -8,6 +8,7 @@ use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use deno_core::anyhow::bail;
use deno_core::error::custom_error; use deno_core::error::custom_error;
use deno_core::error::type_error; use deno_core::error::type_error;
use deno_core::error::AnyError; use deno_core::error::AnyError;
@ -880,7 +881,8 @@ pub fn op_fs_make_temp_dir_sync<P>(
where where
P: FsPermissions + 'static, P: FsPermissions + 'static,
{ {
let (dir, fs) = make_temp_check_sync::<P>(state, dir)?; let (dir, fs) =
make_temp_check_sync::<P>(state, dir, "Deno.makeTempDirSync()")?;
let mut rng = thread_rng(); let mut rng = thread_rng();
@ -914,7 +916,7 @@ pub async fn op_fs_make_temp_dir_async<P>(
where where
P: FsPermissions + 'static, P: FsPermissions + 'static,
{ {
let (dir, fs) = make_temp_check_async::<P>(state, dir)?; let (dir, fs) = make_temp_check_async::<P>(state, dir, "Deno.makeTempDir()")?;
let mut rng = thread_rng(); let mut rng = thread_rng();
@ -948,7 +950,8 @@ pub fn op_fs_make_temp_file_sync<P>(
where where
P: FsPermissions + 'static, P: FsPermissions + 'static,
{ {
let (dir, fs) = make_temp_check_sync::<P>(state, dir)?; let (dir, fs) =
make_temp_check_sync::<P>(state, dir, "Deno.makeTempFileSync()")?;
let open_opts = OpenOptions { let open_opts = OpenOptions {
write: true, write: true,
@ -989,7 +992,8 @@ pub async fn op_fs_make_temp_file_async<P>(
where where
P: FsPermissions + 'static, P: FsPermissions + 'static,
{ {
let (dir, fs) = make_temp_check_async::<P>(state, dir)?; let (dir, fs) =
make_temp_check_async::<P>(state, dir, "Deno.makeTempFile()")?;
let open_opts = OpenOptions { let open_opts = OpenOptions {
write: true, write: true,
@ -1021,6 +1025,7 @@ where
fn make_temp_check_sync<P>( fn make_temp_check_sync<P>(
state: &mut OpState, state: &mut OpState,
dir: Option<String>, dir: Option<String>,
api_name: &str,
) -> Result<(PathBuf, FileSystemRc), AnyError> ) -> Result<(PathBuf, FileSystemRc), AnyError>
where where
P: FsPermissions + 'static, P: FsPermissions + 'static,
@ -1029,18 +1034,14 @@ where
let dir = match dir { let dir = match dir {
Some(dir) => { Some(dir) => {
let dir = PathBuf::from(dir); let dir = PathBuf::from(dir);
state state.borrow_mut::<P>().check_write(&dir, api_name)?;
.borrow_mut::<P>()
.check_write(&dir, "Deno.makeTempFile()")?;
dir dir
} }
None => { None => {
let dir = fs.tmp_dir().context("tmpdir")?; let dir = fs.tmp_dir().context("tmpdir")?;
state.borrow_mut::<P>().check_write_blind( state
&dir, .borrow_mut::<P>()
"TMP", .check_write_blind(&dir, "TMP", api_name)?;
"Deno.makeTempFile()",
)?;
dir dir
} }
}; };
@ -1050,6 +1051,7 @@ where
fn make_temp_check_async<P>( fn make_temp_check_async<P>(
state: Rc<RefCell<OpState>>, state: Rc<RefCell<OpState>>,
dir: Option<String>, dir: Option<String>,
api_name: &str,
) -> Result<(PathBuf, FileSystemRc), AnyError> ) -> Result<(PathBuf, FileSystemRc), AnyError>
where where
P: FsPermissions + 'static, P: FsPermissions + 'static,
@ -1059,24 +1061,57 @@ where
let dir = match dir { let dir = match dir {
Some(dir) => { Some(dir) => {
let dir = PathBuf::from(dir); let dir = PathBuf::from(dir);
state state.borrow_mut::<P>().check_write(&dir, api_name)?;
.borrow_mut::<P>()
.check_write(&dir, "Deno.makeTempFile()")?;
dir dir
} }
None => { None => {
let dir = fs.tmp_dir().context("tmpdir")?; let dir = fs.tmp_dir().context("tmpdir")?;
state.borrow_mut::<P>().check_write_blind( state
&dir, .borrow_mut::<P>()
"TMP", .check_write_blind(&dir, "TMP", api_name)?;
"Deno.makeTempFile()",
)?;
dir dir
} }
}; };
Ok((dir, fs)) Ok((dir, fs))
} }
/// Identify illegal filename characters before attempting to use them in a filesystem
/// operation. We're a bit stricter with tempfile and tempdir names than with regular
/// files.
fn validate_temporary_filename_component(
component: &str,
#[allow(unused_variables)] suffix: bool,
) -> Result<(), AnyError> {
// Ban ASCII and Unicode control characters: these will often fail
if let Some(c) = component.matches(|c: char| c.is_control()).next() {
bail!("Invalid control character in prefix or suffix: {:?}", c);
}
// Windows has the most restrictive filenames. As temp files aren't normal files, we just
// use this set of banned characters for all platforms because wildcard-like files can also
// be problematic in unix-like shells.
// The list of banned characters in Windows:
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
// You might ask why <, >, and " are included in the Windows list? It's because they are
// special wildcard implemented in the filesystem driver!
// https://learn.microsoft.com/en-ca/archive/blogs/jeremykuhne/wildcards-in-windows
if let Some(c) = component
.matches(|c: char| "<>:\"/\\|?*".contains(c))
.next()
{
bail!("Invalid character in prefix or suffix: {:?}", c);
}
// This check is only for Windows
#[cfg(windows)]
if suffix && component.ends_with(|c: char| ". ".contains(c)) {
bail!("Invalid trailing character in suffix");
}
Ok(())
}
fn tmp_name( fn tmp_name(
rng: &mut ThreadRng, rng: &mut ThreadRng,
dir: &Path, dir: &Path,
@ -1084,12 +1119,18 @@ fn tmp_name(
suffix: Option<&str>, suffix: Option<&str>,
) -> Result<PathBuf, AnyError> { ) -> Result<PathBuf, AnyError> {
let prefix = prefix.unwrap_or(""); let prefix = prefix.unwrap_or("");
validate_temporary_filename_component(prefix, false)?;
let suffix = suffix.unwrap_or(""); let suffix = suffix.unwrap_or("");
validate_temporary_filename_component(suffix, true)?;
let mut path = dir.join("_"); // If we use a 32-bit number, we only need ~70k temp files before we have a 50%
// chance of collision. By bumping this up to 64-bits, we require ~5 billion
let unique = rng.gen::<u32>(); // before hitting a 50% chance. We also base32-encode this value so the entire
path.set_file_name(format!("{prefix}{unique:08x}{suffix}")); // thing is 1) case insensitive and 2) slightly shorter than the equivalent hex
// value.
let unique = rng.gen::<u64>();
base32::encode(base32::Alphabet::Crockford, &unique.to_le_bytes());
let path = dir.join(format!("{prefix}{unique:08x}{suffix}"));
Ok(path) Ok(path)
} }

View file

@ -155,3 +155,25 @@ Deno.test(
} }
}, },
); );
Deno.test(
{ permissions: { read: true, write: true } },
function makeTempInvalidCharacter() {
// Various control and ASCII characters that are disallowed on all platforms
for (const invalid of ["\0", "*", "\x9f"]) {
assertThrows(() => Deno.makeTempFileSync({ prefix: invalid }));
assertThrows(() => Deno.makeTempDirSync({ prefix: invalid }));
assertThrows(() => Deno.makeTempFileSync({ suffix: invalid }));
assertThrows(() => Deno.makeTempDirSync({ suffix: invalid }));
}
// On Windows, files can't end with space or period
if (Deno.build.os === "windows") {
assertThrows(() => Deno.makeTempFileSync({ suffix: "." }));
assertThrows(() => Deno.makeTempFileSync({ suffix: " " }));
} else {
Deno.makeTempFileSync({ suffix: "." });
Deno.makeTempFileSync({ suffix: " " });
}
},
);