use std::{
fs::{self, File, OpenOptions},
io::{self, Read, Write},
mem,
os::windows::{
ffi::OsStrExt,
fs::OpenOptionsExt,
io::{AsRawHandle, FromRawHandle},
},
path::Path,
};
use windows_sys::Win32::{
Foundation::{ERROR_INVALID_PARAMETER, GENERIC_WRITE, INVALID_HANDLE_VALUE},
Storage::FileSystem::{
DELETE, FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_FLAG_BACKUP_SEMANTICS,
FILE_FLAG_OPEN_REPARSE_POINT, FILE_RENAME_INFO, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, FileDispositionInfoEx,
FileRenameInfoEx, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW, ReOpenFile, SetFileInformationByHandle,
},
System::WindowsProgramming::{FILE_RENAME_FLAG_POSIX_SEMANTICS, FILE_RENAME_FLAG_REPLACE_IF_EXISTS},
};
use super::{ConfirmError, Staging, confirm_error, parent};
const COPY_BYTES: usize = 64 * 1024;
pub(super) fn create_dir_all(path: &Path) -> io::Result<()> {
if path.is_dir() {
return Ok(());
}
let parent = parent(path);
if parent == path {
return Err(io::Error::new(io::ErrorKind::NotFound, "storage volume does not exist"));
}
create_dir_all(parent)?;
let temporary = tempfile::Builder::new().prefix(".").tempdir_in(parent)?.keep();
let result = move_file(&temporary, path, false);
let _ = fs::remove_dir(&temporary);
match result {
Err(error) if error.kind() == io::ErrorKind::AlreadyExists && path.is_dir() => Ok(()),
other => other,
}
}
pub(super) fn move_directory(source: &Path, destination: &Path) -> io::Result<()> {
move_file(source, destination, false)
}
pub(super) fn install(source: &Path, destination: &Path) -> io::Result<()> {
move_file(source, destination, false)
}
pub(super) fn replace(source: &Path, destination: &Path) -> io::Result<()> {
move_file(source, destination, true)
}
pub(super) fn remove(path: &Path) -> io::Result<()> {
let pinned = match OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)
{
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error),
};
let metadata = pinned.metadata()?;
if !metadata.is_file() || metadata.file_type().is_symlink() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "source must be a regular file"));
}
let deletion = unsafe {
ReOpenFile(pinned.as_raw_handle(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_FLAG_OPEN_REPARSE_POINT)
};
if deletion == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let deletion = unsafe { File::from_raw_handle(deletion) };
let disposition = FILE_DISPOSITION_INFO_EX { Flags: FILE_DISPOSITION_FLAG_DELETE | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS };
if unsafe {
SetFileInformationByHandle(
deletion.as_raw_handle(),
FileDispositionInfoEx,
(&disposition as *const FILE_DISPOSITION_INFO_EX).cast(),
mem::size_of_val(&disposition) as u32,
)
} == 0
{
return Err(io::Error::last_os_error());
}
drop(deletion);
pinned.sync_all()
}
pub(super) fn confirm_with_checkpoint<E>(path: &Path, checkpoint: &mut impl FnMut() -> Result<(), E>) -> Result<(), ConfirmError<E>> {
checkpoint().map_err(ConfirmError::Checkpoint)?;
let mut existing = File::open(path).map_err(|error| confirm_error(path, error))?;
checkpoint().map_err(ConfirmError::Checkpoint)?;
let (staging, mut copy) = Staging::file(parent(path)).map_err(|error| confirm_error(path, error))?;
checkpoint().map_err(ConfirmError::Checkpoint)?;
let mut buffer = [0; COPY_BYTES];
loop {
checkpoint().map_err(ConfirmError::Checkpoint)?;
let count = existing.read(&mut buffer).map_err(|error| confirm_error(path, error))?;
checkpoint().map_err(ConfirmError::Checkpoint)?;
if count == 0 {
break;
}
copy.write_all(&buffer[..count]).map_err(|error| confirm_error(path, error))?;
checkpoint().map_err(ConfirmError::Checkpoint)?;
}
copy.sync_all().map_err(|error| confirm_error(path, error))?;
checkpoint().map_err(ConfirmError::Checkpoint)?;
drop(copy);
drop(existing);
move_file_with_checkpoint(&staging.0, path, true, checkpoint)
}
fn move_file(source: &Path, destination: &Path, replace: bool) -> io::Result<()> {
match move_file_with_checkpoint(source, destination, replace, &mut || Ok::<(), std::convert::Infallible>(())) {
Ok(()) => Ok(()),
Err(ConfirmError::Filesystem(error)) => Err(error.into()),
Err(ConfirmError::Checkpoint(error)) => match error {},
}
}
fn move_file_with_checkpoint<E>(
source: &Path, destination: &Path, replace: bool, checkpoint: &mut impl FnMut() -> Result<(), E>,
) -> Result<(), ConfirmError<E>> {
checkpoint().map_err(ConfirmError::Checkpoint)?;
let destination_wide = wide(destination).map_err(|error| confirm_error(destination, error))?;
checkpoint().map_err(ConfirmError::Checkpoint)?;
let handle = OpenOptions::new()
.access_mode(DELETE | GENERIC_WRITE)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(source)
.map_err(|error| confirm_error(destination, error))?;
checkpoint().map_err(ConfirmError::Checkpoint)?;
match rename_by_handle(&handle, &destination_wide, replace) {
Ok(()) => {
checkpoint().map_err(ConfirmError::Checkpoint)?;
handle.sync_all().map_err(|error| confirm_error(destination, error))?;
checkpoint().map_err(ConfirmError::Checkpoint)
}
Err(error) if error.raw_os_error() == Some(ERROR_INVALID_PARAMETER as i32) => {
checkpoint().map_err(ConfirmError::Checkpoint)?;
drop(handle);
move_file_write_through(source, &destination_wide, replace).map_err(|error| confirm_error(destination, error))?;
checkpoint().map_err(ConfirmError::Checkpoint)
}
Err(error) => Err(confirm_error(destination, error)),
}
}
fn rename_by_handle(handle: &File, destination: &[u16], replace: bool) -> io::Result<()> {
let name_bytes = mem::size_of_val(destination);
let total = mem::size_of::<FILE_RENAME_INFO>() + name_bytes - mem::size_of::<u16>();
let mut buffer = vec![0u64; total.div_ceil(mem::size_of::<u64>())];
let info = buffer.as_mut_ptr().cast::<FILE_RENAME_INFO>();
let mut flags = FILE_RENAME_FLAG_POSIX_SEMANTICS;
if replace {
flags |= FILE_RENAME_FLAG_REPLACE_IF_EXISTS;
}
unsafe {
(*info).Anonymous.Flags = flags;
(*info).RootDirectory = std::ptr::null_mut();
(*info).FileNameLength = (name_bytes - mem::size_of::<u16>()) as u32;
std::ptr::copy_nonoverlapping(destination.as_ptr(), (*info).FileName.as_mut_ptr(), destination.len());
if SetFileInformationByHandle(handle.as_raw_handle(), FileRenameInfoEx, info.cast(), total as u32) == 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn move_file_write_through(source: &Path, destination: &[u16], replace: bool) -> io::Result<()> {
let source = wide(source)?;
let flags = MOVEFILE_WRITE_THROUGH | if replace { MOVEFILE_REPLACE_EXISTING } else { 0 };
if unsafe { MoveFileExW(source.as_ptr(), destination.as_ptr(), flags) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn wide(path: &Path) -> io::Result<Vec<u16>> {
let path = std::path::absolute(path)?;
let name = path.file_name().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "publication path must have a filename"))?;
let path = fs::canonicalize(parent(&path))?.join(name);
let mut value: Vec<u16> = path.as_os_str().encode_wide().collect();
if value.contains(&0) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "path contains a NUL"));
}
value.push(0);
Ok(value)
}