use std::{
collections::HashMap,
ffi::{OsStr, OsString},
os::windows::ffi::{OsStrExt, OsStringExt},
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use objects::sync::LockExt;
use tracing::warn;
use windows::{
Win32::{
Foundation::{ERROR_INSUFFICIENT_BUFFER, FreeLibrary, S_OK},
Storage::ProjectedFileSystem::{
PRJ_CALLBACK_DATA, PRJ_CALLBACKS, PRJ_CB_DATA_FLAG_ENUM_RESTART_SCAN,
PRJ_DIR_ENTRY_BUFFER_HANDLE, PRJ_FILE_BASIC_INFO, PRJ_NAMESPACE_VIRTUALIZATION_CONTEXT,
PRJ_NOTIFICATION, PRJ_NOTIFICATION_FILE_HANDLE_CLOSED_FILE_DELETED,
PRJ_NOTIFICATION_FILE_HANDLE_CLOSED_FILE_MODIFIED,
PRJ_NOTIFICATION_FILE_HANDLE_CLOSED_NO_MODIFICATION, PRJ_NOTIFICATION_FILE_RENAMED,
PRJ_NOTIFICATION_MAPPING, PRJ_NOTIFY_TYPES, PRJ_PLACEHOLDER_INFO,
PRJ_STARTVIRTUALIZING_OPTIONS, PrjAllocateAlignedBuffer, PrjFillDirEntryBuffer,
PrjFreeAlignedBuffer, PrjMarkDirectoryAsPlaceholder, PrjStartVirtualizing,
PrjStopVirtualizing, PrjWriteFileData, PrjWritePlaceholderInfo,
},
System::LibraryLoader::LoadLibraryW,
},
core::{GUID, HRESULT, PCWSTR},
};
use crate::{
core::ContentAddressedMount,
error::{MountError, Result},
shell::{Entry, NodeId, NodeKind, PlatformShell},
};
pub struct ProjFsShell {
inner: Arc<dyn PlatformShell + Send + Sync>,
}
impl ProjFsShell {
pub fn new(mount: ContentAddressedMount) -> Self {
Self::from_shell(Arc::new(mount))
}
pub fn from_shell(shell: Arc<dyn PlatformShell + Send + Sync>) -> Self {
Self { inner: shell }
}
pub fn is_runtime_available() -> bool {
unsafe {
let dll = encode_wide("ProjectedFSLib.dll");
match LoadLibraryW(PCWSTR(dll.as_ptr())) {
Ok(handle) if !handle.is_invalid() => {
let _ = FreeLibrary(handle);
true
}
_ => false,
}
}
}
pub fn mount_background(self, virtualization_root: impl AsRef<Path>) -> Result<ProjFsSession> {
let root = virtualization_root.as_ref().to_path_buf();
std::fs::create_dir_all(&root)
.map_err(|e| MountError::Store(objects::error::HeddleError::Io(e)))?;
let instance_id = load_or_create_instance_id(&root)?;
let root_wide = encode_wide(root.to_string_lossy().as_ref());
unsafe {
PrjMarkDirectoryAsPlaceholder(
PCWSTR(root_wide.as_ptr()),
PCWSTR::null(),
None,
&instance_id,
)
.map_err(|e| hresult_to_mount_error(e.code()))?;
}
let context = Box::new(InstanceContext {
shell: self.inner,
virtualization_root: root.clone(),
enumerations: Mutex::new(HashMap::new()),
});
let instance_context = Box::into_raw(context) as *const std::ffi::c_void;
let callbacks = PRJ_CALLBACKS {
StartDirectoryEnumerationCallback: Some(start_dir_enum_trampoline),
EndDirectoryEnumerationCallback: Some(end_dir_enum_trampoline),
GetDirectoryEnumerationCallback: Some(get_dir_enum_trampoline),
GetPlaceholderInfoCallback: Some(get_placeholder_info_trampoline),
GetFileDataCallback: Some(get_file_data_trampoline),
QueryFileNameCallback: None,
NotificationCallback: Some(notification_trampoline),
CancelCommandCallback: None,
};
let notification_bits = PRJ_NOTIFY_TYPES(
(PRJ_NOTIFICATION_FILE_HANDLE_CLOSED_FILE_MODIFIED.0
| PRJ_NOTIFICATION_FILE_HANDLE_CLOSED_FILE_DELETED.0
| PRJ_NOTIFICATION_FILE_HANDLE_CLOSED_NO_MODIFICATION.0
| PRJ_NOTIFICATION_FILE_RENAMED.0) as u32,
);
let mut notification_mapping = entire_root_notification_mapping(notification_bits);
let options = PRJ_STARTVIRTUALIZING_OPTIONS {
Flags: Default::default(),
PoolThreadCount: 0,
ConcurrentThreadCount: 0,
NotificationMappings: &mut notification_mapping,
NotificationMappingsCount: 1,
};
let handle = match unsafe {
PrjStartVirtualizing(
PCWSTR(root_wide.as_ptr()),
&callbacks,
Some(instance_context),
Some(&options),
)
} {
Ok(h) => h,
Err(e) => {
unsafe {
drop(Box::from_raw(instance_context as *mut InstanceContext));
}
return Err(hresult_to_mount_error(e.code()));
}
};
Ok(ProjFsSession {
handle: Some(handle),
instance_context,
virtualization_root: root,
})
}
}
pub struct ProjFsSession {
handle: Option<PRJ_NAMESPACE_VIRTUALIZATION_CONTEXT>,
instance_context: *const std::ffi::c_void,
virtualization_root: PathBuf,
}
unsafe impl Send for ProjFsSession {}
unsafe impl Sync for ProjFsSession {}
impl ProjFsSession {
pub fn unmount(mut self) -> Result<()> {
let Some(handle) = self.handle.take() else {
return Ok(());
};
unsafe { PrjStopVirtualizing(handle) };
self.reclaim_context();
Ok(())
}
pub fn virtualization_root(&self) -> &Path {
&self.virtualization_root
}
fn reclaim_context(&mut self) {
if self.instance_context.is_null() {
return;
}
unsafe {
drop(Box::from_raw(self.instance_context as *mut InstanceContext));
}
self.instance_context = std::ptr::null();
}
}
struct InstanceContext {
shell: Arc<dyn PlatformShell + Send + Sync>,
virtualization_root: PathBuf,
enumerations: Mutex<HashMap<EnumKey, EnumState>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct EnumKey([u8; 16]);
impl EnumKey {
fn from_guid(g: &GUID) -> Self {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&g.data1.to_le_bytes());
buf[4..6].copy_from_slice(&g.data2.to_le_bytes());
buf[6..8].copy_from_slice(&g.data3.to_le_bytes());
buf[8..16].copy_from_slice(&g.data4);
Self(buf)
}
}
struct EnumState {
entries: Vec<Entry>,
cursor: usize,
populated: bool,
}
impl EnumState {
fn empty() -> Self {
Self {
entries: Vec::new(),
cursor: 0,
populated: false,
}
}
fn reset(&mut self) {
self.entries.clear();
self.cursor = 0;
self.populated = false;
}
}
impl Drop for ProjFsSession {
fn drop(&mut self) {
if let Some(handle) = self.handle.take() {
unsafe { PrjStopVirtualizing(handle) };
}
self.reclaim_context();
}
}
fn encode_wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
unsafe fn decode_wide(ptr: PCWSTR) -> OsString {
if ptr.is_null() {
return OsString::new();
}
let mut len = 0usize;
while unsafe { *ptr.0.add(len) } != 0 {
len += 1;
}
let slice = unsafe { std::slice::from_raw_parts(ptr.0, len) };
OsString::from_wide(slice)
}
fn resolve_path(shell: &Arc<dyn PlatformShell + Send + Sync>, relative: &Path) -> Result<NodeId> {
let mut node = NodeId::ROOT;
for component in relative.components() {
let name = component.as_os_str();
if name.is_empty() {
continue;
}
let entry = shell
.lookup(node, name)?
.ok_or_else(|| MountError::NotFound(relative.display().to_string()))?;
node = entry.node;
}
Ok(node)
}
fn load_or_create_instance_id(root: &Path) -> Result<GUID> {
let primary = instance_id_sidecar_path_primary(root);
let fallback = instance_id_sidecar_path_fallback(root);
for candidate in [&primary, &fallback] {
if let Ok(bytes) = std::fs::read(candidate)
&& bytes.len() == 16
{
return Ok(decode_guid(&bytes));
}
}
let guid = GUID::new().map_err(|e| hresult_to_mount_error(e.code()))?;
let bytes = encode_guid(&guid);
if let Some(parent) = primary.parent() {
let _ = std::fs::create_dir_all(parent);
}
if std::fs::write(&primary, &bytes).is_ok() {
return Ok(guid);
}
tracing::warn!(
primary = %primary.display(),
fallback = %fallback.display(),
"projfs: parent-dir sidecar write failed; falling back to in-root sidecar. \
The sidecar will appear in mountpoint listings; restrict only as needed.",
);
std::fs::write(&fallback, &bytes)
.map_err(|e| MountError::Store(objects::error::HeddleError::Io(e)))?;
Ok(guid)
}
fn instance_id_sidecar_path_primary(root: &Path) -> PathBuf {
if let (Some(parent), Some(basename)) = (root.parent(), root.file_name()) {
let mut name = OsString::from(".");
name.push(basename);
name.push(".heddle-projfs-id");
parent.join(name)
} else {
instance_id_sidecar_path_fallback(root)
}
}
fn instance_id_sidecar_path_fallback(root: &Path) -> PathBuf {
root.join(".heddle-projfs-id")
}
fn decode_guid(bytes: &[u8]) -> GUID {
GUID::from_values(
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
u16::from_le_bytes([bytes[4], bytes[5]]),
u16::from_le_bytes([bytes[6], bytes[7]]),
[
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
],
)
}
fn encode_guid(guid: &GUID) -> Vec<u8> {
let mut bytes = Vec::with_capacity(16);
bytes.extend_from_slice(&guid.data1.to_le_bytes());
bytes.extend_from_slice(&guid.data2.to_le_bytes());
bytes.extend_from_slice(&guid.data3.to_le_bytes());
bytes.extend_from_slice(&guid.data4);
bytes
}
fn entire_root_notification_mapping(bits: PRJ_NOTIFY_TYPES) -> PRJ_NOTIFICATION_MAPPING {
static EMPTY_NOTIFICATION_ROOT: [u16; 1] = [0u16];
PRJ_NOTIFICATION_MAPPING {
NotificationBitMask: bits,
NotificationRoot: PCWSTR(EMPTY_NOTIFICATION_ROOT.as_ptr()),
}
}
fn hresult_to_mount_error(hr: HRESULT) -> MountError {
let win32 = hr.0 as u32 & 0xFFFF;
MountError::Store(objects::error::HeddleError::Io(
std::io::Error::from_raw_os_error(win32 as i32),
))
}
fn mount_error_to_hresult(err: MountError) -> HRESULT {
let errno = err.to_errno();
let win32 = errno_to_win32(errno);
HRESULT(((win32 & 0xFFFF) | 0x8007_0000) as i32)
}
fn errno_to_win32(errno: i32) -> u32 {
const ESTALE: i32 = 116; match errno {
libc::ENOENT => 2, libc::ENOTDIR => 267, ESTALE => 1632, libc::EROFS => 19, libc::EIO => 1117, _ => 31, }
}
unsafe fn instance_from_context<'a>(
context: *const std::ffi::c_void,
) -> Option<&'a InstanceContext> {
if context.is_null() {
return None;
}
Some(unsafe { &*(context as *const InstanceContext) })
}
unsafe fn shell_from_context<'a>(
context: *const std::ffi::c_void,
) -> Option<&'a Arc<dyn PlatformShell + Send + Sync>> {
unsafe { instance_from_context(context) }.map(|ctx| &ctx.shell)
}
#[inline]
fn guarded_hresult<F: FnOnce() -> HRESULT>(label: &'static str, f: F) -> HRESULT {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(rc) => rc,
Err(payload) => {
let msg = crate::error::panic_payload_str(&payload);
tracing::error!(
trampoline = label,
%msg,
"ProjFS trampoline panicked; returning HRESULT_FROM_WIN32(ERROR_IO_DEVICE)",
);
mount_error_to_hresult(MountError::Store(objects::error::HeddleError::Io(
std::io::Error::from_raw_os_error(1117),
)))
}
}
}
unsafe fn callback_data<'a>(data: *const PRJ_CALLBACK_DATA) -> Option<&'a PRJ_CALLBACK_DATA> {
if data.is_null() {
None
} else {
Some(unsafe { &*data })
}
}
unsafe extern "system" fn get_placeholder_info_trampoline(
callback_data: *const PRJ_CALLBACK_DATA,
) -> HRESULT {
guarded_hresult("get_placeholder_info", || unsafe {
get_placeholder_info_impl(callback_data)
})
}
unsafe fn get_placeholder_info_impl(callback_data: *const PRJ_CALLBACK_DATA) -> HRESULT {
let Some(data) = (unsafe { callback_data_or_log("get_placeholder_info", callback_data) })
else {
return mount_error_to_hresult(MountError::Stale("null callback_data".into()));
};
let Some(shell) = (unsafe { shell_from_context(data.InstanceContext) }) else {
return mount_error_to_hresult(MountError::Stale("null instance_context".into()));
};
let rel_path = unsafe { decode_wide(data.FilePathName) };
let rel = Path::new(&rel_path);
let node = match resolve_path(shell, rel) {
Ok(n) => n,
Err(e) => return mount_error_to_hresult(e),
};
let attrs = match shell.attrs(node) {
Ok(a) => a,
Err(e) => return mount_error_to_hresult(e),
};
let info = PRJ_PLACEHOLDER_INFO {
FileBasicInfo: PRJ_FILE_BASIC_INFO {
IsDirectory: matches!(attrs.kind, NodeKind::Directory).into(),
FileSize: attrs.size as i64,
CreationTime: Default::default(),
LastAccessTime: Default::default(),
LastWriteTime: Default::default(),
ChangeTime: Default::default(),
FileAttributes: if matches!(attrs.kind, NodeKind::Directory) {
0x10 } else {
0x80 },
},
..PRJ_PLACEHOLDER_INFO::default()
};
let rc = unsafe {
PrjWritePlaceholderInfo(
data.NamespaceVirtualizationContext,
data.FilePathName,
&info,
std::mem::size_of::<PRJ_PLACEHOLDER_INFO>() as u32,
)
};
match rc {
Ok(()) => S_OK,
Err(e) => e.code(),
}
}
unsafe extern "system" fn get_file_data_trampoline(
callback_data: *const PRJ_CALLBACK_DATA,
byte_offset: u64,
length: u32,
) -> HRESULT {
guarded_hresult("get_file_data", || unsafe {
get_file_data_impl(callback_data, byte_offset, length)
})
}
unsafe fn get_file_data_impl(
callback_data: *const PRJ_CALLBACK_DATA,
byte_offset: u64,
length: u32,
) -> HRESULT {
let Some(data) = (unsafe { callback_data_or_log("get_file_data", callback_data) }) else {
return mount_error_to_hresult(MountError::Stale("null callback_data".into()));
};
let Some(shell) = (unsafe { shell_from_context(data.InstanceContext) }) else {
return mount_error_to_hresult(MountError::Stale("null instance_context".into()));
};
let rel_path = unsafe { decode_wide(data.FilePathName) };
let node = match resolve_path(shell, Path::new(&rel_path)) {
Ok(n) => n,
Err(e) => return mount_error_to_hresult(e),
};
let buffer =
unsafe { PrjAllocateAlignedBuffer(data.NamespaceVirtualizationContext, length as usize) };
if buffer.is_null() {
return mount_error_to_hresult(MountError::Store(objects::error::HeddleError::Io(
std::io::Error::from_raw_os_error(libc::ENOMEM),
)));
}
let slab = unsafe { std::slice::from_raw_parts_mut(buffer as *mut u8, length as usize) };
let n = match shell.read(node, byte_offset, slab) {
Ok(n) => n,
Err(e) => {
unsafe {
PrjFreeAlignedBuffer(buffer);
}
return mount_error_to_hresult(e);
}
};
let rc = unsafe {
PrjWriteFileData(
data.NamespaceVirtualizationContext,
&data.DataStreamId,
buffer,
byte_offset,
n as u32,
)
};
unsafe { PrjFreeAlignedBuffer(buffer) };
match rc {
Ok(()) => S_OK,
Err(e) => e.code(),
}
}
unsafe extern "system" fn start_dir_enum_trampoline(
callback_data: *const PRJ_CALLBACK_DATA,
enumeration_id: *const GUID,
) -> HRESULT {
guarded_hresult("start_dir_enum", || unsafe {
start_dir_enum_impl(callback_data, enumeration_id)
})
}
unsafe fn start_dir_enum_impl(
callback_data: *const PRJ_CALLBACK_DATA,
enumeration_id: *const GUID,
) -> HRESULT {
let Some(data) = (unsafe { callback_data_or_log("start_dir_enum", callback_data) }) else {
return S_OK;
};
let Some(instance) = (unsafe { instance_from_context(data.InstanceContext) }) else {
return S_OK;
};
if enumeration_id.is_null() {
return S_OK;
}
let key = EnumKey::from_guid(unsafe { &*enumeration_id });
let mut guard = instance.enumerations.lock_or_poisoned();
guard.insert(key, EnumState::empty());
S_OK
}
unsafe extern "system" fn end_dir_enum_trampoline(
callback_data: *const PRJ_CALLBACK_DATA,
enumeration_id: *const GUID,
) -> HRESULT {
guarded_hresult("end_dir_enum", || unsafe {
end_dir_enum_impl(callback_data, enumeration_id)
})
}
unsafe fn end_dir_enum_impl(
callback_data: *const PRJ_CALLBACK_DATA,
enumeration_id: *const GUID,
) -> HRESULT {
let Some(data) = (unsafe { callback_data_or_log("end_dir_enum", callback_data) }) else {
return S_OK;
};
let Some(instance) = (unsafe { instance_from_context(data.InstanceContext) }) else {
return S_OK;
};
if enumeration_id.is_null() {
return S_OK;
}
let key = EnumKey::from_guid(unsafe { &*enumeration_id });
let mut guard = instance.enumerations.lock_or_poisoned();
guard.remove(&key);
S_OK
}
unsafe extern "system" fn get_dir_enum_trampoline(
callback_data: *const PRJ_CALLBACK_DATA,
enumeration_id: *const GUID,
_search_expression: PCWSTR,
dir_entry_buffer_handle: PRJ_DIR_ENTRY_BUFFER_HANDLE,
) -> HRESULT {
guarded_hresult("get_dir_enum", || unsafe {
get_dir_enum_impl(callback_data, enumeration_id, dir_entry_buffer_handle)
})
}
unsafe fn get_dir_enum_impl(
callback_data: *const PRJ_CALLBACK_DATA,
enumeration_id: *const GUID,
dir_entry_buffer_handle: PRJ_DIR_ENTRY_BUFFER_HANDLE,
) -> HRESULT {
let Some(data) = (unsafe { callback_data_or_log("get_dir_enum", callback_data) }) else {
return mount_error_to_hresult(MountError::Stale("null callback_data".into()));
};
let Some(instance) = (unsafe { instance_from_context(data.InstanceContext) }) else {
return mount_error_to_hresult(MountError::Stale("null instance_context".into()));
};
let shell = &instance.shell;
let rel_path = unsafe { decode_wide(data.FilePathName) };
let dir_node = match resolve_path(shell, Path::new(&rel_path)) {
Ok(n) => n,
Err(e) => return mount_error_to_hresult(e),
};
if enumeration_id.is_null() {
let entries = match shell.enumerate(dir_node) {
Ok(e) => e,
Err(e) => return mount_error_to_hresult(e),
};
return match emit_entry_slice(&entries, dir_entry_buffer_handle) {
Ok(_) => S_OK,
Err(hr) => hr,
};
}
let key = EnumKey::from_guid(unsafe { &*enumeration_id });
let restart = (data.Flags.0 & PRJ_CB_DATA_FLAG_ENUM_RESTART_SCAN.0) != 0;
let mut guard = instance.enumerations.lock_or_poisoned();
let state = guard.entry(key).or_insert_with(EnumState::empty);
if restart {
state.reset();
}
if !state.populated {
match shell.enumerate(dir_node) {
Ok(entries) => {
state.entries = entries;
state.cursor = 0;
state.populated = true;
}
Err(e) => return mount_error_to_hresult(e),
}
}
match emit_entry_slice(&state.entries[state.cursor..], dir_entry_buffer_handle) {
Ok(written) => {
state.cursor += written;
S_OK
}
Err(hr) => hr,
}
}
fn emit_entry_slice(
entries: &[Entry],
buffer: PRJ_DIR_ENTRY_BUFFER_HANDLE,
) -> std::result::Result<usize, HRESULT> {
for (i, entry) in entries.iter().enumerate() {
let basic = PRJ_FILE_BASIC_INFO {
IsDirectory: matches!(entry.kind, NodeKind::Directory).into(),
FileSize: entry.size as i64,
CreationTime: Default::default(),
LastAccessTime: Default::default(),
LastWriteTime: Default::default(),
ChangeTime: Default::default(),
FileAttributes: if matches!(entry.kind, NodeKind::Directory) {
0x10
} else {
0x80
},
};
let mut name_wide = entry.name.encode_wide().collect::<Vec<u16>>();
name_wide.push(0);
let basic_ptr: *const PRJ_FILE_BASIC_INFO = &basic;
let rc =
unsafe { PrjFillDirEntryBuffer(PCWSTR(name_wide.as_ptr()), Some(basic_ptr), buffer) };
if let Err(e) = rc {
if e.code() == HRESULT::from(ERROR_INSUFFICIENT_BUFFER) {
if i == 0 {
return Err(HRESULT::from(ERROR_INSUFFICIENT_BUFFER));
}
return Ok(i);
}
return Err(e.code());
}
}
Ok(entries.len())
}
unsafe extern "system" fn notification_trampoline(
callback_data: *const PRJ_CALLBACK_DATA,
_is_directory: bool,
notification: PRJ_NOTIFICATION,
destination_file_name: PCWSTR,
_operation_parameters: *mut windows::Win32::Storage::ProjectedFileSystem::PRJ_NOTIFICATION_PARAMETERS,
) -> HRESULT {
guarded_hresult("notification", || unsafe {
notification_impl(callback_data, notification, destination_file_name)
})
}
unsafe fn notification_impl(
callback_data: *const PRJ_CALLBACK_DATA,
notification: PRJ_NOTIFICATION,
destination_file_name: PCWSTR,
) -> HRESULT {
let Some(data) = (unsafe { callback_data_or_log("notification", callback_data) }) else {
return S_OK;
};
let Some(instance) = (unsafe { instance_from_context(data.InstanceContext) }) else {
return S_OK;
};
let shell = &instance.shell;
let virt_root = instance.virtualization_root.as_path();
let rel_path = unsafe { decode_wide(data.FilePathName) };
let rel = Path::new(&rel_path);
let node = match resolve_path(shell, rel) {
Ok(n) => n,
Err(_) => {
return S_OK;
}
};
match notification {
n if n == PRJ_NOTIFICATION_FILE_HANDLE_CLOSED_FILE_MODIFIED => {
let full_path = virt_root.join(rel);
match std::fs::read(&full_path) {
Ok(contents) => {
if let Err(e) = shell.write(node, 0, &contents) {
warn!(
path = %full_path.display(),
error = ?e,
"projfs: shell.write after close-modified failed",
);
return mount_error_to_hresult(e);
}
if let Err(e) = shell.flush(node) {
warn!(
path = %full_path.display(),
error = ?e,
"projfs: shell.flush after close-modified failed",
);
return mount_error_to_hresult(e);
}
}
Err(e) => {
warn!(
path = %full_path.display(),
error = ?e,
"projfs: could not re-read hydrated file on close-modified",
);
return mount_error_to_hresult(MountError::Store(
objects::error::HeddleError::Io(e),
));
}
}
}
n if n == PRJ_NOTIFICATION_FILE_HANDLE_CLOSED_FILE_DELETED => {
if let Err(e) = shell.release(node) {
warn!(
path = %rel.display(),
error = ?e,
"projfs: shell.release after close-deleted failed",
);
}
}
n if n == PRJ_NOTIFICATION_FILE_RENAMED => {
if let Err(e) = shell.release(node) {
warn!(
path = %rel.display(),
error = ?e,
"projfs: rename old-path release failed",
);
return mount_error_to_hresult(e);
}
let dest = unsafe { decode_wide(destination_file_name) };
if !dest.is_empty() {
let dest_path = Path::new(&dest);
match resolve_path(shell, dest_path) {
Ok(new_node) => {
let full_path = virt_root.join(dest_path);
match std::fs::read(&full_path) {
Ok(contents) => {
if let Err(e) = shell.write(new_node, 0, &contents) {
warn!(
path = %full_path.display(),
error = ?e,
"projfs: rename new-path write failed",
);
return mount_error_to_hresult(e);
}
if let Err(e) = shell.flush(new_node) {
warn!(
path = %full_path.display(),
error = ?e,
"projfs: rename new-path flush failed",
);
return mount_error_to_hresult(e);
}
}
Err(e) => {
warn!(
path = %full_path.display(),
error = ?e,
"projfs: could not re-read hydrated file on rename",
);
return mount_error_to_hresult(MountError::Store(
objects::error::HeddleError::Io(e),
));
}
}
}
Err(e) => {
warn!(
dest = %dest_path.display(),
error = ?e,
"projfs: rename new-path resolve failed",
);
return mount_error_to_hresult(e);
}
}
}
}
_ => {}
}
S_OK
}
unsafe fn callback_data_or_log<'a>(
site: &str,
data: *const PRJ_CALLBACK_DATA,
) -> Option<&'a PRJ_CALLBACK_DATA> {
let opt = unsafe { callback_data(data) };
if opt.is_none() {
warn!(site, "projfs: null PRJ_CALLBACK_DATA");
}
opt
}
#[cfg(test)]
mod tests {
use std::sync::atomic::Ordering;
use super::*;
use crate::tests::mocks::CountingShell;
#[cfg(target_os = "windows")]
#[test]
#[ignore = "requires the projfs feature; opt-in via --features projfs"]
fn projfs_shell_does_not_leak_when_mount_skipped() {
let (counting, drops) = CountingShell::new();
let shell = Arc::new(counting);
let projfs = ProjFsShell::from_shell(shell);
assert_eq!(drops.load(Ordering::SeqCst), 0);
drop(projfs);
assert_eq!(drops.load(Ordering::SeqCst), 1);
}
#[cfg(target_os = "windows")]
#[test]
#[ignore = "requires the projfs feature; opt-in via --features projfs"]
fn is_runtime_available_does_not_panic() {
let _ = ProjFsShell::is_runtime_available();
}
#[cfg(target_os = "windows")]
#[test]
#[ignore = "requires the projfs feature; opt-in via --features projfs"]
fn primary_instance_id_sidecar_lives_outside_the_virtualization_root() {
let root = Path::new("C:\\users\\test\\.heddle-mounts\\thread-x");
let sidecar = instance_id_sidecar_path_primary(root);
assert!(
!sidecar.starts_with(root),
"primary sidecar must be outside virt root, got {}",
sidecar.display(),
);
assert!(
sidecar
.file_name()
.unwrap()
.to_string_lossy()
.contains("thread-x"),
"sidecar name must include the mount basename, got {}",
sidecar.display(),
);
}
#[cfg(target_os = "windows")]
#[test]
#[ignore = "requires the projfs feature; opt-in via --features projfs"]
fn fallback_instance_id_sidecar_lives_inside_the_virtualization_root() {
let root = Path::new("C:\\users\\test\\.heddle-mounts\\thread-x");
let sidecar = instance_id_sidecar_path_fallback(root);
assert!(
sidecar.starts_with(root),
"fallback sidecar must be inside virt root, got {}",
sidecar.display(),
);
assert_eq!(sidecar.file_name().unwrap(), ".heddle-projfs-id");
}
#[cfg(target_os = "windows")]
#[test]
#[ignore = "requires the projfs feature; opt-in via --features projfs"]
fn notification_mapping_root_is_non_null_empty_wide_string_heddle108() {
let mapping = entire_root_notification_mapping(PRJ_NOTIFY_TYPES(0));
assert!(
!mapping.NotificationRoot.0.is_null(),
"NotificationRoot must not be null — PrjStartVirtualizing \
STATUS_ACCESS_VIOLATIONs on null (heddle#108)",
);
let first = unsafe { *mapping.NotificationRoot.0 };
assert_eq!(
first, 0u16,
"NotificationRoot must point at a NUL terminator (= empty \
string, meaning 'the whole virtualization root')",
);
}
#[cfg(target_os = "windows")]
#[test]
#[ignore = "requires the projfs feature; opt-in via --features projfs"]
fn enum_key_round_trips_through_guid() {
let g = GUID::from_values(0x1234_5678, 0x9abc, 0xdef0, [1, 2, 3, 4, 5, 6, 7, 8]);
let a = EnumKey::from_guid(&g);
let b = EnumKey::from_guid(&g);
assert_eq!(a, b, "same GUID must produce equal EnumKey");
let g2 = GUID::from_values(0x1234_5678, 0x9abc, 0xdef0, [1, 2, 3, 4, 5, 6, 7, 9]);
assert_ne!(
a,
EnumKey::from_guid(&g2),
"different GUIDs must produce different EnumKeys",
);
}
}