Skip to main content

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}