1use std::ffi::{CString, OsStr, OsString};
21use std::fs::File;
22use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
23use std::os::unix::ffi::OsStrExt;
24use std::path::{Component, Path, PathBuf};
25
26const RESOLVE_NO_XDEV: u64 = 0x01;
28const RESOLVE_NO_MAGICLINKS: u64 = 0x02;
30const RESOLVE_NO_SYMLINKS: u64 = 0x04;
32const RESOLVE_BENEATH: u64 = 0x08;
34
35#[repr(C)]
37struct OpenHow {
38 flags: u64,
40 mode: u64,
42 resolve: u64,
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct ObjectId {
52 pub device: u64,
54 pub inode: u64,
56 pub birth: Option<(i64, u32)>,
58}
59
60impl ObjectId {
61 pub fn encode(&self) -> String {
63 return match self.birth {
64 Some((seconds, nanos)) => format!("{}:{}:{seconds}:{nanos}", self.device, self.inode),
65 None => format!("{}:{}:-:-", self.device, self.inode),
66 };
67 }
68
69 pub fn decode(text: &str) -> Option<ObjectId> {
71 let mut parts = text.split(':');
72 let device: u64 = parts.next()?.parse().ok()?;
73 let inode: u64 = parts.next()?.parse().ok()?;
74 let seconds: &str = parts.next()?;
75 let nanos: &str = parts.next()?;
76 let birth: Option<(i64, u32)> = if seconds == "-" { None } else { Some((seconds.parse().ok()?, nanos.parse().ok()?)) };
77 return if parts.next().is_none() { Some(ObjectId { device, inode, birth }) } else { None };
78 }
79}
80
81pub fn plain_components(relative: &Path) -> std::io::Result<Vec<OsString>> {
85 let mut names: Vec<OsString> = Vec::new();
86 for part in relative.components() {
87 match part {
88 Component::Normal(name) => names.push(name.to_os_string()),
89 _ => return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is not a plain relative path", relative.display()))),
90 }
91 }
92 return Ok(names);
93}
94
95fn name_of(name: &OsStr) -> std::io::Result<CString> {
97 let bytes: &[u8] = name.as_bytes();
98 if bytes.is_empty() || bytes == b"." || bytes == b".." || bytes.contains(&b'/') {
99 return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "a name must be one plain component"));
100 }
101 return CString::new(bytes).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a name holds a NUL byte"); });
102}
103
104fn checked(result: libc::c_int) -> std::io::Result<libc::c_int> {
106 return if result < 0 { Err(std::io::Error::last_os_error()) } else { Ok(result) };
107}
108
109fn open_beneath(dir: &File, name: &OsStr, flags: libc::c_int, mode: u32) -> std::io::Result<File> {
111 let c_name = name_of(name)?;
112 let how: OpenHow = OpenHow {
113 flags: (flags | libc::O_CLOEXEC) as u64,
114 mode: u64::from(mode),
115 resolve: RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS,
116 };
117 let result: libc::c_long = unsafe { libc::syscall(libc::SYS_openat2, dir.as_raw_fd(), c_name.as_ptr(), &how as *const OpenHow, std::mem::size_of::<OpenHow>()) };
120 if result < 0 {
121 return Err(std::io::Error::last_os_error());
122 }
123 return Ok(File::from(unsafe { OwnedFd::from_raw_fd(result as libc::c_int) }));
125}
126
127pub fn open_root(path: &Path) -> std::io::Result<File> {
132 if !path.is_absolute() {
133 return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is not absolute", path.display())));
134 }
135 let name = CString::new(path.as_os_str().as_bytes()).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a path holds a NUL byte"); })?;
136 let descriptor: libc::c_int = checked(unsafe { libc::open(name.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC) })?;
138 let held: File = File::from(unsafe { OwnedFd::from_raw_fd(descriptor) });
140 let real: PathBuf = std::fs::read_link(handle_path(&held))?;
141 if real.as_os_str() != path.as_os_str() {
142 return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is reached through a link: it is {}", path.display(), real.display())));
143 }
144 return Ok(held);
145}
146
147pub fn open_directory_at(parent: &File, name: &OsStr) -> std::io::Result<File> {
149 return open_beneath(parent, name, libc::O_RDONLY | libc::O_DIRECTORY, 0);
150}
151
152pub fn open_directory_beneath(root: &File, names: &[OsString]) -> std::io::Result<File> {
155 let device: u64 = device_of(root)?;
156 let mut current: File = root.try_clone()?;
157 for name in names {
158 let next: File = open_directory_at(¤t, name.as_os_str())?;
159 if device_of(&next)? != device {
160 return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is on another device", name.to_string_lossy())));
161 }
162 current = next;
163 }
164 return Ok(current);
165}
166
167pub fn device_of(file: &File) -> std::io::Result<u64> {
169 use std::os::unix::fs::MetadataExt;
170 return Ok(file.metadata()?.dev());
171}
172
173pub fn open_file_at(dir: &File, name: &OsStr) -> std::io::Result<File> {
175 return open_beneath(dir, name, libc::O_RDONLY | libc::O_NONBLOCK, 0);
176}
177
178pub fn create_file_at(dir: &File, name: &OsStr, mode: u32) -> std::io::Result<File> {
180 return open_beneath(dir, name, libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL, mode);
181}
182
183pub fn make_directory_at(dir: &File, name: &OsStr, mode: u32) -> std::io::Result<bool> {
186 let c_name = name_of(name)?;
187 let result: libc::c_int = unsafe { libc::mkdirat(dir.as_raw_fd(), c_name.as_ptr(), mode as libc::mode_t) };
189 if result == 0 {
190 return Ok(true);
191 }
192 let error: std::io::Error = std::io::Error::last_os_error();
193 if error.kind() == std::io::ErrorKind::AlreadyExists {
194 drop(open_directory_at(dir, name)?);
195 return Ok(false);
196 }
197 return Err(error);
198}
199
200pub fn symlink_at(target: &Path, dir: &File, name: &OsStr) -> std::io::Result<()> {
202 let c_name = name_of(name)?;
203 let c_target = CString::new(target.as_os_str().as_bytes()).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a link target holds a NUL byte"); })?;
204 checked(unsafe { libc::symlinkat(c_target.as_ptr(), dir.as_raw_fd(), c_name.as_ptr()) })?;
206 return Ok(());
207}
208
209pub fn read_link_at(dir: &File, name: &OsStr) -> std::io::Result<PathBuf> {
211 use std::os::unix::ffi::OsStringExt;
212 let c_name = name_of(name)?;
213 let mut buffer: Vec<u8> = vec![0; 4096];
214 let length: libc::ssize_t = unsafe { libc::readlinkat(dir.as_raw_fd(), c_name.as_ptr(), buffer.as_mut_ptr().cast::<libc::c_char>(), buffer.len()) };
216 if length < 0 {
217 return Err(std::io::Error::last_os_error());
218 }
219 buffer.truncate(usize::try_from(length).unwrap_or(0));
220 return Ok(PathBuf::from(OsString::from_vec(buffer)));
221}
222
223pub fn handle_path(directory: &File) -> PathBuf {
225 return Path::new("/proc/self/fd").join(directory.as_raw_fd().to_string());
226}
227
228pub fn entry_metadata_at(dir: &File, name: &OsStr) -> std::io::Result<std::fs::Metadata> {
230 name_of(name)?;
232 return std::fs::symlink_metadata(handle_path(dir).join(name));
233}
234
235pub fn set_times_at(dir: &File, name: &OsStr, times: [libc::timespec; 2]) -> std::io::Result<()> {
237 let c_name = name_of(name)?;
238 checked(unsafe { libc::utimensat(dir.as_raw_fd(), c_name.as_ptr(), times.as_ptr(), libc::AT_SYMLINK_NOFOLLOW) })?;
240 return Ok(());
241}
242
243pub fn remove_at(dir: &File, name: &OsStr, directory: bool) -> std::io::Result<()> {
247 let c_name = name_of(name)?;
248 let flags: libc::c_int = if directory { libc::AT_REMOVEDIR } else { 0 };
249 checked(unsafe { libc::unlinkat(dir.as_raw_fd(), c_name.as_ptr(), flags) })?;
251 return Ok(());
252}
253
254pub fn rename_at(from_dir: &File, from: &OsStr, to_dir: &File, to: &OsStr, replace: bool) -> std::io::Result<()> {
256 let c_from = name_of(from)?;
257 let c_to = name_of(to)?;
258 let flags: libc::c_uint = if replace { 0 } else { libc::RENAME_NOREPLACE };
259 checked(unsafe { libc::renameat2(from_dir.as_raw_fd(), c_from.as_ptr(), to_dir.as_raw_fd(), c_to.as_ptr(), flags) })?;
261 return Ok(());
262}
263
264pub fn exchange_at(dir: &File, first: &OsStr, second: &OsStr) -> std::io::Result<()> {
266 let c_first = name_of(first)?;
267 let c_second = name_of(second)?;
268 checked(unsafe { libc::renameat2(dir.as_raw_fd(), c_first.as_ptr(), dir.as_raw_fd(), c_second.as_ptr(), libc::RENAME_EXCHANGE) })?;
270 return Ok(());
271}
272
273fn id_of_statx(found: &libc::statx) -> ObjectId {
275 let birth: Option<(i64, u32)> = if found.stx_mask & libc::STATX_BTIME != 0 { Some((found.stx_btime.tv_sec, found.stx_btime.tv_nsec)) } else { None };
276 return ObjectId { device: libc::makedev(found.stx_dev_major, found.stx_dev_minor), inode: found.stx_ino, birth };
277}
278
279fn identity_statx(dir: &File, name: Option<&OsStr>) -> std::io::Result<ObjectId> {
281 let (c_name, flags): (CString, libc::c_int) = match name {
282 Some(plain) => (name_of(plain)?, libc::AT_SYMLINK_NOFOLLOW),
283 None => (CString::default(), libc::AT_EMPTY_PATH),
284 };
285 let mut found: libc::statx = unsafe { std::mem::zeroed() };
287 checked(unsafe { libc::statx(dir.as_raw_fd(), c_name.as_ptr(), flags, libc::STATX_BASIC_STATS | libc::STATX_BTIME, &mut found) })?;
289 return Ok(id_of_statx(&found));
290}
291
292pub fn identity_of(file: &File) -> std::io::Result<ObjectId> {
294 return identity_statx(file, None);
295}
296
297pub fn identity_at(dir: &File, name: &OsStr) -> std::io::Result<ObjectId> {
299 return identity_statx(dir, Some(name));
300}
301
302#[derive(Clone, Debug, Eq, PartialEq)]
304pub enum Owned {
305 Removed,
307 Absent,
309 Kept(String),
311}
312
313pub fn remove_if_identical(dir: &File, name: &OsStr, expected: ObjectId) -> std::io::Result<Owned> {
326 let found: ObjectId = match identity_at(dir, name) {
327 Ok(known) => known,
328 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Owned::Absent),
329 Err(error) => return Err(error),
330 };
331 if found != expected {
332 return Ok(Owned::Kept(format!("{} is not the object that was made ({} against {})", name.to_string_lossy(), found.encode(), expected.encode())));
333 }
334 if entry_metadata_at(dir, name)?.is_dir() {
335 return Ok(Owned::Kept(format!("{} is a directory and is not removed", name.to_string_lossy())));
336 }
337 return match remove_at(dir, name, false) {
338 Ok(()) => Ok(Owned::Removed),
339 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Owned::Absent),
340 Err(error) => Err(error),
341 };
342}
343
344pub fn link_open_file_at(source: &File, dir: &File, name: &OsStr) -> std::io::Result<()> {
355 let c_name = name_of(name)?;
356 let through = CString::new(handle_path(source).as_os_str().as_bytes()).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a path holds a NUL byte"); })?;
357 checked(unsafe { libc::linkat(libc::AT_FDCWD, through.as_ptr(), dir.as_raw_fd(), c_name.as_ptr(), libc::AT_SYMLINK_FOLLOW) })?;
360 return Ok(());
361}
362
363pub fn widen_directory(directory: &File) -> std::io::Result<()> {
369 use std::os::unix::fs::{MetadataExt, PermissionsExt};
370 let mode: u32 = directory.metadata()?.mode();
371 if mode & 0o700 == 0o700 {
372 return Ok(());
373 }
374 return directory.set_permissions(std::fs::Permissions::from_mode(mode | 0o700));
375}
376
377pub fn open_directory_widening(parent: &File, name: &OsStr) -> std::io::Result<File> {
383 use std::os::unix::fs::{MetadataExt, PermissionsExt};
384 match open_directory_at(parent, name) {
385 Err(error) if error.raw_os_error() == Some(libc::EACCES) => {
386 let handle: File = open_beneath(parent, name, libc::O_PATH | libc::O_DIRECTORY, 0)?;
387 let mode: u32 = handle.metadata()?.mode();
388 if std::fs::set_permissions(handle_path(&handle), std::fs::Permissions::from_mode(mode | 0o700)).is_err() {
389 return Err(error);
390 }
391 return open_directory_at(parent, name);
392 }
393 other => return other,
394 }
395}
396
397#[cfg(test)]
399#[path = "fs_beneath_tests.rs"]
400mod tests;