Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions crates/bashkit/src/builtins/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use std::path::{Path, PathBuf};
use super::limits::ARCHIVE_MAX_DECOMPRESSION_RATIO as MAX_DECOMPRESSION_RATIO;
use super::{Builtin, Context, resolve_path};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;
use crate::limits::{BudgetedBytes, BudgetedString, BudgetedVec, LimitExceeded};

Expand Down Expand Up @@ -586,7 +587,7 @@ fn add_directory_to_tar<'a>(
// Add directory contents
let entries = ctx.fs.read_dir(path).await?;
for entry in entries {
let child_path = path.join(&entry.name);
let child_path = vfs_join(path, &entry.name);
let child_name = format!("{}/{}", name, entry.name);

if entry.metadata.file_type.is_dir() {
Expand Down Expand Up @@ -661,7 +662,12 @@ fn validate_tar_for_extraction(

if !to_stdout {
let entry = Path::new(name.as_ref());
if entry.is_absolute()
// `has_root()` rather than `is_absolute()`: a `/etc/passwd` entry
// carries no drive prefix, so on Windows `is_absolute()` is false
// and this arm never fired (issue #2425's class). The
// `starts_with(extract_base)` arm below still blocked the write.
if entry.has_root()
|| entry.is_absolute()
|| entry
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
Expand Down
5 changes: 3 additions & 2 deletions crates/bashkit/src/builtins/awk/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::fs::{FileSystem, normalize_path};
use crate::limits::ExecutionLimits;
// On wasm32 there is no OS thread to bridge the sync AWK evaluator to the async
// VFS, so redirected reads/writes drive the VFS future inline with now_or_never.
use crate::fs::vfs_join;
#[cfg(target_family = "wasm")]
use futures_util::FutureExt;

Expand Down Expand Up @@ -234,7 +235,7 @@ impl AwkInterpreter {
let resolved = if path_str.starts_with('/') {
PathBuf::from(path_str)
} else {
self.cwd.join(path_str)
vfs_join(&self.cwd, path_str)
};
normalize_path(&resolved).to_string_lossy().to_string()
}
Expand Down Expand Up @@ -1200,7 +1201,7 @@ impl AwkInterpreter {
let resolved = if path.starts_with('/') {
PathBuf::from(path)
} else {
self.cwd.join(path)
vfs_join(&self.cwd, path)
};
let key = resolved.to_string_lossy().into_owned();
// Bound the number of distinct redirect targets (DoS guard, parity with
Expand Down
5 changes: 3 additions & 2 deletions crates/bashkit/src/builtins/awk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ fn normalize_awk_newlines(input: &str) -> String {

mod interpreter;
mod parser;
use crate::fs::vfs_join;
use interpreter::{AwkFlow, AwkInterpreter};
use parser::AwkParser;

Expand Down Expand Up @@ -535,7 +536,7 @@ impl Builtin for Awk {
let path = if ctx.args[i].starts_with('/') {
std::path::PathBuf::from(&ctx.args[i])
} else {
ctx.cwd.join(&ctx.args[i])
vfs_join(ctx.cwd, &ctx.args[i])
};
program_str = match read_text_file(&*ctx.fs, &path, "awk").await {
Ok(t) => t,
Expand Down Expand Up @@ -615,7 +616,7 @@ impl Builtin for Awk {
let path = if file.starts_with('/') {
std::path::PathBuf::from(file)
} else {
ctx.cwd.join(file)
vfs_join(ctx.cwd, file)
};

let text = match read_text_file(&*ctx.fs, &path, "awk").await {
Expand Down
3 changes: 2 additions & 1 deletion crates/bashkit/src/builtins/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::path::Path;

use super::{Builtin, Context};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;

pub struct Cat;
Expand Down Expand Up @@ -72,7 +73,7 @@ impl Builtin for Cat {
let path = if Path::new(file).is_absolute() {
file.clone()
} else {
ctx.cwd.join(file).to_string_lossy().into_owned()
vfs_join(ctx.cwd, file).to_string_lossy().into_owned()
};
match ctx.fs.read_file(Path::new(&path)).await {
Ok(bytes) => raw.extend_from_slice(&bytes),
Expand Down
3 changes: 2 additions & 1 deletion crates/bashkit/src/builtins/checksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use sha2::{Digest, Sha256};

use super::{Builtin, Context};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;

/// md5sum builtin - compute MD5 message digest
Expand Down Expand Up @@ -100,7 +101,7 @@ async fn checksum_execute<D: Digest>(ctx: &Context<'_>, cmd: &str) -> Result<Exe
let path = if file.starts_with('/') {
std::path::PathBuf::from(file)
} else {
ctx.cwd.join(file)
vfs_join(ctx.cwd, file)
};

match ctx.fs.read_file(&path).await {
Expand Down
3 changes: 2 additions & 1 deletion crates/bashkit/src/builtins/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use async_trait::async_trait;

use super::{Builtin, BuiltinHelper, Context, read_text_file};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;

/// The column builtin - format input into columns.
Expand Down Expand Up @@ -182,7 +183,7 @@ impl Builtin for Column {
let path = if file.starts_with('/') {
std::path::PathBuf::from(file)
} else {
ctx.cwd.join(file)
vfs_join(ctx.cwd, file)
};

let text = match read_text_file(&*ctx.fs, &path, "column").await {
Expand Down
5 changes: 3 additions & 2 deletions crates/bashkit/src/builtins/comm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use async_trait::async_trait;

use super::{Builtin, BuiltinHelper, Context, read_text_file};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;

/// The comm builtin - compare two sorted files line by line.
Expand Down Expand Up @@ -84,7 +85,7 @@ impl Builtin for Comm {
let path = if files[0].starts_with('/') {
std::path::PathBuf::from(&files[0])
} else {
ctx.cwd.join(&files[0])
vfs_join(ctx.cwd, &files[0])
};
match read_text_file(&*ctx.fs, &path, "comm").await {
Ok(text) => text.lines().map(|l| l.to_string()).collect(),
Expand All @@ -100,7 +101,7 @@ impl Builtin for Comm {
let path = if files[1].starts_with('/') {
std::path::PathBuf::from(&files[1])
} else {
ctx.cwd.join(&files[1])
vfs_join(ctx.cwd, &files[1])
};
match read_text_file(&*ctx.fs, &path, "comm").await {
Ok(text) => text.lines().map(|l| l.to_string()).collect(),
Expand Down
3 changes: 2 additions & 1 deletion crates/bashkit/src/builtins/cuttr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use async_trait::async_trait;

use super::{Builtin, BuiltinHelper, Context, read_text_file};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;

// Keep tr set expansion bounded before stdin/output controls can apply.
Expand Down Expand Up @@ -189,7 +190,7 @@ impl Builtin for Cut {
let path = if file.starts_with('/') {
std::path::PathBuf::from(file)
} else {
ctx.cwd.join(file)
vfs_join(ctx.cwd, file)
};

let text = match read_text_file(&*ctx.fs, &path, "cut").await {
Expand Down
5 changes: 3 additions & 2 deletions crates/bashkit/src/builtins/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use async_trait::async_trait;

use super::{Builtin, Context, read_text_file};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;

/// The diff builtin - compare files line by line.
Expand Down Expand Up @@ -348,7 +349,7 @@ impl Builtin for Diff {
let path = if files[0].starts_with('/') {
std::path::PathBuf::from(&files[0])
} else {
ctx.cwd.join(&files[0])
vfs_join(ctx.cwd, &files[0])
};
match read_text_file(&*ctx.fs, &path, "diff").await {
Ok(text) => text.lines().map(|l| l.to_string()).collect(),
Expand All @@ -365,7 +366,7 @@ impl Builtin for Diff {
let path = if files[1].starts_with('/') {
std::path::PathBuf::from(&files[1])
} else {
ctx.cwd.join(&files[1])
vfs_join(ctx.cwd, &files[1])
};
match read_text_file(&*ctx.fs, &path, "diff").await {
Ok(text) => text.lines().map(|l| l.to_string()).collect(),
Expand Down
3 changes: 2 additions & 1 deletion crates/bashkit/src/builtins/dirstack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ use std::path::PathBuf;
use super::limits::DIRSTACK_MAX_SIZE as MAX_DIRSTACK_SIZE;
use super::{Builtin, Context};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;

fn normalize_path(base: &std::path::Path, target: &str) -> PathBuf {
let path = if target.starts_with('/') {
PathBuf::from(target)
} else {
base.join(target)
vfs_join(base, target)
};
super::resolve_path(&PathBuf::from("/"), &path.to_string_lossy())
}
Expand Down
34 changes: 32 additions & 2 deletions crates/bashkit/src/builtins/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::path::Path;

use super::{Builtin, Context};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;

/// The du builtin - estimate file space usage.
Expand Down Expand Up @@ -69,7 +70,7 @@ impl Builtin for Du {
} else if path_str == "." {
ctx.cwd.clone()
} else {
ctx.cwd.join(path_str)
vfs_join(ctx.cwd, path_str)
};

match calculate_size(&ctx, &path, summary_only, human_readable).await {
Expand Down Expand Up @@ -129,7 +130,7 @@ fn calculate_size_recursive<'a>(
ctx.consume_budget_work(u64::try_from(entries.len()).unwrap_or(u64::MAX))?;

for entry in entries {
let child_path = path.join(&entry.name);
let child_path = vfs_join(path, &entry.name);
total += calculate_size_recursive(
ctx,
&child_path,
Expand Down Expand Up @@ -294,6 +295,35 @@ mod tests {

// ==================== du tests ====================

// Issue #2425: `du` prints the paths it builds, so every separator in
// them must be `/` even when the host separator is `\`.
#[tokio::test]
async fn windows_containment_du_prints_slash_separated_paths() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();

fs.mkdir(&cwd.join("proj/src"), true).await.unwrap();
fs.write_file(&cwd.join("proj/src/main.rs"), b"fn main() {}")
.await
.unwrap();

let args = vec!["proj".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);

let result = Du.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(
result.stdout.contains("/home/user/proj/src/main.rs"),
"recursive descent path is not slash-separated:\n{}",
result.stdout
);
assert!(
!result.stdout.contains('\\'),
"output leaked a host separator:\n{}",
result.stdout
);
}

#[tokio::test]
async fn test_du_file() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
Expand Down
5 changes: 3 additions & 2 deletions crates/bashkit/src/builtins/fileops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::path::Path;
use super::limits::MKTEMP_MAX_ATTEMPTS;
use super::{Builtin, Context, resolve_path};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;

/// The mkdir builtin - create directories.
Expand Down Expand Up @@ -320,7 +321,7 @@ impl Builtin for Cp {
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| source.to_string());
dest_path.join(&filename)
vfs_join(&dest_path, &filename)
} else {
dest_path.clone()
};
Expand Down Expand Up @@ -428,7 +429,7 @@ impl Builtin for Mv {
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| source.to_string());
dest_path.join(&filename)
vfs_join(&dest_path, &filename)
} else {
dest_path.clone()
};
Expand Down
Loading
Loading