diff --git a/crates/next-core/src/emit.rs b/crates/next-core/src/emit.rs index ba1a7326e678..317351206e19 100644 --- a/crates/next-core/src/emit.rs +++ b/crates/next-core/src/emit.rs @@ -288,19 +288,19 @@ async fn assets_diff( ( AssetContent::Redirect { target: target1, - link_type: link_type1, + is_directory: is_directory1, }, AssetContent::Redirect { target: target2, - link_type: link_type2, + is_directory: is_directory2, }, ) => { - if target1 == target2 && link_type1 == link_type2 { + if target1 == target2 && is_directory1 == is_directory2 { None } else { Some(format!( "assets at the same path are both redirects but point to different targets: \ - {target1} vs {target2}" + {target1:?} vs {target2:?}" )) } } diff --git a/turbopack/crates/turbo-tasks-fs/src/content.rs b/turbopack/crates/turbo-tasks-fs/src/content.rs index dd6d4bf3db6e..af07750a0c2c 100644 --- a/turbopack/crates/turbo-tasks-fs/src/content.rs +++ b/turbopack/crates/turbo-tasks-fs/src/content.rs @@ -10,7 +10,6 @@ use std::{ use anyhow::{Result, bail}; use bincode::{Decode, Encode}; -use bitflags::bitflags; use jsonc_parser::{ParseOptions, parse_to_serde_value}; use mime::Mime; use serde_json::Value; @@ -163,43 +162,36 @@ pub(crate) enum FileComparison { NotEqual, } -bitflags! { - #[derive( - Default, - TraceRawVcs, - NonLocalValue, - DeterministicHash, - Encode, - Decode, - )] - pub struct LinkType: u8 { - const DIRECTORY = 0b00000001; - const ABSOLUTE = 0b00000010; - } +#[derive( + Clone, Debug, PartialEq, Eq, Hash, TraceRawVcs, NonLocalValue, DeterministicHash, Encode, Decode, +)] +pub enum LinkTarget { + /// A normalized target relative to the filesystem root. + Absolute(RcStr), + /// The raw link-relative value read from disk. + Relative(RcStr), } /// The contents of a symbolic link. On Windows, this may be a junction point. /// /// When reading, we treat symbolic links and junction points on Windows as equivalent. When -/// creating a new link, we always create junction points, because symlink creation may fail if -/// Windows "developer mode" is not enabled and we're running in an unprivileged environment. +/// creating a new link, we always create directory links (i.e. junction points on Windows), because +/// symlink creation may fail if Windows "developer mode" is not enabled and we're running in an +/// unprivileged environment. #[turbo_tasks::value(shared)] #[derive(Debug, DeterministicHash)] pub enum LinkContent { - /// A valid symbolic link pointing to `target`, a unix-style path. + /// A valid symbolic link target, stored as a unix-style path. /// - /// If [`LinkType::ABSOLUTE`] is set, `target` is normalized and relative to the *filesystem - /// root* (so that absolute system paths never end up in the persistent cache). Otherwise, - /// `target` is the raw value read from the link — unnormalized, may contain `..` — and is - /// relative to the *directory containing the link*. + /// [`LinkTarget::Absolute`] targets are normalized and relative to the *filesystem root* (so + /// that absolute system paths never end up in the persistent cache). + /// [`LinkTarget::Relative`] targets are raw values read from the link — unnormalized, may + /// contain `..` — and are relative to the *directory containing the link*. /// - /// A relative `target` must stay raw so that [`FileSystem::write_link`] round-trips it + /// A relative `target` must stay raw so that [`FileSystem::write_link_dir`] round-trips it /// exactly: the value is written verbatim and compared against [`std::fs::read_link`] to /// skip unchanged links. - Link { - target: RcStr, - link_type: LinkType, - }, + Link(LinkTarget), // Invalid means the link is invalid it points out of the filesystem root Invalid, // The target was not found diff --git a/turbopack/crates/turbo-tasks-fs/src/disk.rs b/turbopack/crates/turbo-tasks-fs/src/disk.rs index 2133641cffee..d276b374b18d 100644 --- a/turbopack/crates/turbo-tasks-fs/src/disk.rs +++ b/turbopack/crates/turbo-tasks-fs/src/disk.rs @@ -33,9 +33,8 @@ use turbo_tasks_hash::{hash_xxh3_hash64, hash_xxh3_hash128}; use turbo_unix_path::{normalize_path, sys_to_unix, unix_to_sys}; use crate::{ - AnyhowWrapper, File, FileComparison, FileContent, FileMeta, FileSystem, FileSystemEntryType, - FileSystemPath, LinkContent, LinkType, PersistedFileContent, RawDirectoryContent, - RawDirectoryEntry, + AnyhowWrapper, File, FileComparison, FileContent, FileMeta, FileSystem, FileSystemPath, + LinkContent, LinkTarget, PersistedFileContent, RawDirectoryContent, RawDirectoryEntry, invalidation::Write, invalidator_map::InvalidatorMap, mutex_map::MutexMap, @@ -938,29 +937,18 @@ impl FileSystem for DiskFileSystem { return Ok(LinkContent::Invalid.cell()); }; - let mut link_type = LinkType::default(); - // TODO(bgw): Reading the type here is silly, the callers could do it. - // The reason `LinkContent` contains the type information is just for the `write_link` - // codepath, which needs to know if it can create a Windows junction point or not. - let file_type = target_fs_path.get_type().await?; - if matches!(&*file_type, FileSystemEntryType::Directory) { - link_type |= LinkType::DIRECTORY; - } - - let target; - if target_sys_path.is_absolute() { + let target = if target_sys_path.is_absolute() { // absolute path, rewrite from the sys root to the DiskFileSystem root - target = target_fs_path.path; - link_type |= LinkType::ABSOLUTE; + LinkTarget::Absolute(target_fs_path.path) } else { // link-relative, the raw value read from the link, converted to a unix-style format let target_str = target_sys_path.to_str().with_context(|| { format!("symlink target {target_sys_path:?} is not valid unicode") })?; - target = RcStr::from(sys_to_unix(target_str)); + LinkTarget::Relative(RcStr::from(sys_to_unix(target_str))) }; - Ok(LinkContent::Link { target, link_type }.cell()) + Ok(LinkContent::Link(target).cell()) } #[turbo_tasks::function(fs)] @@ -1182,7 +1170,7 @@ impl FileSystem for DiskFileSystem { } #[turbo_tasks::function(fs)] - async fn write_link( + async fn write_link_dir( self: ResolvedVc, fs_path: FileSystemPath, target: ResolvedVc, @@ -1274,35 +1262,46 @@ impl FileSystem for DiskFileSystem { let _lock = self.inner.lock_path(full_path.clone()).await; enum OsSpecificLinkContent { - Link { - #[cfg(windows)] - is_directory: bool, - target: PathBuf, - }, + Link { target: PathBuf }, NotFound, Invalid, } let os_specific_link_content = match &**content { - LinkContent::Link { target, link_type } => { - let is_directory = link_type.contains(LinkType::DIRECTORY); - let target_path = if link_type.contains(LinkType::ABSOLUTE) { - self.inner.root_path().join(unix_to_sys(target).as_ref()) - } else { - let relative_target = PathBuf::from(unix_to_sys(target).as_ref()); - if cfg!(windows) && is_directory { - // Windows junction points must always be stored as absolute - full_path - .parent() - .unwrap_or(&full_path) - .join(relative_target) - } else { - relative_target + LinkContent::Link(target) => { + let target_path = match target { + LinkTarget::Absolute(target) => { + self.inner.root_path().join(unix_to_sys(target).as_ref()) + } + LinkTarget::Relative(target) => { + let relative_target = PathBuf::from(unix_to_sys(target).as_ref()); + if cfg!(windows) { + // Windows junction points must always be stored as absolute. + full_path + .parent() + .unwrap_or(&full_path) + .join(relative_target) + } else { + relative_target + } } }; + #[cfg(all(debug_assertions, unix))] + { + let absolute_target = if target_path.is_absolute() { + target_path.clone() + } else { + full_path.parent().unwrap_or(&full_path).join(&target_path) + }; + debug_assert!( + !matches!( + std::fs::metadata(&absolute_target), + Ok(metadata) if !metadata.is_dir() + ), + "directory link target is not a directory: {absolute_target:?}" + ); + } OsSpecificLinkContent::Link { - #[cfg(windows)] - is_directory, target: target_path, } } @@ -1331,12 +1330,7 @@ impl FileSystem for DiskFileSystem { } match os_specific_link_content { - OsSpecificLinkContent::Link { - target, - #[cfg(windows)] - is_directory, - .. - } => { + OsSpecificLinkContent::Link { target } => { #[derive(thiserror::Error, Debug)] #[error("{msg}: {source}")] struct SymlinkCreationError { @@ -1374,11 +1368,8 @@ impl FileSystem for DiskFileSystem { #[cfg(not(windows))] let io_result = std::os::unix::fs::symlink(&target, &**full_path); #[cfg(windows)] - let io_result = if is_directory { - std::os::windows::fs::junction_point(&target, &**full_path) - } else { - std::os::windows::fs::symlink_file(&target, &**full_path) - }; + let io_result = + std::os::windows::fs::junction_point(&target, &**full_path); io_result.map_err(|err| { match err.kind() { ErrorKind::NotFound => { @@ -1409,20 +1400,10 @@ impl FileSystem for DiskFileSystem { "failed to create symlink at {full_path:?} pointing to {target:?}" ); #[cfg(windows)] - let message = if is_directory { - format!( - "failed to create junction point at {full_path:?} pointing to \ - {target:?}" - ) - } else { - format!( - "failed to create symlink at {full_path:?} pointing to \ - {target:?}\n\ - (Note: creating file symlinks on Windows require developer \ - mode or admin permissions: \ - https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode)", - ) - }; + let message = format!( + "failed to create junction point at {full_path:?} pointing to \ + {target:?}" + ); message }; retry_blocking_custom(try_create_link, can_retry_link) @@ -1657,7 +1638,7 @@ mod tests { use super::extract_effects_operation; use crate::{ - DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkType, + DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkTarget, canonicalize_to_rcstr, }; @@ -1667,28 +1648,10 @@ mod tests { path: FileSystemPath, target: RcStr, ) -> anyhow::Result<()> { - let write_file = |f| { - fs.write_link( - f, - LinkContent::Link { - target: format!("{target}/data.txt").into(), - link_type: LinkType::empty(), - } - .cell(), - ) - }; - // Write it twice (same content) - write_file(path.join("symlink-file")?).await?; - write_file(path.join("symlink-file")?).await?; - let write_dir = |f| { - fs.write_link( + fs.write_link_dir( f, - LinkContent::Link { - target: target.clone(), - link_type: LinkType::DIRECTORY, - } - .cell(), + LinkContent::Link(LinkTarget::Relative(target.clone())).cell(), ) }; // Write it twice (same content) @@ -1737,7 +1700,6 @@ mod tests { ) .await?; - assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "foo"); assert_eq!( read_to_string(path.join("symlink-dir/data.txt")).unwrap(), "foo" @@ -1754,7 +1716,6 @@ mod tests { ) .await?; - assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "bar"); assert_eq!( read_to_string(path.join("symlink-dir/data.txt")).unwrap(), "bar" @@ -1767,7 +1728,7 @@ mod tests { } /// A relative symlink's `target` must be the raw link-relative value stored on disk - /// (consumers like `realpath_with_links` and `write_link` resolve it against the + /// (consumers like `realpath_with_links` and `write_link_dir` resolve it against the /// directory *containing* the link). It must not be normalized or made root-relative. #[turbo_tasks::function(operation, root)] async fn assert_read_relative_symlink_operation( @@ -1778,20 +1739,14 @@ mod tests { let sibling = fs.read_link(root_path.join("sub/link-sibling")?).await?; assert_eq!( *sibling, - LinkContent::Link { - target: rcstr!("foo.txt"), - link_type: LinkType::empty(), - } + LinkContent::Link(LinkTarget::Relative(rcstr!("foo.txt"))) ); // sub/link-parent -> ../root.txt (resolves to root.txt) let parent = fs.read_link(root_path.join("sub/link-parent")?).await?; assert_eq!( *parent, - LinkContent::Link { - target: rcstr!("../root.txt"), - link_type: LinkType::empty(), - } + LinkContent::Link(LinkTarget::Relative(rcstr!("../root.txt"))) ); Ok(()) @@ -1895,10 +1850,7 @@ mod tests { let via_alias = fs.read_link(root_path.join("link-via-alias")?).await?; assert_eq!( *via_alias, - LinkContent::Link { - target: rcstr!("foo.txt"), - link_type: LinkType::ABSOLUTE, - } + LinkContent::Link(LinkTarget::Absolute(rcstr!("foo.txt"))) ); // link-outside -> /outside.txt (outside of the fs root) @@ -1956,13 +1908,9 @@ mod tests { let target = RcStr::from(format!("../_targets/{target_idx}")); let symlink_path = symlinks_dir.join(&symlink_idx.to_string()).unwrap(); async move { - fs.write_link( + fs.write_link_dir( symlink_path, - LinkContent::Link { - target, - link_type: LinkType::DIRECTORY, - } - .cell(), + LinkContent::Link(LinkTarget::Relative(target)).cell(), ) .await } diff --git a/turbopack/crates/turbo-tasks-fs/src/embed/fs.rs b/turbopack/crates/turbo-tasks-fs/src/embed/fs.rs index 12136726ab6f..a5463a8242d3 100644 --- a/turbopack/crates/turbo-tasks-fs/src/embed/fs.rs +++ b/turbopack/crates/turbo-tasks-fs/src/embed/fs.rs @@ -77,7 +77,7 @@ impl FileSystem for EmbeddedFileSystem { } #[turbo_tasks::function] - fn write_link(&self, _path: FileSystemPath, _target: Vc) -> Result> { + fn write_link_dir(&self, _path: FileSystemPath, _target: Vc) -> Result> { bail!("Writing is not possible to the embedded filesystem") } diff --git a/turbopack/crates/turbo-tasks-fs/src/lib.rs b/turbopack/crates/turbo-tasks-fs/src/lib.rs index d0773b250a9a..0441fb83b2d4 100644 --- a/turbopack/crates/turbo-tasks-fs/src/lib.rs +++ b/turbopack/crates/turbo-tasks-fs/src/lib.rs @@ -53,11 +53,14 @@ pub(crate) use crate::{ pub use crate::{ content::{ File, FileContent, FileJsonContent, FileLine, FileLinesContent, FileMeta, LinkContent, - LinkType, Permissions, PersistedFileContent, + LinkTarget, Permissions, PersistedFileContent, }, disk::{DiskFileSystem, canonicalize_to_rcstr, validate_path_length}, null_fs::NullFileSystem, - path::{FileSystemPath, FileSystemPathOption, RealPathResult, RealPathResultError, rebase}, + path::{ + FileSystemPath, FileSystemPathOption, RealPathResult, RealPathResultError, rebase, + resolve_link_target, + }, read_glob::ReadGlobResult, virtual_fs::VirtualFileSystem, watcher::{DiskWatcherConfig, DiskWatcherRecursiveMode}, @@ -75,9 +78,8 @@ pub trait FileSystem: ValueToString { fn read(self: Vc, fs_path: FileSystemPath) -> Vc; /// Reads the target of a symbolic link (or of a junction point on Windows). /// - /// The base of the returned [`LinkContent::Link`] `target` depends on the link's - /// [`LinkType`]: root-relative and normalized for [`LinkType::ABSOLUTE`] links, or the raw - /// link-relative on-disk value otherwise. + /// The returned [`LinkTarget`] is root-relative and normalized for [`LinkTarget::Absolute`] + /// links, or the raw link-relative on-disk value for [`LinkTarget::Relative`] links. /// /// Returns [`LinkContent::Invalid`] if the target points outside of the filesystem root, and /// [`LinkContent::NotFound`] if `fs_path` doesn't exist or isn't a link. @@ -87,9 +89,9 @@ pub trait FileSystem: ValueToString { fn raw_read_dir(self: Vc, fs_path: FileSystemPath) -> Vc; #[turbo_tasks::function] fn write(self: Vc, fs_path: FileSystemPath, content: Vc) -> Vc<()>; - /// See [`FileSystemPath::write_symbolic_link_dir`]. + /// See [`FileSystemPath::write_dir_link`]. #[turbo_tasks::function] - fn write_link(self: Vc, fs_path: FileSystemPath, target: Vc) -> Vc<()>; + fn write_link_dir(self: Vc, fs_path: FileSystemPath, target: Vc) -> Vc<()>; #[turbo_tasks::function] fn metadata(self: Vc, fs_path: FileSystemPath) -> Vc; } diff --git a/turbopack/crates/turbo-tasks-fs/src/null_fs.rs b/turbopack/crates/turbo-tasks-fs/src/null_fs.rs index ab0266fcd34c..cabdb7405e8c 100644 --- a/turbopack/crates/turbo-tasks-fs/src/null_fs.rs +++ b/turbopack/crates/turbo-tasks-fs/src/null_fs.rs @@ -30,7 +30,7 @@ impl FileSystem for NullFileSystem { fn write(&self, _fs_path: FileSystemPath, _content: Vc) {} #[turbo_tasks::function] - fn write_link(&self, _fs_path: FileSystemPath, _target: Vc) {} + fn write_link_dir(&self, _fs_path: FileSystemPath, _target: Vc) {} #[turbo_tasks::function] fn metadata(&self, _fs_path: FileSystemPath) -> Vc { diff --git a/turbopack/crates/turbo-tasks-fs/src/path.rs b/turbopack/crates/turbo-tasks-fs/src/path.rs index 4863c2ec109f..b3b759f2399e 100644 --- a/turbopack/crates/turbo-tasks-fs/src/path.rs +++ b/turbopack/crates/turbo-tasks-fs/src/path.rs @@ -15,7 +15,7 @@ use turbo_unix_path::{get_parent_path, get_relative_path_to, join_path, normaliz use crate::{ DirectoryContent, DirectoryEntry, FileContent, FileJsonContent, FileMeta, FileSystem, - FileSystemEntryType, LinkContent, LinkType, RawDirectoryContent, RawDirectoryEntry, + FileSystemEntryType, LinkContent, LinkTarget, RawDirectoryContent, RawDirectoryEntry, ReadGlobResult, glob::Glob, read_glob::{read_glob, track_glob}, @@ -417,18 +417,14 @@ impl FileSystemPath { /// privileges][windows-privileges] if "developer mode" is not enabled, so we can't safely use /// them. Using junction points [matches the behavior of pnpm][pnpm-windows]. /// - /// This only supports directories because Windows junction points are incompatible with files. - /// To ensure compatibility, this will return an error if the target is a file, even on - /// platforms with full symlink support. - /// /// **We intentionally do not provide an API for symlinking a file**, as we cannot support that /// on all Windows configurations. /// /// [windows-symlink]: https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10/ /// [windows-privileges]: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/create-symbolic-links /// [pnpm-windows]: https://pnpm.io/faq#does-it-work-on-windows - pub fn write_symbolic_link_dir(&self, target: Vc) -> Vc<()> { - self.fs().write_link(self.clone(), target) + pub fn write_dir_link(&self, target: Vc) -> Vc<()> { + self.fs().write_link_dir(self.clone(), target) } pub fn metadata(&self) -> Vc { @@ -620,12 +616,11 @@ async fn realpath_with_links(path: FileSystemPath) -> Result> .rsplit_once('/') .map_or(current_path.path.as_str(), |(_, name)| name); symlinks.extend(parent_result.symlinks); - let parent_path = match parent_result.path_result { + match parent_result.path_result { Ok(path) => { if path != parent { current_path = path.join(basename)?; } - path } Err(parent_error) => { error = parent_error; @@ -647,14 +642,9 @@ async fn realpath_with_links(path: FileSystemPath) -> Result> } match &*current_path.read_link().await? { - LinkContent::Link { target, link_type } => { + LinkContent::Link(target) => { symlinks.insert(current_path.clone()); - current_path = if link_type.contains(LinkType::ABSOLUTE) { - current_path.root().owned().await? - } else { - parent_path - } - .join(target)?; + current_path = resolve_link_target(¤t_path, target).await?; } LinkContent::NotFound => { error = RealPathResultError::NotFound; @@ -681,6 +671,18 @@ async fn realpath_with_links(path: FileSystemPath) -> Result> .cell()) } +/// Resolves a link target using the storage convention described by [`LinkContent::Link`]. +pub async fn resolve_link_target( + link_path: &FileSystemPath, + target: &LinkTarget, +) -> Result { + let (base, target) = match target { + LinkTarget::Absolute(target) => (link_path.root().owned().await?, target), + LinkTarget::Relative(target) => (link_path.parent(), target), + }; + base.join(target) +} + #[cfg(test)] mod tests { use turbo_rcstr::rcstr; diff --git a/turbopack/crates/turbo-tasks-fs/src/read_glob.rs b/turbopack/crates/turbo-tasks-fs/src/read_glob.rs index c7a5bd459fc8..399204107d06 100644 --- a/turbopack/crates/turbo-tasks-fs/src/read_glob.rs +++ b/turbopack/crates/turbo-tasks-fs/src/read_glob.rs @@ -5,7 +5,8 @@ use turbo_rcstr::RcStr; use turbo_tasks::{Completion, ResolvedVc, TryJoinIterExt, Vc, turbobail}; use crate::{ - DirectoryContent, DirectoryEntry, FileSystem, FileSystemPath, LinkContent, LinkType, glob::Glob, + DirectoryContent, DirectoryEntry, FileSystem, FileSystemEntryType, FileSystemPath, LinkContent, + glob::Glob, resolve_link_target, }; #[turbo_tasks::value] @@ -85,8 +86,11 @@ async fn read_glob_internal( handle_dir(&mut result, entry_path, segment, path).await?; } DirectoryEntry::Symlink(path) => { - if let LinkContent::Link { link_type, .. } = &*path.read_link().await? { - if link_type.contains(LinkType::DIRECTORY) { + if let LinkContent::Link(target) = &*path.read_link().await? { + if matches!( + *resolve_link_target(path, target).await?.get_type().await?, + FileSystemEntryType::Directory + ) { // Ensure that there are no infinite link loops, but don't resolve resolve_symlink_safely(entry.clone()).await?; diff --git a/turbopack/crates/turbo-tasks-fs/src/virtual_fs.rs b/turbopack/crates/turbo-tasks-fs/src/virtual_fs.rs index 32a608d772df..6bc4eb62aa70 100644 --- a/turbopack/crates/turbo-tasks-fs/src/virtual_fs.rs +++ b/turbopack/crates/turbo-tasks-fs/src/virtual_fs.rs @@ -62,7 +62,7 @@ impl FileSystem for VirtualFileSystem { } #[turbo_tasks::function] - fn write_link(&self, _fs_path: FileSystemPath, _target: Vc) -> Result> { + fn write_link_dir(&self, _fs_path: FileSystemPath, _target: Vc) -> Result> { bail!("Writing is not possible on the virtual file system") } diff --git a/turbopack/crates/turbo-tasks-fuzz/src/fs_watcher.rs b/turbopack/crates/turbo-tasks-fuzz/src/fs_watcher.rs index 212efda935a6..52a3fc4fe9cb 100644 --- a/turbopack/crates/turbo-tasks-fuzz/src/fs_watcher.rs +++ b/turbopack/crates/turbo-tasks-fuzz/src/fs_watcher.rs @@ -20,7 +20,7 @@ use turbo_tasks::{ }; use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; use turbo_tasks_fs::{ - DiskFileSystem, File, FileContent, FileSystem, FileSystemPath, LinkContent, LinkType, + DiskFileSystem, File, FileContent, FileSystem, FileSystemPath, LinkContent, LinkTarget, }; // `read_or_write_all_paths_operation` always writes the sentinel values to files/symlinks. We can @@ -64,9 +64,6 @@ pub struct FsWatcher { #[derive(Clone, Copy, Debug, ValueEnum)] enum SymlinkMode { - /// Test file symlinks - #[cfg_attr(windows, doc = "(requires developer mode or admin)")] - File, /// Test directory symlinks #[cfg_attr(windows, doc = "(requires developer mode or admin)")] Directory, @@ -75,17 +72,6 @@ enum SymlinkMode { Junction, } -impl SymlinkMode { - fn to_link_type(self) -> LinkType { - match self { - SymlinkMode::File => LinkType::empty(), - SymlinkMode::Directory => LinkType::DIRECTORY, - #[cfg(windows)] - SymlinkMode::Junction => LinkType::DIRECTORY, - } - } -} - #[derive(Default, NonLocalValue, TraceRawVcs)] struct PathInvalidations(#[turbo_tasks(trace_ignore)] Arc>>); @@ -123,7 +109,7 @@ pub async fn run(args: FsWatcher) -> anyhow::Result<()> { create_directory_tree(&mut FxHashSet::default(), &fs_root, args.depth, args.width)?; let mut symlink_targets = if let Some(mode) = args.symlinks { - create_initial_symlinks(&fs_root, mode, args.symlink_count, args.depth)? + create_initial_symlinks(&fs_root, mode, args.symlink_count)? } else { Vec::new() }; @@ -139,16 +125,12 @@ pub async fn run(args: FsWatcher) -> anyhow::Result<()> { }; let track_writes = args.track_writes; let symlink_mode = args.symlinks; - let symlink_is_directory = - symlink_mode.map(|m| m.to_link_type().contains(LinkType::DIRECTORY)); - let effects_op = extract_effects_operation(read_or_write_all_paths_operation( invalidations.clone(), project_root.clone(), args.depth, args.width, symlink_count, - symlink_is_directory, track_writes, )); if track_writes { @@ -234,7 +216,6 @@ pub async fn run(args: FsWatcher) -> anyhow::Result<()> { args.depth, args.width, symlink_count, - symlink_is_directory, track_writes, )); let symlink_info = if args.symlinks.is_some() { @@ -344,19 +325,13 @@ async fn write_link( invalidations: TransientInstance, path: FileSystemPath, target: RcStr, - is_directory: bool, ) -> anyhow::Result<()> { let path_str = path.path.clone(); invalidations.0.lock().unwrap().insert(path_str); - let link_type = if is_directory { - LinkType::DIRECTORY - } else { - LinkType::empty() - }; - let link_content = LinkContent::Link { target, link_type }; + let link_content = LinkContent::Link(LinkTarget::Relative(target)); let _ = path .fs() - .write_link(path.clone(), link_content.cell()) + .write_link_dir(path.clone(), link_content.cell()) .await?; Ok(()) } @@ -368,7 +343,6 @@ async fn read_or_write_all_paths_operation( depth: usize, width: usize, symlink_count: u32, - symlink_is_directory: Option, write: bool, ) -> anyhow::Result<()> { async fn process_paths_inner( @@ -411,7 +385,6 @@ async fn read_or_write_all_paths_operation( invalidations.clone(), symlink_path, RcStr::from(SYMLINK_SENTINEL_TARGET), - symlink_is_directory.unwrap_or(false), ) .await?; } else { @@ -538,21 +511,12 @@ fn create_initial_symlinks( fs_root: &Path, symlink_mode: SymlinkMode, symlink_count: u32, - depth: usize, ) -> anyhow::Result> { // Use a dedicated "symlinks" directory to avoid conflicts let symlinks_dir = fs_root.join("_symlinks"); std::fs::create_dir_all(&symlinks_dir)?; let initial_target_relative = match symlink_mode { - SymlinkMode::File => { - // Point to a file at depth: 0/0/0/.../0 - let mut path = PathBuf::new(); - for _ in 0..depth { - path.push("0"); - } - path - } SymlinkMode::Directory => PathBuf::from("0"), #[cfg(windows)] SymlinkMode::Junction => PathBuf::from("0"), @@ -579,9 +543,6 @@ fn create_symlink(link_path: &Path, target: &Path, mode: SymlinkMode) -> anyhow: #[cfg(windows)] { match mode { - SymlinkMode::File => { - std::os::windows::fs::symlink_file(target, link_path)?; - } SymlinkMode::Directory => { std::os::windows::fs::symlink_dir(target, link_path)?; } @@ -604,7 +565,7 @@ fn remove_symlink(link_path: &Path, mode: SymlinkMode) -> anyhow::Result<()> { #[cfg(windows)] { match mode { - SymlinkMode::File | SymlinkMode::Directory => { + SymlinkMode::Directory => { std::fs::remove_file(link_path)?; } SymlinkMode::Junction => { @@ -639,7 +600,6 @@ fn pick_random_directory(max_depth: usize, width: usize) -> RandomDirectory { fn pick_random_link_target(depth: usize, width: usize, mode: SymlinkMode) -> PathBuf { match mode { - SymlinkMode::File => pick_random_file(depth, width), SymlinkMode::Directory => pick_random_directory(depth, width).path, #[cfg(windows)] SymlinkMode::Junction => pick_random_directory(depth, width).path, diff --git a/turbopack/crates/turbo-tasks-fuzz/src/symlink_stress.rs b/turbopack/crates/turbo-tasks-fuzz/src/symlink_stress.rs index 721fb225a9bf..77b0c8bda4c2 100644 --- a/turbopack/crates/turbo-tasks-fuzz/src/symlink_stress.rs +++ b/turbopack/crates/turbo-tasks-fuzz/src/symlink_stress.rs @@ -13,7 +13,7 @@ use turbo_tasks::{ read_strongly_consistent_and_apply_effects, take_effects, }; use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; -use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkType}; +use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkTarget}; #[derive(Args)] pub struct SymlinkStress { @@ -215,13 +215,10 @@ async fn write_symlink( target: RcStr, ) -> anyhow::Result<()> { let symlink_path = symlinks_dir.join(&symlink_idx.to_string())?; - let link_content = LinkContent::Link { - target, - link_type: LinkType::DIRECTORY, - }; + let link_content = LinkContent::Link(LinkTarget::Relative(target)); symlink_path .fs() - .write_link(symlink_path.clone(), link_content.cell()) + .write_link_dir(symlink_path.clone(), link_content.cell()) .await?; Ok(()) } diff --git a/turbopack/crates/turbopack-core/src/asset.rs b/turbopack/crates/turbopack-core/src/asset.rs index 860dd6bf169a..418ac481cdf6 100644 --- a/turbopack/crates/turbopack-core/src/asset.rs +++ b/turbopack/crates/turbopack-core/src/asset.rs @@ -1,8 +1,8 @@ -use anyhow::Result; +use anyhow::{Result, bail}; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, Vc}; use turbo_tasks_fs::{ - FileContent, FileJsonContent, FileLinesContent, FileSystemPath, LinkContent, LinkType, + FileContent, FileJsonContent, FileLinesContent, FileSystemPath, LinkContent, LinkTarget, }; use turbo_tasks_hash::{HashAlgorithm, deterministic_hash}; @@ -54,8 +54,13 @@ pub enum AssetContent { File(ResolvedVc), // for the relative link, the target is raw value read from the link // for the absolute link, the target is stripped of the root path while reading + // `is_directory` preserves the target type read from disk so file links cannot be silently + // recreated as directory links. // See [LinkContent::Link] for more details. - Redirect { target: RcStr, link_type: LinkType }, + Redirect { + target: LinkTarget, + is_directory: bool, + }, } #[turbo_tasks::value_impl] @@ -115,16 +120,20 @@ impl AssetContent { AssetContent::File(file) => { path.write(**file).as_side_effect().await?; } - AssetContent::Redirect { target, link_type } => { - path.write_symbolic_link_dir( - LinkContent::Link { - target: target.clone(), - link_type: *link_type, - } - .cell(), - ) - .as_side_effect() - .await?; + AssetContent::Redirect { + target, + is_directory, + } => { + if !is_directory { + bail!( + "cannot create file link to {target:?}: only directory links are \ + supported, because Windows junction points have no file equivalent and \ + file symlinks require elevated privileges" + ); + } + path.write_dir_link(LinkContent::Link(target.clone()).cell()) + .as_side_effect() + .await?; } } Ok(()) @@ -134,9 +143,12 @@ impl AssetContent { pub async fn hash(&self, salt: Vc, algorithm: HashAlgorithm) -> Result> { Ok(match self { AssetContent::File(content) => content.hash(salt, algorithm), - AssetContent::Redirect { target, link_type } => Vc::cell(RcStr::from( + AssetContent::Redirect { + target, + is_directory, + } => Vc::cell(RcStr::from( // no_hash_salt - deterministic_hash(&salt.await?, (target, link_type), algorithm), + deterministic_hash(&salt.await?, (target, is_directory), algorithm), )), }) } diff --git a/turbopack/crates/turbopack-core/src/file_source.rs b/turbopack/crates/turbopack-core/src/file_source.rs index 7d6af4ebbdb8..2edab74ea989 100644 --- a/turbopack/crates/turbopack-core/src/file_source.rs +++ b/turbopack/crates/turbopack-core/src/file_source.rs @@ -1,7 +1,9 @@ use anyhow::{Result, bail}; use turbo_rcstr::RcStr; use turbo_tasks::Vc; -use turbo_tasks_fs::{FileContent, FileSystemEntryType, FileSystemPath, LinkContent}; +use turbo_tasks_fs::{ + FileContent, FileSystemEntryType, FileSystemPath, LinkContent, resolve_link_target, +}; use crate::{ asset::{Asset, AssetContent}, @@ -66,11 +68,20 @@ impl Asset for FileSource { let file_type = &*self.path.get_type().await?; match file_type { FileSystemEntryType::Symlink => match &*self.path.read_link().await? { - LinkContent::Link { target, link_type } => Ok(AssetContent::Redirect { - target: target.clone(), - link_type: *link_type, + LinkContent::Link(target) => { + let is_directory = matches!( + *resolve_link_target(&self.path, target) + .await? + .get_type() + .await?, + FileSystemEntryType::Directory + ); + Ok(AssetContent::Redirect { + target: target.clone(), + is_directory, + } + .cell()) } - .cell()), _ => bail!("Invalid symlink"), }, FileSystemEntryType::File => { diff --git a/turbopack/crates/turbopack-core/src/introspect/utils.rs b/turbopack/crates/turbopack-core/src/introspect/utils.rs index f20ffaaf88a0..af041abcb068 100644 --- a/turbopack/crates/turbopack-core/src/introspect/utils.rs +++ b/turbopack/crates/turbopack-core/src/introspect/utils.rs @@ -54,9 +54,10 @@ pub async fn content_to_details(content: Vc) -> Result> } FileContent::NotFound => Vc::cell(rcstr!("not found")), }, - AssetContent::Redirect { target, link_type } => { - Vc::cell(format!("redirect to {target} with type {link_type:?}").into()) - } + AssetContent::Redirect { + target, + is_directory, + } => Vc::cell(format!("redirect to {target:?}, directory: {is_directory}").into()), }) } diff --git a/turbopack/crates/turbopack-core/src/resolve/pattern.rs b/turbopack/crates/turbopack-core/src/resolve/pattern.rs index 8c73dd246369..b3dae4b2a9ed 100644 --- a/turbopack/crates/turbopack-core/src/resolve/pattern.rs +++ b/turbopack/crates/turbopack-core/src/resolve/pattern.rs @@ -14,7 +14,8 @@ use turbo_tasks::{ NonLocalValue, TaskInput, ValueToString, Vc, debug::ValueDebugFormat, trace::TraceRawVcs, }; use turbo_tasks_fs::{ - FileSystemPath, LinkContent, LinkType, RawDirectoryContent, RawDirectoryEntry, + FileSystemEntryType, FileSystemPath, LinkContent, RawDirectoryContent, RawDirectoryEntry, + resolve_link_target, }; use turbo_unix_path::normalize_path; @@ -1606,12 +1607,17 @@ pub async fn read_matches( )), RawDirectoryEntry::Symlink => { let fs_path = parent_fs_path.join(last_segment)?; - let LinkContent::Link { link_type, .. } = &*fs_path.read_link().await? - else { + let LinkContent::Link(target) = &*fs_path.read_link().await? else { continue; }; let path = concat(&prefix, str).into(); - if link_type.contains(LinkType::DIRECTORY) { + if matches!( + *resolve_link_target(&fs_path, target) + .await? + .get_type() + .await?, + FileSystemEntryType::Directory + ) { results.push((index, PatternMatch::Directory(path, fs_path))); } else { results.push((index, PatternMatch::File(path, fs_path))) @@ -1795,10 +1801,15 @@ pub async fn read_matches( } if let Some(pos) = pat.match_position(&prefix) { let fs_path = lookup_dir.join(key)?; - if let LinkContent::Link { link_type, .. } = - &*fs_path.read_link().await? + if let LinkContent::Link(target) = &*fs_path.read_link().await? { - if link_type.contains(LinkType::DIRECTORY) { + if matches!( + *resolve_link_target(&fs_path, target) + .await? + .get_type() + .await?, + FileSystemEntryType::Directory + ) { results.push(( pos, PatternMatch::Directory( @@ -1817,9 +1828,14 @@ pub async fn read_matches( prefix.push('/'); if let Some(pos) = pat.match_position(&prefix) { let fs_path = lookup_dir.join(key)?; - if let LinkContent::Link { link_type, .. } = - &*fs_path.read_link().await? - && link_type.contains(LinkType::DIRECTORY) + if let LinkContent::Link(target) = &*fs_path.read_link().await? + && matches!( + *resolve_link_target(&fs_path, target) + .await? + .get_type() + .await?, + FileSystemEntryType::Directory + ) { results.push(( pos, @@ -1829,9 +1845,14 @@ pub async fn read_matches( } if let Some(pos) = pat.could_match_position(&prefix) { let fs_path = lookup_dir.join(key)?; - if let LinkContent::Link { link_type, .. } = - &*fs_path.read_link().await? - && link_type.contains(LinkType::DIRECTORY) + if let LinkContent::Link(target) = &*fs_path.read_link().await? + && matches!( + *resolve_link_target(&fs_path, target) + .await? + .get_type() + .await?, + FileSystemEntryType::Directory + ) { results.push(( pos, diff --git a/turbopack/crates/turbopack-core/src/server_fs.rs b/turbopack/crates/turbopack-core/src/server_fs.rs index d632525c4c56..e462f6692cf0 100644 --- a/turbopack/crates/turbopack-core/src/server_fs.rs +++ b/turbopack/crates/turbopack-core/src/server_fs.rs @@ -40,7 +40,7 @@ impl FileSystem for ServerFileSystem { } #[turbo_tasks::function] - fn write_link(&self, _fs_path: FileSystemPath, _target: Vc) -> Result> { + fn write_link_dir(&self, _fs_path: FileSystemPath, _target: Vc) -> Result> { bail!("Writing is not possible to the marker filesystem for the server") } diff --git a/turbopack/crates/turbopack-ecmascript/src/references/external_module.rs b/turbopack/crates/turbopack-ecmascript/src/references/external_module.rs index f900d49a87c8..5a9e6c44d621 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/external_module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/external_module.rs @@ -4,7 +4,9 @@ use anyhow::{Context, Result}; use bincode::{Decode, Encode}; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, TryJoinIterExt, ValueToStringRef, Vc, trace::TraceRawVcs}; -use turbo_tasks_fs::{FileSystem, FileSystemPath, LinkType, VirtualFileSystem, rope::RopeBuilder}; +use turbo_tasks_fs::{ + FileSystem, FileSystemPath, LinkTarget, VirtualFileSystem, rope::RopeBuilder, +}; use turbo_tasks_hash::{encode_hex, hash_xxh3_hash64}; use turbopack_core::{ asset::{Asset, AssetContent}, @@ -509,8 +511,8 @@ impl Asset for ExternalsSymlinkAsset { .into(); Ok(AssetContent::Redirect { - target, - link_type: LinkType::DIRECTORY, + target: LinkTarget::Relative(target), + is_directory: true, } .cell()) } diff --git a/turbopack/crates/turbopack-test-utils/src/snapshot.rs b/turbopack/crates/turbopack-test-utils/src/snapshot.rs index 4d013fc7dff8..23ca39c0b7d4 100644 --- a/turbopack/crates/turbopack-test-utils/src/snapshot.rs +++ b/turbopack/crates/turbopack-test-utils/src/snapshot.rs @@ -169,8 +169,11 @@ async fn get_contents(file: Vc) -> Result> { } } }, - AssetContent::Redirect { target, link_type } => Some(format!( - "Redirect {{ target: {target}, link_type: {link_type:?} }}" + AssetContent::Redirect { + target, + is_directory, + } => Some(format!( + "Redirect {{ target: {target:?}, is_directory: {is_directory} }}" )), }) }