use super::paths::{resolve, tree}; use super::*; // Link authorization and rebasing must not race another managed link mutation. pub(super) static MUTATIONS: Mutex<()> = Mutex::new(()); pub(super) fn authorize_target( caps: &Capabilities, link: &PathTarget, text: &Path, label: &str, ) -> io::Result<()> { if caps.unrestricted_read() && caps.unrestricted_write() { return Ok(()); } let denied_target = || { io::Error::new( io::ErrorKind::PermissionDenied, format!("symlink target for {label}"), ) }; let path = link .as_path() .parent() .ok_or_else(denied_target)? .join(text); let target = PathTarget::for_creation(path).map_err(|_| denied_target())?; tree(caps, &target, label, false).map_err(|_| denied_target())?; tree(caps, &target, label, true).map_err(|_| denied_target())?; let mut root = PathBuf::new(); let mut names = Vec::new(); for component in target.as_path().components() { match component { Component::Prefix(_) | Component::RootDir => root.push(component.as_os_str()), Component::Normal(name) => names.push(name), _ => return Err(denied_target()), } } let mut directory = Dir::open_ambient_dir(root, cap_std::ambient_authority()).map_err(|_| denied_target())?; for (index, name) in names.iter().enumerate() { match directory.symlink_metadata(name) { Ok(metadata) if metadata.is_symlink() => return Err(denied_target()), Ok(_) if index + 1 == names.len() => return Ok(()), Ok(_) => { directory = directory .open_dir_nofollow(name) .map_err(|_| denied_target())?; } Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()), Err(_) => return Err(denied_target()), } } Ok(()) } fn validate(target: &Path, kind: Option<&str>) -> io::Result<()> { if target.as_os_str().is_empty() || target.as_os_str().as_encoded_bytes().contains(&0) || kind.is_some_and(|kind| kind != "file" && kind != "dir") { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Invalid symbolic link target or type", )); } Ok(()) } pub(super) fn create(directory: &Dir, name: &std::ffi::OsStr, target: &Path) -> io::Result<()> { #[cfg(not(windows))] { directory.symlink_contents(target, name) } #[cfg(windows)] { let _ = (directory, name); Err(io::Error::new( io::ErrorKind::Unsupported, "not yet implemented: capability-safe Windows symlink creation", )) } } pub(super) fn contains_links(directory: Dir) -> io::Result { use cap_fs_ext::MetadataExt; let metadata = directory.dir_metadata()?; let mut seen = std::collections::HashSet::from([(metadata.dev(), metadata.ino())]); let mut stack = vec![(directory.entries()?, directory)]; while let Some((entries, directory)) = stack.last_mut() { let Some(entry) = entries.next() else { stack.pop(); continue; }; let entry = entry?; let kind = entry.file_type()?; if kind.is_symlink() { return Ok(true); } #[cfg(windows)] { use cap_std::fs::MetadataExt; if directory .symlink_metadata(entry.file_name())? .file_attributes() & 0x400 != 0 { return Ok(true); } } if kind.is_dir() { let child = directory.open_dir_nofollow(entry.file_name())?; let metadata = child.dir_metadata()?; if seen.insert((metadata.dev(), metadata.ino())) { stack.push((child.entries()?, child)); } } } Ok(false) } pub(super) fn text(path: PathBuf) -> io::Result { path.into_os_string().into_string().map_err(|_| { io::Error::new( io::ErrorKind::Unsupported, "not yet implemented: non-UTF-8 link paths", ) }) } pub(super) fn ops(files: Arc) -> Vec { let ops = files.clone(); let symlink = files.clone(); let read_link = files.clone(); let copy_link = files.clone(); vec![ ops.io_ops( "op_file_copy_link", "op_file_copy_link_sync", move |(source, destination): (String, String)| { let caps = copy_link.caps.clone(); let pending = copy_link.pool.run(move || { let _guard = MUTATIONS .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let from = resolve(&caps, &source, true, false)?; let to = resolve(&caps, &destination, true, true)?; let (from_dir, from_name) = parent(from.as_path())?; let metadata = from_dir.symlink_metadata(&from_name)?; if !metadata.is_symlink() { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Source is not a symbolic link", )); } let contents = from_dir.read_link_contents(from_name)?; validate(&contents, None)?; #[cfg(windows)] { let _ = (to, contents); return Err::(io::Error::new( io::ErrorKind::Unsupported, "not yet implemented: capability-safe Windows symlink creation", )); } #[cfg(not(windows))] { use cap_fs_ext::MetadataExt; let (to_dir, to_name) = super::paths::mutable_entry(&to)?; authorize_target(&caps, &to, &contents, &destination)?; match to_dir.symlink_metadata(&to_name) { Ok(existing) if existing.dev() == metadata.dev() && existing.ino() == metadata.ino() => { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Source and destination are the same link", )); } Ok(existing) if existing.is_dir() => { return Err(io::Error::new( io::ErrorKind::IsADirectory, "Link destination is a directory", )); } Ok(_) => to_dir.remove_file_or_symlink(&to_name)?, Err(error) if error.kind() == io::ErrorKind::NotFound => (), Err(error) => return Err(error), } create(&to_dir, &to_name, &contents)?; Ok(true) } }); async move { Ok::<_, Infallible>(receive(pending).await) } }, ), ops.io_ops( "op_file_symlink", "op_file_symlink_sync", move |(target, link, kind): (String, String, Option)| { let caps = symlink.caps.clone(); let pending = symlink.pool.run(move || { validate(Path::new(&target), kind.as_deref())?; let _guard = MUTATIONS .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let entry = resolve(&caps, &link, true, true)?; let (directory, name) = super::paths::mutable_entry(&entry)?; authorize_target(&caps, &entry, Path::new(&target), &link)?; create(&directory, &name, Path::new(&target))?; Ok(true) }); async move { Ok::<_, Infallible>(receive(pending).await) } }, ), ops.io_ops( "op_file_read_link", "op_file_read_link_sync", move |path: String| { let caps = read_link.caps.clone(); let pending = read_link.pool.run(move || { let entry = resolve(&caps, &path, true, false)?; let (directory, name) = parent(entry.as_path())?; text(directory.read_link_contents(name)?) }); async move { Ok::<_, Infallible>(receive(pending).await) } }, ), ops.io_ops( "op_file_real_path", "op_file_real_path_sync", move |path: String| { let caps = files.caps.clone(); let pending = files.pool.run(move || { let target = resolve(&caps, &path, false, false)?; let (directory, name) = parent(target.as_path())?; if directory.symlink_metadata(name)?.is_symlink() { return Err(io::Error::new( io::ErrorKind::NotFound, "Symbolic link changed or has a missing target", )); } text(target.into_path()) }); async move { Ok::<_, Infallible>(receive(pending).await) } }, ), ] .into_iter() .flatten() .collect() }