use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
use std::path::{Path, PathBuf};
use prikk_error::{PrikkError, Result};
use super::directory::{
MutationRoot, open_existing_windows_directory_required, prepare_windows_directory_required,
};
use super::regular::{required_file_name, required_parent};
use super::{failpoints, prikk_to_io};
use crate::fsutil::contract::DurabilityContract;
use crate::fsutil::temporary_path;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
const SHARE_READ_WRITE_DELETE: u32 = 0x0000_0001 | 0x0000_0002 | 0x0000_0004;
fn io_error(path: &Path, error: io::Error) -> PrikkError {
PrikkError::Io(format!("{}: {error}", path.display()))
}
fn open_no_follow(path: &Path, options: &mut OpenOptions, for_directory: bool) -> io::Result<File> {
let mut flags = FILE_FLAG_OPEN_REPARSE_POINT;
if for_directory {
flags |= FILE_FLAG_BACKUP_SEMANTICS;
}
failpoints::required_open().map_err(prikk_to_io)?;
options
.share_mode(SHARE_READ_WRITE_DELETE)
.custom_flags(flags)
.open(path)
}
fn is_reparse_point(metadata: &std::fs::Metadata) -> bool {
metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
}
fn open_directory_handle(path: &Path) -> io::Result<File> {
open_no_follow(path, OpenOptions::new().read(true), true)
}
fn validate_directory_not_reparse_point(file: &File, path: &Path) -> Result<()> {
let metadata = file.metadata().map_err(|error| io_error(path, error))?;
if is_reparse_point(&metadata) {
return Err(PrikkError::Io(format!(
"refusing to resolve through a reparse point: {}",
path.display()
)));
}
if !metadata.is_dir() {
return Err(PrikkError::Io(format!(
"expected a directory: {}",
path.display()
)));
}
Ok(())
}
pub(super) fn open_directory_no_follow(path: &Path) -> Result<File> {
let file = open_directory_handle(path).map_err(|error| io_error(path, error))?;
validate_directory_not_reparse_point(&file, path)?;
Ok(file)
}
pub(super) fn identity_no_follow(file: &File, path: &Path) -> Result<prikk_ffi::FileIdentity> {
prikk_ffi::identity_of(file).map_err(|error| io_error(path, error))
}
pub(super) fn stat_directory_no_follow(path: &Path) -> Result<Option<()>> {
match open_directory_handle(path) {
Ok(file) => {
validate_directory_not_reparse_point(&file, path)?;
Ok(Some(()))
}
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(io_error(path, error)),
}
}
pub(super) fn ensure_directory_component_no_follow(path: &Path) -> Result<File> {
match open_directory_handle(path) {
Ok(file) => {
validate_directory_not_reparse_point(&file, path)?;
Ok(file)
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
failpoints::directory_create()?;
failpoints::wait_at_directory_create();
match fs::create_dir(path) {
Ok(()) => open_directory_no_follow(path),
Err(create_error) if create_error.kind() == io::ErrorKind::AlreadyExists => {
open_directory_no_follow(path)
}
Err(create_error) => Err(io_error(path, create_error)),
}
}
Err(error) => Err(io_error(path, error)),
}
}
pub(super) enum RawKind {
File,
Directory,
ReparsePoint,
Other,
}
pub(super) fn classify_no_follow(path: &Path) -> Result<Option<RawKind>> {
match open_no_follow(path, OpenOptions::new().read(true), true) {
Ok(file) => {
let metadata = file.metadata().map_err(|error| io_error(path, error))?;
let kind = if is_reparse_point(&metadata) {
RawKind::ReparsePoint
} else if metadata.is_dir() {
RawKind::Directory
} else if metadata.is_file() {
RawKind::File
} else {
RawKind::Other
};
Ok(Some(kind))
}
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(io_error(path, error)),
}
}
pub(super) fn stat_file_no_follow(path: &Path) -> Result<Option<std::fs::Metadata>> {
match open_existing_file_no_follow(path, OpenOptions::new().read(true))? {
Some(file) => Ok(Some(
file.metadata().map_err(|error| io_error(path, error))?,
)),
None => Ok(None),
}
}
pub(super) fn open_existing_file_no_follow(
path: &Path,
options: &mut OpenOptions,
) -> Result<Option<File>> {
match open_no_follow(path, options, false) {
Ok(file) => {
let metadata = file.metadata().map_err(|error| io_error(path, error))?;
if is_reparse_point(&metadata) {
return Err(PrikkError::Io(format!(
"refusing to open a reparse point: {}",
path.display()
)));
}
if !metadata.is_file() {
return Err(PrikkError::Io(format!(
"expected a regular file: {}",
path.display()
)));
}
Ok(Some(file))
}
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(io_error(path, error)),
}
}
fn required_existing_file_no_follow(path: &Path, options: &mut OpenOptions) -> Result<File> {
open_existing_file_no_follow(path, options)?
.ok_or_else(|| PrikkError::Io(format!("required file is absent: {}", path.display())))
}
fn resolved_existing_path(root: &MutationRoot, relative: &Path) -> Result<PathBuf> {
let parent = required_parent(relative)?;
let name = required_file_name(relative)?;
let resolved_parent = open_existing_windows_directory_required(root, parent)?;
Ok(resolved_parent.join(name))
}
fn resolved_prepared_path(root: &MutationRoot, relative: &Path) -> Result<PathBuf> {
let parent = required_parent(relative)?;
let name = required_file_name(relative)?;
let resolved_parent = prepare_windows_directory_required(root, parent)?;
Ok(resolved_parent.join(name))
}
pub(crate) struct WindowsDurability;
impl DurabilityContract for WindowsDurability {
fn atomic_replace(&self, root: &MutationRoot, relative: &Path, bytes: &[u8]) -> Result<()> {
let parent = required_parent(relative)?;
let name = required_file_name(relative)?;
let resolved_parent = prepare_windows_directory_required(root, parent)?;
let destination = resolved_parent.join(name);
let temp_path = temporary_path(relative)?;
let temp_name = required_file_name(&temp_path)?;
let temp_full = resolved_parent.join(temp_name);
let mut file = open_no_follow(
&temp_full,
OpenOptions::new().write(true).create(true).truncate(true),
false,
)
.map_err(|error| io_error(&temp_full, error))?;
file.write_all(bytes)
.map_err(|error| io_error(&temp_full, error))?;
failpoints::mutable_file_sync()?;
file.sync_all()
.map_err(|error| io_error(&temp_full, error))?;
drop(file);
failpoints::mutable_rename()?;
fs::rename(&temp_full, &destination).map_err(|error| io_error(&destination, error))
}
fn durable_append(&self, root: &MutationRoot, relative: &Path, bytes: &[u8]) -> Result<()> {
let path = resolved_existing_path(root, relative)?;
let mut file = required_existing_file_no_follow(&path, OpenOptions::new().append(true))?;
failpoints::append_write()?;
file.write_all(bytes)
.map_err(|error| io_error(&path, error))?;
failpoints::required_file_sync()?;
file.sync_all().map_err(|error| io_error(&path, error))
}
fn durable_truncate(&self, root: &MutationRoot, relative: &Path, len: u64) -> Result<()> {
let path = resolved_existing_path(root, relative)?;
let file = required_existing_file_no_follow(&path, OpenOptions::new().write(true))?;
failpoints::truncate()?;
file.set_len(len).map_err(|error| io_error(&path, error))?;
failpoints::required_file_sync()?;
file.sync_all().map_err(|error| io_error(&path, error))
}
fn durable_truncate_to_empty(&self, root: &MutationRoot, relative: &Path) -> Result<()> {
self.durable_truncate(root, relative, 0)
}
fn create_exclusive(
&self,
root: &MutationRoot,
relative: &Path,
bytes: &[u8],
) -> std::io::Result<()> {
let path = resolved_prepared_path(root, relative).map_err(prikk_to_io)?;
let mut file = open_no_follow(
&path,
OpenOptions::new().write(true).create_new(true),
false,
)?;
file.write_all(bytes)?;
failpoints::required_file_sync().map_err(prikk_to_io)?;
file.sync_all()
}
fn set_permission_bits(&self, root: &MutationRoot, relative: &Path, mode: u32) -> Result<()> {
let _ = (root, relative, mode);
Ok(())
}
fn remove_if_present(&self, root: &MutationRoot, relative: &Path) -> Result<bool> {
let parent = required_parent(relative)?;
let name = required_file_name(relative)?;
let resolved_parent = open_existing_windows_directory_required(root, parent)?;
let path = resolved_parent.join(name);
failpoints::unlink()?;
match fs::remove_file(&path) {
Ok(()) => Ok(true),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(io_error(&path, error)),
}
}
fn ensure_directory(&self, root: &MutationRoot, relative: &Path) -> Result<()> {
prepare_windows_directory_required(root, relative)?;
Ok(())
}
fn durable_directory_entry(&self, root: &MutationRoot, relative: &Path) -> Result<()> {
let _ = (root, relative);
Ok(())
}
}
#[cfg(test)]
mod tests;