use super::{
config::FilesystemConfig,
error::FileWriteError,
path::{resolve_buffer_path, resolve_buffer_path_from},
};
use std::{
ffi::{OsStr, OsString},
fs::{self, File, Metadata},
io::{self, Write},
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
#[cfg(unix)]
use super::platform::open_workspace_directory;
#[cfg(not(unix))]
use super::platform::require_current_path_identity;
#[cfg(unix)]
use rustix::fs::{Mode, OFlags, fchmod, fsync, openat, renameat, unlinkat};
#[cfg(not(unix))]
use std::fs::OpenOptions;
pub fn atomic_write(
requested_path: impl AsRef<Path>,
bytes: &[u8],
config: &FilesystemConfig,
) -> Result<PathBuf, FileWriteError> {
let resolved =
resolve_buffer_path(requested_path.as_ref(), config).map_err(FileWriteError::Policy)?;
write_resolved_path(resolved.path, bytes, config, resolved.exists)
}
pub fn atomic_write_workspace_file(
workspace_relative_path: impl AsRef<Path>,
bytes: &[u8],
config: &FilesystemConfig,
) -> Result<PathBuf, FileWriteError> {
let resolved = resolve_buffer_path_from(
workspace_relative_path.as_ref(),
config,
&config.workspace_root,
)
.map_err(FileWriteError::Policy)?;
write_resolved_path(resolved.path, bytes, config, resolved.exists)
}
fn write_resolved_path(
path: PathBuf,
bytes: &[u8],
config: &FilesystemConfig,
exists: bool,
) -> Result<PathBuf, FileWriteError> {
let parent = path
.parent()
.ok_or_else(|| FileWriteError::MissingParent { path: path.clone() })?;
let temp_name = PathBuf::from(unique_temp_file_name(&path));
let temp_path = parent.join(&temp_name);
let write_result =
write_temp_and_rename(config, parent, &temp_name, &temp_path, &path, bytes, exists);
if write_result.is_err() {
cleanup_temp_file(config, parent, &temp_name, &temp_path);
}
write_result?;
Ok(path)
}
#[cfg(unix)]
fn write_temp_and_rename(
config: &FilesystemConfig,
parent: &Path,
temp_name: &Path,
temp_path: &Path,
target_path: &Path,
bytes: &[u8],
target_exists: bool,
) -> Result<(), FileWriteError> {
let target_name =
target_path
.file_name()
.map(Path::new)
.ok_or_else(|| FileWriteError::MissingParent {
path: target_path.to_owned(),
})?;
let relative_parent = parent
.strip_prefix(&config.workspace_root)
.map_err(|_error| FileWriteError::CreateTemp {
path: temp_path.to_owned(),
source: io::Error::new(
io::ErrorKind::PermissionDenied,
"target parent is outside workspace root",
),
})?;
let parent_dir = open_workspace_directory(relative_parent, config).map_err(|source| {
FileWriteError::CreateTemp {
path: temp_path.to_owned(),
source,
}
})?;
let existing_metadata = if target_exists {
let metadata = existing_file_metadata_at(&parent_dir, target_name, target_path)?;
if !metadata.is_file() {
return Err(FileWriteError::UnsupportedFileType {
path: target_path.to_owned(),
});
}
Some(metadata)
} else {
None
};
let mut temp_file = create_temp_file_at(
&parent_dir,
temp_name,
temp_path,
existing_metadata.as_ref(),
)?;
temp_file
.write_all(bytes)
.map_err(|source| FileWriteError::WriteTemp {
path: temp_path.to_owned(),
source,
})?;
temp_file
.sync_all()
.map_err(|source| FileWriteError::SyncTemp {
path: temp_path.to_owned(),
source,
})?;
drop(temp_file);
renameat(&parent_dir, temp_name, &parent_dir, target_name).map_err(|source| {
FileWriteError::Rename {
temp_path: temp_path.to_owned(),
target_path: target_path.to_owned(),
source: io::Error::from(source),
}
})?;
fsync(&parent_dir).map_err(|source| FileWriteError::SyncTemp {
path: parent.to_owned(),
source: io::Error::from(source),
})?;
Ok(())
}
#[cfg(unix)]
fn existing_file_metadata_at(
parent_dir: &File,
target_name: &Path,
target_path: &Path,
) -> Result<Metadata, FileWriteError> {
let target_fd = openat(
parent_dir,
target_name,
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
)
.map_err(|source| FileWriteError::Metadata {
path: target_path.to_owned(),
source: io::Error::from(source),
})?;
let target_file = File::from(target_fd);
target_file
.metadata()
.map_err(|source| FileWriteError::Metadata {
path: target_path.to_owned(),
source,
})
}
#[cfg(not(unix))]
fn write_temp_and_rename(
config: &FilesystemConfig,
_parent: &Path,
_temp_name: &Path,
temp_path: &Path,
target_path: &Path,
bytes: &[u8],
target_exists: bool,
) -> Result<(), FileWriteError> {
let existing_metadata = if target_exists {
let metadata = fs::metadata(target_path).map_err(|source| FileWriteError::Metadata {
path: target_path.to_owned(),
source,
})?;
if !metadata.is_file() {
return Err(FileWriteError::UnsupportedFileType {
path: target_path.to_owned(),
});
}
require_current_path_identity(target_path, &metadata, config).map_err(|source| {
FileWriteError::Metadata {
path: target_path.to_owned(),
source,
}
})?;
Some(metadata)
} else {
None
};
let mut temp_file = create_temp_file(temp_path, existing_metadata.as_ref())?;
temp_file
.write_all(bytes)
.map_err(|source| FileWriteError::WriteTemp {
path: temp_path.to_owned(),
source,
})?;
temp_file
.sync_all()
.map_err(|source| FileWriteError::SyncTemp {
path: temp_path.to_owned(),
source,
})?;
drop(temp_file);
if let Some(metadata) = existing_metadata.as_ref() {
require_current_path_identity(target_path, metadata, config).map_err(|source| {
FileWriteError::Metadata {
path: target_path.to_owned(),
source,
}
})?;
}
fs::rename(temp_path, target_path).map_err(|source| FileWriteError::Rename {
temp_path: temp_path.to_owned(),
target_path: target_path.to_owned(),
source,
})?;
if let Some(parent) = target_path.parent() {
let _synced_parent = File::open(parent).and_then(|file| file.sync_all());
}
Ok(())
}
#[cfg(unix)]
fn create_temp_file_at(
parent_dir: &File,
temp_name: &Path,
temp_path: &Path,
existing_metadata: Option<&Metadata>,
) -> Result<File, FileWriteError> {
use std::os::unix::fs::PermissionsExt;
let mode = existing_metadata.map_or(0o600, |metadata| metadata.permissions().mode() & 0o777);
let mode = u16::try_from(mode).unwrap_or(0o600);
let temp_fd = openat(
parent_dir,
temp_name,
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::from_raw_mode(mode),
)
.map_err(|source| FileWriteError::CreateTemp {
path: temp_path.to_owned(),
source: io::Error::from(source),
})?;
if existing_metadata.is_some() {
fchmod(&temp_fd, Mode::from_raw_mode(mode)).map_err(|source| {
FileWriteError::SetTempPermissions {
path: temp_path.to_owned(),
source: io::Error::from(source),
}
})?;
}
Ok(File::from(temp_fd))
}
#[cfg(unix)]
fn cleanup_temp_file(config: &FilesystemConfig, parent: &Path, temp_name: &Path, temp_path: &Path) {
if let Ok(relative_parent) = parent.strip_prefix(&config.workspace_root)
&& let Ok(parent_dir) = open_workspace_directory(relative_parent, config)
{
let _removed = unlinkat(&parent_dir, temp_name, rustix::fs::AtFlags::empty());
} else {
let _removed = fs::remove_file(temp_path);
}
}
#[cfg(not(unix))]
fn cleanup_temp_file(
_config: &FilesystemConfig,
_parent: &Path,
_temp_name: &Path,
temp_path: &Path,
) {
let _removed = fs::remove_file(temp_path);
}
#[cfg(not(unix))]
fn create_temp_file(
temp_path: &Path,
existing_metadata: Option<&Metadata>,
) -> Result<File, FileWriteError> {
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(temp_path)
.map_err(|source| FileWriteError::CreateTemp {
path: temp_path.to_owned(),
source,
})?;
if let Some(metadata) = existing_metadata {
file.set_permissions(metadata.permissions())
.map_err(|source| FileWriteError::SetTempPermissions {
path: temp_path.to_owned(),
source,
})?;
}
Ok(file)
}
fn unique_temp_file_name(target_path: &Path) -> OsString {
let name = target_path
.file_name()
.unwrap_or_else(|| OsStr::new("buffer"));
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_nanos());
OsString::from(format!(
".{}.alma-tmp-{}-{nanos}",
name.to_string_lossy(),
std::process::id()
))
}