#![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, SetFilePointerEx,
WriteFile, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, FILE_BEGIN, FILE_FLAG_NO_BUFFERING,
FILE_FLAG_WRITE_THROUGH, FILE_SHARE_READ, FILE_SHARE_WRITE, MOVEFILE_REPLACE_EXISTING,
MOVEFILE_WRITE_THROUGH, OPEN_EXISTING,
};
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};
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 dist_lo = (offset & 0xFFFF_FFFF) as i32;
let dist_hi = (offset >> 32) as i32;
let ok: BOOL =
unsafe { SetFilePointerEx(handle, dist_lo as i64, std::ptr::null_mut(), FILE_BEGIN) };
if ok == FALSE {
return Err(Error::Io(std::io::Error::last_os_error()));
}
let _ = dist_hi;
write_all(file, 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 copy_file(src: &Path, dst: &Path) -> Result<u64> {
std::fs::copy(src, dst).map_err(Error::Io)
}
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),
}
}
}