ic_host_fs/durable/directory/publish/
mod.rs1use std::{ffi::OsStr, fs, io, path::Path};
4
5#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
6use rustix::{
7 fd::AsFd,
8 fs::{self as unix_fs, Mode, OFlags, RenameFlags},
9};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum DirectoryCommitOutcome {
14 Published,
16 Recovered,
18}
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum DirectoryCommitStep {
23 BeforePublication,
25 BeforeParentSync,
27 PublicationDurable,
29}
30
31#[derive(Debug, thiserror::Error)]
33pub enum DirectoryCommitError<E> {
34 #[error("directory commit requires distinct sibling paths")]
36 InvalidPaths,
37 #[error("directory commit has two occupied paths")]
39 Conflict,
40 #[error("directory commit has no existing path")]
42 Missing,
43 #[error("directory preparation failed: {0}")]
45 Prepare(#[source] E),
46 #[error("directory publication failed: {0}")]
48 BeforePublication(#[source] io::Error),
49 #[error("directory publication may be visible: {0}")]
51 AfterPublication(#[source] io::Error),
52}
53
54pub fn commit_existing_directory_with<E>(
71 staging: &Path,
72 canonical: &Path,
73 prepare: impl FnOnce(&fs::File, &Path) -> Result<(), E>,
74 at_step: impl FnMut(DirectoryCommitStep, &Path) -> io::Result<()>,
75) -> Result<DirectoryCommitOutcome, DirectoryCommitError<E>> {
76 let (parent, staging_name, canonical_name) = sibling_paths(staging, canonical)?;
77 #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
78 {
79 let mut at_step = at_step;
80 let staging_exists =
81 exists_without_following(staging).map_err(DirectoryCommitError::BeforePublication)?;
82 let canonical_exists =
83 exists_without_following(canonical).map_err(DirectoryCommitError::BeforePublication)?;
84 let parent_fd = unix_fs::open(
85 parent,
86 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
87 Mode::empty(),
88 )
89 .map_err(io::Error::from)
90 .map_err(DirectoryCommitError::BeforePublication)?;
91
92 let (selected_name, selected_path, publish) = match (staging_exists, canonical_exists) {
93 (true, false) => (staging_name, staging, true),
94 (false, true) => (canonical_name, canonical, false),
95 (true, true) => return Err(DirectoryCommitError::Conflict),
96 (false, false) => return Err(DirectoryCommitError::Missing),
97 };
98 let selected = open_directory_at(&parent_fd, selected_name)
99 .map_err(DirectoryCommitError::BeforePublication)?;
100 prepare(&selected, selected_path).map_err(DirectoryCommitError::Prepare)?;
101 if publish {
102 at_step(DirectoryCommitStep::BeforePublication, canonical)
103 .map_err(DirectoryCommitError::BeforePublication)?;
104 unix_fs::renameat_with(
105 &parent_fd,
106 staging_name,
107 &parent_fd,
108 canonical_name,
109 RenameFlags::NOREPLACE,
110 )
111 .map_err(io::Error::from)
112 .map_err(DirectoryCommitError::BeforePublication)?;
113 }
114 at_step(DirectoryCommitStep::BeforeParentSync, parent)
115 .map_err(DirectoryCommitError::AfterPublication)?;
116 unix_fs::fsync(&parent_fd)
117 .map_err(io::Error::from)
118 .map_err(DirectoryCommitError::AfterPublication)?;
119 at_step(DirectoryCommitStep::PublicationDurable, canonical)
120 .map_err(DirectoryCommitError::AfterPublication)?;
121 Ok(if publish {
122 DirectoryCommitOutcome::Published
123 } else {
124 DirectoryCommitOutcome::Recovered
125 })
126 }
127 #[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
128 {
129 let _ = (parent, staging_name, canonical_name, prepare, at_step);
130 Err(DirectoryCommitError::BeforePublication(io::Error::from(
131 io::ErrorKind::Unsupported,
132 )))
133 }
134}
135
136fn sibling_paths<'a, E>(
137 staging: &'a Path,
138 canonical: &'a Path,
139) -> Result<(&'a Path, &'a OsStr, &'a OsStr), DirectoryCommitError<E>> {
140 let staging_parent = staging.parent().ok_or(DirectoryCommitError::InvalidPaths)?;
141 let canonical_parent = canonical
142 .parent()
143 .ok_or(DirectoryCommitError::InvalidPaths)?;
144 let staging_name = staging
145 .file_name()
146 .ok_or(DirectoryCommitError::InvalidPaths)?;
147 let canonical_name = canonical
148 .file_name()
149 .ok_or(DirectoryCommitError::InvalidPaths)?;
150 if staging == canonical || staging_parent != canonical_parent {
151 return Err(DirectoryCommitError::InvalidPaths);
152 }
153 Ok((staging_parent, staging_name, canonical_name))
154}
155
156#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
157fn exists_without_following(path: &Path) -> io::Result<bool> {
158 match fs::symlink_metadata(path) {
159 Ok(_) => Ok(true),
160 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
161 Err(error) => Err(error),
162 }
163}
164
165#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
166fn open_directory_at(parent: &impl AsFd, name: &OsStr) -> io::Result<fs::File> {
167 let fd = unix_fs::openat(
168 parent,
169 name,
170 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
171 Mode::empty(),
172 )
173 .map_err(io::Error::from)?;
174 Ok(fs::File::from(fd))
175}