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