diff --git a/Cargo.lock b/Cargo.lock
index 81fcf9020..4618472ac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1950,6 +1950,7 @@ dependencies = [
"serde_json",
"serial_test",
"tabled",
+ "tempfile",
"termtree",
"thiserror",
"typed-path",
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 7e9a6bda9..817daa378 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -66,10 +66,14 @@ nix = { version = "0.31.1", features = ["user", "fs", "ioctl"] }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.62.2", features = [
- "Win32_Storage_FileSystem",
+ "Win32_Foundation",
+ "Win32_Security",
"Win32_Security_Authorization",
- "Win32_System_WindowsProgramming",
+ "Win32_Storage_FileSystem",
+ "Win32_System_IO",
+ "Win32_System_Ioctl",
"Win32_System_Threading",
+ "Win32_System_WindowsProgramming",
] }
field-offset = { version = "0.3.6", optional = true }
@@ -78,6 +82,7 @@ maplit = "1.0.2"
path-slash = "0.2.1"
rust-embed = { version = "8.11.0", features = ["debug-embed"] }
scopeguard = "1.2.0"
+tempfile = "3"
walkdir = "2.5.0"
criterion = { version = "0.8.2", default-features = false, features = ["cargo_bench_support", "plotters"] }
diff --git a/cli/src/command/bsdtar.rs b/cli/src/command/bsdtar.rs
index 5e1abc7a9..1ce19f794 100644
--- a/cli/src/command/bsdtar.rs
+++ b/cli/src/command/bsdtar.rs
@@ -592,12 +592,12 @@ pub(crate) struct BsdtarCommand {
to_stdout: bool,
#[arg(
long,
- help = "Allow extracting symbolic links and hard links that contain root or parent paths (default)"
+ help = "Allow extracting symbolic links, hard links, or Windows junctions whose target points outside the extraction root (default)"
)]
allow_unsafe_links: bool,
#[arg(
long,
- help = "Do not allow extracting symbolic links and hard links that contain root or parent paths"
+ help = "Do not allow extracting symbolic links, hard links, or Windows junctions whose target points outside the extraction root"
)]
no_allow_unsafe_links: bool,
#[arg(
diff --git a/cli/src/command/core.rs b/cli/src/command/core.rs
index aaf0cea32..3c4ff5680 100644
--- a/cli/src/command/core.rs
+++ b/cli/src/command/core.rs
@@ -381,6 +381,11 @@ pub(crate) enum StoreAs {
Dir,
Symlink(LinkTargetType),
Hardlink(PathBuf),
+ /// Windows NTFS junction. The inner `PathBuf` is the **external** target
+ /// path (typically absolute). This variant is only produced on Windows
+ /// but is declared unconditionally so that `match` arms remain exhaustive
+ /// on every platform.
+ Junction(PathBuf),
}
/// Source of an archive to include (file path or stdin).
@@ -737,8 +742,12 @@ pub(crate) fn collect_items_with_state(
// Classify entry and maybe add it to output
let store = if is_symlink {
let meta = fs::symlink_metadata(path)?;
- let link_target_type = detect_symlink_target_type(path, &meta)?;
- Some((StoreAs::Symlink(link_target_type), meta))
+ if let Some(target) = classify_junction(path)? {
+ Some((StoreAs::Junction(target), meta))
+ } else {
+ let link_target_type = detect_symlink_target_type(path, &meta)?;
+ Some((StoreAs::Symlink(link_target_type), meta))
+ }
} else if is_file {
if let Some(linked) = hardlink_resolver.resolve(path).ok().flatten() {
Some((StoreAs::Hardlink(linked), fs::symlink_metadata(path)?))
@@ -839,6 +848,26 @@ fn detect_symlink_target_type(
}
}
+/// Returns the junction target if `path` is a Windows junction, or `None` for
+/// any non-junction path (including regular directories, symlinks, and
+/// unknown reparse tags). Errors inside the probe are swallowed to a debug
+/// log so classification still falls through to the existing symlink handler.
+#[cfg(windows)]
+fn classify_junction(path: &Path) -> io::Result