use std::io;
use std::os::windows::io::AsRawHandle as _;
use std::path::Path;
use windows_sys::Win32::Foundation::{
ERROR_HANDLE_EOF, ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER, ERROR_MORE_DATA,
ERROR_NOT_SUPPORTED,
};
use windows_sys::Win32::Storage::FileSystem::GetVolumeInformationByHandleW;
use windows_sys::Win32::System::Ioctl::{
FSCTL_GET_RETRIEVAL_POINTERS_AND_REFCOUNT, RETRIEVAL_POINTERS_AND_REFCOUNT_BUFFER,
RETRIEVAL_POINTERS_AND_REFCOUNT_BUFFER_0, STARTING_VCN_INPUT_BUFFER,
};
use windows_sys::Win32::System::IO::DeviceIoControl;
use crate::platform::fs::ExtentSharing;
const NON_SHARING: &[&str] = &["NTFS", "FAT", "FAT32", "exFAT"];
const REFS: &str = "ReFS";
const EXTENTS_PER_CALL: usize = 256;
type Header = RETRIEVAL_POINTERS_AND_REFCOUNT_BUFFER;
type Extent = RETRIEVAL_POINTERS_AND_REFCOUNT_BUFFER_0;
pub fn extent_sharing(path: &Path) -> io::Result<ExtentSharing> {
let file = std::fs::File::open(path)?;
let name = volume_file_system(&file)?;
if NON_SHARING.iter().any(|fs| name.eq_ignore_ascii_case(fs)) {
return Ok(ExtentSharing::Exclusive);
}
if !name.eq_ignore_ascii_case(REFS) {
return Ok(ExtentSharing::Unknown);
}
match refcount_sharing(&file) {
Ok(sharing) => Ok(sharing),
Err(error) if is_unsupported(&error) => Ok(ExtentSharing::Unknown),
Err(error) => Err(error),
}
}
fn volume_file_system(file: &std::fs::File) -> io::Result<String> {
let mut name = [0_u16; 64];
let ok = unsafe {
GetVolumeInformationByHandleW(
file.as_raw_handle(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
name.as_mut_ptr(),
name.len() as u32,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
let len = name.iter().position(|&unit| unit == 0).unwrap_or(name.len());
Ok(String::from_utf16_lossy(&name[..len]))
}
fn refcount_sharing(file: &std::fs::File) -> io::Result<ExtentSharing> {
let extents_offset = std::mem::offset_of!(Header, Extents);
let bytes = extents_offset + EXTENTS_PER_CALL * std::mem::size_of::<Extent>();
let mut buffer = vec![0_u64; bytes.div_ceil(8)];
let mut starting_vcn = 0_i64;
loop {
let input = STARTING_VCN_INPUT_BUFFER {
StartingVcn: starting_vcn,
};
let mut returned = 0_u32;
let ok = unsafe {
DeviceIoControl(
file.as_raw_handle(),
FSCTL_GET_RETRIEVAL_POINTERS_AND_REFCOUNT,
std::ptr::addr_of!(input).cast(),
std::mem::size_of::<STARTING_VCN_INPUT_BUFFER>() as u32,
buffer.as_mut_ptr().cast(),
bytes as u32,
&mut returned,
std::ptr::null_mut(),
)
};
let more = if ok != 0 {
false
} else {
let error = io::Error::last_os_error();
match error.raw_os_error().map(|code| code as u32) {
Some(ERROR_HANDLE_EOF) => return Ok(ExtentSharing::Exclusive),
Some(ERROR_MORE_DATA) => true,
_ => return Err(error),
}
};
let header = buffer.as_ptr().cast::<Header>();
let count = unsafe { (*header).ExtentCount } as usize;
let count = count.min(EXTENTS_PER_CALL);
let extents = unsafe {
std::slice::from_raw_parts(
buffer.as_ptr().cast::<u8>().add(extents_offset).cast::<Extent>(),
count,
)
};
if let Some(sharing) = classify(extents) {
return Ok(sharing);
}
match extents.last() {
Some(last) if more && last.NextVcn > starting_vcn => starting_vcn = last.NextVcn,
_ => return Ok(ExtentSharing::Exclusive),
}
}
}
fn classify(extents: &[Extent]) -> Option<ExtentSharing> {
extents
.iter()
.any(|extent| extent.Lcn != -1 && extent.ReferenceCount > 1)
.then_some(ExtentSharing::Shared)
}
fn is_unsupported(error: &io::Error) -> bool {
matches!(
error.raw_os_error().map(|code| code as u32),
Some(ERROR_INVALID_FUNCTION | ERROR_NOT_SUPPORTED | ERROR_INVALID_PARAMETER)
)
}
#[cfg(test)]
mod tests {
use super::*;
fn extent(lcn: i64, references: u32) -> Extent {
Extent {
NextVcn: 0,
Lcn: lcn,
ReferenceCount: references,
}
}
#[test]
fn refcount_reply_matches_the_winioctl_layout() {
assert_eq!(std::mem::offset_of!(Header, Extents), 16);
assert_eq!(std::mem::size_of::<Extent>(), 24);
}
#[test]
fn classify_calls_a_second_reference_shared_and_skips_holes() {
assert_eq!(classify(&[]), None);
assert_eq!(classify(&[extent(10, 1), extent(20, 1)]), None);
assert_eq!(classify(&[extent(-1, 2)]), None);
assert_eq!(
classify(&[extent(10, 1), extent(20, 2)]),
Some(ExtentSharing::Shared)
);
}
fn test_dir() -> (tempfile::TempDir, bool) {
match std::env::var_os("KERNAL_REFS_TEST_DIR") {
Some(root) => (tempfile::tempdir_in(root).unwrap(), true),
None => (tempfile::tempdir().unwrap(), false),
}
}
#[test]
fn a_fresh_file_is_exclusive_on_ntfs_and_refs() {
let (dir, refs) = test_dir();
let path = dir.path().join("fresh");
std::fs::write(&path, vec![5_u8; 256 * 1024]).unwrap();
let file = std::fs::File::open(&path).unwrap();
let name = volume_file_system(&file).unwrap();
if refs {
assert!(name.eq_ignore_ascii_case(REFS), "{name}");
}
if name.eq_ignore_ascii_case(REFS) || NON_SHARING.contains(&name.as_str()) {
assert_eq!(extent_sharing(&path).unwrap(), ExtentSharing::Exclusive);
}
}
#[test]
fn a_block_cloned_pair_reports_shared_on_refs() {
let (dir, refs) = test_dir();
let source = dir.path().join("source");
std::fs::write(&source, vec![3_u8; 256 * 1024]).unwrap();
let clone = dir.path().join("clone");
let cloned = crate::platform::fs::reflink_file(&source, &clone);
if refs {
cloned.as_ref().expect("ReFS block clone");
}
if cloned.is_err() {
return;
}
assert_eq!(extent_sharing(&source).unwrap(), ExtentSharing::Shared);
assert_eq!(extent_sharing(&clone).unwrap(), ExtentSharing::Shared);
}
}