1use super::{DirHandle, Excl, GuardIo, NodeId, NodeKind, OpenMode};
8use std::ffi::{CString, OsStr};
9use std::fs::File;
10use std::io;
11use std::os::unix::ffi::OsStrExt;
12use std::os::unix::io::{AsRawFd, FromRawFd};
13use std::path::Path;
14
15pub struct UnixDirHandle(File);
16
17impl crate::sealed::Sealed for UnixDirHandle {}
18
19const DIR_FLAGS: libc::c_int =
20 libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_DIRECTORY;
21
22const FILE_FLAGS: libc::c_int =
27 libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK;
28
29fn cstr(name: &OsStr) -> Result<CString, GuardIo> {
30 CString::new(name.as_bytes())
31 .map_err(|_| GuardIo::io(io::Error::from(io::ErrorKind::InvalidInput)))
32}
33
34fn classify_failure(dir: &File, name: &OsStr, err: io::Error) -> GuardIo {
40 let looks_like_a_link = matches!(err.raw_os_error(), Some(libc::ELOOP) | Some(libc::ENOTDIR))
41 && matches!(
42 kind_at(dir, name),
43 Some(NodeKind::NotFollowable { .. }) | Some(NodeKind::NotAFile)
44 );
45 if looks_like_a_link {
46 GuardIo::link(err)
47 } else {
48 GuardIo::io(err)
49 }
50}
51
52fn stat_at(dir: &File, name: &OsStr) -> Option<libc::stat> {
53 let c = cstr(name).ok()?;
54 let mut st: libc::stat = unsafe { std::mem::zeroed() };
56 let rc = unsafe {
58 libc::fstatat(
59 dir.as_raw_fd(),
60 c.as_ptr(),
61 &mut st,
62 libc::AT_SYMLINK_NOFOLLOW,
63 )
64 };
65 (rc == 0).then_some(st)
66}
67
68fn kind_at(dir: &File, name: &OsStr) -> Option<NodeKind> {
69 let st = stat_at(dir, name)?;
70 Some(match st.st_mode & libc::S_IFMT {
71 libc::S_IFDIR => NodeKind::Dir,
72 libc::S_IFREG => NodeKind::RegularFile,
73 libc::S_IFLNK => NodeKind::NotFollowable { tag: None },
74 _ => NodeKind::NotAFile,
75 })
76}
77
78pub(super) fn identity_of(file: &File) -> io::Result<NodeId> {
79 use std::os::unix::fs::MetadataExt;
80 let m = file.metadata()?;
81 Ok(NodeId {
82 volume: m.dev(),
83 file: m.ino() as u128,
84 })
85}
86
87pub(super) fn identity_at(path: &Path) -> io::Result<NodeId> {
88 use std::os::unix::fs::MetadataExt;
89 let m = std::fs::symlink_metadata(path)?;
90 Ok(NodeId {
91 volume: m.dev(),
92 file: m.ino() as u128,
93 })
94}
95
96pub(super) fn clear_nonblock(file: &File) -> io::Result<()> {
97 let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
99 if flags < 0 {
100 return Err(io::Error::last_os_error());
101 }
102 if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_SETFL, flags & !libc::O_NONBLOCK) } < 0 {
104 return Err(io::Error::last_os_error());
105 }
106 Ok(())
107}
108
109impl DirHandle for UnixDirHandle {
110 type Attrs = libc::mode_t;
113
114 fn open_root(path: &Path) -> Result<Self, GuardIo> {
115 let c = CString::new(path.as_os_str().as_bytes())
116 .map_err(|_| GuardIo::io(io::Error::from(io::ErrorKind::InvalidInput)))?;
117 let fd = unsafe {
122 libc::open(
123 c.as_ptr(),
124 libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY,
125 )
126 };
127 if fd < 0 {
128 return Err(GuardIo::last_os_error());
129 }
130 Ok(Self(unsafe { File::from_raw_fd(fd) }))
132 }
133
134 fn open_child_dir(&self, name: &OsStr) -> Result<Self, GuardIo> {
135 let c = cstr(name)?;
136 let fd = unsafe { libc::openat(self.0.as_raw_fd(), c.as_ptr(), DIR_FLAGS) };
138 if fd < 0 {
139 return Err(classify_failure(&self.0, name, io::Error::last_os_error()));
140 }
141 Ok(Self(unsafe { File::from_raw_fd(fd) }))
143 }
144
145 fn make_child_dir(&self, name: &OsStr) -> Result<(), GuardIo> {
146 let c = cstr(name)?;
147 if unsafe { libc::mkdirat(self.0.as_raw_fd(), c.as_ptr(), 0o755) } < 0 {
149 return Err(GuardIo::last_os_error());
150 }
151 Ok(())
152 }
153
154 fn open_child_file(&self, name: &OsStr, mode: OpenMode) -> Result<File, GuardIo> {
155 let flags = match mode {
156 OpenMode::File => FILE_FLAGS,
157 OpenMode::Dir => DIR_FLAGS,
158 };
159 let c = cstr(name)?;
160 let fd = unsafe { libc::openat(self.0.as_raw_fd(), c.as_ptr(), flags) };
162 if fd < 0 {
163 return Err(classify_failure(&self.0, name, io::Error::last_os_error()));
164 }
165 Ok(unsafe { File::from_raw_fd(fd) })
167 }
168
169 fn create_child_file(&self, name: &OsStr, excl: Excl) -> Result<File, GuardIo> {
170 let extra = match excl {
171 Excl::Truncate => libc::O_TRUNC,
172 Excl::MustNotExist => libc::O_EXCL,
173 };
174 let flags = libc::O_WRONLY | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW | extra;
177 let c = cstr(name)?;
178 let fd =
182 unsafe { libc::openat(self.0.as_raw_fd(), c.as_ptr(), flags, 0o644 as libc::c_uint) };
183 if fd < 0 {
184 return Err(classify_failure(&self.0, name, io::Error::last_os_error()));
185 }
186 Ok(unsafe { File::from_raw_fd(fd) })
188 }
189
190 fn rename_child(&self, from: &OsStr, to: &OsStr) -> Result<(), GuardIo> {
191 let (old, new) = (cstr(from)?, cstr(to)?);
192 if unsafe {
195 libc::renameat(
196 self.0.as_raw_fd(),
197 old.as_ptr(),
198 self.0.as_raw_fd(),
199 new.as_ptr(),
200 )
201 } < 0
202 {
203 return Err(GuardIo::last_os_error());
204 }
205 Ok(())
206 }
207
208 fn unlink_child(&self, name: &OsStr) {
209 let Ok(c) = cstr(name) else { return };
210 unsafe {
212 libc::unlinkat(self.0.as_raw_fd(), c.as_ptr(), 0);
213 }
214 }
215
216 fn child_kind(&self, name: &OsStr) -> Option<NodeKind> {
217 kind_at(&self.0, name)
218 }
219
220 fn child_attrs(&self, name: &OsStr) -> Option<Self::Attrs> {
221 Some(stat_at(&self.0, name)?.st_mode)
222 }
223
224 fn apply_attrs(&self, file: &File, attrs: Self::Attrs) -> io::Result<()> {
225 if unsafe { libc::fchmod(file.as_raw_fd(), attrs & 0o7777) } < 0 {
227 return Err(io::Error::last_os_error());
228 }
229 Ok(())
230 }
231
232 fn identity(&self) -> Result<NodeId, GuardIo> {
233 use std::os::unix::fs::MetadataExt;
234 let meta = self.0.metadata().map_err(GuardIo::io)?;
235 Ok(NodeId {
236 volume: meta.dev(),
237 file: meta.ino() as u128,
238 })
239 }
240
241 #[cfg(target_os = "linux")]
242 fn resolve_beneath(&self, rel: &Path, mode: OpenMode) -> Result<Option<File>, GuardIo> {
243 if std::env::var("HOTL_FSGUARD_FORCE_DESCEND").is_ok_and(|v| !v.is_empty() && v != "0") {
248 return Ok(None);
249 }
250 let c = CString::new(rel.as_os_str().as_bytes())
251 .map_err(|_| GuardIo::io(io::Error::from(io::ErrorKind::InvalidInput)))?;
252 let flags = match mode {
253 OpenMode::File => FILE_FLAGS,
254 OpenMode::Dir => DIR_FLAGS,
255 };
256 let mut how: libc::open_how = unsafe { std::mem::zeroed() };
261 how.flags = (flags & !libc::O_NOFOLLOW) as u64;
262 how.resolve =
263 libc::RESOLVE_BENEATH | libc::RESOLVE_NO_MAGICLINKS | libc::RESOLVE_NO_SYMLINKS;
264 let ret = unsafe {
267 libc::syscall(
268 libc::SYS_openat2,
269 self.0.as_raw_fd(),
270 c.as_ptr(),
271 &how as *const libc::open_how,
272 std::mem::size_of::<libc::open_how>(),
273 )
274 };
275 if ret >= 0 {
276 return Ok(Some(unsafe {
278 File::from_raw_fd(ret as std::os::unix::io::RawFd)
279 }));
280 }
281 let err = io::Error::last_os_error();
282 match err.raw_os_error() {
283 Some(libc::ENOSYS) | Some(libc::EPERM) => Ok(None),
287 Some(libc::ELOOP) | Some(libc::EXDEV) => Err(GuardIo::link(err)),
288 _ => Err(GuardIo::io(err)),
289 }
290 }
291
292 #[cfg(not(target_os = "linux"))]
293 fn resolve_beneath(&self, _rel: &Path, _mode: OpenMode) -> Result<Option<File>, GuardIo> {
294 Ok(None)
297 }
298
299 fn into_file(self) -> File {
300 self.0
301 }
302
303 fn sync_name_durability(&self) -> Result<(), GuardIo> {
304 self.0.sync_all().map_err(GuardIo::io)
305 }
306}