#![cfg(target_os = "windows")]
use crate::{Error, Result};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::os::windows::io::{AsRawHandle, FromRawHandle, RawHandle};
use std::path::Path;
use windows_sys::Win32::Foundation::{
BOOL, FALSE, GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, FlushFileBuffers, GetDiskFreeSpaceW, MoveFileExW, ReadFile, WriteFile, CREATE_NEW,
FILE_ATTRIBUTE_NORMAL, FILE_FLAG_NO_BUFFERING, FILE_FLAG_WRITE_THROUGH, FILE_SHARE_READ,
FILE_SHARE_WRITE, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, OPEN_EXISTING,
};
use windows_sys::Win32::System::IO::OVERLAPPED;
pub(crate) fn open_write_new(path: &Path, use_direct: bool) -> Result<(File, bool)> {
let wide = to_wide(path);
let flags = if use_direct {
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH
} else {
FILE_ATTRIBUTE_NORMAL
};
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
CREATE_NEW,
flags,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
let err = std::io::Error::last_os_error();
if use_direct {
if err.raw_os_error() == Some(87) {
let h2 = unsafe {
CreateFileW(
wide.as_ptr(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
std::ptr::null_mut(),
)
};
if h2 != INVALID_HANDLE_VALUE {
let file = unsafe { File::from_raw_handle(h2 as RawHandle) };
return Ok((file, false));
}
return Err(Error::Io(std::io::Error::last_os_error()));
}
}
return Err(Error::Io(err));
}
Ok((
unsafe { File::from_raw_handle(handle as RawHandle) },
use_direct,
))
}
pub(crate) fn open_read(path: &Path, use_direct: bool) -> Result<(File, bool)> {
let wide = to_wide(path);
let flags = if use_direct {
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING
} else {
FILE_ATTRIBUTE_NORMAL
};
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
flags,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
let err = std::io::Error::last_os_error();
if use_direct && err.raw_os_error() == Some(87) {
let h2 = unsafe {
CreateFileW(
wide.as_ptr(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
std::ptr::null_mut(),
)
};
if h2 != INVALID_HANDLE_VALUE {
let file = unsafe { File::from_raw_handle(h2 as RawHandle) };
return Ok((file, false));
}
return Err(Error::Io(std::io::Error::last_os_error()));
}
return Err(Error::Io(err));
}
Ok((
unsafe { File::from_raw_handle(handle as RawHandle) },
use_direct,
))
}
pub(crate) fn open_append(path: &Path) -> Result<File> {
std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(path)
.map_err(Error::Io)
}
pub(crate) fn open_write_at(path: &Path) -> Result<File> {
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(path)
.map_err(Error::Io)
}
pub(crate) fn write_all(file: &File, data: &[u8]) -> Result<()> {
let handle = file.as_raw_handle() as HANDLE;
let mut written = 0u32;
let mut offset = 0usize;
while offset < data.len() {
let chunk_len = u32::try_from(data.len() - offset).unwrap_or(u32::MAX);
let ok: BOOL = unsafe {
WriteFile(
handle,
data[offset..].as_ptr().cast(),
chunk_len,
&mut written,
std::ptr::null_mut(),
)
};
if ok == FALSE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
offset += written as usize;
}
Ok(())
}
pub(crate) fn write_all_direct(file: &File, data: &[u8], sector_size: u32) -> Result<()> {
use super::{round_up, AlignedBuf};
if data.is_empty() {
return Ok(());
}
let ss = sector_size as usize;
let aligned_len = round_up(data.len(), ss);
let mut buf = AlignedBuf::new(aligned_len, ss)?;
buf.as_mut_slice()[..data.len()].copy_from_slice(data);
write_all(file, buf.as_slice())
}
pub(crate) fn write_at(file: &File, offset: u64, data: &[u8]) -> Result<()> {
let handle = file.as_raw_handle() as HANDLE;
let mut written_total: u32 = 0;
while (written_total as usize) < data.len() {
let remaining = data.len() - written_total as usize;
let chunk_len: u32 = remaining.min(u32::MAX as usize) as u32;
let chunk_offset = offset + written_total as u64;
let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
overlapped.Anonymous.Anonymous.Offset = (chunk_offset & 0xFFFF_FFFF) as u32;
overlapped.Anonymous.Anonymous.OffsetHigh = (chunk_offset >> 32) as u32;
let mut written: u32 = 0;
let buf_ptr = data[written_total as usize..].as_ptr();
let ok: BOOL =
unsafe { WriteFile(handle, buf_ptr, chunk_len, &mut written, &mut overlapped) };
if ok == FALSE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
if written == 0 {
return Err(Error::Io(std::io::Error::other(
"WriteFile returned 0 bytes written in write_at",
)));
}
written_total += written;
}
Ok(())
}
pub(crate) fn write_at_direct(file: &File, offset: u64, data: &[u8]) -> Result<()> {
write_at(file, offset, data)
}
pub(crate) fn read_all(file: &File) -> Result<Vec<u8>> {
let mut buf = Vec::new();
let _ = (&*file).read_to_end(&mut buf).map_err(Error::Io)?;
Ok(buf)
}
pub(crate) fn read_all_direct(file: &File, file_size: u64, sector_size: u32) -> Result<Vec<u8>> {
use super::{round_up, AlignedBuf};
if file_size == 0 {
return Ok(Vec::new());
}
let ss = sector_size as usize;
let aligned_len = round_up(file_size as usize, ss);
let mut buf = AlignedBuf::new(aligned_len, ss)?;
let handle = file.as_raw_handle() as HANDLE;
let mut bytes_read: u32 = 0;
let ok: BOOL = unsafe {
ReadFile(
handle,
buf.as_mut_slice().as_mut_ptr().cast(),
aligned_len as u32,
&mut bytes_read,
std::ptr::null_mut(),
)
};
if ok == FALSE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
let trimmed = usize::min(bytes_read as usize, file_size as usize);
Ok(buf.as_slice()[..trimmed].to_vec())
}
pub(crate) fn read_range(file: &File, offset: u64, len: usize) -> Result<Vec<u8>> {
let mut seekable = file.try_clone().map_err(Error::Io)?;
let _pos = seekable.seek(SeekFrom::Start(offset)).map_err(Error::Io)?;
let mut buf = vec![0u8; len];
let mut total = 0usize;
while total < len {
let n = seekable.read(&mut buf[total..]).map_err(Error::Io)?;
if n == 0 {
break;
}
total += n;
}
buf.truncate(total);
Ok(buf)
}
pub(crate) fn sync_data(file: &File) -> Result<()> {
sync_full(file)
}
pub(crate) fn sync_full(file: &File) -> Result<()> {
let handle = file.as_raw_handle() as HANDLE;
let ok: BOOL = unsafe { FlushFileBuffers(handle) };
if ok != FALSE {
Ok(())
} else {
Err(Error::Io(std::io::Error::last_os_error()))
}
}
pub(crate) fn atomic_rename(from: &Path, to: &Path) -> Result<()> {
let from_wide = to_wide(from);
let to_wide = to_wide(to);
let ok: BOOL = unsafe {
MoveFileExW(
from_wide.as_ptr(),
to_wide.as_ptr(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
)
};
if ok != FALSE {
Ok(())
} else {
Err(Error::Io(std::io::Error::last_os_error()))
}
}
pub(crate) fn sync_parent_dir(_path: &Path) -> Result<()> {
Ok(())
}
pub(crate) fn punch_hole(file: &File, offset: u64, len: u64) -> Result<()> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::System::IO::DeviceIoControl;
if len == 0 {
return Ok(());
}
#[repr(C)]
struct FileZeroDataInformation {
file_offset: i64,
beyond_final_zero: i64,
}
const FSCTL_SET_ZERO_DATA: u32 = 0x0009_80c8;
let payload = FileZeroDataInformation {
file_offset: offset as i64,
beyond_final_zero: offset.saturating_add(len) as i64,
};
let handle = file.as_raw_handle() as HANDLE;
let mut bytes_returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
FSCTL_SET_ZERO_DATA,
&payload as *const _ as *const std::ffi::c_void,
std::mem::size_of::<FileZeroDataInformation>() as u32,
std::ptr::null_mut(),
0,
&mut bytes_returned,
std::ptr::null_mut(),
)
};
if ok != FALSE {
Ok(())
} else {
Err(Error::Io(std::io::Error::last_os_error()))
}
}
pub(crate) fn copy_file(src: &Path, dst: &Path) -> Result<u64> {
if let Ok(bytes) = try_reflink_refs(src, dst) {
return Ok(bytes);
}
std::fs::copy(src, dst).map_err(Error::Io)
}
fn try_reflink_refs(src: &Path, dst: &Path) -> Result<u64> {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Storage::FileSystem::{
FileEndOfFileInfo, GetFileSizeEx, SetFileInformationByHandle, FILE_END_OF_FILE_INFO,
FILE_SHARE_DELETE,
};
use windows_sys::Win32::System::Ioctl::{
DUPLICATE_EXTENTS_DATA, FSCTL_DUPLICATE_EXTENTS_TO_FILE,
};
use windows_sys::Win32::System::IO::DeviceIoControl;
let src_wide = to_wide(src);
let dst_wide = to_wide(dst);
let share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
let src_handle = unsafe {
CreateFileW(
src_wide.as_ptr(),
GENERIC_READ,
share,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null_mut::<core::ffi::c_void>(),
)
};
if src_handle.is_null() || src_handle == INVALID_HANDLE_VALUE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
let src_file = unsafe { File::from_raw_handle(src_handle as RawHandle) };
let mut src_size: i64 = 0;
let ok: BOOL = unsafe { GetFileSizeEx(src_handle, &mut src_size) };
if ok == FALSE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
let dst_handle = unsafe {
CreateFileW(
dst_wide.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
share,
std::ptr::null(),
CREATE_NEW,
0,
std::ptr::null_mut::<core::ffi::c_void>(),
)
};
if dst_handle.is_null() || dst_handle == INVALID_HANDLE_VALUE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
let _dst_file = unsafe { File::from_raw_handle(dst_handle as RawHandle) };
let eof_info = FILE_END_OF_FILE_INFO {
EndOfFile: src_size,
};
let ok: BOOL = unsafe {
SetFileInformationByHandle(
dst_handle,
FileEndOfFileInfo,
&eof_info as *const _ as *const core::ffi::c_void,
std::mem::size_of::<FILE_END_OF_FILE_INFO>() as u32,
)
};
if ok == FALSE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
if src_size == 0 {
return Ok(0);
}
let mut params = DUPLICATE_EXTENTS_DATA {
FileHandle: src_handle as HANDLE,
SourceFileOffset: 0,
TargetFileOffset: 0,
ByteCount: src_size,
};
let mut bytes_returned: u32 = 0;
let ok: BOOL = unsafe {
DeviceIoControl(
dst_handle,
FSCTL_DUPLICATE_EXTENTS_TO_FILE,
&mut params as *mut _ as *mut core::ffi::c_void,
std::mem::size_of::<DUPLICATE_EXTENTS_DATA>() as u32,
std::ptr::null_mut(),
0,
&mut bytes_returned,
std::ptr::null_mut::<OVERLAPPED>(),
)
};
if ok == FALSE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
let _ = &src_file;
Ok(src_size as u64)
}
pub(crate) fn preallocate(file: &File, offset: u64, len: u64) -> Result<()> {
if len == 0 {
return Ok(());
}
use windows_sys::Win32::Storage::FileSystem::{
FileAllocationInfo, GetFileSizeEx, SetFileInformationByHandle, FILE_ALLOCATION_INFO,
};
let handle = file.as_raw_handle() as HANDLE;
let end = offset.saturating_add(len);
let target = i64::try_from(end).map_err(|_| {
Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"preallocate target offset exceeds i64::MAX",
))
})?;
let mut current: i64 = 0;
let ok: BOOL = unsafe { GetFileSizeEx(handle, &mut current) };
if ok == FALSE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
if target <= current {
return Ok(());
}
let info = FILE_ALLOCATION_INFO {
AllocationSize: target,
};
let ok: BOOL = unsafe {
SetFileInformationByHandle(
handle,
FileAllocationInfo,
&info as *const _ as *const _,
std::mem::size_of::<FILE_ALLOCATION_INFO>() as u32,
)
};
if ok == FALSE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
Ok(())
}
pub(crate) fn advise(_file: &File, _offset: u64, _len: u64, _advice: crate::Advice) -> Result<()> {
Ok(())
}
pub(crate) fn probe_sector_size(path: &Path) -> u32 {
let root = path
.components()
.next()
.map(|c| {
let mut s = c.as_os_str().to_os_string();
s.push("\\");
s
})
.unwrap_or_else(|| std::ffi::OsString::from(".\\"));
let wide = to_wide_os_string(&root);
let mut sectors_per_cluster: u32 = 0;
let mut bytes_per_sector: u32 = 0;
let mut free_clusters: u32 = 0;
let mut total_clusters: u32 = 0;
let ok: BOOL = unsafe {
GetDiskFreeSpaceW(
wide.as_ptr(),
&mut sectors_per_cluster,
&mut bytes_per_sector,
&mut free_clusters,
&mut total_clusters,
)
};
if ok != FALSE && bytes_per_sector >= 512 {
bytes_per_sector
} else {
512
}
}
#[allow(dead_code)]
pub(crate) fn probe_direct_io_available() -> bool {
true
}
fn to_wide(path: &Path) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt;
path.as_os_str()
.encode_wide()
.chain(std::iter::once(0u16))
.collect()
}
fn to_wide_os_string(s: &std::ffi::OsStr) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt;
s.encode_wide().chain(std::iter::once(0u16)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn tmp_path(suffix: &str) -> std::path::PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("fsys_win_{}_{}_{}", std::process::id(), n, suffix))
}
struct TmpFile(std::path::PathBuf);
impl Drop for TmpFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[test]
fn test_open_write_new_creates_file() {
let path = tmp_path("create");
let _g = TmpFile(path.clone());
let (f, _) = open_write_new(&path, false).expect("open");
drop(f);
assert!(path.exists());
}
#[test]
fn test_open_write_new_fails_if_exists() {
let path = tmp_path("exists");
let _g = TmpFile(path.clone());
std::fs::write(&path, b"existing").expect("create");
assert!(open_write_new(&path, false).is_err());
}
#[test]
fn test_write_all_and_read_all_roundtrip() {
let path = tmp_path("rw");
let _g = TmpFile(path.clone());
let (f, _) = open_write_new(&path, false).expect("open");
write_all(&f, b"windows fsys").expect("write");
drop(f);
let (rf, _) = open_read(&path, false).expect("read");
let data = read_all(&rf).expect("read_all");
assert_eq!(data, b"windows fsys");
}
#[test]
fn test_sync_full_does_not_fail() {
let path = tmp_path("sync");
let _g = TmpFile(path.clone());
let (f, _) = open_write_new(&path, false).expect("open");
write_all(&f, b"sync test").expect("write");
sync_full(&f).expect("flush");
}
#[test]
fn test_atomic_rename_replaces_destination() {
let src = tmp_path("ren_src");
let dst = tmp_path("ren_dst");
let _gs = TmpFile(src.clone());
let _gd = TmpFile(dst.clone());
std::fs::write(&src, b"new").expect("write src");
std::fs::write(&dst, b"old").expect("write dst");
atomic_rename(&src, &dst).expect("rename");
assert!(!src.exists());
assert_eq!(std::fs::read(&dst).expect("read"), b"new");
}
#[test]
fn test_write_at_updates_correct_offset() {
let path = tmp_path("write_at");
let _g = TmpFile(path.clone());
std::fs::write(&path, b"000000000").expect("create");
let f = open_write_at(&path).expect("open");
write_at(&f, 3, b"XXX").expect("write_at");
drop(f);
let content = std::fs::read(&path).expect("read");
assert_eq!(&content[3..6], b"XXX");
}
#[test]
fn test_probe_sector_size_returns_at_least_512() {
let size = probe_sector_size(Path::new("."));
assert!(size >= 512, "sector size {} must be ≥ 512", size);
}
#[test]
fn test_copy_file_content_matches() {
let src = tmp_path("cp_src");
let dst = tmp_path("cp_dst");
let _gs = TmpFile(src.clone());
let _gd = TmpFile(dst.clone());
std::fs::write(&src, b"windows copy").expect("write");
let bytes = copy_file(&src, &dst).expect("copy");
assert_eq!(bytes, 12);
assert_eq!(std::fs::read(&dst).expect("read"), b"windows copy");
}
#[test]
fn test_open_direct_falls_back_gracefully() {
let path = tmp_path("direct_fb");
let _g = TmpFile(path.clone());
let result = open_write_new(&path, true);
match result {
Ok((f, direct)) => {
if direct {
let sector = probe_sector_size(&path);
write_all_direct(&f, b"direct test", sector).expect("write after direct open");
} else {
write_all(&f, b"direct test").expect("write after direct open");
}
}
Err(e) => panic!("open_write_new(direct=true) should not hard-fail: {}", e),
}
}
}