Files
codex/codex-rs/exec-server/src/local_file_system.rs
pakrym-oai 76d8f20241 [codex] Add size to internal filesystem metadata (#27927)
## Why

`ExecutorFileSystem::get_metadata` reports file kind and timestamps but
not size. Internal callers that need to enforce a size limit therefore
have to read the complete file first, which is especially wasteful for
remote filesystems.

This adds the missing internal metadata so consumers can reject
oversized files before transferring or buffering them. The field is
named `size`, matching VS Code's `FileStat.size` filesystem convention.

## What changed

- add `size: u64` to internal `FileMetadata`
- populate it from the underlying filesystem metadata
- carry it through sandbox-helper and remote exec-server responses
- cover files, directories, symlink targets, and sandboxed reads across
local and remote filesystem implementations

The new field is intentionally not exposed through the app-server API.

## Testing

- `just test -p codex-exec-server get_metadata`
- `just test -p codex-exec-server
file_system_sandboxed_metadata_and_read_allow_readable_root`
- `just test -p codex-core-plugins`
- `just test -p codex-skills-extension`
2026-06-12 12:12:08 -07:00

849 lines
26 KiB
Rust

use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tokio::io;
use crate::CopyOptions;
use crate::CreateDirectoryOptions;
use crate::ExecServerRuntimePaths;
use crate::ExecutorFileSystem;
use crate::ExecutorFileSystemFuture;
use crate::FileMetadata;
use crate::FileSystemResult;
use crate::FileSystemSandboxContext;
use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
use crate::sandboxed_file_system::SandboxedFileSystem;
const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024;
pub static LOCAL_FS: LazyLock<Arc<dyn ExecutorFileSystem>> =
LazyLock::new(|| -> Arc<dyn ExecutorFileSystem> { Arc::new(LocalFileSystem::unsandboxed()) });
#[derive(Clone, Default)]
pub(crate) struct DirectFileSystem;
#[derive(Clone, Default)]
pub(crate) struct UnsandboxedFileSystem {
file_system: DirectFileSystem,
}
#[derive(Clone, Default)]
pub struct LocalFileSystem {
unsandboxed: UnsandboxedFileSystem,
sandboxed: Option<SandboxedFileSystem>,
}
impl LocalFileSystem {
pub fn unsandboxed() -> Self {
Self {
unsandboxed: UnsandboxedFileSystem::default(),
sandboxed: None,
}
}
pub fn with_runtime_paths(runtime_paths: ExecServerRuntimePaths) -> Self {
Self {
unsandboxed: UnsandboxedFileSystem::default(),
sandboxed: Some(SandboxedFileSystem::new(runtime_paths)),
}
}
fn sandboxed(&self) -> io::Result<&SandboxedFileSystem> {
self.sandboxed.as_ref().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"sandboxed filesystem operations require configured runtime paths",
)
})
}
fn file_system_for<'a>(
&'a self,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> io::Result<(
&'a dyn ExecutorFileSystem,
Option<&'a FileSystemSandboxContext>,
)> {
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
Ok((self.sandboxed()?, sandbox))
} else {
Ok((&self.unsandboxed, sandbox))
}
}
}
impl LocalFileSystem {
async fn canonicalize(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<PathUri> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.canonicalize(path, sandbox).await
}
async fn read_file(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.read_file(path, sandbox).await
}
async fn write_file(
&self,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.write_file(path, contents, sandbox).await
}
async fn create_directory(
&self,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.create_directory(path, options, sandbox).await
}
async fn get_metadata(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.get_metadata(path, sandbox).await
}
async fn read_directory(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.read_directory(path, sandbox).await
}
async fn remove(
&self,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.remove(path, options, sandbox).await
}
async fn copy(
&self,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system
.copy(source_path, destination_path, options, sandbox)
.await
}
}
impl ExecutorFileSystem for LocalFileSystem {
fn canonicalize<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, PathUri> {
Box::pin(LocalFileSystem::canonicalize(self, path, sandbox))
}
fn read_file<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<u8>> {
Box::pin(LocalFileSystem::read_file(self, path, sandbox))
}
fn write_file<'a>(
&'a self,
path: &'a PathUri,
contents: Vec<u8>,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(LocalFileSystem::write_file(self, path, contents, sandbox))
}
fn create_directory<'a>(
&'a self,
path: &'a PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(LocalFileSystem::create_directory(
self, path, options, sandbox,
))
}
fn get_metadata<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, FileMetadata> {
Box::pin(LocalFileSystem::get_metadata(self, path, sandbox))
}
fn read_directory<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {
Box::pin(LocalFileSystem::read_directory(self, path, sandbox))
}
fn remove<'a>(
&'a self,
path: &'a PathUri,
options: RemoveOptions,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(LocalFileSystem::remove(self, path, options, sandbox))
}
fn copy<'a>(
&'a self,
source_path: &'a PathUri,
destination_path: &'a PathUri,
options: CopyOptions,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(LocalFileSystem::copy(
self,
source_path,
destination_path,
options,
sandbox,
))
}
}
impl UnsandboxedFileSystem {
async fn canonicalize(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<PathUri> {
reject_platform_sandbox_context(sandbox)?;
self.file_system.canonicalize(path, /*sandbox*/ None).await
}
async fn read_file(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
reject_platform_sandbox_context(sandbox)?;
self.file_system.read_file(path, /*sandbox*/ None).await
}
async fn write_file(
&self,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_platform_sandbox_context(sandbox)?;
self.file_system
.write_file(path, contents, /*sandbox*/ None)
.await
}
async fn create_directory(
&self,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_platform_sandbox_context(sandbox)?;
self.file_system
.create_directory(path, options, /*sandbox*/ None)
.await
}
async fn get_metadata(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
reject_platform_sandbox_context(sandbox)?;
self.file_system.get_metadata(path, /*sandbox*/ None).await
}
async fn read_directory(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
reject_platform_sandbox_context(sandbox)?;
self.file_system
.read_directory(path, /*sandbox*/ None)
.await
}
async fn remove(
&self,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_platform_sandbox_context(sandbox)?;
self.file_system
.remove(path, options, /*sandbox*/ None)
.await
}
async fn copy(
&self,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_platform_sandbox_context(sandbox)?;
self.file_system
.copy(
source_path,
destination_path,
options,
/*sandbox*/ None,
)
.await
}
}
impl ExecutorFileSystem for UnsandboxedFileSystem {
fn canonicalize<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, PathUri> {
Box::pin(UnsandboxedFileSystem::canonicalize(self, path, sandbox))
}
fn read_file<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<u8>> {
Box::pin(UnsandboxedFileSystem::read_file(self, path, sandbox))
}
fn write_file<'a>(
&'a self,
path: &'a PathUri,
contents: Vec<u8>,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(UnsandboxedFileSystem::write_file(
self, path, contents, sandbox,
))
}
fn create_directory<'a>(
&'a self,
path: &'a PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(UnsandboxedFileSystem::create_directory(
self, path, options, sandbox,
))
}
fn get_metadata<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, FileMetadata> {
Box::pin(UnsandboxedFileSystem::get_metadata(self, path, sandbox))
}
fn read_directory<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {
Box::pin(UnsandboxedFileSystem::read_directory(self, path, sandbox))
}
fn remove<'a>(
&'a self,
path: &'a PathUri,
options: RemoveOptions,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(UnsandboxedFileSystem::remove(self, path, options, sandbox))
}
fn copy<'a>(
&'a self,
source_path: &'a PathUri,
destination_path: &'a PathUri,
options: CopyOptions,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(UnsandboxedFileSystem::copy(
self,
source_path,
destination_path,
options,
sandbox,
))
}
}
impl DirectFileSystem {
async fn canonicalize(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<PathUri> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let canonicalized =
AbsolutePathBuf::from_absolute_path(tokio::fs::canonicalize(path.as_path()).await?)?;
PathUri::from_abs_path(&canonicalized)
}
async fn read_file(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let metadata = tokio::fs::metadata(path.as_path()).await?;
if metadata.len() > MAX_READ_FILE_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("file is too large to read: limit is {MAX_READ_FILE_BYTES} bytes"),
));
}
tokio::fs::read(path.as_path()).await
}
async fn write_file(
&self,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
tokio::fs::write(path.as_path(), contents).await
}
async fn create_directory(
&self,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
if options.recursive {
tokio::fs::create_dir_all(path.as_path()).await?;
} else {
tokio::fs::create_dir(path.as_path()).await?;
}
Ok(())
}
async fn get_metadata(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let metadata = tokio::fs::metadata(path.as_path()).await?;
let symlink_metadata = tokio::fs::symlink_metadata(path.as_path()).await?;
Ok(FileMetadata {
is_directory: metadata.is_dir(),
is_file: metadata.is_file(),
is_symlink: symlink_metadata.file_type().is_symlink(),
size: metadata.len(),
created_at_ms: metadata.created().ok().map_or(0, system_time_to_unix_ms),
modified_at_ms: metadata.modified().ok().map_or(0, system_time_to_unix_ms),
})
}
async fn read_directory(
&self,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let mut entries = Vec::new();
let mut read_dir = tokio::fs::read_dir(path.as_path()).await?;
while let Some(entry) = read_dir.next_entry().await? {
let Ok(metadata) = tokio::fs::metadata(entry.path()).await else {
continue;
};
entries.push(ReadDirectoryEntry {
file_name: entry.file_name().to_string_lossy().into_owned(),
is_directory: metadata.is_dir(),
is_file: metadata.is_file(),
});
}
Ok(entries)
}
async fn remove(
&self,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
match tokio::fs::symlink_metadata(path.as_path()).await {
Ok(metadata) => {
let file_type = metadata.file_type();
if file_type.is_dir() {
if options.recursive {
tokio::fs::remove_dir_all(path.as_path()).await?;
} else {
tokio::fs::remove_dir(path.as_path()).await?;
}
} else {
tokio::fs::remove_file(path.as_path()).await?;
}
Ok(())
}
Err(err) if err.kind() == io::ErrorKind::NotFound && options.force => Ok(()),
Err(err) => Err(err),
}
}
async fn copy(
&self,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let source_path = source_path.to_abs_path()?.into_path_buf();
let destination_path = destination_path.to_abs_path()?.into_path_buf();
tokio::task::spawn_blocking(move || -> FileSystemResult<()> {
let metadata = std::fs::symlink_metadata(source_path.as_path())?;
let file_type = metadata.file_type();
if file_type.is_dir() {
if !options.recursive {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"fs/copy requires recursive: true when sourcePath is a directory",
));
}
if destination_is_same_or_descendant_of_source(
source_path.as_path(),
destination_path.as_path(),
)? {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"fs/copy cannot copy a directory to itself or one of its descendants",
));
}
copy_dir_recursive(source_path.as_path(), destination_path.as_path())?;
return Ok(());
}
if file_type.is_symlink() {
copy_symlink(source_path.as_path(), destination_path.as_path())?;
return Ok(());
}
if file_type.is_file() {
std::fs::copy(source_path.as_path(), destination_path.as_path())?;
return Ok(());
}
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"fs/copy only supports regular files, directories, and symlinks",
))
})
.await
.map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))?
}
}
impl ExecutorFileSystem for DirectFileSystem {
fn canonicalize<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, PathUri> {
Box::pin(DirectFileSystem::canonicalize(self, path, sandbox))
}
fn read_file<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<u8>> {
Box::pin(DirectFileSystem::read_file(self, path, sandbox))
}
fn write_file<'a>(
&'a self,
path: &'a PathUri,
contents: Vec<u8>,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(DirectFileSystem::write_file(self, path, contents, sandbox))
}
fn create_directory<'a>(
&'a self,
path: &'a PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(DirectFileSystem::create_directory(
self, path, options, sandbox,
))
}
fn get_metadata<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, FileMetadata> {
Box::pin(DirectFileSystem::get_metadata(self, path, sandbox))
}
fn read_directory<'a>(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {
Box::pin(DirectFileSystem::read_directory(self, path, sandbox))
}
fn remove<'a>(
&'a self,
path: &'a PathUri,
options: RemoveOptions,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(DirectFileSystem::remove(self, path, options, sandbox))
}
fn copy<'a>(
&'a self,
source_path: &'a PathUri,
destination_path: &'a PathUri,
options: CopyOptions,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(DirectFileSystem::copy(
self,
source_path,
destination_path,
options,
sandbox,
))
}
}
fn reject_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
if sandbox.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"direct filesystem operations do not accept sandbox context",
));
}
Ok(())
}
fn reject_platform_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"sandboxed filesystem operations require configured runtime paths",
));
}
Ok(())
}
fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> {
std::fs::create_dir_all(target)?;
for entry in std::fs::read_dir(source)? {
let entry = entry?;
let source_path = entry.path();
let target_path = target.join(entry.file_name());
let file_type = entry.file_type()?;
if file_type.is_dir() {
copy_dir_recursive(&source_path, &target_path)?;
} else if file_type.is_file() {
std::fs::copy(&source_path, &target_path)?;
} else if file_type.is_symlink() {
copy_symlink(&source_path, &target_path)?;
}
}
Ok(())
}
fn destination_is_same_or_descendant_of_source(
source: &Path,
destination: &Path,
) -> io::Result<bool> {
let source = std::fs::canonicalize(source)?;
let destination = resolve_existing_path(destination)?;
Ok(destination.starts_with(&source))
}
pub(crate) fn resolve_existing_path(path: &Path) -> io::Result<PathBuf> {
let mut unresolved_suffix = Vec::new();
let mut existing_path = path;
while !existing_path.exists() {
let Some(file_name) = existing_path.file_name() else {
break;
};
unresolved_suffix.push(file_name.to_os_string());
let Some(parent) = existing_path.parent() else {
break;
};
existing_path = parent;
}
let mut resolved = std::fs::canonicalize(existing_path)?;
for file_name in unresolved_suffix.iter().rev() {
resolved.push(file_name);
}
Ok(resolved)
}
pub(crate) fn current_sandbox_cwd() -> io::Result<PathBuf> {
let cwd = std::env::current_dir()
.map_err(|err| io::Error::other(format!("failed to read current dir: {err}")))?;
resolve_existing_path(cwd.as_path())
}
fn copy_symlink(source: &Path, target: &Path) -> io::Result<()> {
let link_target = std::fs::read_link(source)?;
#[cfg(unix)]
{
std::os::unix::fs::symlink(&link_target, target)
}
#[cfg(windows)]
{
if symlink_points_to_directory(source)? {
std::os::windows::fs::symlink_dir(&link_target, target)
} else {
std::os::windows::fs::symlink_file(&link_target, target)
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = link_target;
let _ = target;
Err(io::Error::new(
io::ErrorKind::Unsupported,
"copying symlinks is unsupported on this platform",
))
}
}
#[cfg(windows)]
fn symlink_points_to_directory(source: &Path) -> io::Result<bool> {
use std::os::windows::fs::FileTypeExt;
Ok(std::fs::symlink_metadata(source)?
.file_type()
.is_symlink_dir())
}
fn system_time_to_unix_ms(time: SystemTime) -> i64 {
time.duration_since(UNIX_EPOCH)
.ok()
.and_then(|duration| i64::try_from(duration.as_millis()).ok())
.unwrap_or(0)
}
#[cfg(all(test, any(unix, windows)))]
#[path = "local_file_system_path_uri_tests.rs"]
mod path_uri_tests;
#[cfg(all(test, unix))]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::os::unix::fs::symlink;
#[test]
fn resolve_existing_path_handles_symlink_parent_dotdot_escape() -> io::Result<()> {
let temp_dir = tempfile::TempDir::new()?;
let allowed_dir = temp_dir.path().join("allowed");
let outside_dir = temp_dir.path().join("outside");
std::fs::create_dir_all(&allowed_dir)?;
std::fs::create_dir_all(&outside_dir)?;
symlink(&outside_dir, allowed_dir.join("link"))?;
let resolved = resolve_existing_path(
allowed_dir
.join("link")
.join("..")
.join("secret.txt")
.as_path(),
)?;
assert_eq!(
resolved,
resolve_existing_path(temp_dir.path())?.join("secret.txt")
);
Ok(())
}
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn symlink_points_to_directory_handles_dangling_directory_symlinks() -> io::Result<()> {
use std::os::windows::fs::symlink_dir;
let temp_dir = tempfile::TempDir::new()?;
let source_dir = temp_dir.path().join("source");
let link_path = temp_dir.path().join("source-link");
std::fs::create_dir(&source_dir)?;
if symlink_dir(&source_dir, &link_path).is_err() {
return Ok(());
}
std::fs::remove_dir(&source_dir)?;
assert_eq!(symlink_points_to_directory(&link_path)?, true);
Ok(())
}
}