1#![cfg_attr(windows, allow(unsafe_code))]
4
5use std::{error::Error, fmt};
6
7#[cfg(any(windows, test))]
8const ACCESS_ALLOWED_ACE_TYPE: u8 = 0;
9#[cfg(any(windows, test))]
10const ACL_REVISION: u8 = 2;
11#[cfg(any(windows, test))]
12const DIRECTORY_INHERIT_FLAGS: u8 = 0x01 | 0x02;
13#[cfg(any(windows, test))]
14const FILE_ACE_FLAGS: u8 = 0;
15#[cfg(any(windows, test))]
16const FILE_ALL_ACCESS_MASK: u32 = 0x001f_01ff;
17#[cfg(any(windows, test))]
18const ACL_HEADER_BYTES: usize = 8;
19#[cfg(any(windows, test))]
20const ACE_HEADER_BYTES: usize = 4;
21#[cfg(any(windows, test))]
22const ACCESS_ALLOWED_ACE_PREFIX_BYTES: usize = 8;
23#[cfg(any(windows, test))]
24const SID_HEADER_BYTES: usize = 8;
25#[cfg(any(windows, test))]
26const SID_REVISION: u8 = 1;
27#[cfg(any(windows, test))]
28const SID_MAX_SUB_AUTHORITIES: u8 = 15;
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32#[non_exhaustive]
33pub enum PrivateDirectoryOperation {
34 QueryProcessToken,
36 CreateWellKnownSid,
38 BuildAcl,
40 BuildSecurityDescriptor,
42 CreateDirectory,
44 CreateFile,
46 OpenFile,
48 OpenDirectory,
50 QueryAttributes,
52 ResolvePath,
54 EnumerateDirectory,
56 OpenTreeEntry,
58 QueryFileSystem,
60 FlushDirectory,
62 QuerySecurityDescriptor,
64 RemoveDirectory,
66 RemoveFile,
68 RenameFile,
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74#[non_exhaustive]
75pub enum PrivateFileLinkState {
76 Single,
78 PublicationPair,
80}
81
82impl PrivateFileLinkState {
83 #[cfg(windows)]
84 const fn expected_links(self) -> u32 {
85 match self {
86 Self::Single => 1,
87 Self::PublicationPair => 2,
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94#[non_exhaustive]
95pub enum PrivateDirectoryAclViolation {
96 Malformed,
98 WrongRevision,
100 WrongAceCount,
102 UnsupportedAceType,
104 UnexpectedAceFlags,
106 UnexpectedAccessMask,
108 InvalidSid,
110 UnknownPrincipal,
112 DuplicatePrincipal,
114 MissingPrincipal,
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120#[non_exhaustive]
121pub enum PrivateDirectoryErrorKind {
122 InvalidPath,
124 AlreadyExists,
126 WindowsApi(PrivateDirectoryOperation),
128 UnsupportedFileSystem,
130 NotDirectory,
132 NotRegularFile,
134 ReparsePoint,
136 MultipleLinks,
138 IdentityMismatch,
140 OwnerMissing,
142 OwnerMismatch,
144 DaclMissing,
146 DaclUnprotected,
148 DaclPolicy(PrivateDirectoryAclViolation),
150}
151
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154#[non_exhaustive]
155pub enum PrivateDirectoryCleanupStatus {
156 NotRequired,
158 Confirmed,
160 Uncertain,
162}
163
164#[derive(Debug)]
166pub struct PrivateDirectoryError {
167 kind: PrivateDirectoryErrorKind,
168 cleanup: PrivateDirectoryCleanupStatus,
169 os_code: Option<i32>,
170 cleanup_os_code: Option<i32>,
171}
172
173impl PrivateDirectoryError {
174 #[cfg(windows)]
175 const fn new(kind: PrivateDirectoryErrorKind, os_code: Option<i32>) -> Self {
176 Self {
177 kind,
178 cleanup: PrivateDirectoryCleanupStatus::NotRequired,
179 os_code,
180 cleanup_os_code: None,
181 }
182 }
183
184 pub const fn kind(&self) -> PrivateDirectoryErrorKind {
186 self.kind
187 }
188
189 pub const fn cleanup_status(&self) -> PrivateDirectoryCleanupStatus {
191 self.cleanup
192 }
193
194 pub const fn os_code(&self) -> Option<i32> {
196 self.os_code
197 }
198
199 pub const fn cleanup_os_code(&self) -> Option<i32> {
201 self.cleanup_os_code
202 }
203
204 #[cfg(windows)]
205 fn with_cleanup(
206 mut self,
207 cleanup: PrivateDirectoryCleanupStatus,
208 cleanup_os_code: Option<i32>,
209 ) -> Self {
210 self.cleanup = cleanup;
211 self.cleanup_os_code = cleanup_os_code;
212 self
213 }
214}
215
216impl fmt::Display for PrivateDirectoryError {
217 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218 write!(
219 formatter,
220 "private filesystem object rejected: {:?}",
221 self.kind
222 )?;
223 if let Some(code) = self.os_code {
224 write!(formatter, " (Windows error {code})")?;
225 }
226 if self.cleanup == PrivateDirectoryCleanupStatus::Uncertain {
227 write!(formatter, "; identity-bound cleanup is uncertain")?;
228 if let Some(code) = self.cleanup_os_code {
229 write!(formatter, " (Windows error {code})")?;
230 }
231 }
232 Ok(())
233 }
234}
235
236impl Error for PrivateDirectoryError {}
237
238#[cfg(windows)]
240#[derive(Clone, Copy, Debug, Eq, PartialEq)]
241#[non_exhaustive]
242pub enum PrivatePublicationPhase {
243 Unpublished,
245 CommitUncertain,
247}
248
249#[cfg(windows)]
255#[derive(Debug)]
256pub struct PrivatePublicationError {
257 phase: PrivatePublicationPhase,
258 file: std::fs::File,
259 error: PrivateDirectoryError,
260}
261
262#[cfg(windows)]
263impl PrivatePublicationError {
264 fn new(
265 phase: PrivatePublicationPhase,
266 file: std::fs::File,
267 error: PrivateDirectoryError,
268 ) -> Self {
269 Self { phase, file, error }
270 }
271
272 pub const fn phase(&self) -> PrivatePublicationPhase {
274 self.phase
275 }
276
277 pub const fn error(&self) -> &PrivateDirectoryError {
279 &self.error
280 }
281
282 pub const fn retained_file(&self) -> &std::fs::File {
284 &self.file
285 }
286
287 pub fn into_parts(
289 self,
290 ) -> (
291 PrivatePublicationPhase,
292 std::fs::File,
293 PrivateDirectoryError,
294 ) {
295 (self.phase, self.file, self.error)
296 }
297}
298
299#[cfg(windows)]
300impl fmt::Display for PrivatePublicationError {
301 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302 write!(
303 formatter,
304 "private file publication failed in phase {:?}: {}",
305 self.phase, self.error
306 )
307 }
308}
309
310#[cfg(windows)]
311impl Error for PrivatePublicationError {
312 fn source(&self) -> Option<&(dyn Error + 'static)> {
313 Some(&self.error)
314 }
315}
316
317#[cfg(windows)]
319#[derive(Clone, Copy, Debug, Eq, PartialEq)]
320#[non_exhaustive]
321pub enum PrivatePublicationPairPhase {
322 PairIntact,
324 FinalSingleSharedDelete,
326 FinalSingleSealed,
328}
329
330#[cfg(windows)]
332#[derive(Debug)]
333pub struct PrivatePublicationPairError {
334 phase: PrivatePublicationPairPhase,
335 file: std::fs::File,
336 error: PrivateDirectoryError,
337}
338
339#[cfg(windows)]
340impl PrivatePublicationPairError {
341 fn new(
342 phase: PrivatePublicationPairPhase,
343 file: std::fs::File,
344 error: PrivateDirectoryError,
345 ) -> Self {
346 Self { phase, file, error }
347 }
348
349 pub const fn phase(&self) -> PrivatePublicationPairPhase {
351 self.phase
352 }
353
354 pub const fn error(&self) -> &PrivateDirectoryError {
356 &self.error
357 }
358
359 pub const fn retained_file(&self) -> &std::fs::File {
361 &self.file
362 }
363
364 pub fn into_parts(
366 self,
367 ) -> (
368 PrivatePublicationPairPhase,
369 std::fs::File,
370 PrivateDirectoryError,
371 ) {
372 (self.phase, self.file, self.error)
373 }
374}
375
376#[cfg(windows)]
377impl fmt::Display for PrivatePublicationPairError {
378 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
379 write!(
380 formatter,
381 "private publication-pair recovery failed in phase {:?}: {}",
382 self.phase, self.error
383 )
384 }
385}
386
387#[cfg(windows)]
388impl Error for PrivatePublicationPairError {
389 fn source(&self) -> Option<&(dyn Error + 'static)> {
390 Some(&self.error)
391 }
392}
393
394#[cfg(test)]
395fn validate_acl(raw: &[u8], principals: &[&[u8]]) -> Result<(), PrivateDirectoryAclViolation> {
396 validate_acl_with_flags(raw, principals, DIRECTORY_INHERIT_FLAGS)
397}
398
399#[cfg(any(windows, test))]
400fn validate_acl_with_flags(
401 raw: &[u8],
402 principals: &[&[u8]],
403 expected_flags: u8,
404) -> Result<(), PrivateDirectoryAclViolation> {
405 if raw.len() < ACL_HEADER_BYTES {
406 return Err(PrivateDirectoryAclViolation::Malformed);
407 }
408 if raw[0] != ACL_REVISION {
409 return Err(PrivateDirectoryAclViolation::WrongRevision);
410 }
411 let declared_size = usize::from(u16::from_le_bytes([raw[2], raw[3]]));
412 if declared_size != raw.len() {
413 return Err(PrivateDirectoryAclViolation::Malformed);
414 }
415 let ace_count = usize::from(u16::from_le_bytes([raw[4], raw[5]]));
416 if ace_count != principals.len() {
417 return Err(PrivateDirectoryAclViolation::WrongAceCount);
418 }
419
420 let mut found = vec![false; principals.len()];
421 let mut offset = ACL_HEADER_BYTES;
422 for _ in 0..ace_count {
423 let header = raw
424 .get(offset..offset + ACE_HEADER_BYTES)
425 .ok_or(PrivateDirectoryAclViolation::Malformed)?;
426 if header[0] != ACCESS_ALLOWED_ACE_TYPE {
427 return Err(PrivateDirectoryAclViolation::UnsupportedAceType);
428 }
429 if header[1] != expected_flags {
430 return Err(PrivateDirectoryAclViolation::UnexpectedAceFlags);
431 }
432 let ace_size = usize::from(u16::from_le_bytes([header[2], header[3]]));
433 if ace_size < ACCESS_ALLOWED_ACE_PREFIX_BYTES + SID_HEADER_BYTES || ace_size % 4 != 0 {
434 return Err(PrivateDirectoryAclViolation::Malformed);
435 }
436 let end = offset
437 .checked_add(ace_size)
438 .filter(|end| *end <= raw.len())
439 .ok_or(PrivateDirectoryAclViolation::Malformed)?;
440 let mask = u32::from_le_bytes(
441 raw[offset + ACE_HEADER_BYTES..offset + ACCESS_ALLOWED_ACE_PREFIX_BYTES]
442 .try_into()
443 .map_err(|_| PrivateDirectoryAclViolation::Malformed)?,
444 );
445 if mask != FILE_ALL_ACCESS_MASK {
446 return Err(PrivateDirectoryAclViolation::UnexpectedAccessMask);
447 }
448 let sid = &raw[offset + ACCESS_ALLOWED_ACE_PREFIX_BYTES..end];
449 if !valid_sid_bytes(sid) {
450 return Err(PrivateDirectoryAclViolation::InvalidSid);
451 }
452 let principal = principals
453 .iter()
454 .position(|expected| *expected == sid)
455 .ok_or(PrivateDirectoryAclViolation::UnknownPrincipal)?;
456 if found[principal] {
457 return Err(PrivateDirectoryAclViolation::DuplicatePrincipal);
458 }
459 found[principal] = true;
460 offset = end;
461 }
462 if !raw[offset..].iter().all(|byte| *byte == 0) {
463 return Err(PrivateDirectoryAclViolation::Malformed);
464 }
465 if found.iter().all(|present| *present) {
466 Ok(())
467 } else {
468 Err(PrivateDirectoryAclViolation::MissingPrincipal)
469 }
470}
471
472#[cfg(any(windows, test))]
473fn valid_sid_bytes(sid: &[u8]) -> bool {
474 let Some(header) = sid.get(..SID_HEADER_BYTES) else {
475 return false;
476 };
477 let sub_authorities = header[1];
478 if header[0] != SID_REVISION || sub_authorities > SID_MAX_SUB_AUTHORITIES {
479 return false;
480 }
481 SID_HEADER_BYTES
482 .checked_add(usize::from(sub_authorities) * size_of::<u32>())
483 .is_some_and(|expected| expected == sid.len())
484}
485
486#[cfg(windows)]
487mod platform {
488 use std::{
489 collections::BTreeSet,
490 ffi::c_void,
491 ffi::{OsStr, OsString},
492 fs::File,
493 io,
494 mem::{offset_of, size_of, size_of_val},
495 os::windows::{
496 ffi::{OsStrExt as _, OsStringExt as _},
497 io::{AsRawHandle as _, BorrowedHandle, FromRawHandle as _, OwnedHandle},
498 },
499 path::{Component, Path, PathBuf},
500 ptr, slice,
501 };
502
503 use windows_sys::Wdk::Storage::FileSystem::{
504 FILE_RENAME_INFORMATION, FileRenameInformation, NtFlushBuffersFileEx, NtSetInformationFile,
505 };
506 use windows_sys::Win32::{
507 Foundation::{
508 ERROR_ALREADY_EXISTS, ERROR_FILE_EXISTS, ERROR_INSUFFICIENT_BUFFER,
509 ERROR_NO_MORE_FILES, HANDLE, INVALID_HANDLE_VALUE, RtlNtStatusToDosError,
510 },
511 Security::{
512 ACCESS_ALLOWED_ACE, ACL, ACL_REVISION, AddAccessAllowedAceEx, CopySid,
513 CreateWellKnownSid, DACL_SECURITY_INFORMATION, GetKernelObjectSecurity, GetLengthSid,
514 GetSecurityDescriptorControl, GetSecurityDescriptorDacl, GetSecurityDescriptorOwner,
515 GetTokenInformation, InitializeAcl, InitializeSecurityDescriptor,
516 IsValidSecurityDescriptor, IsValidSid, OWNER_SECURITY_INFORMATION, PSID,
517 SE_DACL_PROTECTED, SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR, SECURITY_MAX_SID_SIZE,
518 SetSecurityDescriptorControl, SetSecurityDescriptorDacl, SetSecurityDescriptorOwner,
519 TOKEN_QUERY, TOKEN_USER, TokenUser, WinBuiltinAdministratorsSid, WinLocalSystemSid,
520 },
521 Storage::FileSystem::{
522 BY_HANDLE_FILE_INFORMATION, CREATE_NEW, CreateDirectoryW, CreateFileW, DELETE,
523 FILE_ALL_ACCESS, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL,
524 FILE_ATTRIBUTE_REPARSE_POINT, FILE_DISPOSITION_FLAG_DELETE,
525 FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO, FILE_DISPOSITION_INFO_EX,
526 FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_READ,
527 FILE_GENERIC_WRITE, FILE_ID_EXTD_DIR_INFO, FILE_ID_INFO, FILE_SHARE_DELETE,
528 FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_WRITE_DATA, FileDispositionInfo,
529 FileDispositionInfoEx, FileIdExtdDirectoryInfo, FileIdExtdDirectoryRestartInfo,
530 FileIdInfo, GetFileInformationByHandle, GetFileInformationByHandleEx,
531 GetFinalPathNameByHandleW, GetVolumeInformationByHandleW, OPEN_EXISTING, ReOpenFile,
532 SetFileInformationByHandle, WRITE_DAC,
533 },
534 System::{
535 IO::IO_STATUS_BLOCK,
536 SystemServices::{FILE_PERSISTENT_ACLS, SECURITY_DESCRIPTOR_REVISION},
537 Threading::{GetCurrentProcess, OpenProcessToken},
538 },
539 };
540
541 use super::{
542 DIRECTORY_INHERIT_FLAGS, FILE_ACE_FLAGS, PrivateDirectoryAclViolation,
543 PrivateDirectoryCleanupStatus, PrivateDirectoryError, PrivateDirectoryErrorKind,
544 PrivateDirectoryOperation, PrivateFileLinkState, PrivatePublicationError,
545 PrivatePublicationPairError, PrivatePublicationPairPhase, PrivatePublicationPhase,
546 SID_HEADER_BYTES, valid_sid_bytes, validate_acl_with_flags,
547 };
548
549 const MAX_SECURITY_DESCRIPTOR_BYTES: usize = 128 * 1024;
550 const DIRECTORY_ENUMERATION_BUFFER_BYTES: usize = 64 * 1024;
551
552 #[derive(Debug, Eq, PartialEq)]
553 struct DirectoryEntryIdentity {
554 name: OsString,
555 file_id: [u8; 16],
556 }
557
558 enum BoundTreeEntry {
559 Directory { handle: File, path: PathBuf },
560 RegularFile(File),
561 }
562
563 #[derive(Clone, Copy)]
564 enum PrivateObjectKind {
565 Directory,
566 RegularFile(PrivateFileLinkState),
567 }
568
569 impl PrivateObjectKind {
570 const fn ace_flags(self) -> u8 {
571 match self {
572 Self::Directory => DIRECTORY_INHERIT_FLAGS,
573 Self::RegularFile(_) => FILE_ACE_FLAGS,
574 }
575 }
576
577 const fn remove_operation(self) -> PrivateDirectoryOperation {
578 match self {
579 Self::Directory => PrivateDirectoryOperation::RemoveDirectory,
580 Self::RegularFile(_) => PrivateDirectoryOperation::RemoveFile,
581 }
582 }
583 }
584
585 pub(super) fn create(path: &Path) -> Result<File, PrivateDirectoryError> {
586 let wide_path = wide_path(path)?;
587 let ((), user) =
588 with_private_security_attributes(PrivateObjectKind::Directory, |attributes| {
589 let created = unsafe { CreateDirectoryW(wide_path.as_ptr(), attributes) };
592 if created == 0 {
593 let code = last_os_code();
594 return Err(PrivateDirectoryError::new(
595 if is_win32_error(code, ERROR_ALREADY_EXISTS) {
596 PrivateDirectoryErrorKind::AlreadyExists
597 } else {
598 PrivateDirectoryErrorKind::WindowsApi(
599 PrivateDirectoryOperation::CreateDirectory,
600 )
601 },
602 code,
603 ));
604 }
605 Ok(())
606 })?;
607
608 let directory = match open_directory(&wide_path, false) {
609 Ok(directory) => directory,
610 Err(error) => {
611 return Err(error.with_cleanup(PrivateDirectoryCleanupStatus::Uncertain, None));
614 }
615 };
616 if let Err(error) = verify_with_user(
617 directory.as_raw_handle(),
618 &user,
619 PrivateObjectKind::Directory,
620 ) {
621 let cleanup = remove_verified_private(
622 directory.as_raw_handle(),
623 &user,
624 PrivateObjectKind::Directory,
625 );
626 return Err(match cleanup {
627 Ok(()) => error.with_cleanup(PrivateDirectoryCleanupStatus::Confirmed, None),
628 Err(cleanup) => {
629 error.with_cleanup(PrivateDirectoryCleanupStatus::Uncertain, cleanup.os_code())
630 }
631 });
632 }
633 Ok(directory)
634 }
635
636 pub(super) fn open(path: &Path) -> Result<File, PrivateDirectoryError> {
637 let wide_path = wide_path(path)?;
638 let directory = open_directory(&wide_path, false)?;
639 let user = current_process_user_sid()?;
640 verify_with_user(
641 directory.as_raw_handle(),
642 &user,
643 PrivateObjectKind::Directory,
644 )?;
645 Ok(directory)
646 }
647
648 pub(super) fn open_read_guard(path: &Path) -> Result<File, PrivateDirectoryError> {
649 let wide_path = wide_path(path)?;
650 let directory = open_directory_read_guard(&wide_path)?;
651 let user = current_process_user_sid()?;
652 verify_with_user(
653 directory.as_raw_handle(),
654 &user,
655 PrivateObjectKind::Directory,
656 )?;
657 Ok(directory)
658 }
659
660 pub(super) fn create_file(
661 path: &Path,
662 share_delete: bool,
663 ) -> Result<File, PrivateDirectoryError> {
664 let wide_path = wide_path(path)?;
665 let share_mode = FILE_SHARE_READ | if share_delete { FILE_SHARE_DELETE } else { 0 };
666 let (file, user) = with_private_security_attributes(
667 PrivateObjectKind::RegularFile(PrivateFileLinkState::Single),
668 |attributes| {
669 let handle = unsafe {
672 CreateFileW(
673 wide_path.as_ptr(),
674 FILE_GENERIC_READ | FILE_GENERIC_WRITE | DELETE,
675 share_mode,
676 attributes,
677 CREATE_NEW,
678 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
679 ptr::null_mut(),
680 )
681 };
682 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
683 let code = last_os_code();
684 return Err(PrivateDirectoryError::new(
685 if is_win32_error(code, ERROR_ALREADY_EXISTS)
686 || is_win32_error(code, ERROR_FILE_EXISTS)
687 {
688 PrivateDirectoryErrorKind::AlreadyExists
689 } else {
690 PrivateDirectoryErrorKind::WindowsApi(
691 PrivateDirectoryOperation::CreateFile,
692 )
693 },
694 code,
695 ));
696 }
697 Ok(unsafe { File::from_raw_handle(handle) })
699 },
700 )?;
701 let kind = PrivateObjectKind::RegularFile(PrivateFileLinkState::Single);
702 if let Err(error) = verify_with_user(file.as_raw_handle(), &user, kind) {
703 let cleanup = remove_verified_private(file.as_raw_handle(), &user, kind);
704 return Err(match cleanup {
705 Ok(()) => error.with_cleanup(PrivateDirectoryCleanupStatus::Confirmed, None),
706 Err(cleanup) => {
707 error.with_cleanup(PrivateDirectoryCleanupStatus::Uncertain, cleanup.os_code())
708 }
709 });
710 }
711 Ok(file)
712 }
713
714 pub(super) fn create_lock_file(path: &Path) -> Result<File, PrivateDirectoryError> {
715 create_lock_file_with_callback(path, || {})
716 }
717
718 fn create_lock_file_with_callback(
719 path: &Path,
720 after_create: impl FnOnce(),
721 ) -> Result<File, PrivateDirectoryError> {
722 let wide_path = wide_path(path)?;
723 let (file, user) = with_private_security_attributes(
724 PrivateObjectKind::RegularFile(PrivateFileLinkState::Single),
725 |attributes| {
726 let handle = unsafe {
729 CreateFileW(
730 wide_path.as_ptr(),
731 FILE_GENERIC_READ | FILE_GENERIC_WRITE,
732 FILE_SHARE_READ | FILE_SHARE_WRITE,
733 attributes,
734 CREATE_NEW,
735 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
736 ptr::null_mut(),
737 )
738 };
739 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
740 let code = last_os_code();
741 return Err(PrivateDirectoryError::new(
742 if is_win32_error(code, ERROR_ALREADY_EXISTS)
743 || is_win32_error(code, ERROR_FILE_EXISTS)
744 {
745 PrivateDirectoryErrorKind::AlreadyExists
746 } else {
747 PrivateDirectoryErrorKind::WindowsApi(
748 PrivateDirectoryOperation::CreateFile,
749 )
750 },
751 code,
752 ));
753 }
754 Ok(unsafe { File::from_raw_handle(handle) })
756 },
757 )?;
758 after_create();
759 let kind = PrivateObjectKind::RegularFile(PrivateFileLinkState::Single);
760 if let Err(error) = verify_with_user(file.as_raw_handle(), &user, kind) {
761 drop(file);
762 return Err(error.with_cleanup(PrivateDirectoryCleanupStatus::Uncertain, None));
763 }
764 Ok(file)
765 }
766
767 pub(super) fn open_lock_file(path: &Path) -> Result<File, PrivateDirectoryError> {
768 let wide_path = wide_path(path)?;
769 let file = open_regular_file_with_access(
770 &wide_path,
771 FILE_GENERIC_READ | FILE_GENERIC_WRITE,
772 FILE_SHARE_READ | FILE_SHARE_WRITE,
773 )?;
774 let user = current_process_user_sid()?;
775 verify_with_user(
776 file.as_raw_handle(),
777 &user,
778 PrivateObjectKind::RegularFile(PrivateFileLinkState::Single),
779 )?;
780 Ok(file)
781 }
782
783 pub(super) fn publish_file_create_new(
784 file: File,
785 destination_directory: BorrowedHandle<'_>,
786 destination_name: &OsStr,
787 ) -> Result<File, PrivatePublicationError> {
788 publish_file_create_new_with_callback(file, destination_directory, destination_name, || {})
789 }
790
791 fn publish_file_create_new_with_callback(
792 file: File,
793 destination_directory: BorrowedHandle<'_>,
794 destination_name: &OsStr,
795 after_rename: impl FnOnce(),
796 ) -> Result<File, PrivatePublicationError> {
797 let encoded_name = match private_destination_name(destination_name) {
798 Ok(name) => name,
799 Err(error) => {
800 return Err(PrivatePublicationError::new(
801 PrivatePublicationPhase::Unpublished,
802 file,
803 error,
804 ));
805 }
806 };
807 let user = match current_process_user_sid() {
808 Ok(user) => user,
809 Err(error) => {
810 return Err(PrivatePublicationError::new(
811 PrivatePublicationPhase::Unpublished,
812 file,
813 error,
814 ));
815 }
816 };
817 if let Err(error) = verify_with_user(
818 destination_directory.as_raw_handle(),
819 &user,
820 PrivateObjectKind::Directory,
821 ) {
822 return Err(PrivatePublicationError::new(
823 PrivatePublicationPhase::Unpublished,
824 file,
825 error,
826 ));
827 }
828 let file_kind = PrivateObjectKind::RegularFile(PrivateFileLinkState::Single);
829 if let Err(error) = verify_with_user(file.as_raw_handle(), &user, file_kind) {
830 return Err(PrivatePublicationError::new(
831 PrivatePublicationPhase::Unpublished,
832 file,
833 error,
834 ));
835 }
836 if let Err(error) = verify_directory_contains_file_identity(
837 destination_directory.as_raw_handle(),
838 file.as_raw_handle(),
839 ) {
840 return Err(PrivatePublicationError::new(
841 PrivatePublicationPhase::Unpublished,
842 file,
843 error,
844 ));
845 }
846 if let Err(error) = rename_file_handle_create_new(
847 file.as_raw_handle(),
848 destination_directory.as_raw_handle(),
849 &encoded_name,
850 ) {
851 return Err(PrivatePublicationError::new(
852 PrivatePublicationPhase::Unpublished,
853 file,
854 error,
855 ));
856 }
857
858 after_rename();
859 let postcondition = (|| {
860 verify_with_user(
861 destination_directory.as_raw_handle(),
862 &user,
863 PrivateObjectKind::Directory,
864 )?;
865 verify_with_user(file.as_raw_handle(), &user, file_kind)?;
866 verify_directory_entry_identity(
867 destination_directory.as_raw_handle(),
868 destination_name,
869 file.as_raw_handle(),
870 )
871 })();
872 if let Err(error) = postcondition {
873 return Err(PrivatePublicationError::new(
874 PrivatePublicationPhase::CommitUncertain,
875 file,
876 error,
877 ));
878 }
879 Ok(file)
880 }
881
882 pub(super) fn complete_publication_pair(
883 staging: File,
884 destination_directory: BorrowedHandle<'_>,
885 destination_name: &OsStr,
886 ) -> Result<File, PrivatePublicationPairError> {
887 complete_publication_pair_with_callback(
888 staging,
889 destination_directory,
890 destination_name,
891 || {},
892 )
893 }
894
895 fn complete_publication_pair_with_callback(
896 staging: File,
897 destination_directory: BorrowedHandle<'_>,
898 destination_name: &OsStr,
899 after_staging_unlink: impl FnOnce(),
900 ) -> Result<File, PrivatePublicationPairError> {
901 let user = match current_process_user_sid() {
902 Ok(user) => user,
903 Err(error) => {
904 return Err(PrivatePublicationPairError::new(
905 PrivatePublicationPairPhase::PairIntact,
906 staging,
907 error,
908 ));
909 }
910 };
911 let final_transient = match open_bound_publication_pair_final(
912 &staging,
913 destination_directory,
914 destination_name,
915 &user,
916 ) {
917 Ok(file) => file,
918 Err(error) => {
919 return Err(PrivatePublicationPairError::new(
920 PrivatePublicationPairPhase::PairIntact,
921 staging,
922 error,
923 ));
924 }
925 };
926 if let Err(error) = remove_link_by_handle_posix(staging.as_raw_handle()) {
927 drop(final_transient);
928 return Err(PrivatePublicationPairError::new(
929 PrivatePublicationPairPhase::PairIntact,
930 staging,
931 error,
932 ));
933 }
934 drop(staging);
935
936 after_staging_unlink();
937 let sealed = match reopen_private_file(
938 final_transient.as_raw_handle(),
939 FILE_GENERIC_READ | DELETE,
940 FILE_SHARE_READ,
941 ) {
942 Ok(file) => file,
943 Err(error) => {
944 return Err(PrivatePublicationPairError::new(
945 PrivatePublicationPairPhase::FinalSingleSharedDelete,
946 final_transient,
947 error,
948 ));
949 }
950 };
951 drop(final_transient);
952
953 if let Err(error) =
954 verify_completed_publication(&sealed, destination_directory, destination_name, &user)
955 {
956 return Err(PrivatePublicationPairError::new(
957 PrivatePublicationPairPhase::FinalSingleSealed,
958 sealed,
959 error,
960 ));
961 }
962 Ok(sealed)
963 }
964
965 fn open_bound_publication_pair_final(
966 staging: &File,
967 destination_directory: BorrowedHandle<'_>,
968 destination_name: &OsStr,
969 user: &Sid,
970 ) -> Result<File, PrivateDirectoryError> {
971 private_destination_name(destination_name)?;
972 let pair_kind = PrivateObjectKind::RegularFile(PrivateFileLinkState::PublicationPair);
973 verify_with_user(
974 destination_directory.as_raw_handle(),
975 user,
976 PrivateObjectKind::Directory,
977 )?;
978 verify_with_user(staging.as_raw_handle(), user, pair_kind)?;
979 verify_directory_entry_identity(
980 destination_directory.as_raw_handle(),
981 destination_name,
982 staging.as_raw_handle(),
983 )?;
984 let destination_path =
985 final_path_from_handle(destination_directory.as_raw_handle())?.join(destination_name);
986 let destination_path = wide_path(&destination_path)?;
987 let final_file = open_regular_file_with_access(
988 &destination_path,
989 FILE_GENERIC_READ,
990 FILE_SHARE_READ | FILE_SHARE_DELETE,
991 )?;
992 verify_with_user(final_file.as_raw_handle(), user, pair_kind)?;
993 verify_same_file(staging.as_raw_handle(), final_file.as_raw_handle())?;
994 verify_directory_entry_identity(
995 destination_directory.as_raw_handle(),
996 destination_name,
997 final_file.as_raw_handle(),
998 )?;
999 verify_with_user(staging.as_raw_handle(), user, pair_kind)?;
1000 Ok(final_file)
1001 }
1002
1003 fn verify_completed_publication(
1004 final_file: &File,
1005 destination_directory: BorrowedHandle<'_>,
1006 destination_name: &OsStr,
1007 user: &Sid,
1008 ) -> Result<(), PrivateDirectoryError> {
1009 verify_with_user(
1010 final_file.as_raw_handle(),
1011 user,
1012 PrivateObjectKind::RegularFile(PrivateFileLinkState::Single),
1013 )?;
1014 verify_with_user(
1015 destination_directory.as_raw_handle(),
1016 user,
1017 PrivateObjectKind::Directory,
1018 )?;
1019 verify_directory_entry_identity(
1020 destination_directory.as_raw_handle(),
1021 destination_name,
1022 final_file.as_raw_handle(),
1023 )
1024 }
1025
1026 pub(super) fn seal_staging_file(file: File) -> Result<File, PrivateDirectoryError> {
1027 seal_staging_file_with_transition(file, || {})
1028 }
1029
1030 fn seal_staging_file_with_transition(
1031 file: File,
1032 after_writer_drop: impl FnOnce(),
1033 ) -> Result<File, PrivateDirectoryError> {
1034 let kind = PrivateObjectKind::RegularFile(PrivateFileLinkState::Single);
1035 let user = match current_process_user_sid() {
1036 Ok(user) => user,
1037 Err(error) => {
1038 drop(file);
1039 return Err(error.with_cleanup(PrivateDirectoryCleanupStatus::Uncertain, None));
1040 }
1041 };
1042 if let Err(error) = verify_with_user(file.as_raw_handle(), &user, kind) {
1043 let cleanup = remove_verified_private(file.as_raw_handle(), &user, kind);
1044 drop(file);
1045 return Err(error_after_cleanup(error, cleanup));
1046 }
1047 let transient = match reopen_private_staging_file(
1048 file.as_raw_handle(),
1049 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
1050 ) {
1051 Ok(transient) => transient,
1052 Err(error) => {
1053 let cleanup = remove_verified_private(file.as_raw_handle(), &user, kind);
1054 drop(file);
1055 return Err(error_after_cleanup(error, cleanup));
1056 }
1057 };
1058 drop(file);
1059 after_writer_drop();
1060
1061 let sealed = match reopen_private_staging_file(
1062 transient.as_raw_handle(),
1063 FILE_SHARE_READ | FILE_SHARE_DELETE,
1064 ) {
1065 Ok(sealed) => sealed,
1066 Err(error) => {
1067 let cleanup = remove_verified_private(transient.as_raw_handle(), &user, kind);
1068 drop(transient);
1069 return Err(error_after_cleanup(error, cleanup));
1070 }
1071 };
1072 drop(transient);
1073 if let Err(error) = verify_with_user(sealed.as_raw_handle(), &user, kind) {
1074 let cleanup = remove_verified_private(sealed.as_raw_handle(), &user, kind);
1075 drop(sealed);
1076 return Err(error_after_cleanup(error, cleanup));
1077 }
1078 Ok(sealed)
1079 }
1080
1081 pub(super) fn verify(handle: BorrowedHandle<'_>) -> Result<(), PrivateDirectoryError> {
1082 let user = current_process_user_sid()?;
1083 verify_with_user(handle.as_raw_handle(), &user, PrivateObjectKind::Directory)
1084 }
1085
1086 pub(super) fn sync_directory(
1087 directory: BorrowedHandle<'_>,
1088 ) -> Result<(), PrivateDirectoryError> {
1089 let user = current_process_user_sid()?;
1090 verify_with_user(
1091 directory.as_raw_handle(),
1092 &user,
1093 PrivateObjectKind::Directory,
1094 )?;
1095
1096 let mut io_status = IO_STATUS_BLOCK::default();
1097 let status = unsafe {
1101 NtFlushBuffersFileEx(
1102 directory.as_raw_handle(),
1103 0,
1104 ptr::null(),
1105 0,
1106 &raw mut io_status,
1107 )
1108 };
1109 if status != 0 {
1110 let code = i32::try_from(unsafe { RtlNtStatusToDosError(status) }).ok();
1112 return Err(operation_error(
1113 PrivateDirectoryOperation::FlushDirectory,
1114 code,
1115 ));
1116 }
1117
1118 verify_with_user(
1119 directory.as_raw_handle(),
1120 &user,
1121 PrivateObjectKind::Directory,
1122 )
1123 }
1124
1125 pub(super) fn open_file(path: &Path) -> Result<File, PrivateDirectoryError> {
1126 let wide_path = wide_path(path)?;
1127 let file = open_regular_file(&wide_path)?;
1128 let user = current_process_user_sid()?;
1129 verify_with_user(
1130 file.as_raw_handle(),
1131 &user,
1132 PrivateObjectKind::RegularFile(PrivateFileLinkState::Single),
1133 )?;
1134 Ok(file)
1135 }
1136
1137 pub(super) fn open_file_for_sync(path: &Path) -> Result<File, PrivateDirectoryError> {
1138 let wide_path = wide_path(path)?;
1139 let file = open_regular_file_for_sync(&wide_path)?;
1140 let user = current_process_user_sid()?;
1141 verify_with_user(
1142 file.as_raw_handle(),
1143 &user,
1144 PrivateObjectKind::RegularFile(PrivateFileLinkState::Single),
1145 )?;
1146 Ok(file)
1147 }
1148
1149 pub(super) fn open_file_for_removal(path: &Path) -> Result<File, PrivateDirectoryError> {
1150 open_file_for_removal_in_state(path, PrivateFileLinkState::Single)
1151 }
1152
1153 pub(super) fn open_file_for_removal_in_state(
1154 path: &Path,
1155 link_state: PrivateFileLinkState,
1156 ) -> Result<File, PrivateDirectoryError> {
1157 let wide_path = wide_path(path)?;
1158 let file = open_regular_file_for_removal(&wide_path)?;
1159 let user = current_process_user_sid()?;
1160 verify_with_user(
1161 file.as_raw_handle(),
1162 &user,
1163 PrivateObjectKind::RegularFile(link_state),
1164 )?;
1165 Ok(file)
1166 }
1167
1168 pub(super) fn verify_file(
1169 handle: BorrowedHandle<'_>,
1170 link_state: PrivateFileLinkState,
1171 ) -> Result<(), PrivateDirectoryError> {
1172 let user = current_process_user_sid()?;
1173 verify_with_user(
1174 handle.as_raw_handle(),
1175 &user,
1176 PrivateObjectKind::RegularFile(link_state),
1177 )
1178 }
1179
1180 pub(super) fn regular_file_link_count(
1181 handle: BorrowedHandle<'_>,
1182 ) -> Result<u32, PrivateDirectoryError> {
1183 let information = query_attributes(handle.as_raw_handle())?;
1184 verify_regular_file_attributes(&information)?;
1185 Ok(information.nNumberOfLinks)
1186 }
1187
1188 pub(super) fn remove(directory: File) -> Result<(), PrivateDirectoryError> {
1189 remove_private_object(directory, PrivateObjectKind::Directory)
1190 }
1191
1192 pub(super) fn remove_file(
1193 file: File,
1194 link_state: PrivateFileLinkState,
1195 ) -> Result<(), PrivateDirectoryError> {
1196 remove_private_object(file, PrivateObjectKind::RegularFile(link_state))
1197 }
1198
1199 pub(super) fn remove_directory_tree(directory: File) -> Result<(), PrivateDirectoryError> {
1200 remove_directory_tree_with_callback(directory, |_| {})
1201 }
1202
1203 fn remove_directory_tree_with_callback(
1204 directory: File,
1205 before_removal: impl FnOnce(&Path),
1206 ) -> Result<(), PrivateDirectoryError> {
1207 let result = (|| {
1208 let user = current_process_user_sid()?;
1209 verify_with_user(
1210 directory.as_raw_handle(),
1211 &user,
1212 PrivateObjectKind::Directory,
1213 )?;
1214 let path = final_path_from_handle(directory.as_raw_handle())?;
1215 let children = bind_directory_children(&directory, &path)?;
1216 before_removal(&path);
1217 remove_bound_directory(directory, children)
1218 })();
1219 result.map_err(|error| {
1220 let code = error.os_code();
1221 error.with_cleanup(PrivateDirectoryCleanupStatus::Uncertain, code)
1222 })
1223 }
1224
1225 fn bind_directory_children(
1226 directory: &File,
1227 path: &Path,
1228 ) -> Result<Vec<BoundTreeEntry>, PrivateDirectoryError> {
1229 let entries = enumerate_directory(directory.as_raw_handle())?;
1230 let mut children = Vec::with_capacity(entries.len());
1231 for entry in entries {
1232 let child_path = path.join(&entry.name);
1233 let child = open_tree_entry(&child_path)?;
1234 let identity = query_file_identity(child.as_raw_handle())?;
1235 if identity.FileId.Identifier != entry.file_id {
1236 return Err(PrivateDirectoryError::new(
1237 PrivateDirectoryErrorKind::IdentityMismatch,
1238 None,
1239 ));
1240 }
1241 let information = query_attributes(child.as_raw_handle())?;
1242 if information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1243 return Err(PrivateDirectoryError::new(
1244 PrivateDirectoryErrorKind::ReparsePoint,
1245 None,
1246 ));
1247 }
1248 if information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0 {
1249 let child_path = final_path_from_handle(child.as_raw_handle())?;
1250 children.push(BoundTreeEntry::Directory {
1251 handle: child,
1252 path: child_path,
1253 });
1254 } else {
1255 verify_regular_file_attributes(&information)?;
1256 if information.nNumberOfLinks != 1 {
1257 return Err(PrivateDirectoryError::new(
1258 PrivateDirectoryErrorKind::MultipleLinks,
1259 None,
1260 ));
1261 }
1262 children.push(BoundTreeEntry::RegularFile(child));
1263 }
1264 }
1265 Ok(children)
1266 }
1267
1268 fn remove_bound_directory(
1269 directory: File,
1270 children: Vec<BoundTreeEntry>,
1271 ) -> Result<(), PrivateDirectoryError> {
1272 for child in children {
1273 match child {
1274 BoundTreeEntry::Directory { handle, path } => {
1275 let information = query_attributes(handle.as_raw_handle())?;
1276 if information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1277 return Err(PrivateDirectoryError::new(
1278 PrivateDirectoryErrorKind::ReparsePoint,
1279 None,
1280 ));
1281 }
1282 if information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
1283 return Err(PrivateDirectoryError::new(
1284 PrivateDirectoryErrorKind::NotDirectory,
1285 None,
1286 ));
1287 }
1288 let grandchildren = bind_directory_children(&handle, &path)?;
1289 remove_bound_directory(handle, grandchildren)?;
1290 }
1291 BoundTreeEntry::RegularFile(file) => {
1292 let information = query_attributes(file.as_raw_handle())?;
1293 verify_regular_file_attributes(&information)?;
1294 if information.nNumberOfLinks != 1 {
1295 return Err(PrivateDirectoryError::new(
1296 PrivateDirectoryErrorKind::MultipleLinks,
1297 None,
1298 ));
1299 }
1300 remove_by_handle(
1301 file.as_raw_handle(),
1302 PrivateObjectKind::RegularFile(PrivateFileLinkState::Single),
1303 )?;
1304 drop(file);
1305 }
1306 }
1307 }
1308 let information = query_attributes(directory.as_raw_handle())?;
1309 if information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1310 return Err(PrivateDirectoryError::new(
1311 PrivateDirectoryErrorKind::ReparsePoint,
1312 None,
1313 ));
1314 }
1315 if information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
1316 return Err(PrivateDirectoryError::new(
1317 PrivateDirectoryErrorKind::NotDirectory,
1318 None,
1319 ));
1320 }
1321 remove_by_handle(directory.as_raw_handle(), PrivateObjectKind::Directory)?;
1322 drop(directory);
1323 Ok(())
1324 }
1325
1326 fn remove_private_object(
1327 object: File,
1328 kind: PrivateObjectKind,
1329 ) -> Result<(), PrivateDirectoryError> {
1330 let cleanup = current_process_user_sid().and_then(|user| {
1331 verify_with_user(object.as_raw_handle(), &user, kind)?;
1332 remove_by_handle(object.as_raw_handle(), kind)
1333 });
1334 match cleanup {
1335 Ok(()) => {
1336 drop(object);
1337 Ok(())
1338 }
1339 Err(error) => {
1340 let code = error.os_code();
1341 Err(error.with_cleanup(PrivateDirectoryCleanupStatus::Uncertain, code))
1342 }
1343 }
1344 }
1345
1346 fn verify_with_user(
1347 handle: HANDLE,
1348 user: &Sid,
1349 kind: PrivateObjectKind,
1350 ) -> Result<(), PrivateDirectoryError> {
1351 verify_object_policy(handle, user, kind)?;
1352 verify_filesystem(handle)
1353 }
1354
1355 fn verify_object_policy(
1356 handle: HANDLE,
1357 user: &Sid,
1358 kind: PrivateObjectKind,
1359 ) -> Result<(), PrivateDirectoryError> {
1360 verify_attributes(handle, kind)?;
1361 let descriptor = read_security_descriptor(handle)?;
1362 verify_security_descriptor(&descriptor, user, kind)
1363 }
1364
1365 fn remove_verified_private(
1366 handle: HANDLE,
1367 user: &Sid,
1368 kind: PrivateObjectKind,
1369 ) -> Result<(), PrivateDirectoryError> {
1370 verify_object_policy(handle, user, kind)?;
1371 remove_by_handle(handle, kind)
1372 }
1373
1374 fn verify_attributes(
1375 handle: HANDLE,
1376 kind: PrivateObjectKind,
1377 ) -> Result<(), PrivateDirectoryError> {
1378 let information = query_attributes(handle)?;
1379 if information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1380 return Err(PrivateDirectoryError::new(
1381 PrivateDirectoryErrorKind::ReparsePoint,
1382 None,
1383 ));
1384 }
1385 match kind {
1386 PrivateObjectKind::Directory
1387 if information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0 =>
1388 {
1389 return Err(PrivateDirectoryError::new(
1390 PrivateDirectoryErrorKind::NotDirectory,
1391 None,
1392 ));
1393 }
1394 PrivateObjectKind::RegularFile(link_state) => {
1395 verify_regular_file_attributes(&information)?;
1396 if information.nNumberOfLinks != link_state.expected_links() {
1397 return Err(PrivateDirectoryError::new(
1398 PrivateDirectoryErrorKind::MultipleLinks,
1399 None,
1400 ));
1401 }
1402 }
1403 PrivateObjectKind::Directory => {}
1404 }
1405 Ok(())
1406 }
1407
1408 fn query_attributes(
1409 handle: HANDLE,
1410 ) -> Result<BY_HANDLE_FILE_INFORMATION, PrivateDirectoryError> {
1411 let mut information = BY_HANDLE_FILE_INFORMATION::default();
1412 unsafe {
1415 win_bool(
1416 GetFileInformationByHandle(handle, &raw mut information),
1417 PrivateDirectoryOperation::QueryAttributes,
1418 )?;
1419 }
1420 Ok(information)
1421 }
1422
1423 fn query_file_identity(handle: HANDLE) -> Result<FILE_ID_INFO, PrivateDirectoryError> {
1424 let mut identity = FILE_ID_INFO::default();
1425 let size = u32::try_from(size_of::<FILE_ID_INFO>())
1426 .map_err(|_| operation_error(PrivateDirectoryOperation::QueryAttributes, None))?;
1427 unsafe {
1429 win_bool(
1430 GetFileInformationByHandleEx(handle, FileIdInfo, (&raw mut identity).cast(), size),
1431 PrivateDirectoryOperation::QueryAttributes,
1432 )?;
1433 }
1434 Ok(identity)
1435 }
1436
1437 fn verify_directory_entry_identity(
1438 directory: HANDLE,
1439 name: &OsStr,
1440 file: HANDLE,
1441 ) -> Result<(), PrivateDirectoryError> {
1442 let directory_identity = query_file_identity(directory)?;
1443 let file_identity = query_file_identity(file)?;
1444 if directory_identity.VolumeSerialNumber != file_identity.VolumeSerialNumber {
1445 return Err(directory_identity_error());
1446 }
1447 let entry = enumerate_directory(directory)?
1448 .into_iter()
1449 .find(|entry| entry.name == name)
1450 .ok_or_else(directory_identity_error)?;
1451 if entry.file_id != file_identity.FileId.Identifier {
1452 return Err(directory_identity_error());
1453 }
1454 Ok(())
1455 }
1456
1457 fn verify_directory_contains_file_identity(
1458 directory: HANDLE,
1459 file: HANDLE,
1460 ) -> Result<(), PrivateDirectoryError> {
1461 let directory_identity = query_file_identity(directory)?;
1462 let file_identity = query_file_identity(file)?;
1463 if directory_identity.VolumeSerialNumber != file_identity.VolumeSerialNumber {
1464 return Err(directory_identity_error());
1465 }
1466 let matching_entries = enumerate_directory(directory)?
1467 .into_iter()
1468 .filter(|entry| entry.file_id == file_identity.FileId.Identifier)
1469 .count();
1470 if matching_entries != 1 {
1471 return Err(directory_identity_error());
1472 }
1473 Ok(())
1474 }
1475
1476 fn verify_same_file(left: HANDLE, right: HANDLE) -> Result<(), PrivateDirectoryError> {
1477 let left = query_file_identity(left)?;
1478 let right = query_file_identity(right)?;
1479 if left.VolumeSerialNumber != right.VolumeSerialNumber
1480 || left.FileId.Identifier != right.FileId.Identifier
1481 {
1482 return Err(directory_identity_error());
1483 }
1484 Ok(())
1485 }
1486
1487 fn enumerate_directory(
1488 handle: HANDLE,
1489 ) -> Result<Vec<DirectoryEntryIdentity>, PrivateDirectoryError> {
1490 let words = DIRECTORY_ENUMERATION_BUFFER_BYTES / size_of::<u64>();
1491 let buffer_size = u32::try_from(DIRECTORY_ENUMERATION_BUFFER_BYTES)
1492 .map_err(|_| operation_error(PrivateDirectoryOperation::EnumerateDirectory, None))?;
1493 let mut buffer = vec![0_u64; words];
1494 let mut entries = Vec::new();
1495 let mut names = BTreeSet::new();
1496 let mut restart = true;
1497 loop {
1498 let information_class = if restart {
1499 FileIdExtdDirectoryRestartInfo
1500 } else {
1501 FileIdExtdDirectoryInfo
1502 };
1503 let succeeded = unsafe {
1506 GetFileInformationByHandleEx(
1507 handle,
1508 information_class,
1509 buffer.as_mut_ptr().cast(),
1510 buffer_size,
1511 )
1512 };
1513 if succeeded == 0 {
1514 let code = last_os_code();
1515 if is_win32_error(code, ERROR_NO_MORE_FILES) {
1516 break;
1517 }
1518 return Err(operation_error(
1519 PrivateDirectoryOperation::EnumerateDirectory,
1520 code,
1521 ));
1522 }
1523 restart = false;
1524 for entry in parse_directory_entries(&buffer)? {
1525 if entry.name.as_os_str() == "." || entry.name.as_os_str() == ".." {
1526 continue;
1527 }
1528 let mut components = Path::new(&entry.name).components();
1529 if !matches!(components.next(), Some(Component::Normal(_)))
1530 || components.next().is_some()
1531 || !names.insert(entry.name.clone())
1532 {
1533 return Err(PrivateDirectoryError::new(
1534 PrivateDirectoryErrorKind::IdentityMismatch,
1535 None,
1536 ));
1537 }
1538 entries.push(entry);
1539 }
1540 }
1541 Ok(entries)
1542 }
1543
1544 fn parse_directory_entries(
1545 buffer: &[u64],
1546 ) -> Result<Vec<DirectoryEntryIdentity>, PrivateDirectoryError> {
1547 let bytes = size_of_val(buffer);
1548 let raw_buffer = unsafe { slice::from_raw_parts(buffer.as_ptr().cast::<u8>(), bytes) };
1550 let name_offset = offset_of!(FILE_ID_EXTD_DIR_INFO, FileName);
1551 let mut cursor = 0_usize;
1552 let mut entries = Vec::new();
1553 loop {
1554 let _header_end = cursor
1555 .checked_add(size_of::<FILE_ID_EXTD_DIR_INFO>())
1556 .filter(|end| *end <= bytes)
1557 .ok_or_else(directory_identity_error)?;
1558 let information = unsafe {
1562 ptr::read_unaligned(
1563 raw_buffer
1564 .as_ptr()
1565 .add(cursor)
1566 .cast::<FILE_ID_EXTD_DIR_INFO>(),
1567 )
1568 };
1569 let name_bytes = usize::try_from(information.FileNameLength)
1570 .map_err(|_| directory_identity_error())?;
1571 if name_bytes % size_of::<u16>() != 0 {
1572 return Err(directory_identity_error());
1573 }
1574 let name_start = cursor
1575 .checked_add(name_offset)
1576 .ok_or_else(directory_identity_error)?;
1577 let name_end = name_start
1578 .checked_add(name_bytes)
1579 .filter(|end| *end <= bytes)
1580 .ok_or_else(directory_identity_error)?;
1581 let name = raw_buffer[name_start..name_end]
1582 .chunks_exact(size_of::<u16>())
1583 .map(|unit| u16::from_le_bytes([unit[0], unit[1]]))
1584 .collect::<Vec<_>>();
1585 entries.push(DirectoryEntryIdentity {
1586 name: OsString::from_wide(&name),
1587 file_id: information.FileId.Identifier,
1588 });
1589
1590 if information.NextEntryOffset == 0 {
1591 break;
1592 }
1593 let next = usize::try_from(information.NextEntryOffset)
1594 .map_err(|_| directory_identity_error())?;
1595 if next < name_end - cursor {
1596 return Err(directory_identity_error());
1597 }
1598 cursor = cursor
1599 .checked_add(next)
1600 .filter(|next| *next < bytes)
1601 .ok_or_else(directory_identity_error)?;
1602 }
1603 Ok(entries)
1604 }
1605
1606 fn directory_identity_error() -> PrivateDirectoryError {
1607 PrivateDirectoryError::new(PrivateDirectoryErrorKind::IdentityMismatch, None)
1608 }
1609
1610 fn verify_regular_file_attributes(
1611 information: &BY_HANDLE_FILE_INFORMATION,
1612 ) -> Result<(), PrivateDirectoryError> {
1613 if information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1614 return Err(PrivateDirectoryError::new(
1615 PrivateDirectoryErrorKind::ReparsePoint,
1616 None,
1617 ));
1618 }
1619 if information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0 {
1620 return Err(PrivateDirectoryError::new(
1621 PrivateDirectoryErrorKind::NotRegularFile,
1622 None,
1623 ));
1624 }
1625 Ok(())
1626 }
1627
1628 fn verify_filesystem(handle: HANDLE) -> Result<(), PrivateDirectoryError> {
1629 let mut flags = 0;
1630 let succeeded = unsafe {
1633 GetVolumeInformationByHandleW(
1634 handle,
1635 ptr::null_mut(),
1636 0,
1637 ptr::null_mut(),
1638 ptr::null_mut(),
1639 &raw mut flags,
1640 ptr::null_mut(),
1641 0,
1642 )
1643 };
1644 win_bool(succeeded, PrivateDirectoryOperation::QueryFileSystem)?;
1645 if flags & FILE_PERSISTENT_ACLS == 0 {
1646 return Err(PrivateDirectoryError::new(
1647 PrivateDirectoryErrorKind::UnsupportedFileSystem,
1648 None,
1649 ));
1650 }
1651 Ok(())
1652 }
1653
1654 fn read_security_descriptor(handle: HANDLE) -> Result<AlignedBuffer, PrivateDirectoryError> {
1655 let requested = OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION;
1656 let mut required = 0;
1657 let sized = unsafe {
1660 GetKernelObjectSecurity(handle, requested, ptr::null_mut(), 0, &raw mut required)
1661 };
1662 let sizing_code = last_os_code();
1663 if sized != 0 || !is_win32_error(sizing_code, ERROR_INSUFFICIENT_BUFFER) {
1664 return Err(security_query_error(sizing_code));
1665 }
1666 let required = usize::try_from(required).map_err(|_| security_query_error(None))?;
1667 if required == 0 || required > MAX_SECURITY_DESCRIPTOR_BYTES {
1668 return Err(security_query_error(None));
1669 }
1670 let mut descriptor = AlignedBuffer::new(required);
1671 let length = u32::try_from(descriptor.len()).map_err(|_| security_query_error(None))?;
1672 let mut returned = length;
1673 let succeeded = unsafe {
1676 GetKernelObjectSecurity(
1677 handle,
1678 requested,
1679 descriptor.as_mut_ptr(),
1680 length,
1681 &raw mut returned,
1682 )
1683 };
1684 win_bool(
1685 succeeded,
1686 PrivateDirectoryOperation::QuerySecurityDescriptor,
1687 )?;
1688 if returned == 0 || returned > length {
1689 return Err(security_query_error(None));
1690 }
1691 descriptor.len = usize::try_from(returned).map_err(|_| security_query_error(None))?;
1692 Ok(descriptor)
1693 }
1694
1695 fn verify_security_descriptor(
1696 descriptor: &AlignedBuffer,
1697 user: &Sid,
1698 kind: PrivateObjectKind,
1699 ) -> Result<(), PrivateDirectoryError> {
1700 let valid = unsafe { IsValidSecurityDescriptor(descriptor.as_ptr()) };
1703 if valid == 0 {
1704 return Err(security_query_error(last_os_code()));
1705 }
1706 let mut control = 0;
1707 let mut revision = 0;
1708 unsafe {
1710 win_bool(
1711 GetSecurityDescriptorControl(
1712 descriptor.as_ptr(),
1713 &raw mut control,
1714 &raw mut revision,
1715 ),
1716 PrivateDirectoryOperation::QuerySecurityDescriptor,
1717 )?;
1718 }
1719 if control & SE_DACL_PROTECTED == 0 {
1720 return Err(PrivateDirectoryError::new(
1721 PrivateDirectoryErrorKind::DaclUnprotected,
1722 None,
1723 ));
1724 }
1725
1726 let mut owner = ptr::null_mut();
1727 let mut owner_defaulted = 0;
1728 unsafe {
1730 win_bool(
1731 GetSecurityDescriptorOwner(
1732 descriptor.as_ptr(),
1733 &raw mut owner,
1734 &raw mut owner_defaulted,
1735 ),
1736 PrivateDirectoryOperation::QuerySecurityDescriptor,
1737 )?;
1738 }
1739 if owner.is_null() {
1740 return Err(PrivateDirectoryError::new(
1741 PrivateDirectoryErrorKind::OwnerMissing,
1742 None,
1743 ));
1744 }
1745 let owner = descriptor_sid(descriptor, owner).ok_or_else(|| {
1746 PrivateDirectoryError::new(PrivateDirectoryErrorKind::OwnerMismatch, None)
1747 })?;
1748 if owner != user.as_bytes() {
1749 return Err(PrivateDirectoryError::new(
1750 PrivateDirectoryErrorKind::OwnerMismatch,
1751 None,
1752 ));
1753 }
1754
1755 let mut dacl_present = 0;
1756 let mut dacl = ptr::null_mut();
1757 let mut dacl_defaulted = 0;
1758 unsafe {
1760 win_bool(
1761 GetSecurityDescriptorDacl(
1762 descriptor.as_ptr(),
1763 &raw mut dacl_present,
1764 &raw mut dacl,
1765 &raw mut dacl_defaulted,
1766 ),
1767 PrivateDirectoryOperation::QuerySecurityDescriptor,
1768 )?;
1769 }
1770 if dacl_present == 0 || dacl.is_null() {
1771 return Err(PrivateDirectoryError::new(
1772 PrivateDirectoryErrorKind::DaclMissing,
1773 None,
1774 ));
1775 }
1776 let raw_acl = descriptor_acl(descriptor, dacl).ok_or_else(|| {
1777 PrivateDirectoryError::new(
1778 PrivateDirectoryErrorKind::DaclPolicy(PrivateDirectoryAclViolation::Malformed),
1779 None,
1780 )
1781 })?;
1782 let system = Sid::well_known(WinLocalSystemSid)?;
1783 let administrators = Sid::well_known(WinBuiltinAdministratorsSid)?;
1784 let allowed = allowed_sids(user, &system, &administrators);
1785 let allowed = allowed.iter().map(|sid| sid.as_bytes()).collect::<Vec<_>>();
1786 validate_acl_with_flags(raw_acl, &allowed, kind.ace_flags()).map_err(|violation| {
1787 PrivateDirectoryError::new(PrivateDirectoryErrorKind::DaclPolicy(violation), None)
1788 })
1789 }
1790
1791 fn allowed_sids<'a>(user: &'a Sid, system: &'a Sid, administrators: &'a Sid) -> Vec<&'a Sid> {
1792 let mut allowed = Vec::with_capacity(3);
1793 for sid in [user, system, administrators] {
1794 if !allowed
1795 .iter()
1796 .any(|present: &&Sid| present.as_bytes() == sid.as_bytes())
1797 {
1798 allowed.push(sid);
1799 }
1800 }
1801 allowed
1802 }
1803
1804 fn descriptor_sid(descriptor: &AlignedBuffer, sid: PSID) -> Option<&[u8]> {
1805 let bytes = descriptor.as_bytes();
1806 let offset = pointer_offset(bytes, sid.cast(), SID_HEADER_BYTES)?;
1807 let header = bytes.get(offset..offset + SID_HEADER_BYTES)?;
1808 let length = SID_HEADER_BYTES.checked_add(usize::from(header[1]) * size_of::<u32>())?;
1809 let sid = bytes.get(offset..offset.checked_add(length)?)?;
1810 valid_sid_bytes(sid).then_some(sid)
1811 }
1812
1813 fn descriptor_acl(descriptor: &AlignedBuffer, acl: *const ACL) -> Option<&[u8]> {
1814 let bytes = descriptor.as_bytes();
1815 let offset = pointer_offset(bytes, acl.cast(), size_of::<ACL>())?;
1816 let header = bytes.get(offset..offset + size_of::<ACL>())?;
1817 let length = usize::from(u16::from_le_bytes([header[2], header[3]]));
1818 bytes.get(offset..offset.checked_add(length)?)
1819 }
1820
1821 fn pointer_offset(bytes: &[u8], pointer: *const u8, minimum: usize) -> Option<usize> {
1822 let base = bytes.as_ptr() as usize;
1823 let end = base.checked_add(bytes.len())?;
1824 let pointer = pointer as usize;
1825 let required_end = pointer.checked_add(minimum)?;
1826 (pointer >= base && required_end <= end).then_some(pointer - base)
1827 }
1828
1829 fn current_process_user_sid() -> Result<Sid, PrivateDirectoryError> {
1830 let mut token: HANDLE = ptr::null_mut();
1831 unsafe {
1833 win_bool(
1834 OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token),
1835 PrivateDirectoryOperation::QueryProcessToken,
1836 )?;
1837 }
1838 if token.is_null() {
1839 return Err(operation_error(
1840 PrivateDirectoryOperation::QueryProcessToken,
1841 None,
1842 ));
1843 }
1844 let token = unsafe { OwnedHandle::from_raw_handle(token) };
1846 let mut required = 0;
1847 let sized = unsafe {
1849 GetTokenInformation(
1850 token.as_raw_handle(),
1851 TokenUser,
1852 ptr::null_mut(),
1853 0,
1854 &raw mut required,
1855 )
1856 };
1857 let sizing_code = last_os_code();
1858 if sized != 0 || !is_win32_error(sizing_code, ERROR_INSUFFICIENT_BUFFER) || required == 0 {
1859 return Err(operation_error(
1860 PrivateDirectoryOperation::QueryProcessToken,
1861 sizing_code,
1862 ));
1863 }
1864 let mut information =
1865 AlignedBuffer::new(usize::try_from(required).map_err(|_| {
1866 operation_error(PrivateDirectoryOperation::QueryProcessToken, None)
1867 })?);
1868 unsafe {
1870 win_bool(
1871 GetTokenInformation(
1872 token.as_raw_handle(),
1873 TokenUser,
1874 information.as_mut_ptr(),
1875 required,
1876 &raw mut required,
1877 ),
1878 PrivateDirectoryOperation::QueryProcessToken,
1879 )?;
1880 let token_user = information.as_ptr().cast::<TOKEN_USER>();
1881 Sid::copy_from((*token_user).User.Sid)
1882 }
1883 }
1884
1885 fn with_private_security_attributes<T>(
1886 kind: PrivateObjectKind,
1887 operation: impl FnOnce(*const SECURITY_ATTRIBUTES) -> Result<T, PrivateDirectoryError>,
1888 ) -> Result<(T, Sid), PrivateDirectoryError> {
1889 let user = current_process_user_sid()?;
1890 let system = Sid::well_known(WinLocalSystemSid)?;
1891 let administrators = Sid::well_known(WinBuiltinAdministratorsSid)?;
1892 let allowed = allowed_sids(&user, &system, &administrators);
1893 let mut acl = build_acl_with_flags(&allowed, kind.ace_flags())?;
1894 let mut descriptor = SECURITY_DESCRIPTOR::default();
1895
1896 unsafe {
1899 win_bool(
1900 InitializeSecurityDescriptor(
1901 (&raw mut descriptor).cast(),
1902 SECURITY_DESCRIPTOR_REVISION,
1903 ),
1904 PrivateDirectoryOperation::BuildSecurityDescriptor,
1905 )?;
1906 win_bool(
1907 SetSecurityDescriptorOwner((&raw mut descriptor).cast(), user.as_psid(), 0),
1908 PrivateDirectoryOperation::BuildSecurityDescriptor,
1909 )?;
1910 win_bool(
1911 SetSecurityDescriptorDacl(
1912 (&raw mut descriptor).cast(),
1913 1,
1914 acl.as_mut_ptr().cast(),
1915 0,
1916 ),
1917 PrivateDirectoryOperation::BuildSecurityDescriptor,
1918 )?;
1919 win_bool(
1920 SetSecurityDescriptorControl(
1921 (&raw mut descriptor).cast(),
1922 SE_DACL_PROTECTED,
1923 SE_DACL_PROTECTED,
1924 ),
1925 PrivateDirectoryOperation::BuildSecurityDescriptor,
1926 )?;
1927 }
1928
1929 let attributes = SECURITY_ATTRIBUTES {
1930 nLength: u32::try_from(size_of::<SECURITY_ATTRIBUTES>()).map_err(|_| {
1931 operation_error(PrivateDirectoryOperation::BuildSecurityDescriptor, None)
1932 })?,
1933 lpSecurityDescriptor: (&raw mut descriptor).cast(),
1934 bInheritHandle: 0,
1935 };
1936 let value = operation(&raw const attributes)?;
1937 Ok((value, user))
1938 }
1939
1940 #[cfg(test)]
1941 fn build_acl(sids: &[&Sid]) -> Result<AlignedBuffer, PrivateDirectoryError> {
1942 build_acl_with_flags(sids, DIRECTORY_INHERIT_FLAGS)
1943 }
1944
1945 fn build_acl_with_flags(
1946 sids: &[&Sid],
1947 ace_flags: u8,
1948 ) -> Result<AlignedBuffer, PrivateDirectoryError> {
1949 let length = size_of::<ACL>()
1950 + sids
1951 .iter()
1952 .map(|sid| {
1953 size_of::<ACCESS_ALLOWED_ACE>() - size_of::<u32>() + sid.as_bytes().len()
1954 })
1955 .sum::<usize>();
1956 let mut acl = AlignedBuffer::new(length);
1957 let length = u32::try_from(length)
1958 .map_err(|_| operation_error(PrivateDirectoryOperation::BuildAcl, None))?;
1959 unsafe {
1962 win_bool(
1963 InitializeAcl(acl.as_mut_ptr().cast(), length, ACL_REVISION),
1964 PrivateDirectoryOperation::BuildAcl,
1965 )?;
1966 for sid in sids {
1967 win_bool(
1968 AddAccessAllowedAceEx(
1969 acl.as_mut_ptr().cast(),
1970 ACL_REVISION,
1971 u32::from(ace_flags),
1972 FILE_ALL_ACCESS,
1973 sid.as_psid(),
1974 ),
1975 PrivateDirectoryOperation::BuildAcl,
1976 )?;
1977 }
1978 }
1979 Ok(acl)
1980 }
1981
1982 fn open_directory(path: &[u16], write_dacl: bool) -> Result<File, PrivateDirectoryError> {
1983 let desired_access =
1984 FILE_GENERIC_READ | FILE_WRITE_DATA | DELETE | if write_dacl { WRITE_DAC } else { 0 };
1985 open_directory_with_access(path, desired_access, FILE_SHARE_READ | FILE_SHARE_WRITE)
1986 }
1987
1988 fn open_directory_read_guard(path: &[u16]) -> Result<File, PrivateDirectoryError> {
1989 open_directory_with_access(
1990 path,
1991 FILE_GENERIC_READ | FILE_WRITE_DATA,
1992 FILE_SHARE_READ | FILE_SHARE_WRITE,
1993 )
1994 }
1995
1996 fn open_directory_with_access(
1997 path: &[u16],
1998 desired_access: u32,
1999 share_mode: u32,
2000 ) -> Result<File, PrivateDirectoryError> {
2001 let handle = unsafe {
2004 CreateFileW(
2005 path.as_ptr(),
2006 desired_access,
2007 share_mode,
2008 ptr::null(),
2009 OPEN_EXISTING,
2010 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
2011 ptr::null_mut(),
2012 )
2013 };
2014 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
2015 return Err(operation_error(
2016 PrivateDirectoryOperation::OpenDirectory,
2017 last_os_code(),
2018 ));
2019 }
2020 Ok(unsafe { File::from_raw_handle(handle) })
2022 }
2023
2024 fn open_tree_entry(path: &Path) -> Result<File, PrivateDirectoryError> {
2025 let wide_path = wide_path(path)?;
2026 let handle = unsafe {
2030 CreateFileW(
2031 wide_path.as_ptr(),
2032 FILE_GENERIC_READ | DELETE,
2033 FILE_SHARE_READ,
2034 ptr::null(),
2035 OPEN_EXISTING,
2036 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
2037 ptr::null_mut(),
2038 )
2039 };
2040 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
2041 return Err(operation_error(
2042 PrivateDirectoryOperation::OpenTreeEntry,
2043 last_os_code(),
2044 ));
2045 }
2046 Ok(unsafe { File::from_raw_handle(handle) })
2048 }
2049
2050 fn final_path_from_handle(handle: HANDLE) -> Result<PathBuf, PrivateDirectoryError> {
2051 let mut buffer = vec![0_u16; 512];
2052 loop {
2053 let capacity = u32::try_from(buffer.len())
2054 .map_err(|_| operation_error(PrivateDirectoryOperation::ResolvePath, None))?;
2055 let length =
2057 unsafe { GetFinalPathNameByHandleW(handle, buffer.as_mut_ptr(), capacity, 0) };
2058 if length == 0 {
2059 return Err(operation_error(
2060 PrivateDirectoryOperation::ResolvePath,
2061 last_os_code(),
2062 ));
2063 }
2064 let length = usize::try_from(length)
2065 .map_err(|_| operation_error(PrivateDirectoryOperation::ResolvePath, None))?;
2066 if length < buffer.len() {
2067 buffer.truncate(length);
2068 return Ok(PathBuf::from(OsString::from_wide(&buffer)));
2069 }
2070 let required = length
2071 .checked_add(1)
2072 .ok_or_else(|| operation_error(PrivateDirectoryOperation::ResolvePath, None))?;
2073 buffer.resize(required, 0);
2074 }
2075 }
2076
2077 fn open_regular_file(path: &[u16]) -> Result<File, PrivateDirectoryError> {
2078 open_regular_file_with_access(path, FILE_GENERIC_READ, FILE_SHARE_READ)
2079 }
2080
2081 fn open_regular_file_for_sync(path: &[u16]) -> Result<File, PrivateDirectoryError> {
2082 open_regular_file_with_access(
2083 path,
2084 FILE_GENERIC_READ | FILE_GENERIC_WRITE,
2085 FILE_SHARE_READ,
2086 )
2087 }
2088
2089 fn open_regular_file_for_removal(path: &[u16]) -> Result<File, PrivateDirectoryError> {
2090 open_regular_file_with_access(
2091 path,
2092 FILE_GENERIC_READ | DELETE,
2093 FILE_SHARE_READ | FILE_SHARE_DELETE,
2094 )
2095 }
2096
2097 fn reopen_private_staging_file(
2098 original: HANDLE,
2099 share_mode: u32,
2100 ) -> Result<File, PrivateDirectoryError> {
2101 reopen_private_file(original, FILE_GENERIC_READ | DELETE, share_mode)
2102 }
2103
2104 fn reopen_private_file(
2105 original: HANDLE,
2106 desired_access: u32,
2107 share_mode: u32,
2108 ) -> Result<File, PrivateDirectoryError> {
2109 let handle = unsafe {
2112 ReOpenFile(
2113 original,
2114 desired_access,
2115 share_mode,
2116 FILE_FLAG_OPEN_REPARSE_POINT,
2117 )
2118 };
2119 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
2120 return Err(operation_error(
2121 PrivateDirectoryOperation::OpenFile,
2122 last_os_code(),
2123 ));
2124 }
2125 Ok(unsafe { File::from_raw_handle(handle) })
2127 }
2128
2129 fn open_regular_file_with_access(
2130 path: &[u16],
2131 desired_access: u32,
2132 share_mode: u32,
2133 ) -> Result<File, PrivateDirectoryError> {
2134 let handle = unsafe {
2137 CreateFileW(
2138 path.as_ptr(),
2139 desired_access,
2140 share_mode,
2141 ptr::null(),
2142 OPEN_EXISTING,
2143 FILE_FLAG_OPEN_REPARSE_POINT,
2144 ptr::null_mut(),
2145 )
2146 };
2147 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
2148 return Err(operation_error(
2149 PrivateDirectoryOperation::OpenFile,
2150 last_os_code(),
2151 ));
2152 }
2153 Ok(unsafe { File::from_raw_handle(handle) })
2155 }
2156
2157 fn remove_by_handle(
2158 handle: HANDLE,
2159 kind: PrivateObjectKind,
2160 ) -> Result<(), PrivateDirectoryError> {
2161 let operation = kind.remove_operation();
2162 let disposition = FILE_DISPOSITION_INFO { DeleteFile: true };
2163 let size = u32::try_from(size_of::<FILE_DISPOSITION_INFO>())
2164 .map_err(|_| operation_error(operation, None))?;
2165 let succeeded = unsafe {
2167 SetFileInformationByHandle(
2168 handle,
2169 FileDispositionInfo,
2170 (&raw const disposition).cast(),
2171 size,
2172 )
2173 };
2174 win_bool(succeeded, operation)
2175 }
2176
2177 fn remove_link_by_handle_posix(handle: HANDLE) -> Result<(), PrivateDirectoryError> {
2178 let disposition = FILE_DISPOSITION_INFO_EX {
2179 Flags: FILE_DISPOSITION_FLAG_DELETE | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS,
2180 };
2181 let size = u32::try_from(size_of::<FILE_DISPOSITION_INFO_EX>())
2182 .map_err(|_| operation_error(PrivateDirectoryOperation::RemoveFile, None))?;
2183 let succeeded = unsafe {
2186 SetFileInformationByHandle(
2187 handle,
2188 FileDispositionInfoEx,
2189 (&raw const disposition).cast(),
2190 size,
2191 )
2192 };
2193 win_bool(succeeded, PrivateDirectoryOperation::RemoveFile)
2194 }
2195
2196 fn rename_file_handle_create_new(
2197 file: HANDLE,
2198 destination_directory: HANDLE,
2199 destination_name: &[u16],
2200 ) -> Result<(), PrivateDirectoryError> {
2201 let name_bytes = destination_name
2202 .len()
2203 .checked_mul(size_of::<u16>())
2204 .ok_or_else(|| operation_error(PrivateDirectoryOperation::RenameFile, None))?;
2205 let total_bytes = size_of::<FILE_RENAME_INFORMATION>()
2208 .checked_add(name_bytes)
2209 .ok_or_else(|| operation_error(PrivateDirectoryOperation::RenameFile, None))?;
2210 let name_bytes = u32::try_from(name_bytes)
2211 .map_err(|_| operation_error(PrivateDirectoryOperation::RenameFile, None))?;
2212 let total_bytes = u32::try_from(total_bytes)
2213 .map_err(|_| operation_error(PrivateDirectoryOperation::RenameFile, None))?;
2214 let mut buffer = AlignedBuffer::new(total_bytes as usize);
2215 let information = buffer.as_mut_ptr().cast::<FILE_RENAME_INFORMATION>();
2216 let mut io_status = IO_STATUS_BLOCK::default();
2217 let status = unsafe {
2221 information.write(FILE_RENAME_INFORMATION::default());
2222 (*information).Anonymous.ReplaceIfExists = false;
2223 (*information).RootDirectory = destination_directory;
2224 (*information).FileNameLength = name_bytes;
2225 ptr::copy_nonoverlapping(
2226 destination_name.as_ptr(),
2227 ptr::addr_of_mut!((*information).FileName).cast::<u16>(),
2228 destination_name.len(),
2229 );
2230 NtSetInformationFile(
2231 file,
2232 &raw mut io_status,
2233 buffer.as_ptr(),
2234 total_bytes,
2235 FileRenameInformation,
2236 )
2237 };
2238 if status >= 0 {
2239 return Ok(());
2240 }
2241 let code = i32::try_from(unsafe { RtlNtStatusToDosError(status) }).ok();
2243 Err(PrivateDirectoryError::new(
2244 if is_win32_error(code, ERROR_ALREADY_EXISTS) || is_win32_error(code, ERROR_FILE_EXISTS)
2245 {
2246 PrivateDirectoryErrorKind::AlreadyExists
2247 } else {
2248 PrivateDirectoryErrorKind::WindowsApi(PrivateDirectoryOperation::RenameFile)
2249 },
2250 code,
2251 ))
2252 }
2253
2254 fn private_destination_name(name: &OsStr) -> Result<Vec<u16>, PrivateDirectoryError> {
2255 const INVALID_ASCII: [u16; 9] = [0x22, 0x2a, 0x2f, 0x3a, 0x3c, 0x3e, 0x3f, 0x5c, 0x7c];
2256
2257 let mut components = Path::new(name).components();
2258 if !matches!(components.next(), Some(Component::Normal(component)) if component == name)
2259 || components.next().is_some()
2260 {
2261 return Err(PrivateDirectoryError::new(
2262 PrivateDirectoryErrorKind::InvalidPath,
2263 None,
2264 ));
2265 }
2266 let encoded = name.encode_wide().collect::<Vec<_>>();
2267 let invalid_ascii = |unit: u16| unit < 32 || INVALID_ASCII.contains(&unit);
2268 if encoded.is_empty()
2269 || encoded.iter().copied().any(invalid_ascii)
2270 || matches!(encoded.last(), Some(unit) if *unit == u16::from(b'.') || *unit == u16::from(b' '))
2271 {
2272 return Err(PrivateDirectoryError::new(
2273 PrivateDirectoryErrorKind::InvalidPath,
2274 None,
2275 ));
2276 }
2277 Ok(encoded)
2278 }
2279
2280 fn wide_path(path: &Path) -> Result<Vec<u16>, PrivateDirectoryError> {
2281 const BACKSLASH: u16 = b'\\' as u16;
2282 const FORWARD_SLASH: u16 = b'/' as u16;
2283 const VERBATIM_PREFIX: &[u16] = &[BACKSLASH, BACKSLASH, b'?' as u16, BACKSLASH];
2284 const VERBATIM_UNC_PREFIX: &[u16] = &[
2285 BACKSLASH,
2286 BACKSLASH,
2287 b'?' as u16,
2288 BACKSLASH,
2289 b'U' as u16,
2290 b'N' as u16,
2291 b'C' as u16,
2292 BACKSLASH,
2293 ];
2294
2295 let mut encoded = path.as_os_str().encode_wide().collect::<Vec<_>>();
2296 if !path.is_absolute() || encoded.contains(&0) {
2297 return Err(PrivateDirectoryError::new(
2298 PrivateDirectoryErrorKind::InvalidPath,
2299 None,
2300 ));
2301 }
2302 if encoded.starts_with(VERBATIM_PREFIX) {
2303 encoded.push(0);
2304 return Ok(encoded);
2305 }
2306 for unit in &mut encoded {
2307 if *unit == FORWARD_SLASH {
2308 *unit = BACKSLASH;
2309 }
2310 }
2311 let mut wide = if encoded.starts_with(&[BACKSLASH, BACKSLASH]) {
2312 let mut wide = Vec::with_capacity(VERBATIM_UNC_PREFIX.len() + encoded.len() - 2 + 1);
2313 wide.extend_from_slice(VERBATIM_UNC_PREFIX);
2314 wide.extend_from_slice(&encoded[2..]);
2315 wide
2316 } else {
2317 let mut wide = Vec::with_capacity(VERBATIM_PREFIX.len() + encoded.len() + 1);
2318 wide.extend_from_slice(VERBATIM_PREFIX);
2319 wide.extend_from_slice(&encoded);
2320 wide
2321 };
2322 wide.push(0);
2323 Ok(wide)
2324 }
2325
2326 fn security_query_error(code: Option<i32>) -> PrivateDirectoryError {
2327 operation_error(PrivateDirectoryOperation::QuerySecurityDescriptor, code)
2328 }
2329
2330 fn error_after_cleanup(
2331 error: PrivateDirectoryError,
2332 cleanup: Result<(), PrivateDirectoryError>,
2333 ) -> PrivateDirectoryError {
2334 match cleanup {
2335 Ok(()) => error.with_cleanup(PrivateDirectoryCleanupStatus::Confirmed, None),
2336 Err(cleanup) => {
2337 error.with_cleanup(PrivateDirectoryCleanupStatus::Uncertain, cleanup.os_code())
2338 }
2339 }
2340 }
2341
2342 fn operation_error(
2343 operation: PrivateDirectoryOperation,
2344 code: Option<i32>,
2345 ) -> PrivateDirectoryError {
2346 PrivateDirectoryError::new(PrivateDirectoryErrorKind::WindowsApi(operation), code)
2347 }
2348
2349 fn win_bool(
2350 result: i32,
2351 operation: PrivateDirectoryOperation,
2352 ) -> Result<(), PrivateDirectoryError> {
2353 if result != 0 {
2354 Ok(())
2355 } else {
2356 Err(operation_error(operation, last_os_code()))
2357 }
2358 }
2359
2360 fn last_os_code() -> Option<i32> {
2361 io::Error::last_os_error().raw_os_error()
2362 }
2363
2364 fn is_win32_error(code: Option<i32>, expected: u32) -> bool {
2365 code.and_then(|code| u32::try_from(code).ok()) == Some(expected)
2366 }
2367
2368 struct AlignedBuffer {
2369 words: Vec<usize>,
2370 len: usize,
2371 }
2372
2373 impl AlignedBuffer {
2374 fn new(len: usize) -> Self {
2375 let words = len.div_ceil(size_of::<usize>()).max(1);
2376 Self {
2377 words: vec![0; words],
2378 len,
2379 }
2380 }
2381
2382 fn len(&self) -> usize {
2383 self.len
2384 }
2385
2386 fn as_ptr(&self) -> *mut c_void {
2387 self.words.as_ptr().cast_mut().cast()
2388 }
2389
2390 fn as_mut_ptr(&mut self) -> *mut c_void {
2391 self.words.as_mut_ptr().cast()
2392 }
2393
2394 fn as_bytes(&self) -> &[u8] {
2395 unsafe { slice::from_raw_parts(self.words.as_ptr().cast(), self.len) }
2397 }
2398 }
2399
2400 struct Sid {
2401 bytes: AlignedBuffer,
2402 }
2403
2404 impl Sid {
2405 fn well_known(kind: i32) -> Result<Self, PrivateDirectoryError> {
2406 let mut bytes = AlignedBuffer::new(SECURITY_MAX_SID_SIZE as usize);
2407 let mut length = SECURITY_MAX_SID_SIZE;
2408 let succeeded = unsafe {
2411 CreateWellKnownSid(kind, ptr::null_mut(), bytes.as_mut_ptr(), &raw mut length)
2412 };
2413 win_bool(succeeded, PrivateDirectoryOperation::CreateWellKnownSid)?;
2414 bytes.len = usize::try_from(length).map_err(|_| {
2415 operation_error(PrivateDirectoryOperation::CreateWellKnownSid, None)
2416 })?;
2417 Ok(Self { bytes })
2418 }
2419
2420 unsafe fn copy_from(source: PSID) -> Result<Self, PrivateDirectoryError> {
2421 if source.is_null() || unsafe { IsValidSid(source) } == 0 {
2422 return Err(operation_error(
2423 PrivateDirectoryOperation::QueryProcessToken,
2424 last_os_code(),
2425 ));
2426 }
2427 let length = unsafe { GetLengthSid(source) };
2428 let mut bytes = AlignedBuffer::new(usize::try_from(length).map_err(|_| {
2429 operation_error(PrivateDirectoryOperation::QueryProcessToken, None)
2430 })?);
2431 unsafe {
2434 win_bool(
2435 CopySid(length, bytes.as_mut_ptr(), source),
2436 PrivateDirectoryOperation::QueryProcessToken,
2437 )?;
2438 }
2439 Ok(Self { bytes })
2440 }
2441
2442 fn as_psid(&self) -> PSID {
2443 self.bytes.as_ptr()
2444 }
2445
2446 fn as_bytes(&self) -> &[u8] {
2447 self.bytes.as_bytes()
2448 }
2449 }
2450
2451 #[cfg(test)]
2452 mod tests {
2453 use std::{
2454 fs,
2455 os::windows::{
2456 fs::{symlink_dir, symlink_file},
2457 io::AsHandle as _,
2458 },
2459 };
2460
2461 use super::*;
2462 use windows_sys::Win32::Security::{
2463 SetKernelObjectSecurity, UNPROTECTED_DACL_SECURITY_INFORMATION, WinWorldSid,
2464 };
2465 use windows_sys::Win32::Storage::FileSystem::{
2466 FILE_GENERIC_READ, FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ,
2467 FILE_SHARE_WRITE, READ_CONTROL,
2468 };
2469
2470 #[test]
2471 fn protected_child_ignores_permissive_parent_inheritance() {
2472 let temporary = tempfile::tempdir().expect("temporary directory");
2473 let parent = temporary.path().join("permissive");
2474 fs::create_dir(&parent).expect("parent directory");
2475 set_world_writable(&parent);
2476 let child = parent.join("private");
2477
2478 let handle = create(&child).expect("protected private directory");
2479 verify(handle.as_handle()).expect("retained handle policy");
2480 drop(handle);
2481 fs::remove_dir(&child).expect("remove child");
2482 }
2483
2484 #[test]
2485 fn retained_handle_detects_dacl_tampering() {
2486 let temporary = tempfile::tempdir().expect("temporary directory");
2487 let child = temporary.path().join("private");
2488 let handle = create(&child).expect("protected private directory");
2489 set_world_writable(&child);
2490
2491 let error = verify(handle.as_handle()).expect_err("tampered DACL");
2492 assert!(matches!(
2493 error.kind(),
2494 PrivateDirectoryErrorKind::DaclUnprotected
2495 | PrivateDirectoryErrorKind::DaclPolicy(_)
2496 ));
2497 remove(handle).expect_err("tampered object must not be cleanup-eligible");
2498 assert!(child.is_dir());
2499 fs::remove_dir(&child).expect("remove child");
2500 }
2501
2502 #[test]
2503 fn retained_reparse_handle_is_rejected_when_symlinks_are_available() {
2504 let temporary = tempfile::tempdir().expect("temporary directory");
2505 let target = temporary.path().join("target");
2506 let link = temporary.path().join("link");
2507 fs::create_dir(&target).expect("target directory");
2508 if symlink_dir(&target, &link).is_err() {
2509 return;
2510 }
2511 let wide = wide_path(&link).expect("wide path");
2512 let handle = open_directory(&wide, false).expect("open reparse point");
2513 let error = verify(handle.as_handle()).expect_err("reparse rejected");
2514 assert_eq!(error.kind(), PrivateDirectoryErrorKind::ReparsePoint);
2515 }
2516
2517 #[test]
2518 fn existing_directory_is_never_removed() {
2519 let temporary = tempfile::tempdir().expect("temporary directory");
2520 let existing = temporary.path().join("existing");
2521 fs::create_dir(&existing).expect("existing directory");
2522
2523 let error = create(&existing).expect_err("atomic create must reject an existing path");
2524 assert_eq!(error.kind(), PrivateDirectoryErrorKind::AlreadyExists);
2525 assert_eq!(
2526 error.cleanup_status(),
2527 PrivateDirectoryCleanupStatus::NotRequired
2528 );
2529 assert!(existing.is_dir());
2530 }
2531
2532 #[test]
2533 fn retained_handle_blocks_path_replacement_until_drop() {
2534 let temporary = tempfile::tempdir().expect("temporary directory");
2535 let child = temporary.path().join("private");
2536 let renamed = temporary.path().join("renamed");
2537 let handle = create(&child).expect("protected private directory");
2538
2539 assert!(fs::rename(&child, &renamed).is_err());
2540 assert!(fs::remove_dir(&child).is_err());
2541 verify(handle.as_handle()).expect("retained handle policy");
2542
2543 drop(handle);
2544 fs::rename(&child, &renamed).expect("rename after retained handle drop");
2545 fs::remove_dir(&renamed).expect("cleanup after retained handle drop");
2546 }
2547
2548 #[test]
2549 fn private_directory_read_guards_coexist_and_collectively_block_rename() {
2550 let temporary = tempfile::tempdir().expect("temporary directory");
2551 let child = temporary.path().join("private");
2552 let renamed = temporary.path().join("renamed");
2553 drop(create(&child).expect("protected private directory"));
2554
2555 let first = open_read_guard(&child).expect("first private read guard");
2556 let second = open_read_guard(&child).expect("second private read guard");
2557 verify(first.as_handle()).expect("first strict directory policy");
2558 verify(second.as_handle()).expect("second strict directory policy");
2559 assert!(fs::rename(&child, &renamed).is_err());
2560
2561 drop(first);
2562 assert!(fs::rename(&child, &renamed).is_err());
2563 drop(second);
2564 fs::rename(&child, &renamed).expect("rename after all read guards drop");
2565 fs::remove_dir(&renamed).expect("cleanup renamed directory");
2566 }
2567
2568 #[test]
2569 fn private_directory_metadata_flush_accepts_mutation_and_read_guards() {
2570 let temporary = tempfile::tempdir().expect("temporary directory");
2571 let directory_path = temporary.path().join("private");
2572 let staging = directory_path.join("staging");
2573 let final_path = directory_path.join("final");
2574 let directory = create(&directory_path).expect("protected private directory");
2575 fs::write(&staging, b"durable namespace").expect("write staging file");
2576 fs::rename(&staging, &final_path).expect("publish final file");
2577
2578 crate::windows_private_directory::sync_private_directory_handle(directory.as_handle())
2579 .expect("flush mutation guard");
2580 verify(directory.as_handle()).expect("mutation guard remains strict");
2581 drop(directory);
2582
2583 let read_guard = open_read_guard(&directory_path).expect("private read guard");
2584 crate::windows_private_directory::sync_private_directory_handle(read_guard.as_handle())
2585 .expect("flush read guard");
2586 verify(read_guard.as_handle()).expect("read guard remains strict");
2587 assert_eq!(
2588 fs::read(&final_path).expect("read final file"),
2589 b"durable namespace"
2590 );
2591
2592 drop(read_guard);
2593 fs::remove_file(&final_path).expect("remove final file");
2594 fs::remove_dir(&directory_path).expect("remove private directory");
2595 }
2596
2597 #[test]
2598 fn private_directory_metadata_flush_fails_closed_without_write_access() {
2599 let temporary = tempfile::tempdir().expect("temporary directory");
2600 let directory_path = temporary.path().join("private");
2601 drop(create(&directory_path).expect("protected private directory"));
2602 let wide = wide_path(&directory_path).expect("wide directory path");
2603 let read_only = open_directory_with_access(
2604 &wide,
2605 FILE_GENERIC_READ,
2606 FILE_SHARE_READ | FILE_SHARE_WRITE,
2607 )
2608 .expect("strict read-only directory handle");
2609 verify(read_only.as_handle()).expect("read-only handle remains strict");
2610
2611 let error = crate::windows_private_directory::sync_private_directory_handle(
2612 read_only.as_handle(),
2613 )
2614 .expect_err("Windows must reject a flush without directory-write access");
2615 assert_eq!(
2616 error.kind(),
2617 PrivateDirectoryErrorKind::WindowsApi(PrivateDirectoryOperation::FlushDirectory)
2618 );
2619 assert_eq!(error.os_code(), Some(5));
2620 verify(read_only.as_handle()).expect("failed flush leaves exact handle usable");
2621 assert!(directory_path.is_dir());
2622
2623 drop(read_only);
2624 fs::remove_dir(&directory_path).expect("remove private directory");
2625 }
2626
2627 #[test]
2628 fn retained_handle_removes_the_exact_empty_directory() {
2629 let temporary = tempfile::tempdir().expect("temporary directory");
2630 let child = temporary.path().join("private");
2631 let handle = create(&child).expect("protected private directory");
2632
2633 remove(handle).expect("handle-bound removal");
2634 assert!(!child.exists());
2635 }
2636
2637 #[test]
2638 fn private_lock_file_peers_coexist_and_fs2_arbitrates() {
2639 let temporary = tempfile::tempdir().expect("temporary directory");
2640 let root = temporary.path().join("private");
2641 let moved_root = temporary.path().join("moved-private");
2642 let lock_path = root.join("store.lock");
2643 let directory = create(&root).expect("protected private directory");
2644 let creator = create_lock_file(&lock_path).expect("create strict private lock file");
2645 let peer = open_lock_file(&lock_path).expect("open compatible lock-file peer");
2646
2647 fs2::FileExt::try_lock_exclusive(&creator).expect("creator exclusive lock");
2648 assert!(fs2::FileExt::try_lock_shared(&peer).is_err());
2649 fs2::FileExt::unlock(&creator).expect("unlock creator");
2650 fs2::FileExt::try_lock_shared(&creator).expect("creator shared lock");
2651 fs2::FileExt::try_lock_shared(&peer).expect("peer shared lock");
2652 let contender = open_lock_file(&lock_path).expect("open exclusive contender");
2653 assert!(fs2::FileExt::try_lock_exclusive(&contender).is_err());
2654
2655 assert!(fs::remove_file(&lock_path).is_err());
2656 assert!(fs::rename(&root, &moved_root).is_err());
2657 assert!(!moved_root.exists());
2658 fs2::FileExt::unlock(&peer).expect("unlock peer");
2659 fs2::FileExt::unlock(&creator).expect("unlock creator shared lock");
2660 drop(contender);
2661 drop(peer);
2662 drop(creator);
2663
2664 let removal = open_file_for_removal(&lock_path).expect("open exact lock removal");
2665 remove_file(removal, PrivateFileLinkState::Single).expect("remove exact lock file");
2666 remove(directory).expect("remove empty private directory");
2667 }
2668
2669 #[test]
2670 fn private_lock_file_creation_never_replaces_existing_peer() {
2671 let temporary = tempfile::tempdir().expect("temporary directory");
2672 let root = temporary.path().join("private");
2673 let lock_path = root.join("store.lock");
2674 let directory = create(&root).expect("protected private directory");
2675 let existing = create_lock_file(&lock_path).expect("create strict private lock file");
2676
2677 let error = create_lock_file(&lock_path).expect_err("CREATE_NEW must reject peer");
2678 assert_eq!(error.kind(), PrivateDirectoryErrorKind::AlreadyExists);
2679 assert_eq!(
2680 error.cleanup_status(),
2681 PrivateDirectoryCleanupStatus::NotRequired
2682 );
2683 verify_file(existing.as_handle(), PrivateFileLinkState::Single)
2684 .expect("existing peer remains strict");
2685
2686 drop(existing);
2687 let removal = open_file_for_removal(&lock_path).expect("open exact lock removal");
2688 remove_file(removal, PrivateFileLinkState::Single).expect("remove exact lock file");
2689 remove(directory).expect("remove empty private directory");
2690 }
2691
2692 #[test]
2693 fn private_lock_file_post_create_policy_failure_is_uncertain() {
2694 let temporary = tempfile::tempdir().expect("temporary directory");
2695 let path = temporary.path().join("store.lock");
2696
2697 let error = create_lock_file_with_callback(&path, || set_world_writable(&path))
2698 .expect_err("post-create DACL tamper must fail strict verification");
2699 assert!(matches!(
2700 error.kind(),
2701 PrivateDirectoryErrorKind::DaclUnprotected
2702 | PrivateDirectoryErrorKind::DaclPolicy(_)
2703 ));
2704 assert_eq!(
2705 error.cleanup_status(),
2706 PrivateDirectoryCleanupStatus::Uncertain
2707 );
2708 assert!(path.is_file());
2709 fs::remove_file(&path).expect("remove test-owned uncertain residue");
2710 }
2711
2712 #[test]
2713 fn handle_bound_publication_is_create_new_and_retains_final_guard() {
2714 use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
2715
2716 let temporary = tempfile::tempdir().expect("temporary directory");
2717 let root = temporary.path().join("private");
2718 let staging_path = root.join("artifact.tmp");
2719 let final_path = root.join("artifact.bin");
2720 let renamed_path = root.join("renamed.bin");
2721 let directory = create(&root).expect("protected private directory");
2722 let mut staging = create_file(&staging_path, false).expect("protected staging file");
2723 staging
2724 .write_all(b"complete artifact\n")
2725 .expect("write complete staging file");
2726 staging.sync_all().expect("sync complete staging file");
2727
2728 let mut published = publish_file_create_new_with_callback(
2729 staging,
2730 directory.as_handle(),
2731 OsStr::new("artifact.bin"),
2732 || {
2733 assert!(fs::rename(&final_path, &renamed_path).is_err());
2734 assert!(fs::remove_file(&final_path).is_err());
2735 },
2736 )
2737 .expect("atomic handle-bound publication");
2738
2739 assert!(!staging_path.exists());
2740 assert!(final_path.is_file());
2741 assert!(!renamed_path.exists());
2742 verify_file(published.as_handle(), PrivateFileLinkState::Single)
2743 .expect("retained strict final handle");
2744 published
2745 .seek(SeekFrom::Start(0))
2746 .expect("rewind final handle");
2747 let mut bytes = Vec::new();
2748 published
2749 .read_to_end(&mut bytes)
2750 .expect("read final through retained handle");
2751 assert_eq!(bytes, b"complete artifact\n");
2752
2753 remove_file(published, PrivateFileLinkState::Single)
2754 .expect("remove exact published file");
2755 remove(directory).expect("remove empty private directory");
2756 }
2757
2758 #[test]
2759 fn handle_bound_publication_preserves_occupied_destination() {
2760 use std::io::Write as _;
2761
2762 let temporary = tempfile::tempdir().expect("temporary directory");
2763 let root = temporary.path().join("private");
2764 let staging_path = root.join("artifact.tmp");
2765 let final_path = root.join("artifact.bin");
2766 let directory = create(&root).expect("protected private directory");
2767 let mut staging = create_file(&staging_path, false).expect("protected staging file");
2768 staging
2769 .write_all(b"new artifact\n")
2770 .expect("write staging file");
2771 staging.sync_all().expect("sync staging file");
2772 let mut existing = create_file(&final_path, false).expect("existing private file");
2773 existing
2774 .write_all(b"existing artifact\n")
2775 .expect("write existing file");
2776 existing.sync_all().expect("sync existing file");
2777 drop(existing);
2778
2779 let publication =
2780 publish_file_create_new(staging, directory.as_handle(), OsStr::new("artifact.bin"))
2781 .expect_err("occupied destination must not be replaced");
2782 assert_eq!(publication.phase(), PrivatePublicationPhase::Unpublished);
2783 assert_eq!(
2784 publication.error().kind(),
2785 PrivateDirectoryErrorKind::AlreadyExists
2786 );
2787 let (_, staging, _) = publication.into_parts();
2788 verify_file(staging.as_handle(), PrivateFileLinkState::Single)
2789 .expect("exact staging handle retained");
2790 assert_eq!(
2791 fs::read(&final_path).expect("read existing destination"),
2792 b"existing artifact\n"
2793 );
2794
2795 remove_file(staging, PrivateFileLinkState::Single).expect("remove exact staging file");
2796 let existing = open_file_for_removal(&final_path).expect("open existing destination");
2797 remove_file(existing, PrivateFileLinkState::Single)
2798 .expect("remove existing destination");
2799 remove(directory).expect("remove empty private directory");
2800 }
2801
2802 #[test]
2803 fn handle_bound_publication_rejects_unsafe_names_before_mutation() {
2804 let temporary = tempfile::tempdir().expect("temporary directory");
2805 let root = temporary.path().join("private");
2806 let staging_path = root.join("artifact.tmp");
2807 let directory = create(&root).expect("protected private directory");
2808 let mut staging = create_file(&staging_path, false).expect("protected staging file");
2809
2810 for name in ["", "..", r"nested\artifact", "artifact:stream", "artifact."] {
2811 let error =
2812 publish_file_create_new(staging, directory.as_handle(), OsStr::new(name))
2813 .expect_err("unsafe destination name must be rejected");
2814 assert_eq!(error.phase(), PrivatePublicationPhase::Unpublished);
2815 assert_eq!(error.error().kind(), PrivateDirectoryErrorKind::InvalidPath);
2816 let (_, retained, _) = error.into_parts();
2817 staging = retained;
2818 assert!(staging_path.is_file());
2819 }
2820
2821 remove_file(staging, PrivateFileLinkState::Single).expect("remove exact staging file");
2822 remove(directory).expect("remove empty private directory");
2823 }
2824
2825 #[test]
2826 fn post_rename_link_injection_returns_recoverable_exact_handle() {
2827 let temporary = tempfile::tempdir().expect("temporary directory");
2828 let root = temporary.path().join("private");
2829 let staging_path = root.join("artifact.tmp");
2830 let final_path = root.join("artifact.bin");
2831 let injected_link = root.join("injected.bin");
2832 let directory = create(&root).expect("protected private directory");
2833 let staging = create_file(&staging_path, false).expect("protected staging file");
2834
2835 let publication = publish_file_create_new_with_callback(
2836 staging,
2837 directory.as_handle(),
2838 OsStr::new("artifact.bin"),
2839 || fs::hard_link(&final_path, &injected_link).expect("inject second hard link"),
2840 )
2841 .expect_err("post-rename link-state mismatch must fail closed");
2842 assert_eq!(
2843 publication.phase(),
2844 PrivatePublicationPhase::CommitUncertain
2845 );
2846 assert_eq!(
2847 publication.error().kind(),
2848 PrivateDirectoryErrorKind::MultipleLinks
2849 );
2850 let (_, published, _) = publication.into_parts();
2851 verify_file(published.as_handle(), PrivateFileLinkState::PublicationPair)
2852 .expect("exact publication-pair handle retained");
2853
2854 remove_file(published, PrivateFileLinkState::PublicationPair)
2855 .expect("remove exact final link");
2856 assert!(!final_path.exists());
2857 assert!(injected_link.is_file());
2858 let injected =
2859 open_file_for_removal(&injected_link).expect("open remaining exact link");
2860 remove_file(injected, PrivateFileLinkState::Single)
2861 .expect("remove remaining injected link");
2862 remove(directory).expect("remove empty private directory");
2863 }
2864
2865 #[test]
2866 fn publication_pair_completion_returns_sealed_exact_final_handle() {
2867 use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
2868
2869 let temporary = tempfile::tempdir().expect("temporary directory");
2870 let root = temporary.path().join("private");
2871 let staging_path = root.join("artifact.tmp");
2872 let final_path = root.join("artifact.bin");
2873 let moved_path = root.join("moved.bin");
2874 let directory = create(&root).expect("protected private directory");
2875 let mut writer = create_file(&staging_path, true).expect("protected staging file");
2876 writer
2877 .write_all(b"legacy artifact\n")
2878 .expect("write complete staging file");
2879 writer.sync_all().expect("sync complete staging file");
2880 let staging = seal_staging_file(writer).expect("seal staging handle");
2881 fs::hard_link(&staging_path, &final_path).expect("create legacy final hard link");
2882
2883 let mut published = complete_publication_pair(
2884 staging,
2885 directory.as_handle(),
2886 OsStr::new("artifact.bin"),
2887 )
2888 .expect("collapse publication pair");
2889
2890 assert!(!staging_path.exists());
2891 assert!(final_path.is_file());
2892 assert!(fs::rename(&final_path, &moved_path).is_err());
2893 assert!(fs::remove_file(&final_path).is_err());
2894 verify_file(published.as_handle(), PrivateFileLinkState::Single)
2895 .expect("strict single final handle");
2896 published
2897 .seek(SeekFrom::Start(0))
2898 .expect("rewind final handle");
2899 let mut bytes = Vec::new();
2900 published
2901 .read_to_end(&mut bytes)
2902 .expect("read final through sealed handle");
2903 assert_eq!(bytes, b"legacy artifact\n");
2904
2905 remove_file(published, PrivateFileLinkState::Single).expect("remove exact final link");
2906 remove(directory).expect("remove empty private directory");
2907 }
2908
2909 #[test]
2910 fn publication_pair_boundary_replacement_is_preserved_and_reported() {
2911 use std::io::Write as _;
2912
2913 let temporary = tempfile::tempdir().expect("temporary directory");
2914 let root = temporary.path().join("private");
2915 let staging_path = root.join("artifact.tmp");
2916 let final_path = root.join("artifact.bin");
2917 let moved_path = root.join("moved.bin");
2918 let directory = create(&root).expect("protected private directory");
2919 let mut writer = create_file(&staging_path, true).expect("protected staging file");
2920 writer.write_all(b"original\n").expect("write staging file");
2921 writer.sync_all().expect("sync staging file");
2922 let staging = seal_staging_file(writer).expect("seal staging handle");
2923 fs::hard_link(&staging_path, &final_path).expect("create legacy final hard link");
2924
2925 let recovery = complete_publication_pair_with_callback(
2926 staging,
2927 directory.as_handle(),
2928 OsStr::new("artifact.bin"),
2929 || {
2930 fs::rename(&final_path, &moved_path).expect("move original final link");
2931 fs::write(&final_path, b"replacement\n").expect("create replacement");
2932 },
2933 )
2934 .expect_err("replacement boundary must fail closed");
2935 assert_eq!(
2936 recovery.phase(),
2937 PrivatePublicationPairPhase::FinalSingleSealed
2938 );
2939 assert_eq!(
2940 recovery.error().kind(),
2941 PrivateDirectoryErrorKind::IdentityMismatch
2942 );
2943 let (_, original, _) = recovery.into_parts();
2944 verify_file(original.as_handle(), PrivateFileLinkState::Single)
2945 .expect("exact original retained and sealed");
2946
2947 remove_file(original, PrivateFileLinkState::Single)
2948 .expect("remove exact moved original");
2949 assert!(!moved_path.exists());
2950 assert_eq!(
2951 fs::read(&final_path).expect("read preserved replacement"),
2952 b"replacement\n"
2953 );
2954 fs::remove_file(&final_path).expect("remove preserved replacement");
2955 remove(directory).expect("remove empty private directory");
2956 }
2957
2958 #[test]
2959 fn private_regular_file_is_created_verified_and_removed_by_handle() {
2960 use std::io::Write as _;
2961
2962 let temporary = tempfile::tempdir().expect("temporary directory");
2963 let path = temporary.path().join("endpoint.json");
2964 let mut handle = create_file(&path, false).expect("protected private file");
2965 handle.write_all(b"{}\n").expect("write private file");
2966 handle.sync_all().expect("sync private file");
2967 verify_file(handle.as_handle(), PrivateFileLinkState::Single)
2968 .expect("retained private-file policy");
2969 assert!(fs::rename(&path, temporary.path().join("moved.json")).is_err());
2970 assert!(fs::remove_file(&path).is_err());
2971
2972 remove_file(handle, PrivateFileLinkState::Single)
2973 .expect("handle-bound private-file removal");
2974 assert!(!path.exists());
2975 }
2976
2977 #[test]
2978 fn private_file_sync_handle_flushes_and_blocks_mutation_or_replacement() {
2979 use std::io::{Read as _, Write as _};
2980
2981 let temporary = tempfile::tempdir().expect("temporary directory");
2982 let path = temporary.path().join("revision.json");
2983 let moved = temporary.path().join("moved.json");
2984 let mut creator = create_file(&path, false).expect("protected private file");
2985 creator
2986 .write_all(b"published\n")
2987 .expect("write final bytes");
2988 creator.sync_all().expect("initial file sync");
2989 drop(creator);
2990
2991 let sync = crate::windows_private_directory::open_private_file_for_sync(&path)
2992 .expect("open strict sync handle");
2993 sync.sync_all().expect("retry final file sync");
2994 verify_file(sync.as_handle(), PrivateFileLinkState::Single)
2995 .expect("sync handle remains strict");
2996 let mut reader = fs::File::open(&path).expect("compatible read peer");
2997 let mut bytes = Vec::new();
2998 reader.read_to_end(&mut bytes).expect("read final bytes");
2999 assert_eq!(bytes, b"published\n");
3000
3001 assert!(fs::OpenOptions::new().write(true).open(&path).is_err());
3002 assert!(fs::rename(&path, &moved).is_err());
3003 assert!(fs::remove_file(&path).is_err());
3004 assert!(!moved.exists());
3005
3006 drop(reader);
3007 drop(sync);
3008 fs::rename(&path, &moved).expect("rename after sync guard drop");
3009 fs::write(&path, b"replacement\n").expect("create replacement");
3010 assert_eq!(fs::read(&moved).expect("read original"), b"published\n");
3011 assert_eq!(fs::read(&path).expect("read replacement"), b"replacement\n");
3012 fs::remove_file(&path).expect("remove replacement");
3013 fs::remove_file(&moved).expect("remove original");
3014 }
3015
3016 #[test]
3017 fn private_file_sync_open_rejects_dacl_and_hardlink_policy_violations() {
3018 let temporary = tempfile::tempdir().expect("temporary directory");
3019 let dacl_path = temporary.path().join("dacl.json");
3020 drop(create_file(&dacl_path, false).expect("protected private file"));
3021 set_world_writable(&dacl_path);
3022
3023 let dacl_error =
3024 crate::windows_private_directory::open_private_file_for_sync(&dacl_path)
3025 .expect_err("permissive DACL must be rejected");
3026 assert!(matches!(
3027 dacl_error.kind(),
3028 PrivateDirectoryErrorKind::DaclUnprotected
3029 | PrivateDirectoryErrorKind::DaclPolicy(_)
3030 ));
3031 fs::remove_file(&dacl_path).expect("remove permissive test file");
3032
3033 let linked_path = temporary.path().join("linked.json");
3034 let extra_link = temporary.path().join("extra-link.json");
3035 drop(create_file(&linked_path, false).expect("protected private file"));
3036 fs::hard_link(&linked_path, &extra_link).expect("create extra hard link");
3037 let link_error =
3038 crate::windows_private_directory::open_private_file_for_sync(&linked_path)
3039 .expect_err("multiple links must be rejected");
3040 assert_eq!(link_error.kind(), PrivateDirectoryErrorKind::MultipleLinks);
3041 fs::remove_file(&extra_link).expect("remove extra hard link");
3042 fs::remove_file(&linked_path).expect("remove original hard link");
3043 }
3044
3045 #[test]
3046 fn private_file_sync_open_rejects_final_reparse_point_when_supported() {
3047 let temporary = tempfile::tempdir().expect("temporary directory");
3048 let target = temporary.path().join("target.json");
3049 let link = temporary.path().join("link.json");
3050 drop(create_file(&target, false).expect("protected private file"));
3051 if symlink_file(&target, &link).is_err() {
3052 fs::remove_file(&target).expect("remove target after skipped symlink test");
3053 return;
3054 }
3055
3056 let error = crate::windows_private_directory::open_private_file_for_sync(&link)
3057 .expect_err("final reparse point must be rejected");
3058 assert_eq!(error.kind(), PrivateDirectoryErrorKind::ReparsePoint);
3059 fs::remove_file(&link).expect("remove test symlink");
3060 fs::remove_file(&target).expect("remove target");
3061 }
3062
3063 #[test]
3064 fn private_regular_file_rejects_additional_hard_links() {
3065 let temporary = tempfile::tempdir().expect("temporary directory");
3066 let path = temporary.path().join("endpoint.json");
3067 let linked = temporary.path().join("linked.json");
3068 let handle = create_file(&path, false).expect("protected private file");
3069 fs::hard_link(&path, &linked).expect("test hard link");
3070
3071 let error = verify_file(handle.as_handle(), PrivateFileLinkState::Single)
3072 .expect_err("multiple links rejected");
3073 assert_eq!(error.kind(), PrivateDirectoryErrorKind::MultipleLinks);
3074 drop(handle);
3075 fs::remove_file(&linked).expect("remove added link");
3076 fs::remove_file(&path).expect("remove original link");
3077 }
3078
3079 #[test]
3080 fn removal_handle_survives_rename_and_removes_the_exact_file() {
3081 let temporary = tempfile::tempdir().expect("temporary directory");
3082 let original = temporary.path().join("endpoint.json");
3083 let renamed = temporary.path().join("renamed.json");
3084 let replacement = b"replacement\n";
3085 drop(create_file(&original, false).expect("protected private file"));
3086
3087 let handle = open_file_for_removal(&original).expect("open private file for removal");
3088 fs::rename(&original, &renamed).expect("rename while removal handle is retained");
3089 fs::write(&original, replacement).expect("create replacement at original path");
3090
3091 remove_file(handle, PrivateFileLinkState::Single)
3092 .expect("remove renamed object by retained handle");
3093 assert!(!renamed.exists());
3094 assert_eq!(fs::read(&original).expect("read replacement"), replacement);
3095 fs::remove_file(&original).expect("remove replacement");
3096 }
3097
3098 #[test]
3099 fn removal_open_rejects_additional_hard_links() {
3100 let temporary = tempfile::tempdir().expect("temporary directory");
3101 let original = temporary.path().join("endpoint.json");
3102 let linked = temporary.path().join("linked.json");
3103 drop(create_file(&original, false).expect("protected private file"));
3104 fs::hard_link(&original, &linked).expect("test hard link");
3105
3106 let error = open_file_for_removal(&original)
3107 .expect_err("removal open must reject multiple links");
3108 assert_eq!(error.kind(), PrivateDirectoryErrorKind::MultipleLinks);
3109 fs::remove_file(&linked).expect("remove added link");
3110 fs::remove_file(&original).expect("remove original link");
3111 }
3112
3113 #[test]
3114 fn publication_pair_can_be_reopened_and_staging_link_removed_by_handle() {
3115 let temporary = tempfile::tempdir().expect("temporary directory");
3116 let staged = temporary.path().join("endpoint.tmp");
3117 let published = temporary.path().join("endpoint.json");
3118 drop(create_file(&staged, true).expect("protected staging file"));
3119 fs::hard_link(&staged, &published).expect("publish final hard link");
3120
3121 let handle =
3122 open_file_for_removal_in_state(&staged, PrivateFileLinkState::PublicationPair)
3123 .expect("reopen publication pair for removal");
3124 remove_file(handle, PrivateFileLinkState::PublicationPair)
3125 .expect("remove exact staging link");
3126
3127 assert!(!staged.exists());
3128 let final_handle = open_file(&published).expect("open final private file");
3129 verify_file(final_handle.as_handle(), PrivateFileLinkState::Single)
3130 .expect("final file returned to single-link state");
3131 drop(final_handle);
3132 fs::remove_file(&published).expect("remove final file");
3133 }
3134
3135 #[test]
3136 fn publication_pair_removal_open_rejects_single_link_state() {
3137 let temporary = tempfile::tempdir().expect("temporary directory");
3138 let staged = temporary.path().join("endpoint.tmp");
3139 let published = temporary.path().join("endpoint.json");
3140 drop(create_file(&staged, true).expect("protected staging file"));
3141 fs::hard_link(&staged, &published).expect("publish final hard link");
3142
3143 let error = open_file_for_removal(&staged)
3144 .expect_err("single-link removal open must reject publication pair");
3145 assert_eq!(error.kind(), PrivateDirectoryErrorKind::MultipleLinks);
3146 fs::remove_file(&staged).expect("remove staging link");
3147 fs::remove_file(&published).expect("remove final link");
3148 }
3149
3150 #[test]
3151 fn regular_file_link_count_tracks_added_hard_link_without_acl_policy() {
3152 let temporary = tempfile::tempdir().expect("temporary directory");
3153 let original = temporary.path().join("ordinary.txt");
3154 let linked = temporary.path().join("linked.txt");
3155 fs::write(&original, b"ordinary\n").expect("ordinary inherited-ACL file");
3156 let handle = File::open(&original).expect("open ordinary file");
3157
3158 assert_eq!(
3159 regular_file_link_count(handle.as_handle()).expect("single link count"),
3160 1
3161 );
3162 fs::hard_link(&original, &linked).expect("add hard link");
3163 assert_eq!(
3164 regular_file_link_count(handle.as_handle()).expect("two-link count"),
3165 2
3166 );
3167
3168 drop(handle);
3169 fs::remove_file(&linked).expect("remove added link");
3170 fs::remove_file(&original).expect("remove original link");
3171 }
3172
3173 #[test]
3174 fn inherited_regular_file_acl_is_not_private_policy() {
3175 let temporary = tempfile::tempdir().expect("temporary directory");
3176 let path = temporary.path().join("ordinary.json");
3177 fs::write(&path, b"{}\n").expect("ordinary file");
3178 let file = File::open(&path).expect("open ordinary file");
3179
3180 assert!(verify_file(file.as_handle(), PrivateFileLinkState::Single).is_err());
3181 }
3182
3183 #[test]
3184 fn staging_file_publication_transitions_from_one_link_to_two_and_back() {
3185 use std::io::Write as _;
3186
3187 let temporary = tempfile::tempdir().expect("temporary directory");
3188 let staged = temporary.path().join("endpoint.tmp");
3189 let published = temporary.path().join("endpoint.json");
3190 let mut handle = create_file(&staged, true).expect("protected staging file");
3191 handle.write_all(b"{}\n").expect("write staging file");
3192 handle.sync_all().expect("sync staging file");
3193 verify_file(handle.as_handle(), PrivateFileLinkState::Single)
3194 .expect("single staging link");
3195
3196 fs::hard_link(&staged, &published).expect("create final hard link");
3197 verify_file(handle.as_handle(), PrivateFileLinkState::PublicationPair)
3198 .expect("staging and final links");
3199 remove_file(handle, PrivateFileLinkState::PublicationPair)
3200 .expect("remove exact staging link");
3201
3202 assert!(!staged.exists());
3203 let final_handle = open_file(&published).expect("open final private file");
3204 verify_file(final_handle.as_handle(), PrivateFileLinkState::Single)
3205 .expect("single final link");
3206 drop(final_handle);
3207 fs::remove_file(&published).expect("remove final file");
3208 }
3209
3210 #[test]
3211 fn sealed_staging_handle_is_path_free_and_denies_new_writers() {
3212 use std::{fs::OpenOptions, io::Write as _, os::windows::fs::OpenOptionsExt as _};
3213
3214 let temporary = tempfile::tempdir().expect("temporary directory");
3215 let original = temporary.path().join("endpoint.tmp");
3216 let renamed = temporary.path().join("renamed.tmp");
3217 let mut writer = create_file(&original, true).expect("protected staging file");
3218 writer.write_all(b"sealed\n").expect("write staging file");
3219 writer.sync_all().expect("sync staging file");
3220 fs::rename(&original, &renamed).expect("rename retained staging file");
3221
3222 let sealed = seal_staging_file(writer).expect("path-free staging seal");
3223 verify_file(sealed.as_handle(), PrivateFileLinkState::Single)
3224 .expect("sealed private-file policy");
3225 assert!(
3226 OpenOptions::new()
3227 .write(true)
3228 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
3229 .open(&renamed)
3230 .is_err()
3231 );
3232
3233 remove_file(sealed, PrivateFileLinkState::Single).expect("remove sealed staging file");
3234 assert!(!renamed.exists());
3235 }
3236
3237 #[test]
3238 fn sealed_staging_handle_supports_strict_dual_handle_publication() {
3239 use std::io::Write as _;
3240
3241 let temporary = tempfile::tempdir().expect("temporary directory");
3242 let staged = temporary.path().join("endpoint.tmp");
3243 let published = temporary.path().join("endpoint.json");
3244 let mut writer = create_file(&staged, true).expect("protected staging file");
3245 writer.write_all(b"sealed\n").expect("write staging file");
3246 writer.sync_all().expect("sync staging file");
3247 let sealed = seal_staging_file(writer).expect("sealed staging handle");
3248
3249 fs::hard_link(&staged, &published).expect("publish final hard link");
3250 verify_file(sealed.as_handle(), PrivateFileLinkState::PublicationPair)
3251 .expect("staging publication pair");
3252 let final_handle =
3253 open_file_for_removal_in_state(&published, PrivateFileLinkState::PublicationPair)
3254 .expect("strict final publication handle");
3255
3256 remove_file(final_handle, PrivateFileLinkState::PublicationPair)
3257 .expect("roll back exact final link");
3258 remove_file(sealed, PrivateFileLinkState::Single).expect("remove exact staging link");
3259 assert!(!staged.exists());
3260 assert!(!published.exists());
3261 }
3262
3263 #[test]
3264 fn competing_writer_makes_seal_fail_with_confirmed_exact_cleanup() {
3265 use std::{cell::RefCell, fs::OpenOptions, os::windows::fs::OpenOptionsExt as _};
3266
3267 let temporary = tempfile::tempdir().expect("temporary directory");
3268 let staged = temporary.path().join("endpoint.tmp");
3269 let writer = create_file(&staged, true).expect("protected staging file");
3270 let competing = RefCell::new(None);
3271
3272 let error = seal_staging_file_with_transition(writer, || {
3273 let writer = OpenOptions::new()
3274 .write(true)
3275 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
3276 .open(&staged)
3277 .expect("competing writer during transition");
3278 competing.replace(Some(writer));
3279 })
3280 .expect_err("competing writer must prevent sealed reopen");
3281
3282 assert_eq!(
3283 error.kind(),
3284 PrivateDirectoryErrorKind::WindowsApi(PrivateDirectoryOperation::OpenFile)
3285 );
3286 assert_eq!(
3287 error.cleanup_status(),
3288 PrivateDirectoryCleanupStatus::Confirmed
3289 );
3290 drop(competing.take());
3291 assert!(!staged.exists());
3292 }
3293
3294 #[test]
3295 fn closed_mutating_writer_requires_post_seal_content_revalidation() {
3296 use std::{
3297 fs::OpenOptions,
3298 io::{Read as _, Write as _},
3299 os::windows::fs::OpenOptionsExt as _,
3300 };
3301
3302 let temporary = tempfile::tempdir().expect("temporary directory");
3303 let staged = temporary.path().join("endpoint.tmp");
3304 let mut writer = create_file(&staged, true).expect("protected staging file");
3305 writer.write_all(b"trusted").expect("write trusted bytes");
3306 writer.sync_all().expect("sync trusted bytes");
3307
3308 let mut sealed = seal_staging_file_with_transition(writer, || {
3309 let mut competing = OpenOptions::new()
3310 .write(true)
3311 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
3312 .open(&staged)
3313 .expect("short-lived competing writer");
3314 competing.set_len(0).expect("truncate staging file");
3315 competing
3316 .write_all(b"mutated")
3317 .expect("mutate staging file");
3318 competing.sync_all().expect("sync mutation");
3319 })
3320 .expect("closed writer does not block sealed reopen");
3321 let mut bytes = Vec::new();
3322 sealed
3323 .read_to_end(&mut bytes)
3324 .expect("read sealed staging bytes");
3325 assert_eq!(bytes, b"mutated");
3326
3327 remove_file(sealed, PrivateFileLinkState::Single).expect("remove mutated staging file");
3328 assert!(!staged.exists());
3329 }
3330
3331 #[test]
3332 fn private_tree_removal_accepts_ordinary_inherited_descendants() {
3333 let temporary = tempfile::tempdir().expect("temporary directory");
3334 let root = temporary.path().join("private");
3335 let handle = create(&root).expect("strict private root");
3336 let nested = root.join("Products/Applications/App.app");
3337 fs::create_dir_all(&nested).expect("ordinary nested directories");
3338 fs::write(nested.join("App"), b"ordinary inherited child")
3339 .expect("ordinary nested file");
3340
3341 remove_directory_tree(handle).expect("exact recursive removal");
3342 assert!(!root.exists());
3343 }
3344
3345 #[test]
3346 fn private_tree_removal_rejects_hard_linked_descendant() {
3347 let temporary = tempfile::tempdir().expect("temporary directory");
3348 let root = temporary.path().join("private");
3349 let handle = create(&root).expect("strict private root");
3350 let child = root.join("child.txt");
3351 let linked = temporary.path().join("linked.txt");
3352 fs::write(&child, b"preserve").expect("ordinary nested file");
3353 fs::hard_link(&child, &linked).expect("additional hard link");
3354
3355 let error = remove_directory_tree(handle).expect_err("hard link must fail closed");
3356 assert_eq!(error.kind(), PrivateDirectoryErrorKind::MultipleLinks);
3357 assert_eq!(
3358 error.cleanup_status(),
3359 PrivateDirectoryCleanupStatus::Uncertain
3360 );
3361 assert_eq!(fs::read(&child).expect("original survives"), b"preserve");
3362 assert_eq!(
3363 fs::read(&linked).expect("linked copy survives"),
3364 b"preserve"
3365 );
3366
3367 fs::remove_file(&linked).expect("remove added link");
3368 fs::remove_file(&child).expect("remove original link");
3369 fs::remove_dir(&root).expect("remove root");
3370 }
3371
3372 #[test]
3373 fn private_tree_guard_blocks_root_replacement_at_removal_boundary() {
3374 use std::cell::RefCell;
3375
3376 let temporary = tempfile::tempdir().expect("temporary directory");
3377 let root = temporary.path().join("private");
3378 let moved = temporary.path().join("moved");
3379 let replacement = temporary.path().join("replacement");
3380 let replacement_marker = replacement.join("preserve.txt");
3381 let handle = create(&root).expect("strict private root");
3382 fs::write(root.join("owned.txt"), b"owned").expect("owned nested file");
3383 fs::create_dir(&replacement).expect("replacement candidate");
3384 fs::write(&replacement_marker, b"preserve").expect("replacement marker");
3385 let attempts = RefCell::new(None);
3386
3387 remove_directory_tree_with_callback(handle, |_| {
3388 let move_owned = fs::rename(&root, &moved);
3389 let install_replacement = fs::rename(&replacement, &root);
3390 attempts.replace(Some((move_owned, install_replacement)));
3391 })
3392 .expect("remove retained original tree");
3393
3394 let (move_owned, install_replacement) = attempts
3395 .into_inner()
3396 .expect("replacement attempts recorded");
3397 assert!(move_owned.is_err());
3398 assert!(install_replacement.is_err());
3399 assert!(!root.exists());
3400 assert!(!moved.exists());
3401 assert_eq!(
3402 fs::read(&replacement_marker).expect("replacement survives"),
3403 b"preserve"
3404 );
3405 }
3406
3407 #[test]
3408 fn private_tree_cleanup_failure_is_reported_uncertain() {
3409 use std::{fs::OpenOptions, os::windows::fs::OpenOptionsExt as _};
3410
3411 let temporary = tempfile::tempdir().expect("temporary directory");
3412 let root = temporary.path().join("private");
3413 let handle = create(&root).expect("strict private root");
3414 let child = root.join("busy.txt");
3415 fs::write(&child, b"busy").expect("ordinary nested file");
3416 let busy = OpenOptions::new()
3417 .read(true)
3418 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
3419 .open(&child)
3420 .expect("non-delete-sharing competing handle");
3421
3422 let error = remove_directory_tree(handle).expect_err("busy child blocks cleanup");
3423 assert_eq!(
3424 error.kind(),
3425 PrivateDirectoryErrorKind::WindowsApi(PrivateDirectoryOperation::OpenTreeEntry)
3426 );
3427 assert_eq!(
3428 error.cleanup_status(),
3429 PrivateDirectoryCleanupStatus::Uncertain
3430 );
3431 assert!(root.is_dir());
3432 assert_eq!(fs::read(&child).expect("busy child survives"), b"busy");
3433
3434 drop(busy);
3435 fs::remove_file(&child).expect("remove busy child");
3436 fs::remove_dir(&root).expect("remove root");
3437 }
3438
3439 #[test]
3440 fn private_tree_removal_rejects_nested_reparse_without_touching_target() {
3441 let temporary = tempfile::tempdir().expect("temporary directory");
3442 let root = temporary.path().join("private");
3443 let target = temporary.path().join("target");
3444 let marker = target.join("preserve.txt");
3445 let linked = root.join("linked");
3446 let handle = create(&root).expect("strict private root");
3447 fs::create_dir(&target).expect("external target");
3448 fs::write(&marker, b"preserve").expect("external marker");
3449 if symlink_dir(&target, &linked).is_err() {
3450 drop(handle);
3451 fs::remove_dir(&root).expect("remove unused root");
3452 return;
3453 }
3454
3455 let error = remove_directory_tree(handle).expect_err("reparse must fail closed");
3456 assert_eq!(error.kind(), PrivateDirectoryErrorKind::ReparsePoint);
3457 assert_eq!(
3458 error.cleanup_status(),
3459 PrivateDirectoryCleanupStatus::Uncertain
3460 );
3461 assert_eq!(fs::read(&marker).expect("target survives"), b"preserve");
3462
3463 fs::remove_dir(&linked).expect("remove directory symlink");
3464 assert_eq!(
3465 fs::symlink_metadata(&linked)
3466 .expect_err("directory symlink is absent")
3467 .kind(),
3468 std::io::ErrorKind::NotFound
3469 );
3470 assert!(target.is_dir());
3471 assert_eq!(
3472 fs::read(&marker).expect("target survives link cleanup"),
3473 b"preserve"
3474 );
3475 fs::remove_dir(&root).expect("remove root");
3476 }
3477
3478 #[test]
3479 fn private_objects_support_extended_length_absolute_paths() {
3480 let temporary = tempfile::tempdir().expect("temporary directory");
3481 let mut parent = temporary.path().to_path_buf();
3482 while parent.as_os_str().encode_wide().count() < 280 {
3483 parent.push("long-private-path-segment");
3484 }
3485 fs::create_dir_all(&parent).expect("long parent path");
3486 let directory_path = parent.join("private");
3487 let directory = create(&directory_path).expect("long private directory path");
3488 verify(directory.as_handle()).expect("long directory policy");
3489
3490 let file_path = directory_path.join("endpoint.json");
3491 let file = create_file(&file_path, false).expect("long private file path");
3492 verify_file(file.as_handle(), PrivateFileLinkState::Single).expect("long file policy");
3493 remove_file(file, PrivateFileLinkState::Single).expect("remove long private file");
3494 remove(directory).expect("remove long private directory");
3495 }
3496
3497 fn set_world_writable(path: &Path) {
3498 let wide = wide_path(path).expect("wide path");
3499 let handle = open_test_directory(&wide);
3500 let world = Sid::well_known(WinWorldSid).expect("world SID");
3501 let mut acl = build_acl(&[&world]).expect("world ACL");
3502 let mut descriptor = SECURITY_DESCRIPTOR::default();
3503 unsafe {
3506 assert_ne!(
3507 InitializeSecurityDescriptor(
3508 (&raw mut descriptor).cast(),
3509 SECURITY_DESCRIPTOR_REVISION,
3510 ),
3511 0
3512 );
3513 assert_ne!(
3514 SetSecurityDescriptorDacl(
3515 (&raw mut descriptor).cast(),
3516 1,
3517 acl.as_mut_ptr().cast(),
3518 0,
3519 ),
3520 0
3521 );
3522 assert_eq!(
3523 SetKernelObjectSecurity(
3524 handle.as_raw_handle(),
3525 DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION,
3526 (&raw mut descriptor).cast(),
3527 ),
3528 1
3529 );
3530 }
3531 }
3532
3533 fn open_test_directory(path: &[u16]) -> File {
3534 let handle = unsafe {
3537 CreateFileW(
3538 path.as_ptr(),
3539 READ_CONTROL | WRITE_DAC | FILE_READ_ATTRIBUTES,
3540 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
3541 ptr::null(),
3542 OPEN_EXISTING,
3543 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
3544 ptr::null_mut(),
3545 )
3546 };
3547 assert_ne!(handle, INVALID_HANDLE_VALUE);
3548 unsafe { File::from_raw_handle(handle) }
3550 }
3551 }
3552}
3553
3554#[cfg(windows)]
3574pub fn create_private_directory(
3575 path: &std::path::Path,
3576) -> Result<std::fs::File, PrivateDirectoryError> {
3577 platform::create(path)
3578}
3579
3580#[cfg(windows)]
3588pub fn open_private_directory(
3589 path: &std::path::Path,
3590) -> Result<std::fs::File, PrivateDirectoryError> {
3591 platform::open(path)
3592}
3593
3594#[cfg(windows)]
3613pub fn open_private_directory_read_guard(
3614 path: &std::path::Path,
3615) -> Result<std::fs::File, PrivateDirectoryError> {
3616 platform::open_read_guard(path)
3617}
3618
3619#[cfg(windows)]
3629pub fn create_private_file(path: &std::path::Path) -> Result<std::fs::File, PrivateDirectoryError> {
3630 platform::create_file(path, false)
3631}
3632
3633#[cfg(windows)]
3648pub fn create_private_lock_file(
3649 path: &std::path::Path,
3650) -> Result<std::fs::File, PrivateDirectoryError> {
3651 platform::create_lock_file(path)
3652}
3653
3654#[cfg(windows)]
3665pub fn open_private_lock_file(
3666 path: &std::path::Path,
3667) -> Result<std::fs::File, PrivateDirectoryError> {
3668 platform::open_lock_file(path)
3669}
3670
3671#[cfg(windows)]
3693pub fn publish_private_file_handle_create_new(
3694 staging: std::fs::File,
3695 destination_directory: std::os::windows::io::BorrowedHandle<'_>,
3696 destination_name: &std::ffi::OsStr,
3697) -> Result<std::fs::File, PrivatePublicationError> {
3698 platform::publish_file_create_new(staging, destination_directory, destination_name)
3699}
3700
3701#[cfg(windows)]
3721pub fn complete_private_publication_pair(
3722 staging: std::fs::File,
3723 destination_directory: std::os::windows::io::BorrowedHandle<'_>,
3724 destination_name: &std::ffi::OsStr,
3725) -> Result<std::fs::File, PrivatePublicationPairError> {
3726 platform::complete_publication_pair(staging, destination_directory, destination_name)
3727}
3728
3729#[cfg(windows)]
3740pub fn create_private_staging_file(
3741 path: &std::path::Path,
3742) -> Result<std::fs::File, PrivateDirectoryError> {
3743 platform::create_file(path, true)
3744}
3745
3746#[cfg(windows)]
3764pub fn seal_private_staging_file(
3765 file: std::fs::File,
3766) -> Result<std::fs::File, PrivateDirectoryError> {
3767 platform::seal_staging_file(file)
3768}
3769
3770#[cfg(windows)]
3780pub fn open_private_file(path: &std::path::Path) -> Result<std::fs::File, PrivateDirectoryError> {
3781 platform::open_file(path)
3782}
3783
3784#[cfg(windows)]
3799pub fn open_private_file_for_sync(
3800 path: &std::path::Path,
3801) -> Result<std::fs::File, PrivateDirectoryError> {
3802 platform::open_file_for_sync(path)
3803}
3804
3805#[cfg(windows)]
3817pub fn open_private_file_for_removal(
3818 path: &std::path::Path,
3819) -> Result<std::fs::File, PrivateDirectoryError> {
3820 platform::open_file_for_removal(path)
3821}
3822
3823#[cfg(windows)]
3837pub fn open_private_file_for_removal_in_state(
3838 path: &std::path::Path,
3839 link_state: PrivateFileLinkState,
3840) -> Result<std::fs::File, PrivateDirectoryError> {
3841 platform::open_file_for_removal_in_state(path, link_state)
3842}
3843
3844#[cfg(windows)]
3851pub fn verify_private_directory_handle(
3852 handle: std::os::windows::io::BorrowedHandle<'_>,
3853) -> Result<(), PrivateDirectoryError> {
3854 platform::verify(handle)
3855}
3856
3857#[cfg(windows)]
3877pub fn sync_private_directory_handle(
3878 directory: std::os::windows::io::BorrowedHandle<'_>,
3879) -> Result<(), PrivateDirectoryError> {
3880 platform::sync_directory(directory)
3881}
3882
3883#[cfg(windows)]
3892pub fn verify_private_file_handle(
3893 handle: std::os::windows::io::BorrowedHandle<'_>,
3894) -> Result<(), PrivateDirectoryError> {
3895 platform::verify_file(handle, PrivateFileLinkState::Single)
3896}
3897
3898#[cfg(windows)]
3904pub fn verify_private_file_handle_in_state(
3905 handle: std::os::windows::io::BorrowedHandle<'_>,
3906 link_state: PrivateFileLinkState,
3907) -> Result<(), PrivateDirectoryError> {
3908 platform::verify_file(handle, link_state)
3909}
3910
3911#[cfg(windows)]
3923pub fn regular_file_link_count(
3924 handle: std::os::windows::io::BorrowedHandle<'_>,
3925) -> Result<u32, PrivateDirectoryError> {
3926 platform::regular_file_link_count(handle)
3927}
3928
3929#[cfg(windows)]
3940pub fn remove_private_directory_handle(
3941 directory: std::fs::File,
3942) -> Result<(), PrivateDirectoryError> {
3943 platform::remove(directory)
3944}
3945
3946#[cfg(windows)]
3962pub fn remove_private_directory_tree_handle(
3963 directory: std::fs::File,
3964) -> Result<(), PrivateDirectoryError> {
3965 platform::remove_directory_tree(directory)
3966}
3967
3968#[cfg(windows)]
3975pub fn remove_private_file_handle(file: std::fs::File) -> Result<(), PrivateDirectoryError> {
3976 platform::remove_file(file, PrivateFileLinkState::Single)
3977}
3978
3979#[cfg(windows)]
3990pub fn remove_private_file_handle_in_state(
3991 file: std::fs::File,
3992 link_state: PrivateFileLinkState,
3993) -> Result<(), PrivateDirectoryError> {
3994 platform::remove_file(file, link_state)
3995}
3996
3997#[cfg(test)]
3998mod tests {
3999 use super::*;
4000
4001 const USER: &[u8] = &[1, 1, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0];
4002 const SYSTEM: &[u8] = &[1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0];
4003 const ADMINISTRATORS: &[u8] = &[1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0];
4004
4005 #[test]
4006 fn exact_private_acl_is_accepted_independent_of_ace_order() {
4007 let raw = acl(&[
4008 ace(
4009 SYSTEM,
4010 ACCESS_ALLOWED_ACE_TYPE,
4011 DIRECTORY_INHERIT_FLAGS,
4012 FILE_ALL_ACCESS_MASK,
4013 ),
4014 ace(
4015 USER,
4016 ACCESS_ALLOWED_ACE_TYPE,
4017 DIRECTORY_INHERIT_FLAGS,
4018 FILE_ALL_ACCESS_MASK,
4019 ),
4020 ace(
4021 ADMINISTRATORS,
4022 ACCESS_ALLOWED_ACE_TYPE,
4023 DIRECTORY_INHERIT_FLAGS,
4024 FILE_ALL_ACCESS_MASK,
4025 ),
4026 ]);
4027 assert_eq!(validate_acl(&raw, &[USER, SYSTEM, ADMINISTRATORS]), Ok(()));
4028 }
4029
4030 #[test]
4031 fn regular_file_acl_requires_non_inheriting_aces() {
4032 let raw = acl(&[
4033 ace(
4034 USER,
4035 ACCESS_ALLOWED_ACE_TYPE,
4036 FILE_ACE_FLAGS,
4037 FILE_ALL_ACCESS_MASK,
4038 ),
4039 ace(
4040 SYSTEM,
4041 ACCESS_ALLOWED_ACE_TYPE,
4042 FILE_ACE_FLAGS,
4043 FILE_ALL_ACCESS_MASK,
4044 ),
4045 ace(
4046 ADMINISTRATORS,
4047 ACCESS_ALLOWED_ACE_TYPE,
4048 FILE_ACE_FLAGS,
4049 FILE_ALL_ACCESS_MASK,
4050 ),
4051 ]);
4052 assert_eq!(
4053 validate_acl_with_flags(&raw, &[USER, SYSTEM, ADMINISTRATORS], FILE_ACE_FLAGS),
4054 Ok(())
4055 );
4056 assert_eq!(
4057 validate_acl(&raw, &[USER, SYSTEM, ADMINISTRATORS]),
4058 Err(PrivateDirectoryAclViolation::UnexpectedAceFlags)
4059 );
4060 }
4061
4062 #[test]
4063 fn unknown_allow_ace_is_rejected() {
4064 let unknown = [1, 1, 0, 0, 0, 0, 0, 5, 11, 0, 0, 0];
4065 let raw = acl(&[
4066 ace(
4067 USER,
4068 ACCESS_ALLOWED_ACE_TYPE,
4069 DIRECTORY_INHERIT_FLAGS,
4070 FILE_ALL_ACCESS_MASK,
4071 ),
4072 ace(
4073 SYSTEM,
4074 ACCESS_ALLOWED_ACE_TYPE,
4075 DIRECTORY_INHERIT_FLAGS,
4076 FILE_ALL_ACCESS_MASK,
4077 ),
4078 ace(
4079 &unknown,
4080 ACCESS_ALLOWED_ACE_TYPE,
4081 DIRECTORY_INHERIT_FLAGS,
4082 FILE_ALL_ACCESS_MASK,
4083 ),
4084 ]);
4085 assert_eq!(
4086 validate_acl(&raw, &[USER, SYSTEM, ADMINISTRATORS]),
4087 Err(PrivateDirectoryAclViolation::UnknownPrincipal)
4088 );
4089 }
4090
4091 #[test]
4092 fn deny_object_and_inherited_aces_are_rejected() {
4093 for (ace_type, flags, expected) in [
4094 (
4095 1,
4096 DIRECTORY_INHERIT_FLAGS,
4097 PrivateDirectoryAclViolation::UnsupportedAceType,
4098 ),
4099 (
4100 ACCESS_ALLOWED_ACE_TYPE,
4101 DIRECTORY_INHERIT_FLAGS | 0x10,
4102 PrivateDirectoryAclViolation::UnexpectedAceFlags,
4103 ),
4104 (
4105 5,
4106 DIRECTORY_INHERIT_FLAGS,
4107 PrivateDirectoryAclViolation::UnsupportedAceType,
4108 ),
4109 ] {
4110 let raw = acl(&[
4111 ace(USER, ace_type, flags, FILE_ALL_ACCESS_MASK),
4112 ace(
4113 SYSTEM,
4114 ACCESS_ALLOWED_ACE_TYPE,
4115 DIRECTORY_INHERIT_FLAGS,
4116 FILE_ALL_ACCESS_MASK,
4117 ),
4118 ace(
4119 ADMINISTRATORS,
4120 ACCESS_ALLOWED_ACE_TYPE,
4121 DIRECTORY_INHERIT_FLAGS,
4122 FILE_ALL_ACCESS_MASK,
4123 ),
4124 ]);
4125 assert_eq!(
4126 validate_acl(&raw, &[USER, SYSTEM, ADMINISTRATORS]),
4127 Err(expected)
4128 );
4129 }
4130 }
4131
4132 #[test]
4133 fn broad_or_root_inherit_only_access_is_rejected() {
4134 for (flags, mask, expected) in [
4135 (
4136 DIRECTORY_INHERIT_FLAGS,
4137 FILE_ALL_ACCESS_MASK | 0x0040_0000,
4138 PrivateDirectoryAclViolation::UnexpectedAccessMask,
4139 ),
4140 (
4141 DIRECTORY_INHERIT_FLAGS | 0x08,
4142 FILE_ALL_ACCESS_MASK,
4143 PrivateDirectoryAclViolation::UnexpectedAceFlags,
4144 ),
4145 ] {
4146 let raw = acl(&[
4147 ace(USER, ACCESS_ALLOWED_ACE_TYPE, flags, mask),
4148 ace(
4149 SYSTEM,
4150 ACCESS_ALLOWED_ACE_TYPE,
4151 DIRECTORY_INHERIT_FLAGS,
4152 FILE_ALL_ACCESS_MASK,
4153 ),
4154 ace(
4155 ADMINISTRATORS,
4156 ACCESS_ALLOWED_ACE_TYPE,
4157 DIRECTORY_INHERIT_FLAGS,
4158 FILE_ALL_ACCESS_MASK,
4159 ),
4160 ]);
4161 assert_eq!(
4162 validate_acl(&raw, &[USER, SYSTEM, ADMINISTRATORS]),
4163 Err(expected)
4164 );
4165 }
4166 }
4167
4168 #[test]
4169 fn duplicate_missing_and_malformed_principals_are_rejected() {
4170 let duplicate = acl(&[
4171 ace(
4172 USER,
4173 ACCESS_ALLOWED_ACE_TYPE,
4174 DIRECTORY_INHERIT_FLAGS,
4175 FILE_ALL_ACCESS_MASK,
4176 ),
4177 ace(
4178 USER,
4179 ACCESS_ALLOWED_ACE_TYPE,
4180 DIRECTORY_INHERIT_FLAGS,
4181 FILE_ALL_ACCESS_MASK,
4182 ),
4183 ace(
4184 ADMINISTRATORS,
4185 ACCESS_ALLOWED_ACE_TYPE,
4186 DIRECTORY_INHERIT_FLAGS,
4187 FILE_ALL_ACCESS_MASK,
4188 ),
4189 ]);
4190 assert_eq!(
4191 validate_acl(&duplicate, &[USER, SYSTEM, ADMINISTRATORS]),
4192 Err(PrivateDirectoryAclViolation::DuplicatePrincipal)
4193 );
4194
4195 let malformed_sid = [1, 2, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0];
4196 let malformed = acl(&[
4197 ace(
4198 &malformed_sid,
4199 ACCESS_ALLOWED_ACE_TYPE,
4200 DIRECTORY_INHERIT_FLAGS,
4201 FILE_ALL_ACCESS_MASK,
4202 ),
4203 ace(
4204 SYSTEM,
4205 ACCESS_ALLOWED_ACE_TYPE,
4206 DIRECTORY_INHERIT_FLAGS,
4207 FILE_ALL_ACCESS_MASK,
4208 ),
4209 ace(
4210 ADMINISTRATORS,
4211 ACCESS_ALLOWED_ACE_TYPE,
4212 DIRECTORY_INHERIT_FLAGS,
4213 FILE_ALL_ACCESS_MASK,
4214 ),
4215 ]);
4216 assert_eq!(
4217 validate_acl(&malformed, &[USER, SYSTEM, ADMINISTRATORS]),
4218 Err(PrivateDirectoryAclViolation::InvalidSid)
4219 );
4220
4221 let missing = acl(&[
4222 ace(
4223 USER,
4224 ACCESS_ALLOWED_ACE_TYPE,
4225 DIRECTORY_INHERIT_FLAGS,
4226 FILE_ALL_ACCESS_MASK,
4227 ),
4228 ace(
4229 SYSTEM,
4230 ACCESS_ALLOWED_ACE_TYPE,
4231 DIRECTORY_INHERIT_FLAGS,
4232 FILE_ALL_ACCESS_MASK,
4233 ),
4234 ]);
4235 assert_eq!(
4236 validate_acl(&missing, &[USER, SYSTEM, ADMINISTRATORS]),
4237 Err(PrivateDirectoryAclViolation::WrongAceCount)
4238 );
4239
4240 let mut truncated = acl(&[
4241 ace(
4242 USER,
4243 ACCESS_ALLOWED_ACE_TYPE,
4244 DIRECTORY_INHERIT_FLAGS,
4245 FILE_ALL_ACCESS_MASK,
4246 ),
4247 ace(
4248 SYSTEM,
4249 ACCESS_ALLOWED_ACE_TYPE,
4250 DIRECTORY_INHERIT_FLAGS,
4251 FILE_ALL_ACCESS_MASK,
4252 ),
4253 ace(
4254 ADMINISTRATORS,
4255 ACCESS_ALLOWED_ACE_TYPE,
4256 DIRECTORY_INHERIT_FLAGS,
4257 FILE_ALL_ACCESS_MASK,
4258 ),
4259 ]);
4260 truncated.pop();
4261 assert_eq!(
4262 validate_acl(&truncated, &[USER, SYSTEM, ADMINISTRATORS]),
4263 Err(PrivateDirectoryAclViolation::Malformed)
4264 );
4265 }
4266
4267 #[test]
4268 fn duplicate_fixed_principal_uses_one_exact_ace() {
4269 let deduplicated = acl(&[
4270 ace(
4271 SYSTEM,
4272 ACCESS_ALLOWED_ACE_TYPE,
4273 DIRECTORY_INHERIT_FLAGS,
4274 FILE_ALL_ACCESS_MASK,
4275 ),
4276 ace(
4277 ADMINISTRATORS,
4278 ACCESS_ALLOWED_ACE_TYPE,
4279 DIRECTORY_INHERIT_FLAGS,
4280 FILE_ALL_ACCESS_MASK,
4281 ),
4282 ]);
4283 assert_eq!(
4284 validate_acl(&deduplicated, &[SYSTEM, ADMINISTRATORS]),
4285 Ok(())
4286 );
4287 }
4288
4289 fn acl(aces: &[Vec<u8>]) -> Vec<u8> {
4290 let length = ACL_HEADER_BYTES + aces.iter().map(Vec::len).sum::<usize>();
4291 let mut raw = vec![0; ACL_HEADER_BYTES];
4292 raw[0] = ACL_REVISION;
4293 raw[2..4].copy_from_slice(
4294 &u16::try_from(length)
4295 .expect("test ACL length")
4296 .to_le_bytes(),
4297 );
4298 raw[4..6].copy_from_slice(
4299 &u16::try_from(aces.len())
4300 .expect("test ACE count")
4301 .to_le_bytes(),
4302 );
4303 for ace in aces {
4304 raw.extend_from_slice(ace);
4305 }
4306 raw
4307 }
4308
4309 fn ace(sid: &[u8], ace_type: u8, flags: u8, mask: u32) -> Vec<u8> {
4310 let length = ACCESS_ALLOWED_ACE_PREFIX_BYTES + sid.len();
4311 let mut raw = Vec::with_capacity(length);
4312 raw.push(ace_type);
4313 raw.push(flags);
4314 raw.extend_from_slice(
4315 &u16::try_from(length)
4316 .expect("test ACE length")
4317 .to_le_bytes(),
4318 );
4319 raw.extend_from_slice(&mask.to_le_bytes());
4320 raw.extend_from_slice(sid);
4321 raw
4322 }
4323}