use super::*; pub(super) fn resolve( caps: &Capabilities, label: &str, entry: bool, write: bool, ) -> io::Result { if label.is_empty() { return Err(io::Error::new( io::ErrorKind::NotFound, "Empty filesystem path", )); } let path = filesystem_path(caps, label)?; let target = if entry { PathTarget::for_entry(path) } else { PathTarget::for_creation(path) }; let target = match target { Ok(target) => target, Err(_) if !(if write { caps.unrestricted_write() } else { caps.unrestricted_read() }) => { return Err(denied(label, write)); } Err(error) => return Err(error), }; authorize(caps, &target, label, write)?; Ok(target) } pub(super) fn tree( caps: &Capabilities, target: &PathTarget, label: &str, write: bool, ) -> io::Result<()> { if write { caps.check_write_tree(target) } else { caps.check_read_tree(target) } .map_err(|_| { io::Error::new( io::ErrorKind::PermissionDenied, format!("{} tree {label}", if write { "write" } else { "read" }), ) }) } pub(super) fn follows_link(label: &str) -> bool { // A trailing slash names the referent, not the link. label.ends_with('/') || label.ends_with("/.") || (cfg!(windows) && (label.ends_with('\\') || label.ends_with("\\."))) } pub(super) fn mutable_entry(target: &PathTarget) -> io::Result<(Dir, OsString)> { let (dir, name) = parent(target.as_path())?; if name == "." { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Cannot mutate a filesystem root", )); } Ok((dir, name)) } fn regular(metadata: &Metadata) -> io::Result<()> { if metadata.is_file() { Ok(()) } else { Err(io::Error::new( io::ErrorKind::Unsupported, "not yet implemented: copying non-regular files", )) } } pub(super) fn entries( caps: &Capabilities, path: &str, whole_tree: bool, ) -> io::Result> { let target = resolve(caps, path, false, false)?; if whole_tree { tree(caps, &target, path, false)?; } let (parent, name) = parent(target.as_path())?; let directory = parent.open_dir_nofollow(name)?; let mut entries = Vec::new(); for entry in directory.entries()? { let entry = entry?; let name = entry.file_name(); if !whole_tree && caps.check_read(&target.child(&name)?).is_err() { continue; } let name = name.into_string().map_err(|_| { io::Error::new( io::ErrorKind::Unsupported, "not yet implemented: non-UTF-8 directory entries", ) })?; let kind = entry.file_type()?; entries.push((name, kind.is_file(), kind.is_dir(), kind.is_symlink())); } Ok(entries) } pub(super) fn ops(files: Arc) -> Vec { let ops = files.clone(); let read_dir = files.clone(); let mkdir = files.clone(); let remove = files.clone(); let rename = files.clone(); vec![ ops.io_ops("op_file_read_dir", "op_file_read_dir_sync", move |path: String| { let caps = read_dir.caps.clone(); let pending = read_dir.pool.run(move || entries(&caps, &path, false)); async move { Ok::<_, Infallible>(receive(pending).await) } }), ops.io_ops("op_file_mkdir", "op_file_mkdir_sync", move |(path, mode): (String, Option)| { let caps = mkdir.caps.clone(); let pending = mkdir.pool.run(move || { if mode.is_some_and(|mode| mode > 0o7777) { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Invalid directory mode", )); } let target = resolve(&caps, &path, true, true)?; let (directory, name) = mutable_entry(&target)?; let mut builder = cap_std::fs::DirBuilder::new(); #[cfg(unix)] { use cap_std::fs::DirBuilderExt; if let Some(mode) = mode { builder.mode(mode); } } authorize(&caps, &target, &path, true)?; directory.create_dir_with(name, &builder)?; Ok(true) }); async move { Ok::<_, Infallible>(receive(pending).await) } }, ), ops.io_ops("op_file_remove", "op_file_remove_sync", move |(path, recursive): (String, bool)| { let caps = remove.caps.clone(); let pending = remove.pool.run(move || { let target = resolve(&caps, &path, true, true)?; if recursive { tree(&caps, &target, &path, true)?; } let (directory, name) = mutable_entry(&target)?; let metadata = directory.symlink_metadata(&name)?; authorize(&caps, &target, &path, true)?; if metadata.is_dir() { if recursive { directory.remove_dir_all(name)?; } else { directory.remove_dir(name)?; } } else { directory.remove_file_or_symlink(name)?; } Ok(true) }); async move { Ok::<_, Infallible>(receive(pending).await) } }, ), ops.io_ops("op_file_rename", "op_file_rename_sync", move |(source, destination): (String, String)| { let caps = rename.caps.clone(); let pending = rename.pool.run(move || { let _guard = super::links::MUTATIONS .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let from = resolve(&caps, &source, true, false)?; let to = resolve(&caps, &destination, true, true)?; // rename cannot atomically insist that a concurrently replaced leaf is a file. tree(&caps, &from, &source, false)?; tree(&caps, &from, &source, true)?; tree(&caps, &to, &destination, true)?; let (from_dir, from_name) = mutable_entry(&from)?; let (to_dir, to_name) = mutable_entry(&to)?; let metadata = from_dir.symlink_metadata(&from_name)?; if metadata.is_symlink() { let text = from_dir.read_link_contents(&from_name)?; super::links::authorize_target(&caps, &to, &text, &destination)?; } else if metadata.is_dir() && !(caps.unrestricted_read() && caps.unrestricted_write()) && super::links::contains_links(from_dir.open_dir_nofollow(&from_name)?)? { return Err(io::Error::new( io::ErrorKind::Unsupported, "not yet implemented: rebasing directory symlinks with scoped permissions", )); } from_dir.rename(from_name, &to_dir, to_name)?; Ok(true) }); async move { Ok::<_, Infallible>(receive(pending).await) } }, ), ops.io_ops("op_file_copy", "op_file_copy_sync", move |(source, destination): (String, String)| { let caps = files.caps.clone(); let pending = files.pool.run(move || { let from = resolve(&caps, &source, false, false)?; let to = resolve(&caps, &destination, false, true)?; let (from_dir, from_name) = parent(from.as_path())?; regular(&from_dir.symlink_metadata(&from_name)?)?; let mut read_options = OpenOptions::new(); read_options .read(true) .follow(FollowSymlinks::No) .nonblock(true); let source_file = from_dir.open_with(from_name, &read_options)?; let metadata = source_file.metadata()?; regular(&metadata)?; let (to_dir, to_name) = mutable_entry(&to)?; match to_dir.symlink_metadata(&to_name) { Ok(metadata) => regular(&metadata)?, Err(error) if error.kind() == io::ErrorKind::NotFound => (), Err(error) => return Err(error), } authorize(&caps, &to, &destination, true)?; let mut write_options = OpenOptions::new(); write_options .write(true) .create(true) .follow(FollowSymlinks::No) .nonblock(true); let mut permissions = metadata.permissions(); #[cfg(unix)] { use cap_std::fs::{MetadataExt, OpenOptionsExt, PermissionsExt}; let mode = metadata.mode() & 0o777; write_options.mode(mode); permissions.set_mode(mode); } let destination_file = to_dir.open_with(to_name, &write_options)?; regular(&destination_file.metadata()?)?; let a = same_file::Handle::from_file(source_file.try_clone()?.into_std())?; let b = same_file::Handle::from_file(destination_file.try_clone()?.into_std())?; if a == b { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Source and destination are the same file", )); } destination_file.set_permissions(permissions)?; destination_file.set_len(0)?; let mut source_file = source_file.into_std(); let mut destination_file = destination_file.into_std(); io::copy(&mut source_file, &mut destination_file)?; Ok(true) }); async move { Ok::<_, Infallible>(receive(pending).await) } }, ), ].into_iter().flatten().collect() }