Skip to main content

heddle_fs_prims/
directory.rs

1// SPDX-License-Identifier: Apache-2.0
2//! A held directory capability for repository installation. All child names
3//! are single components. Unix uses *at syscalls with no-follow opens; Windows
4//! opens children relative to handles and pins every ancestor before using
5//! write-through Win32 publication paths.
6use std::{
7    ffi::OsStr,
8    fs::File,
9    io,
10    path::{Component, Path},
11};
12
13pub fn relative(path: &Path) -> io::Result<()> {
14    if path.as_os_str().is_empty() || !path.components().all(|c| matches!(c, Component::Normal(_)))
15    {
16        return Err(io::Error::new(
17            io::ErrorKind::InvalidInput,
18            "installation paths must be relative, without traversal",
19        ));
20    }
21    #[cfg(windows)]
22    for component in path.components() {
23        let value = component.as_os_str().to_string_lossy();
24        let stem = value
25            .split('.')
26            .next()
27            .unwrap_or_default()
28            .trim_end_matches(' ')
29            .to_ascii_uppercase();
30        let device = matches!(
31            stem.as_str(),
32            "CON" | "PRN" | "AUX" | "NUL" | "CONIN$" | "CONOUT$" | "CLOCK$"
33        ) || (["COM", "LPT"].iter().any(|prefix| {
34            stem.strip_prefix(prefix).is_some_and(|n| {
35                matches!(
36                    n,
37                    "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "¹" | "²" | "³"
38                )
39            })
40        }));
41        if value.contains(':') || value.ends_with(['.', ' ']) || device {
42            return Err(io::Error::new(
43                io::ErrorKind::InvalidInput,
44                "Windows aliases and alternate streams are not installation paths",
45            ));
46        }
47    }
48    Ok(())
49}
50fn name(name: &OsStr) -> io::Result<&Path> {
51    let path = Path::new(name);
52    relative(path)?;
53    if path.components().count() != 1 {
54        return Err(io::Error::new(
55            io::ErrorKind::InvalidInput,
56            "expected one child name",
57        ));
58    }
59    Ok(path)
60}
61
62pub struct Directory {
63    file: File,
64    #[cfg(windows)]
65    path: std::path::PathBuf,
66    #[cfg(windows)]
67    ancestors: Vec<File>,
68    #[cfg(windows)]
69    pin: std::sync::Arc<File>,
70}
71impl Directory {
72    pub fn open(path: &Path) -> io::Result<Self> {
73        platform::open(path)
74    }
75    pub fn child(&self, child: &OsStr) -> io::Result<Self> {
76        platform::child(self, name(child)?)
77    }
78    pub fn descend(&self, path: &Path) -> io::Result<Self> {
79        let mut directory = self.try_clone()?;
80        if !path.as_os_str().is_empty() {
81            relative(path)?;
82            for component in path.components() {
83                directory = directory.child(component.as_os_str())?;
84            }
85        }
86        Ok(directory)
87    }
88    pub fn try_clone(&self) -> io::Result<Self> {
89        platform::clone(self)
90    }
91    pub fn identity(&self) -> io::Result<Vec<u8>> {
92        platform::identity(&self.file)
93    }
94    pub fn open_file(&self, child: &OsStr) -> io::Result<File> {
95        platform::file(self, name(child)?, false)
96    }
97    pub fn create_file(&self, child: &OsStr) -> io::Result<File> {
98        platform::file(self, name(child)?, true)
99    }
100    pub fn hard_link(&self, source: &OsStr, destination: &OsStr) -> io::Result<()> {
101        platform::link(self, name(source)?, name(destination)?)
102    }
103    /// Source data must be flushed first. Persist both directory entries before
104    /// return, independently of reconstructible-clone durability suppression.
105    pub fn durable_rename(&self, source: &OsStr, destination: &OsStr) -> io::Result<()> {
106        platform::rename(self, name(source)?, name(destination)?)
107    }
108    pub fn remove_file(&self, child: &OsStr) -> io::Result<()> {
109        platform::remove(self, name(child)?)
110    }
111}
112
113#[cfg(unix)]
114mod platform {
115    use std::{
116        ffi::CString,
117        os::{
118            fd::{AsRawFd, FromRawFd},
119            unix::{
120                ffi::OsStrExt,
121                fs::{MetadataExt, OpenOptionsExt},
122            },
123        },
124    };
125
126    use super::*;
127    fn c(path: &Path) -> io::Result<CString> {
128        CString::new(path.as_os_str().as_bytes())
129            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "NUL in path"))
130    }
131    fn result(rc: i32) -> io::Result<()> {
132        if rc == 0 {
133            Ok(())
134        } else {
135            Err(io::Error::last_os_error())
136        }
137    }
138    fn open_at(parent: &Directory, path: &Path, flags: i32) -> io::Result<File> {
139        let path = c(path)?;
140        // SAFETY: live directory descriptor and NUL-terminated child name.
141        let fd = unsafe {
142            libc::openat(
143                parent.file.as_raw_fd(),
144                path.as_ptr(),
145                flags | libc::O_CLOEXEC | libc::O_NOFOLLOW,
146                0o600,
147            )
148        };
149        if fd < 0 {
150            return Err(io::Error::last_os_error());
151        }
152        // SAFETY: successful openat transferred this descriptor to us.
153        Ok(unsafe { File::from_raw_fd(fd) })
154    }
155    pub fn open(path: &Path) -> io::Result<Directory> {
156        let file = std::fs::OpenOptions::new()
157            .read(true)
158            .custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
159            .open(path)?;
160        Ok(Directory { file })
161    }
162    pub fn child(parent: &Directory, path: &Path) -> io::Result<Directory> {
163        Ok(Directory {
164            file: open_at(parent, path, libc::O_RDONLY | libc::O_DIRECTORY)?,
165        })
166    }
167    pub fn clone(directory: &Directory) -> io::Result<Directory> {
168        Ok(Directory {
169            file: directory.file.try_clone()?,
170        })
171    }
172    pub fn identity(file: &File) -> io::Result<Vec<u8>> {
173        let meta = file.metadata()?;
174        Ok([meta.dev().to_le_bytes(), meta.ino().to_le_bytes()].concat())
175    }
176    pub fn file(directory: &Directory, path: &Path, create: bool) -> io::Result<File> {
177        let flags = if create {
178            libc::O_RDWR | libc::O_CREAT | libc::O_EXCL
179        } else {
180            libc::O_RDONLY | libc::O_NONBLOCK
181        };
182        let file = open_at(directory, path, flags)?;
183        if !file.metadata()?.is_file() {
184            return Err(io::Error::new(
185                io::ErrorKind::InvalidInput,
186                "artifact must be a regular file",
187            ));
188        }
189        Ok(file)
190    }
191    pub fn link(dir: &Directory, source: &Path, destination: &Path) -> io::Result<()> {
192        let source = c(source)?;
193        let destination = c(destination)?;
194        // SAFETY: held directory and validated, terminated child names. linkat
195        // does not follow the source symlink.
196        result(unsafe {
197            libc::linkat(
198                dir.file.as_raw_fd(),
199                source.as_ptr(),
200                dir.file.as_raw_fd(),
201                destination.as_ptr(),
202                0,
203            )
204        })
205    }
206    pub fn rename(dir: &Directory, source: &Path, destination: &Path) -> io::Result<()> {
207        let source = c(source)?;
208        let destination = c(destination)?;
209        // SAFETY: both names are relative to the same live directory handle.
210        result(unsafe {
211            libc::renameat(
212                dir.file.as_raw_fd(),
213                source.as_ptr(),
214                dir.file.as_raw_fd(),
215                destination.as_ptr(),
216            )
217        })?;
218        dir.file.sync_all()
219    }
220    pub fn remove(dir: &Directory, child: &Path) -> io::Result<()> {
221        let child = c(child)?;
222        // SAFETY: held directory handle and terminated child name.
223        result(unsafe { libc::unlinkat(dir.file.as_raw_fd(), child.as_ptr(), 0) })?;
224        dir.file.sync_all()
225    }
226}
227
228#[cfg(windows)]
229mod platform {
230    use std::os::windows::{
231        ffi::OsStrExt,
232        fs::{MetadataExt, OpenOptionsExt},
233        io::{AsRawHandle, FromRawHandle},
234    };
235
236    use windows_sys::{
237        Wdk::{
238            Foundation::OBJECT_ATTRIBUTES,
239            Storage::FileSystem::{
240                FILE_CREATE, FILE_DELETE_ON_CLOSE, FILE_DIRECTORY_FILE, FILE_NON_DIRECTORY_FILE,
241                FILE_OPEN, FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, NtCreateFile,
242            },
243        },
244        Win32::{
245            Foundation::{
246                OBJ_CASE_INSENSITIVE, OBJ_DONT_REPARSE, RtlNtStatusToDosError, UNICODE_STRING,
247            },
248            Storage::FileSystem::{
249                DELETE, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
250                FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_ID_INFO,
251                FILE_READ_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TRAVERSE, FileIdInfo,
252                GetFileInformationByHandleEx, SYNCHRONIZE,
253            },
254            System::IO::IO_STATUS_BLOCK,
255        },
256    };
257
258    use super::*;
259    fn directory_file(path: &Path) -> io::Result<File> {
260        // Only the volume root is opened by path. Child renames open their
261        // destination directory with FILE_ADD_FILE, so allow write sharing.
262        // Deny delete sharing to keep the directory's own name pinned.
263        let file = std::fs::OpenOptions::new()
264            .access_mode(FILE_READ_ATTRIBUTES | FILE_TRAVERSE | SYNCHRONIZE)
265            .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
266            .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
267            .open(path)?;
268        validate_directory(&file)?;
269        Ok(file)
270    }
271    fn validate_directory(file: &File) -> io::Result<()> {
272        let meta = file.metadata()?;
273        if !meta.is_dir() || meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
274            return Err(io::Error::new(
275                io::ErrorKind::InvalidInput,
276                "installation ancestor is a reparse point or not a directory",
277            ));
278        }
279        Ok(())
280    }
281    fn relative_open(parent: &File, child: &Path, directory: bool, pin: bool) -> io::Result<File> {
282        name(child.as_os_str())?;
283        let mut wide: Vec<u16> = child.as_os_str().encode_wide().collect();
284        let length = wide
285            .len()
286            .checked_mul(2)
287            .and_then(|n| u16::try_from(n).ok())
288            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "child name too long"))?;
289        if wide.contains(&0) {
290            return Err(io::Error::new(
291                io::ErrorKind::InvalidInput,
292                "NUL in child name",
293            ));
294        }
295        let object_name = UNICODE_STRING {
296            Length: length,
297            MaximumLength: length,
298            Buffer: wide.as_mut_ptr(),
299        };
300        let attributes = OBJECT_ATTRIBUTES {
301            Length: std::mem::size_of::<OBJECT_ATTRIBUTES>() as u32,
302            RootDirectory: parent.as_raw_handle(),
303            ObjectName: &object_name,
304            Attributes: OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE,
305            ..Default::default()
306        };
307        let mut handle = std::ptr::null_mut();
308        let mut status_block = IO_STATUS_BLOCK::default();
309        let access = if pin {
310            FILE_GENERIC_READ | FILE_GENERIC_WRITE | DELETE
311        } else {
312            FILE_READ_ATTRIBUTES | FILE_TRAVERSE | SYNCHRONIZE
313        };
314        let options = FILE_OPEN_REPARSE_POINT
315            | FILE_SYNCHRONOUS_IO_NONALERT
316            | if directory {
317                FILE_DIRECTORY_FILE
318            } else {
319                FILE_NON_DIRECTORY_FILE
320            }
321            | if pin { FILE_DELETE_ON_CLOSE } else { 0 };
322        // SAFETY: live RootDirectory handle, validated single-component UTF-16
323        // name and correctly sized output buffers. Synchronous mode cannot pend.
324        let status = unsafe {
325            NtCreateFile(
326                &mut handle,
327                access,
328                &attributes,
329                &mut status_block,
330                std::ptr::null(),
331                0,
332                // MS-FSA 2.1.5.15.12 opens the rename destination directory with
333                // FILE_ADD_FILE. Share writes there, but not deletion of its
334                // own name. The pin file must still deny writes and deletion.
335                if directory {
336                    FILE_SHARE_READ | FILE_SHARE_WRITE
337                } else {
338                    FILE_SHARE_READ
339                },
340                if pin { FILE_CREATE } else { FILE_OPEN },
341                options,
342                std::ptr::null(),
343                0,
344            )
345        };
346        if status < 0 {
347            // SAFETY: conversion accepts every NTSTATUS value.
348            return Err(io::Error::from_raw_os_error(
349                unsafe { RtlNtStatusToDosError(status) } as i32,
350            ));
351        }
352        // SAFETY: successful NtCreateFile transferred ownership of the handle.
353        Ok(unsafe { File::from_raw_handle(handle) })
354    }
355    fn pinned(file: File, path: std::path::PathBuf, ancestors: Vec<File>) -> io::Result<Directory> {
356        validate_directory(&file)?;
357        let pin_path = crate::fs_atomic::temp_path(Path::new("hosted-dir-pin"));
358        let pin_name = pin_path
359            .file_name()
360            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing pin name"))?;
361        let pin = relative_open(&file, Path::new(pin_name), false, true)?;
362        validate_directory(&file)?;
363        // With all names pinned, canonicalization cannot cross a substituted
364        // ancestor. Keep a verbatim Win32 path so publication does not apply DOS
365        // device parsing to names resolved by NtCreateFile.
366        let path = path.canonicalize()?;
367        // Denying delete sharing pins names, but attribute-only handles bypass
368        // sharing restrictions. MS-FSA forbids setting reparse points on nonempty
369        // directories: this undeletable file pins the leaf; held children pin
370        // ancestors. Delete-on-close also survives process exit without a janitor.
371        Ok(Directory {
372            file,
373            path,
374            ancestors,
375            pin: std::sync::Arc::new(pin),
376        })
377    }
378    pub fn open(path: &Path) -> io::Result<Directory> {
379        let path = std::path::absolute(path)?;
380        let mut prefix = std::path::PathBuf::new();
381        let mut ancestors = Vec::new();
382        let mut current = None;
383        for component in path.components() {
384            prefix.push(component);
385            if component == Component::RootDir {
386                current = Some(directory_file(&prefix)?);
387            } else if let Component::Normal(child) = component {
388                let parent = current.take().ok_or_else(|| {
389                    io::Error::new(io::ErrorKind::InvalidInput, "missing volume root")
390                })?;
391                let next = relative_open(&parent, Path::new(child), true, false)?;
392                validate_directory(&next)?;
393                ancestors.push(parent);
394                current = Some(next);
395            }
396            if matches!(component, Component::ParentDir | Component::CurDir) {
397                return Err(io::Error::new(
398                    io::ErrorKind::InvalidInput,
399                    "invalid root path",
400                ));
401            }
402        }
403        let file = current
404            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing directory"))?;
405        pinned(file, path, ancestors)
406    }
407    pub fn child(parent: &Directory, child: &Path) -> io::Result<Directory> {
408        let path = parent.path.join(child);
409        let mut ancestors = parent
410            .ancestors
411            .iter()
412            .map(File::try_clone)
413            .collect::<io::Result<Vec<_>>>()?;
414        ancestors.push(parent.file.try_clone()?);
415        let file = relative_open(&parent.file, child, true, false)?;
416        pinned(file, path, ancestors)
417    }
418    pub fn clone(dir: &Directory) -> io::Result<Directory> {
419        Ok(Directory {
420            file: dir.file.try_clone()?,
421            path: dir.path.clone(),
422            pin: dir.pin.clone(),
423            ancestors: dir
424                .ancestors
425                .iter()
426                .map(File::try_clone)
427                .collect::<io::Result<_>>()?,
428        })
429    }
430    pub fn identity(file: &File) -> io::Result<Vec<u8>> {
431        let mut info = std::mem::MaybeUninit::<FILE_ID_INFO>::uninit();
432        // SAFETY: live file handle and correctly sized output buffer.
433        if unsafe {
434            GetFileInformationByHandleEx(
435                file.as_raw_handle(),
436                FileIdInfo,
437                info.as_mut_ptr().cast(),
438                std::mem::size_of::<FILE_ID_INFO>() as u32,
439            )
440        } == 0
441        {
442            return Err(io::Error::last_os_error());
443        }
444        // SAFETY: successful call initialized FILE_ID_INFO.
445        let info = unsafe { info.assume_init() };
446        Ok([
447            info.VolumeSerialNumber.to_le_bytes().as_slice(),
448            &info.FileId.Identifier,
449        ]
450        .concat())
451    }
452    pub fn file(dir: &Directory, child: &Path, create: bool) -> io::Result<File> {
453        let mut options = std::fs::OpenOptions::new();
454        options
455            .read(true)
456            .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
457        if create {
458            options.write(true).create_new(true);
459        }
460        let file = options.open(dir.path.join(child))?;
461        let meta = file.metadata()?;
462        if !meta.is_file() || meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
463            return Err(io::Error::new(
464                io::ErrorKind::InvalidInput,
465                "artifact must be a regular file",
466            ));
467        }
468        Ok(file)
469    }
470    pub fn link(dir: &Directory, source: &Path, destination: &Path) -> io::Result<()> {
471        std::fs::hard_link(dir.path.join(source), dir.path.join(destination))
472    }
473    pub fn rename(dir: &Directory, source: &Path, destination: &Path) -> io::Result<()> {
474        crate::fs_atomic::durable_rename(&dir.path.join(source), &dir.path.join(destination))
475    }
476    pub fn remove(dir: &Directory, child: &Path) -> io::Result<()> {
477        std::fs::remove_file(dir.path.join(child))
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use std::io::Write;
484
485    use super::*;
486    #[cfg(windows)]
487    #[test]
488    fn windows_aliases_are_not_relative_installation_paths() {
489        for value in [
490            "NUL",
491            "COM1.txt",
492            "COM¹",
493            "LPT².txt",
494            "COM1 .txt",
495            "CONIN$",
496            "a:b",
497            "a.",
498        ] {
499            assert!(relative(Path::new(value)).is_err(), "accepted {value}");
500        }
501        assert!(relative(Path::new("parent/pin")).is_ok());
502    }
503    #[test]
504    fn flushed_publish_and_restore_use_durable_renames() {
505        let root = tempfile::tempdir().expect("root");
506        let directory = Directory::open(root.path()).expect("capability");
507        for (name, bytes) in [("backup", b"old"), ("new", b"new")] {
508            let mut file = directory.create_file(OsStr::new(name)).expect("create");
509            file.write_all(bytes).expect("write");
510            file.sync_all().expect("file flush");
511            drop(file);
512        }
513        directory
514            .durable_rename(OsStr::new("new"), OsStr::new("pin"))
515            .expect("publish");
516        assert_eq!(std::fs::read(root.path().join("pin")).expect("pin"), b"new");
517        directory
518            .hard_link(OsStr::new("backup"), OsStr::new("restore"))
519            .expect("preserve undo");
520        directory
521            .durable_rename(OsStr::new("restore"), OsStr::new("pin"))
522            .expect("restore");
523        assert_eq!(std::fs::read(root.path().join("pin")).expect("pin"), b"old");
524        assert!(root.path().join("backup").exists());
525    }
526    #[cfg(windows)]
527    #[test]
528    fn held_windows_ancestors_prevent_parent_replacement() {
529        let root = tempfile::tempdir().expect("root");
530        std::fs::create_dir_all(root.path().join("ancestor/parent")).expect("parent");
531        let directory = Directory::open(root.path()).expect("root capability");
532        let ancestor = directory
533            .child(OsStr::new("ancestor"))
534            .expect("ancestor capability");
535        let parent = ancestor
536            .child(OsStr::new("parent"))
537            .expect("parent capability");
538        drop(ancestor);
539        drop(directory);
540        let parent_path = root.path().join("ancestor/parent");
541        assert!(std::fs::rename(&parent_path, root.path().join("moved-parent")).is_err());
542        assert!(std::fs::rename(root.path().join("ancestor"), root.path().join("moved")).is_err());
543        let mut file = parent.create_file(OsStr::new("new")).expect("held create");
544        file.write_all(b"data").expect("write");
545        file.sync_all().expect("non-privileged file flush");
546        drop(file);
547        parent
548            .durable_rename(OsStr::new("new"), OsStr::new("pin"))
549            .expect("non-privileged write-through move");
550        assert_eq!(
551            std::fs::read(parent_path.join("pin")).expect("pin"),
552            b"data"
553        );
554        assert!(std::fs::rename(&parent_path, root.path().join("moved-parent")).is_err());
555        assert!(std::fs::rename(root.path().join("ancestor"), root.path().join("moved")).is_err());
556        drop(parent);
557        std::fs::rename(&parent_path, root.path().join("moved-parent"))
558            .expect("released leaf handle");
559        std::fs::rename(root.path().join("ancestor"), root.path().join("moved"))
560            .expect("released handles");
561    }
562    #[cfg(windows)]
563    #[test]
564    fn windows_attribute_only_junction_substitution_is_blocked() {
565        use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt, io::AsRawHandle};
566
567        use windows_sys::Win32::{
568            Storage::FileSystem::{
569                FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE,
570                FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_WRITE_ATTRIBUTES,
571            },
572            System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_REPARSE_POINT},
573        };
574        fn set_junction(directory: &Path, target: &Path) -> io::Result<()> {
575            let print: Vec<u16> = target.as_os_str().encode_wide().collect();
576            let substitute: Vec<u16> = format!("\\??\\{}", target.display())
577                .encode_utf16()
578                .collect();
579            let mut data = Vec::new();
580            data.extend_from_slice(&0xA0000003u32.to_le_bytes()); // mount-point tag
581            data.extend_from_slice(
582                &((8 + 2 * (substitute.len() + print.len() + 2)) as u16).to_le_bytes(),
583            );
584            data.extend_from_slice(&0u16.to_le_bytes());
585            for value in [
586                0,
587                substitute.len() * 2,
588                (substitute.len() + 1) * 2,
589                print.len() * 2,
590            ] {
591                data.extend_from_slice(&(value as u16).to_le_bytes());
592            }
593            for value in substitute.into_iter().chain([0]).chain(print).chain([0]) {
594                data.extend_from_slice(&value.to_le_bytes());
595            }
596            // Attribute-only access bypasses share-mode write restrictions.
597            let file = std::fs::OpenOptions::new()
598                .access_mode(FILE_WRITE_ATTRIBUTES)
599                .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
600                .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
601                .open(directory)?;
602            let mut returned = 0;
603            // SAFETY: live handle and a complete mount-point reparse buffer.
604            if unsafe {
605                DeviceIoControl(
606                    file.as_raw_handle(),
607                    FSCTL_SET_REPARSE_POINT,
608                    data.as_ptr().cast(),
609                    data.len() as u32,
610                    std::ptr::null_mut(),
611                    0,
612                    &mut returned,
613                    std::ptr::null_mut(),
614                )
615            } == 0
616            {
617                Err(io::Error::last_os_error())
618            } else {
619                Ok(())
620            }
621        }
622        let root = tempfile::tempdir().expect("root");
623        let outside = tempfile::tempdir().expect("outside");
624        for name in ["control", "parent"] {
625            std::fs::create_dir(root.path().join(name)).expect("directory");
626        }
627        set_junction(&root.path().join("control"), outside.path())
628            .expect("non-privileged junction control");
629        let directory = Directory::open(root.path()).expect("root capability");
630        assert!(directory.child(OsStr::new("control")).is_err());
631        let parent = directory.child(OsStr::new("parent")).expect("held parent");
632        let error = set_junction(&root.path().join("parent"), outside.path())
633            .expect_err("nonempty pin must prevent reparse substitution");
634        assert_eq!(error.raw_os_error(), Some(145)); // ERROR_DIR_NOT_EMPTY
635        let mut file = parent
636            .create_file(OsStr::new("new"))
637            .expect("contained create");
638        file.write_all(b"inside").expect("write");
639        file.sync_all().expect("flush");
640        drop(file);
641        parent
642            .durable_rename(OsStr::new("new"), OsStr::new("artifact"))
643            .expect("publish");
644        assert!(!outside.path().join("artifact").exists());
645        drop(parent);
646        drop(directory);
647        std::fs::remove_dir(root.path().join("control")).expect("remove control junction");
648    }
649}