use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::io::Write;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, GetFileAttributesW, GetVolumeInformationByHandleW, MoveFileExW,
};
use windows_sys::Win32::System::IO::DeviceIoControl;
use crate::{io, Error, Support, UnsupportedReason};
const GENERIC_READ: u32 = 0x8000_0000;
const GENERIC_WRITE: u32 = 0x4000_0000;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const OPEN_EXISTING: u32 = 3;
const CREATE_NEW: u32 = 1;
const MOVEFILE_REPLACE_EXISTING: u32 = 0x0000_0001;
const MOVEFILE_WRITE_THROUGH: u32 = 0x0000_0008;
const FSCTL_SET_COMPRESSION: u32 = 0x0009_C040;
const COMPRESSION_FORMAT_DEFAULT: u16 = 1; const FILE_FILE_COMPRESSION: u32 = 0x0000_0010; const FILE_ATTRIBUTE_COMPRESSED: u32 = 0x0000_0800;
const INVALID_FILE_ATTRIBUTES: u32 = u32::MAX;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
fn wide(path: &Path) -> Vec<u16> {
path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
struct Handle(HANDLE);
impl Drop for Handle {
fn drop(&mut self) {
unsafe {
CloseHandle(self.0);
}
}
}
fn open(path: &Path, access: u32) -> Result<Handle, Error> {
open_with(path, access, OPEN_EXISTING)
}
fn open_with(path: &Path, access: u32, disposition: u32) -> Result<Handle, Error> {
let wpath = wide(path);
let handle = unsafe {
CreateFileW(
wpath.as_ptr(),
access,
FILE_SHARE_READ,
std::ptr::null(),
disposition,
FILE_FLAG_BACKUP_SEMANTICS,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE || handle.is_null() {
return Err(io("CreateFileW"));
}
Ok(Handle(handle))
}
fn set_compression(handle: HANDLE) -> Result<(), Error> {
let format: u16 = COMPRESSION_FORMAT_DEFAULT;
let mut returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
FSCTL_SET_COMPRESSION,
(&format as *const u16).cast(),
std::mem::size_of::<u16>() as u32,
std::ptr::null_mut(),
0,
&mut returned,
std::ptr::null_mut(),
)
};
if ok == 0 {
return Err(io("FSCTL_SET_COMPRESSION"));
}
Ok(())
}
pub(crate) fn detect(path: &Path) -> Result<Support, Error> {
let handle = open(path, GENERIC_READ)?;
let mut flags: u32 = 0;
let ok = unsafe {
GetVolumeInformationByHandleW(
handle.0,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut flags,
std::ptr::null_mut(),
0,
)
};
if ok == 0 {
return Err(io("GetVolumeInformationByHandleW"));
}
if flags & FILE_FILE_COMPRESSION != 0 {
Ok(Support::Supported)
} else {
Ok(Support::Unsupported(UnsupportedReason::Filesystem))
}
}
pub(crate) fn is_already_compressed(path: &Path) -> Result<bool, Error> {
let attrs = unsafe { GetFileAttributesW(wide(path).as_ptr()) };
if attrs == INVALID_FILE_ATTRIBUTES {
return Err(io("GetFileAttributesW"));
}
Ok(attrs & FILE_ATTRIBUTE_COMPRESSED != 0)
}
pub(crate) fn apply_inplace(path: &Path, _snapshot: &[u8]) -> Result<(), Error> {
let handle = open(path, GENERIC_READ | GENERIC_WRITE)?;
set_compression(handle.0)
}
fn unique_tmp(dir: &Path, name: &str) -> std::path::PathBuf {
static TMP_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = TMP_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
dir.join(format!(
".{name}.decmpfs-{}-{nanos}-{seq}.tmp",
std::process::id()
))
}
fn move_file_replace(tmp: &Path, dest: &Path) -> Result<(), Error> {
let wtmp = wide(tmp);
let wdest = wide(dest);
let ok = unsafe {
MoveFileExW(
wtmp.as_ptr(),
wdest.as_ptr(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
)
};
if ok == 0 {
return Err(io("MoveFileExW"));
}
Ok(())
}
pub(crate) fn prepare_stream(file: &std::fs::File) -> Result<(), Error> {
use std::os::windows::io::AsRawHandle;
set_compression(file.as_raw_handle() as HANDLE)
}
pub(crate) fn publish_stream(tmp: &Path, dest: &Path) -> Result<(), Error> {
move_file_replace(tmp, dest)
}
pub(crate) fn apply_bytes(
path: &Path,
content: &[u8],
_mode: Option<std::fs::Permissions>,
) -> Result<(), Error> {
let dir = path.parent().ok_or_else(|| io("no parent dir"))?;
let name = path.file_name().map_or_else(
|| std::borrow::Cow::Borrowed("addon"),
|n| n.to_string_lossy(),
);
let tmp = unique_tmp(dir, &name);
let result = (|| {
let handle = open_with(&tmp, GENERIC_READ | GENERIC_WRITE, CREATE_NEW)?;
set_compression(handle.0)?;
use std::os::windows::io::FromRawHandle;
let mut file = unsafe { std::fs::File::from_raw_handle(handle.0 as _) };
let res = file
.write_all(content)
.and_then(|()| file.flush())
.and_then(|()| file.sync_all());
std::mem::forget(file);
res.map_err(|source| Error::Io {
context: "write temp",
source,
})
})();
match result {
Ok(()) => move_file_replace(&tmp, path).inspect_err(|_| {
let _ = std::fs::remove_file(&tmp);
}),
Err(err) => {
let _ = std::fs::remove_file(&tmp);
Err(err)
}
}
}
pub(crate) fn compressed_on_disk(_path: &Path) -> Result<Option<bool>, Error> {
Ok(None)
}
pub(crate) fn clone_file(_src: &Path, _dest: &Path) -> Result<bool, Error> {
Ok(false)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
fn scratch(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("decmpfs-win-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn on_ntfs(dir: &Path) -> bool {
let probe = dir.join(".ntfs-probe");
std::fs::write(&probe, b"x").unwrap();
let yes = matches!(detect(&probe), Ok(Support::Supported));
std::fs::remove_file(&probe).ok();
yes
}
#[test]
fn unique_tmp_never_collides_and_is_well_formed() {
let dir = std::env::temp_dir();
let a = unique_tmp(&dir, "addon.node");
let b = unique_tmp(&dir, "addon.node");
assert_ne!(a, b, "successive temps must differ (the seq counter)");
for p in [&a, &b] {
let f = p.file_name().unwrap().to_string_lossy();
assert!(
f.starts_with(".addon.node.decmpfs-"),
"unexpected temp name: {f}"
);
assert!(f.ends_with(".tmp"), "unexpected temp name: {f}");
}
}
#[test]
fn clone_file_always_declines_on_ntfs() {
let dir = scratch("clone");
assert!(!clone_file(&dir.join("a"), &dir.join("b")).unwrap());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn detect_is_ok_and_a_fresh_file_is_not_compressed() {
let dir = scratch("detect");
let path = dir.join("f.bin");
std::fs::write(&path, b"MZ plain").unwrap();
assert!(detect(&path).is_ok());
assert!(!is_already_compressed(&path).unwrap());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn apply_bytes_round_trips_atomically_and_races_converge_on_ntfs() {
let dir = scratch("rt");
if !on_ntfs(&dir) {
std::fs::remove_dir_all(&dir).ok();
return;
}
let content = vec![0xCDu8; 128 * 1024];
let dest = dir.join("addon.node");
std::thread::scope(|s| {
for _ in 0..8 {
let dest = dest.clone();
let content = content.clone();
s.spawn(move || {
let _ = apply_bytes(&dest, &content, None);
});
}
});
assert_eq!(
std::fs::read(&dest).unwrap(),
content,
"bytes survive the race"
);
assert!(
is_already_compressed(&dest).unwrap(),
"landed NTFS-compressed"
);
std::fs::remove_dir_all(&dir).ok();
}
}