mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 07:14:47 -05:00
fix(compile): inline symlinks as files outside node_modules dir and warn for directories (#19285)
If a symlink within the `node_modules` directory lies outside that directory, it will now warn and inline the file. For directories, it will just warn for now. Probably fixes #19251 (I'm still unable to reproduce).
This commit is contained in:
parent
be59e93220
commit
a96844118c
7 changed files with 229 additions and 39 deletions
|
@ -24,9 +24,19 @@ use deno_runtime::deno_io::fs::FsResult;
|
||||||
use deno_runtime::deno_io::fs::FsStat;
|
use deno_runtime::deno_io::fs::FsStat;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
#[error(
|
||||||
|
"Failed to strip prefix '{}' from '{}'", root_path.display(), target.display()
|
||||||
|
)]
|
||||||
|
pub struct StripRootError {
|
||||||
|
root_path: PathBuf,
|
||||||
|
target: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct VfsBuilder {
|
pub struct VfsBuilder {
|
||||||
root_path: PathBuf,
|
root_path: PathBuf,
|
||||||
root_dir: VirtualDirectory,
|
root_dir: VirtualDirectory,
|
||||||
|
@ -37,6 +47,7 @@ pub struct VfsBuilder {
|
||||||
|
|
||||||
impl VfsBuilder {
|
impl VfsBuilder {
|
||||||
pub fn new(root_path: PathBuf) -> Self {
|
pub fn new(root_path: PathBuf) -> Self {
|
||||||
|
log::debug!("Building vfs with root '{}'", root_path.display());
|
||||||
Self {
|
Self {
|
||||||
root_dir: VirtualDirectory {
|
root_dir: VirtualDirectory {
|
||||||
name: root_path
|
name: root_path
|
||||||
|
@ -58,7 +69,7 @@ impl VfsBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_dir_recursive(&mut self, path: &Path) -> Result<(), AnyError> {
|
pub fn add_dir_recursive(&mut self, path: &Path) -> Result<(), AnyError> {
|
||||||
self.add_dir(path);
|
self.add_dir(path)?;
|
||||||
let read_dir = std::fs::read_dir(path)
|
let read_dir = std::fs::read_dir(path)
|
||||||
.with_context(|| format!("Reading {}", path.display()))?;
|
.with_context(|| format!("Reading {}", path.display()))?;
|
||||||
|
|
||||||
|
@ -72,19 +83,44 @@ impl VfsBuilder {
|
||||||
} else if file_type.is_file() {
|
} else if file_type.is_file() {
|
||||||
let file_bytes = std::fs::read(&path)
|
let file_bytes = std::fs::read(&path)
|
||||||
.with_context(|| format!("Reading {}", path.display()))?;
|
.with_context(|| format!("Reading {}", path.display()))?;
|
||||||
self.add_file(&path, file_bytes);
|
self.add_file(&path, file_bytes)?;
|
||||||
} else if file_type.is_symlink() {
|
} else if file_type.is_symlink() {
|
||||||
let target = std::fs::read_link(&path)
|
let target = util::fs::canonicalize_path(&path)
|
||||||
.with_context(|| format!("Reading symlink {}", path.display()))?;
|
.with_context(|| format!("Reading symlink {}", path.display()))?;
|
||||||
self.add_symlink(&path, &target);
|
if let Err(StripRootError { .. }) = self.add_symlink(&path, &target) {
|
||||||
|
if target.is_file() {
|
||||||
|
// this may change behavior, so warn the user about it
|
||||||
|
log::warn!(
|
||||||
|
"Symlink target is outside '{}'. Inlining symlink at '{}' to '{}' as file.",
|
||||||
|
self.root_path.display(),
|
||||||
|
path.display(),
|
||||||
|
target.display(),
|
||||||
|
);
|
||||||
|
// inline the symlink and make the target file
|
||||||
|
let file_bytes = std::fs::read(&target)
|
||||||
|
.with_context(|| format!("Reading {}", path.display()))?;
|
||||||
|
self.add_file(&path, file_bytes)?;
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Symlink target is outside '{}'. Excluding symlink at '{}' with target '{}'.",
|
||||||
|
self.root_path.display(),
|
||||||
|
path.display(),
|
||||||
|
target.display(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_dir(&mut self, path: &Path) -> &mut VirtualDirectory {
|
pub fn add_dir(
|
||||||
let path = self.path_relative_root(path);
|
&mut self,
|
||||||
|
path: &Path,
|
||||||
|
) -> Result<&mut VirtualDirectory, StripRootError> {
|
||||||
|
log::debug!("Ensuring directory '{}'", path.display());
|
||||||
|
let path = self.path_relative_root(path)?;
|
||||||
let mut current_dir = &mut self.root_dir;
|
let mut current_dir = &mut self.root_dir;
|
||||||
|
|
||||||
for component in path.components() {
|
for component in path.components() {
|
||||||
|
@ -113,10 +149,15 @@ impl VfsBuilder {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
current_dir
|
Ok(current_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_file(&mut self, path: &Path, data: Vec<u8>) {
|
pub fn add_file(
|
||||||
|
&mut self,
|
||||||
|
path: &Path,
|
||||||
|
data: Vec<u8>,
|
||||||
|
) -> Result<(), AnyError> {
|
||||||
|
log::debug!("Adding file '{}'", path.display());
|
||||||
let checksum = util::checksum::gen(&[&data]);
|
let checksum = util::checksum::gen(&[&data]);
|
||||||
let offset = if let Some(offset) = self.file_offsets.get(&checksum) {
|
let offset = if let Some(offset) = self.file_offsets.get(&checksum) {
|
||||||
// duplicate file, reuse an old offset
|
// duplicate file, reuse an old offset
|
||||||
|
@ -126,7 +167,7 @@ impl VfsBuilder {
|
||||||
self.current_offset
|
self.current_offset
|
||||||
};
|
};
|
||||||
|
|
||||||
let dir = self.add_dir(path.parent().unwrap());
|
let dir = self.add_dir(path.parent().unwrap())?;
|
||||||
let name = path.file_name().unwrap().to_string_lossy();
|
let name = path.file_name().unwrap().to_string_lossy();
|
||||||
let data_len = data.len();
|
let data_len = data.len();
|
||||||
match dir.entries.binary_search_by(|e| e.name().cmp(&name)) {
|
match dir.entries.binary_search_by(|e| e.name().cmp(&name)) {
|
||||||
|
@ -148,11 +189,22 @@ impl VfsBuilder {
|
||||||
self.files.push(data);
|
self.files.push(data);
|
||||||
self.current_offset += data_len as u64;
|
self.current_offset += data_len as u64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_symlink(&mut self, path: &Path, target: &Path) {
|
pub fn add_symlink(
|
||||||
let dest = self.path_relative_root(target);
|
&mut self,
|
||||||
let dir = self.add_dir(path.parent().unwrap());
|
path: &Path,
|
||||||
|
target: &Path,
|
||||||
|
) -> Result<(), StripRootError> {
|
||||||
|
log::debug!(
|
||||||
|
"Adding symlink '{}' to '{}'",
|
||||||
|
path.display(),
|
||||||
|
target.display()
|
||||||
|
);
|
||||||
|
let dest = self.path_relative_root(target)?;
|
||||||
|
let dir = self.add_dir(path.parent().unwrap())?;
|
||||||
let name = path.file_name().unwrap().to_string_lossy();
|
let name = path.file_name().unwrap().to_string_lossy();
|
||||||
match dir.entries.binary_search_by(|e| e.name().cmp(&name)) {
|
match dir.entries.binary_search_by(|e| e.name().cmp(&name)) {
|
||||||
Ok(_) => unreachable!(),
|
Ok(_) => unreachable!(),
|
||||||
|
@ -169,23 +221,20 @@ impl VfsBuilder {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_dir_and_files(self) -> (VirtualDirectory, Vec<Vec<u8>>) {
|
pub fn into_dir_and_files(self) -> (VirtualDirectory, Vec<Vec<u8>>) {
|
||||||
(self.root_dir, self.files)
|
(self.root_dir, self.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_relative_root(&self, path: &Path) -> PathBuf {
|
fn path_relative_root(&self, path: &Path) -> Result<PathBuf, StripRootError> {
|
||||||
match path.strip_prefix(&self.root_path) {
|
match path.strip_prefix(&self.root_path) {
|
||||||
Ok(p) => p.to_path_buf(),
|
Ok(p) => Ok(p.to_path_buf()),
|
||||||
Err(err) => {
|
Err(_) => Err(StripRootError {
|
||||||
panic!(
|
root_path: self.root_path.clone(),
|
||||||
"Failed to strip prefix '{}' from '{}': {:#}",
|
target: path.to_path_buf(),
|
||||||
self.root_path.display(),
|
}),
|
||||||
path.display(),
|
|
||||||
err
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -457,7 +506,11 @@ impl FileBackedVfsFile {
|
||||||
}
|
}
|
||||||
SeekFrom::End(offset) => {
|
SeekFrom::End(offset) => {
|
||||||
if offset < 0 && -offset as u64 > self.file.len {
|
if offset < 0 && -offset as u64 > self.file.len {
|
||||||
Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "An attempt was made to move the file pointer before the beginning of the file.").into())
|
let msg = "An attempt was made to move the file pointer before the beginning of the file.";
|
||||||
|
Err(
|
||||||
|
std::io::Error::new(std::io::ErrorKind::PermissionDenied, msg)
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
let mut current_pos = self.pos.lock();
|
let mut current_pos = self.pos.lock();
|
||||||
*current_pos = if offset >= 0 {
|
*current_pos = if offset >= 0 {
|
||||||
|
@ -790,16 +843,28 @@ mod test {
|
||||||
let temp_dir = TempDir::new();
|
let temp_dir = TempDir::new();
|
||||||
let src_path = temp_dir.path().join("src");
|
let src_path = temp_dir.path().join("src");
|
||||||
let mut builder = VfsBuilder::new(src_path.clone());
|
let mut builder = VfsBuilder::new(src_path.clone());
|
||||||
builder.add_file(&src_path.join("a.txt"), "data".into());
|
builder
|
||||||
builder.add_file(&src_path.join("b.txt"), "data".into());
|
.add_file(&src_path.join("a.txt"), "data".into())
|
||||||
|
.unwrap();
|
||||||
|
builder
|
||||||
|
.add_file(&src_path.join("b.txt"), "data".into())
|
||||||
|
.unwrap();
|
||||||
assert_eq!(builder.files.len(), 1); // because duplicate data
|
assert_eq!(builder.files.len(), 1); // because duplicate data
|
||||||
builder.add_file(&src_path.join("c.txt"), "c".into());
|
builder
|
||||||
builder.add_file(&src_path.join("sub_dir").join("d.txt"), "d".into());
|
.add_file(&src_path.join("c.txt"), "c".into())
|
||||||
builder.add_file(&src_path.join("e.txt"), "e".into());
|
.unwrap();
|
||||||
builder.add_symlink(
|
builder
|
||||||
&src_path.join("sub_dir").join("e.txt"),
|
.add_file(&src_path.join("sub_dir").join("d.txt"), "d".into())
|
||||||
&src_path.join("e.txt"),
|
.unwrap();
|
||||||
);
|
builder
|
||||||
|
.add_file(&src_path.join("e.txt"), "e".into())
|
||||||
|
.unwrap();
|
||||||
|
builder
|
||||||
|
.add_symlink(
|
||||||
|
&src_path.join("sub_dir").join("e.txt"),
|
||||||
|
&src_path.join("e.txt"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// get the virtual fs
|
// get the virtual fs
|
||||||
let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir);
|
let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir);
|
||||||
|
@ -923,9 +988,15 @@ mod test {
|
||||||
let temp_dir = TempDir::new();
|
let temp_dir = TempDir::new();
|
||||||
let src_path = temp_dir.path().join("src");
|
let src_path = temp_dir.path().join("src");
|
||||||
let mut builder = VfsBuilder::new(src_path.clone());
|
let mut builder = VfsBuilder::new(src_path.clone());
|
||||||
builder.add_symlink(&src_path.join("a.txt"), &src_path.join("b.txt"));
|
builder
|
||||||
builder.add_symlink(&src_path.join("b.txt"), &src_path.join("c.txt"));
|
.add_symlink(&src_path.join("a.txt"), &src_path.join("b.txt"))
|
||||||
builder.add_symlink(&src_path.join("c.txt"), &src_path.join("a.txt"));
|
.unwrap();
|
||||||
|
builder
|
||||||
|
.add_symlink(&src_path.join("b.txt"), &src_path.join("c.txt"))
|
||||||
|
.unwrap();
|
||||||
|
builder
|
||||||
|
.add_symlink(&src_path.join("c.txt"), &src_path.join("a.txt"))
|
||||||
|
.unwrap();
|
||||||
let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir);
|
let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
virtual_fs
|
virtual_fs
|
||||||
|
@ -950,10 +1021,12 @@ mod test {
|
||||||
let temp_dir = TempDir::new();
|
let temp_dir = TempDir::new();
|
||||||
let temp_path = temp_dir.path();
|
let temp_path = temp_dir.path();
|
||||||
let mut builder = VfsBuilder::new(temp_path.to_path_buf());
|
let mut builder = VfsBuilder::new(temp_path.to_path_buf());
|
||||||
builder.add_file(
|
builder
|
||||||
&temp_path.join("a.txt"),
|
.add_file(
|
||||||
"0123456789".to_string().into_bytes(),
|
&temp_path.join("a.txt"),
|
||||||
);
|
"0123456789".to_string().into_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir);
|
let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir);
|
||||||
let virtual_fs = Arc::new(virtual_fs);
|
let virtual_fs = Arc::new(virtual_fs);
|
||||||
let file = virtual_fs.open_file(&dest_path.join("a.txt")).unwrap();
|
let file = virtual_fs.open_file(&dest_path.join("a.txt")).unwrap();
|
||||||
|
|
|
@ -1082,3 +1082,68 @@ fn run_npm_bin_compile_test(opts: RunNpmBinCompileOptions) {
|
||||||
output.assert_matches_file(opts.output_file);
|
output.assert_matches_file(opts.output_file);
|
||||||
output.assert_exit_code(opts.exit_code);
|
output.assert_exit_code(opts.exit_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compile_node_modules_symlink_outside() {
|
||||||
|
let context = TestContextBuilder::for_npm()
|
||||||
|
.use_sync_npm_download()
|
||||||
|
.use_copy_temp_dir("compile/node_modules_symlink_outside")
|
||||||
|
.cwd("compile/node_modules_symlink_outside")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let temp_dir = context.temp_dir();
|
||||||
|
let project_dir = temp_dir
|
||||||
|
.path()
|
||||||
|
.join("compile")
|
||||||
|
.join("node_modules_symlink_outside");
|
||||||
|
temp_dir.create_dir_all(project_dir.join("node_modules"));
|
||||||
|
temp_dir.create_dir_all(project_dir.join("some_folder"));
|
||||||
|
temp_dir.write(project_dir.join("test.txt"), "5");
|
||||||
|
|
||||||
|
// create a symlink in the node_modules directory that points to a folder in the cwd
|
||||||
|
temp_dir.symlink_dir(
|
||||||
|
project_dir.join("some_folder"),
|
||||||
|
project_dir.join("node_modules").join("some_folder"),
|
||||||
|
);
|
||||||
|
// compile folder
|
||||||
|
let output = context
|
||||||
|
.new_command()
|
||||||
|
.args("compile --allow-read --node-modules-dir --output bin main.ts")
|
||||||
|
.run();
|
||||||
|
output.assert_exit_code(0);
|
||||||
|
output.assert_matches_file(
|
||||||
|
"compile/node_modules_symlink_outside/main_compile_folder.out",
|
||||||
|
);
|
||||||
|
assert!(project_dir.join("node_modules/some_folder").exists());
|
||||||
|
|
||||||
|
// Cleanup and remove the folder. The folder test is done separately from
|
||||||
|
// the file symlink test because different systems would traverse
|
||||||
|
// the directory items in different order.
|
||||||
|
temp_dir.remove_dir_all(project_dir.join("node_modules/some_folder"));
|
||||||
|
|
||||||
|
// create a symlink in the node_modules directory that points to a file in the cwd
|
||||||
|
temp_dir.symlink_file(
|
||||||
|
project_dir.join("test.txt"),
|
||||||
|
project_dir.join("node_modules").join("test.txt"),
|
||||||
|
);
|
||||||
|
assert!(project_dir.join("node_modules/test.txt").exists());
|
||||||
|
|
||||||
|
// compile
|
||||||
|
let output = context
|
||||||
|
.new_command()
|
||||||
|
.args("compile --allow-read --node-modules-dir --output bin main.ts")
|
||||||
|
.run();
|
||||||
|
output.assert_exit_code(0);
|
||||||
|
output.assert_matches_file(
|
||||||
|
"compile/node_modules_symlink_outside/main_compile_file.out",
|
||||||
|
);
|
||||||
|
|
||||||
|
// run
|
||||||
|
let binary_path =
|
||||||
|
project_dir.join(if cfg!(windows) { "bin.exe" } else { "bin" });
|
||||||
|
let output = context
|
||||||
|
.new_command()
|
||||||
|
.command_name(binary_path.to_string_lossy())
|
||||||
|
.run();
|
||||||
|
output.assert_matches_file("compile/node_modules_symlink_outside/main.out");
|
||||||
|
}
|
||||||
|
|
2
cli/tests/testdata/compile/node_modules_symlink_outside/main.out
vendored
Normal file
2
cli/tests/testdata/compile/node_modules_symlink_outside/main.out
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
4
|
||||||
|
5
|
6
cli/tests/testdata/compile/node_modules_symlink_outside/main.ts
vendored
Normal file
6
cli/tests/testdata/compile/node_modules_symlink_outside/main.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { getValue, setValue } from "npm:@denotest/esm-basic";
|
||||||
|
|
||||||
|
setValue(4);
|
||||||
|
|
||||||
|
console.log(getValue());
|
||||||
|
console.log(Deno.readTextFileSync("./node_modules/test.txt"));
|
2
cli/tests/testdata/compile/node_modules_symlink_outside/main_compile_file.out
vendored
Normal file
2
cli/tests/testdata/compile/node_modules_symlink_outside/main_compile_file.out
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Compile file:///[WILDCARD]/node_modules_symlink_outside/main.ts to [WILDCARD]
|
||||||
|
Symlink target is outside '[WILDCARD]node_modules_symlink_outside[WILDCARD]node_modules'. Inlining symlink at '[WILDCARD]node_modules_symlink_outside[WILDCARD]node_modules[WILDCARD]test.txt' to '[WILDCARD]node_modules_symlink_outside[WILDCARD]test.txt' as file.
|
6
cli/tests/testdata/compile/node_modules_symlink_outside/main_compile_folder.out
vendored
Normal file
6
cli/tests/testdata/compile/node_modules_symlink_outside/main_compile_folder.out
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Download http://localhost:4545/npm/registry/@denotest/esm-basic
|
||||||
|
Download http://localhost:4545/npm/registry/@denotest/esm-basic/1.0.0.tgz
|
||||||
|
Initialize @denotest/esm-basic@1.0.0
|
||||||
|
Check file:///[WILDCARD]/node_modules_symlink_outside/main.ts
|
||||||
|
Compile file:///[WILDCARD]/node_modules_symlink_outside/main.ts to [WILDCARD]
|
||||||
|
Symlink target is outside '[WILDCARD]node_modules_symlink_outside[WILDCARD]node_modules'. Excluding symlink at '[WILDCARD]node_modules_symlink_outside[WILDCARD]node_modules[WILDCARD]some_folder' with target '[WILDCARD]node_modules_symlink_outside[WILDCARD]some_folder'.
|
|
@ -80,4 +80,40 @@ impl TempDir {
|
||||||
pub fn write(&self, path: impl AsRef<Path>, text: impl AsRef<str>) {
|
pub fn write(&self, path: impl AsRef<Path>, text: impl AsRef<str>) {
|
||||||
fs::write(self.path().join(path), text.as_ref()).unwrap();
|
fs::write(self.path().join(path), text.as_ref()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn symlink_dir(
|
||||||
|
&self,
|
||||||
|
oldpath: impl AsRef<Path>,
|
||||||
|
newpath: impl AsRef<Path>,
|
||||||
|
) {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
symlink(self.path().join(oldpath), self.path().join(newpath)).unwrap();
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
use std::os::windows::fs::symlink_dir;
|
||||||
|
symlink_dir(self.path().join(oldpath), self.path().join(newpath))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symlink_file(
|
||||||
|
&self,
|
||||||
|
oldpath: impl AsRef<Path>,
|
||||||
|
newpath: impl AsRef<Path>,
|
||||||
|
) {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
symlink(self.path().join(oldpath), self.path().join(newpath)).unwrap();
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
use std::os::windows::fs::symlink_file;
|
||||||
|
symlink_file(self.path().join(oldpath), self.path().join(newpath))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue