io_redirect/
lib.rs

1//! Cross-platform I/O redirection.
2//!
3//! This crate provides a `Redirectable<T>` trait, platform-specific implementations of this trait,
4//! and convenience methods for handling stdout and stderr redirection. This is most useful if you
5//! can't print to stdout or stderr directly (e.g., a systemd generator) or need to hijack file
6//! access without changing user code.
7//!
8//! ## Platform Support
9//! | Platform  | Required Features | File to File | Stdout/Stderr to File | Any FD to Any FD |
10//! | -         | -                 | -            | -                     | -                |
11//! | Unix-like | `libc_on_unix`    | Yes          | Yes                   | Yes              |
12//! | Windows   | `windows-sys`     | No           | Yes                   | No               |
13//! | Windows   | `libc_on_windows` | Yes          | Yes                   | Yes              |
14//!
15//! On Unix-like systems such as Linux and macOS, the default is `libc_on_unix`.
16//! On Windows, the default is `windows-sys`.
17//!
18//! <div class="warning">`libc_on_windows` feature is experimental! It should not be relied on.</div>
19//!
20//! ## Usage
21//! For a more detailed example, see the `selftest` executable.
22//!
23//! ### File to File
24//! ```ignore
25//! use io_redirect::Redirectable;
26//! use std::fs::File;
27//! let mut file_src = File::create("src.txt").unwrap();
28//! let mut file_dst = File::create("dst.txt").unwrap();
29//!
30//! file_src.redirect(&file_dst).unwrap();
31//! ```
32//!
33//! ### Redirect Standard Streams to a File
34//! ```ignore
35//! let some_path = PathBuf::new("/dev/kmsg".into());
36//!
37//! redirect_std_to_path(some_path.as_path(), true).unwrap();
38//!
39//! // or just one stream
40//! stdout().redirect(some_path.as_path()).unwrap();
41//! ```
42//!
43//! ## Notes and Caveats
44//! - **Resource Management**: Avoid using `Redirectable<Path>::redirect(...)` multiple times on the same entity as each call will leak a file descriptor. `Redirectable<File>` does not suffer from the same.
45//! - **OS-Specific Behavior**: Not all features may function identically across platforms; ensure
46//!   feature flags match the intended target for compilation.
47//!
48
49use std::io;
50use std::io::{Stdout, Stderr};
51use std::fs::{File, OpenOptions};
52
53/// A trait to represent entities that can have their I/O redirected to a specified target.
54///
55/// # Type Parameters
56/// - `T`: The type of the destination. It is a dynamically sized type (`?Sized`) so that
57///   it can be used with types that do not have a statically known size.
58///
59/// # Notes
60/// Be cautious of potential side effects or resource management issues when implementing
61/// this trait, especially in cases where redirection involves I/O operations or state transitions.
62pub trait Redirectable<T: ?Sized>
63{
64    /// Redirects I/O to a specified destination.
65    ///
66    /// # Parameters
67    /// - `destination`: A reference to the target destination.
68    ///
69    /// # Returns
70    /// - `io::Result<()>`: `Ok` if successful, `Err` otherwise.
71    ///
72    /// # Examples
73    /// ```no_run
74    /// use io_redirect::Redirectable;
75    ///
76    /// let source = std::io::stdout();
77    /// let destination = std::fs::File::create("dst.txt").unwrap();
78    /// match source.redirect(&destination) {
79    ///     Ok(_) => println!("Redirection successful!"),
80    ///     Err(_) => eprintln!("Failed to redirect!"),
81    /// }
82    /// ```
83    ///
84    /// # Notes
85    /// The behavior of this function depends on the implementation.
86    fn redirect(&self, destination: &T) -> io::Result<()>;
87}
88
89#[cfg(any(unix))]
90mod platform
91{
92    use super::*;
93    use std::os::fd::{AsRawFd, RawFd};
94
95    pub type Descriptor = RawFd;
96
97    pub trait Descriptable: AsRawFd {}
98    impl<T: AsRawFd> Descriptable for T {}
99
100    impl<T1: Descriptable, T2: Descriptable> Redirectable<T2> for T1 {
101        fn redirect(&self, destination: &T2) -> io::Result<()> {
102            let src_fd = self.as_raw_fd();
103            let dst_fd = destination.as_raw_fd();
104            return crate::libc_common::redirect_fd_to_fd(src_fd, dst_fd);
105        }
106    }
107}
108
109#[cfg(any(target_os = "windows"))]
110mod platform
111{
112    use super::*;
113    use std::os::windows::io::AsRawHandle;
114
115    pub trait Descriptable: AsRawHandle {}
116    impl<T: AsRawHandle> Descriptable for T {}
117
118    #[cfg(feature = "libc_on_windows")]
119    mod libc_backend
120    {
121        use super::*;
122        use crate::{Descriptable, Redirectable};
123        use libc::{c_int, open_osfhandle};
124
125        pub type Descriptor = c_int;
126
127        impl<T1: Descriptable, T2: Descriptable> Redirectable<T2> for T1 {
128            fn redirect(&self, destination: &T2) -> io::Result<()> {
129                let src_handle = self.as_raw_handle() as isize;
130                let dst_handle = destination.as_raw_handle() as isize;
131
132                let src_fd = unsafe { open_osfhandle(src_handle, 0) };
133                if src_fd < 0 {
134                    return Err(io::Error::last_os_error());
135                }
136
137                let dst_fd = unsafe { open_osfhandle(dst_handle, 0) };
138                if dst_fd < 0 {
139                    return Err(io::Error::last_os_error());
140                }
141
142                return crate::libc_common::redirect_fd_to_fd(src_fd, dst_fd);
143            }
144        }
145    }
146
147    #[cfg(feature = "libc_on_windows")]
148    pub use libc_backend::*;
149
150    #[cfg(feature = "windows-sys")]
151    mod windows_sys_backend
152    {
153        use super::*;
154        use windows_sys::Win32::Foundation::HANDLE;
155        use windows_sys::Win32::System::Console::{SetStdHandle, STD_ERROR_HANDLE, STD_HANDLE, STD_OUTPUT_HANDLE};
156
157        impl<T: Descriptable> Redirectable<T> for Stdout {
158            fn redirect(&self, destination: &T) -> io::Result<()> {
159                redirect_using_setstdhandle(STD_OUTPUT_HANDLE, destination)
160            }
161        }
162
163        impl<T: Descriptable> Redirectable<T> for Stderr {
164            fn redirect(&self, destination: &T) -> io::Result<()> {
165                redirect_using_setstdhandle(STD_ERROR_HANDLE, destination)
166            }
167        }
168
169        fn redirect_using_setstdhandle<T: Descriptable>(std_handle: STD_HANDLE, destination: &T) -> io::Result<()> {
170            let dst_handle = destination.as_raw_handle() as HANDLE;
171            let result = unsafe { SetStdHandle(std_handle, dst_handle) };
172            if result == 0 {
173                return Err(io::Error::last_os_error());
174            }
175            return Ok(());
176        }
177    }
178
179    #[cfg(feature = "windows-sys")]
180    pub use windows_sys_backend::*;
181}
182
183#[cfg(any(all(unix, feature = "libc_on_unix"), all(target_os = "windows", feature = "libc_on_windows")))]
184mod libc_common
185{
186    use super::*;
187    use crate::platform::Descriptor;
188    use libc::dup2;
189
190    pub fn redirect_fd_to_fd(src: Descriptor, dst: Descriptor) -> io::Result<()> {
191        let result = unsafe {
192            dup2(dst, src)
193            // After this call on Windows, get_osfhandle seems to return a different value
194            // than the one passed to open_osfhandle. This is why the libc backend is off on Windows.
195        };
196        if result < 0 {
197            return Err(io::Error::last_os_error());
198        }
199
200        return Ok(());
201    }
202}
203
204#[cfg(any(all(unix, feature = "libc_on_unix"), all(target_os = "windows", feature = "libc_on_windows")))]
205mod libc_convenience
206{
207    use super::*;
208    use std::fs::OpenOptions;
209    use std::path::Path;
210
211    impl<T: Descriptable> Redirectable<Path> for T {
212        fn redirect(&self, destination: &Path) -> io::Result<()> {
213            let dst = OpenOptions::new().read(false).write(true).create(true).append(true).open(destination)?;
214            let result = self.redirect(&dst);
215            if result.is_ok() {
216                std::mem::forget(dst);
217            }
218            return result;
219        }
220    }
221}
222
223#[cfg(any(all(unix, feature = "libc_on_unix"), all(target_os = "windows", feature = "libc_on_windows")))]
224pub use libc_convenience::*;
225
226mod convenience
227{
228    use super::*;
229    use std::fs::OpenOptions;
230    use std::io::{stderr, stdout};
231    use std::path::Path;
232    pub fn redirect_std_to_path(destination: &Path, append: bool) -> io::Result<()> {
233        let dst = OpenOptions::new().read(false).write(true).create(true).append(append).open(destination)?;
234        stdout().redirect(&dst)?;
235        stderr().redirect(&dst)?;
236        std::mem::forget(dst);
237        return Ok(());
238    }
239}
240
241pub use convenience::*;
242pub use platform::*;
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use std::io::{Read, Write};
248    use std::mem::ManuallyDrop;
249    use libc::close;
250
251    #[cfg(any(all(unix, feature = "libc_on_unix"), all(target_os = "windows", feature = "libc_on_windows")))]
252    #[test]
253    fn redirects_file_to_file() {
254        // Arrange
255        let tempdir = tempfile::tempdir().unwrap();
256        let mut file1 = File::create(tempdir.path().join("file1.txt")).unwrap();
257        let mut file2 = File::create(tempdir.path().join("file2.txt")).unwrap();
258
259        // Act
260        file1.redirect(&file2).unwrap();
261        file1.write_all(b"Hello,").unwrap();
262        file1.flush().unwrap();
263        file2.write_all(b" World!").unwrap();
264        file2.flush().unwrap();
265
266        // Assert
267        let mut dst_file = File::open(tempdir.path().join("file2.txt")).unwrap();
268        let mut dst_contents = String::new();
269        dst_file.read_to_string(&mut dst_contents).unwrap();
270        assert_eq!(dst_contents, "Hello, World!");
271
272        let mut old_file1_contents = String::new();
273        let mut old_file1 = File::open(tempdir.path().join("file1.txt")).unwrap();
274        old_file1.read_to_string(&mut old_file1_contents).unwrap();
275        assert_eq!(old_file1_contents, "");
276    }
277
278    #[cfg(any(all(unix, feature = "libc_on_unix"), all(target_os = "windows", feature = "libc_on_windows")))]
279    #[test]
280    fn redirects_file_to_path() {
281        // Arrange
282        let tempdir = tempfile::tempdir().unwrap();
283        let src_path = tempdir.path().join("src.txt");
284        let dst_path = tempdir.path().join("dst.txt");
285        let mut src = OpenOptions::new().create(true).read(true).write(true).open(&src_path).unwrap();
286
287        // Act
288        src.redirect(dst_path.as_path()).unwrap();
289        src.write_all(b"abc").unwrap();
290        src.flush().unwrap();
291
292        // Assert
293        let mut dst_contents = String::new();
294        File::open(&dst_path).unwrap().read_to_string(&mut dst_contents).unwrap();
295        assert_eq!(dst_contents, "abc");
296
297        let mut original_contents = String::new();
298        File::open(&src_path).unwrap().read_to_string(&mut original_contents).unwrap();
299        assert_eq!(original_contents, "");
300    }
301
302    #[cfg(any(all(unix, feature = "libc_on_unix"), all(target_os = "windows", feature = "libc_on_windows")))]
303    #[test]
304    fn errors_on_redirect_to_directory() {
305        // Arrange
306        let tempdir = tempfile::tempdir().unwrap();
307        let dir_path = tempdir.path();
308        let src = File::create(dir_path.join("somefile.txt")).unwrap();
309
310        // Act
311        let err = src.redirect(dir_path).unwrap_err();
312
313        // Assert
314        assert!(err.raw_os_error().is_some());
315    }
316
317    #[cfg(any(all(unix, feature = "libc_on_unix"), all(target_os = "windows", feature = "libc_on_windows")))]
318    #[test]
319    fn errors_on_redirect_with_missing_parent_directory() {
320        // Arrange
321        let tempdir = tempfile::tempdir().unwrap();
322        let src = File::create(tempdir.path().join("s.txt")).unwrap();
323        let bad_path = tempdir.path().join("no_such_dir").join("f.txt");
324
325        // Act
326        let err = src.redirect(bad_path.as_path()).unwrap_err();
327
328        // Assert
329        assert!(err.raw_os_error().is_some());
330    }
331
332    #[cfg(any(all(unix, feature = "libc_on_unix")))]
333    #[test]
334    fn errors_on_redirect_to_closed_fd() {
335        use std::os::fd::AsRawFd;
336        // Arrange
337        let tempdir = tempfile::tempdir().unwrap();
338        let src_file = File::create(tempdir.path().join("src.txt")).unwrap();
339        let dst_file = File::create(tempdir.path().join("dst.txt")).unwrap();
340
341        let dst_file = ManuallyDrop::new(dst_file);
342        let fd = dst_file.as_raw_fd();
343        unsafe { close(fd) };
344
345        // Act
346        let err = src_file.redirect(&*dst_file).unwrap_err();
347
348        // Assert
349        assert!(err.raw_os_error().is_some());
350    }
351}