luff_sys/lib.rs
1//! Low-level system primitives for luff
2//!
3//! This crate isolates all `unsafe` usage and platform-specific FFI calls.
4//! It provides safe abstractions for file descriptor manipulation.
5
6#![deny(unsafe_op_in_unsafe_fn)]
7#![deny(clippy::all)]
8
9use std::fs::File;
10use std::io;
11
12/// Re-export libc constants needed by the main crate.
13/// This allows the main crate to avoid a direct dependency on libc.
14#[cfg(unix)]
15pub mod constants {
16 pub use libc::{ELOOP, O_NOFOLLOW, O_NONBLOCK};
17}
18
19/// Drop the `O_NONBLOCK` flag from an open file
20///
21/// This function takes **ownership** of the `File` to ensure that no other references
22/// to this specific file handle exist within the current scope while the flags are
23/// being modified. This mitigates race conditions where other threads might be
24/// attempting to read/write to the file or modify its flags concurrently via the
25/// same handle.
26///
27/// # Platform Behavior
28///
29/// - **Unix**: Uses `fcntl` to clear `O_NONBLOCK`.
30/// - **Windows**: No-op (Windows file handles do not use `O_NONBLOCK` in the same way).
31///
32/// # Errors
33///
34/// Returns an error if the underlying syscall fails.
35#[cfg(unix)]
36pub fn drop_nonblock(file: File) -> io::Result<File> {
37 use std::os::unix::io::AsRawFd;
38
39 let fd = file.as_raw_fd();
40
41 // SAFETY:
42 // 1. **FD Validity**: We hold an owned `File` instance. Rust's `File` guarantees
43 // that the underlying file descriptor (`fd`) is valid and open for the lifetime
44 // of the struct. Since we own the struct, the FD cannot be closed underneath us.
45 //
46 // 2. **Memory Safety**: `libc::fcntl` is a variadic C function. We are using the
47 // `F_GETFL` (void) and `F_SETFL` (int) commands. These operate strictly on
48 // integers (flags). We are not passing pointers, buffers, or complex structures,
49 // so there is no risk of memory corruption, buffer overflows, or lifetime issues
50 // associated with pointer dereferencing.
51 //
52 // 3. **Concurrency & Race Conditions**:
53 // Modifying file flags is a Read-Modify-Write (RMW) operation in user space:
54 // `flags = fcntl(GET); fcntl(SET, flags & ~NONBLOCK)`.
55 // This is *not* atomic. If another thread shares this specific Open File Description
56 // (OFD) and modifies flags (e.g. `O_APPEND`) concurrently, we could overwrite
57 // their changes.
58 //
59 // *Mitigation*: We require passing `file` by value (ownership). This guarantees
60 // that the caller has given up their reference to the `File`. While it is
61 // technically possible for the FD to have been duplicated (`dup`) externally
62 // before calling this, within the context of `luff`'s usage (immediately after open),
63 // this effectively guarantees exclusive access during this operation.
64 unsafe {
65 // Step 1: Get current flags
66 let flags = libc::fcntl(fd, libc::F_GETFL);
67 if flags < 0 {
68 return Err(io::Error::last_os_error());
69 }
70
71 // Step 2: Check if modification is needed
72 if flags & libc::O_NONBLOCK != 0 {
73 // Step 3: Clear O_NONBLOCK, preserving all other flags (e.g. O_APPEND)
74 let new_flags = flags & !libc::O_NONBLOCK;
75 if libc::fcntl(fd, libc::F_SETFL, new_flags) < 0 {
76 return Err(io::Error::last_os_error());
77 }
78 }
79 }
80
81 Ok(file)
82}
83
84/// Drop the `O_NONBLOCK` flag (Windows/Non-Unix fallback)
85///
86/// On Windows, `O_NONBLOCK` semantics for file opens are not applicable in the same
87/// way as Unix. This function simply returns the file ownership back to the caller.
88#[cfg(not(unix))]
89pub fn drop_nonblock(file: File) -> io::Result<File> {
90 Ok(file)
91}
92
93/// Get the device and inode for a file descriptor
94///
95/// This provides a direct way to identify a file from its open file descriptor,
96/// which is essential for accurately identifying `stdout` when it is redirected
97/// to a file.
98///
99/// # Implementation Note
100///
101/// This uses `std::fs::File::from_raw_fd` wrapped in `ManuallyDrop` to leverage
102/// the standard library's `Metadata` implementation. This ensures that the
103/// `dev` and `ino` values returned here exactly match those returned by
104/// `std::fs::metadata` (which is used by the directory walker), avoiding
105/// potential mismatches due to platform-specific `stat` struct layouts or
106/// type casting differences.
107///
108/// # Arguments
109///
110/// * `fd` - A borrowed file descriptor to inspect. `BorrowedFd` guarantees the
111/// descriptor is valid and open for the duration of the borrow, making this
112/// function safe to call without requiring `unsafe` at the call site.
113///
114/// # Returns
115///
116/// A tuple containing `(device_id, inode_number)` on success.
117///
118/// # Errors
119///
120/// Returns an `io::Error` if the underlying `fstat` syscall fails.
121#[cfg(unix)]
122pub fn get_fd_identity(fd: std::os::unix::io::BorrowedFd<'_>) -> io::Result<(u64, u64)> {
123 use std::mem::ManuallyDrop;
124 use std::os::unix::fs::MetadataExt;
125 use std::os::unix::io::{AsRawFd, FromRawFd};
126
127 let raw_fd = fd.as_raw_fd();
128
129 // SAFETY:
130 // 1. `BorrowedFd<'_>` guarantees the file descriptor is valid and open for
131 // the lifetime of the borrow. The caller cannot close it while we hold it.
132 // 2. We wrap the `File` in `ManuallyDrop` to prevent the destructor from
133 // running, ensuring the FD remains open after this function returns.
134 // Ownership semantics are preserved — we never actually take ownership.
135 // 3. We only call `metadata()`, which is a read-only `fstat` syscall.
136 // It does not modify the file state or position.
137 let metadata = unsafe {
138 let file = File::from_raw_fd(raw_fd);
139 let file = ManuallyDrop::new(file);
140 file.metadata()?
141 };
142
143 Ok((metadata.dev(), metadata.ino()))
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use tempfile::TempDir;
150
151 #[test]
152 fn test_drop_nonblock_regular_file() {
153 let temp = TempDir::new().unwrap();
154 let path = temp.path().join("test.txt");
155 std::fs::write(&path, "content").unwrap();
156
157 let file = File::open(&path).unwrap();
158
159 // Should succeed and return the file
160 let result = drop_nonblock(file);
161 assert!(result.is_ok());
162
163 // Verify file is still usable
164 let file = result.unwrap();
165 let meta = file.metadata().unwrap();
166 assert!(meta.is_file());
167 }
168
169 #[cfg(unix)]
170 #[test]
171 fn test_get_fd_identity() {
172 use std::os::unix::fs::MetadataExt;
173 use std::os::unix::io::AsFd;
174
175 let temp = TempDir::new().unwrap();
176 let path = temp.path().join("identity.txt");
177 std::fs::write(&path, "content").unwrap();
178
179 let file = File::open(&path).unwrap();
180 let meta = file.metadata().unwrap();
181
182 let (dev, ino) = get_fd_identity(file.as_fd()).unwrap();
183
184 assert_eq!(dev, meta.dev());
185 assert_eq!(ino, meta.ino());
186 }
187}