Skip to main content

syd/
io.rs

1//
2// Syd: rock-solid application kernel
3// src/io.rs: I/O utilities
4//
5// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
6//
7// SPDX-License-Identifier: GPL-3.0
8
9use std::{
10    io::{Read, Stdin, Write},
11    os::fd::AsFd,
12};
13
14use nix::{errno::Errno, sys::sendfile::sendfile64};
15
16use crate::{
17    compat::{fstatx, STATX_SIZE},
18    cookie::{safe_read, safe_write},
19    err2no,
20    fd::SafeOwnedFd,
21    retry::retry_on_eintr,
22};
23
24/// Read until EOF or `buf` is full from the given file.
25///
26/// Returns number of bytes read. NO-OP if `buf` is empty.
27pub fn read_buf<Fd: AsFd>(fd: Fd, buf: &mut [u8]) -> Result<usize, Errno> {
28    let mut nread = 0;
29
30    while nread < buf.len() {
31        match retry_on_eintr(|| safe_read(&fd, &mut buf[nread..]))? {
32            0 => break,
33            n => nread = nread.checked_add(n).ok_or(Errno::EOVERFLOW)?,
34        }
35    }
36
37    Ok(nread)
38}
39
40/// Read until EOF from the given file.
41///
42/// Returns number of bytes read.
43pub fn read_all<Fd: AsFd>(fd: Fd) -> Result<Vec<u8>, Errno> {
44    let mut buf = Vec::new();
45
46    let size = fstatx(&fd, STATX_SIZE)
47        .map(|stx| stx.stx_size)
48        .and_then(|size| usize::try_from(size).or(Err(Errno::EOVERFLOW)))?;
49    if size == 0 {
50        return Ok(buf);
51    }
52
53    buf.try_reserve(size).or(Err(Errno::ENOMEM))?;
54    buf.resize(size, 0);
55
56    let n = read_buf(fd, &mut buf)?;
57    buf.truncate(n);
58
59    Ok(buf)
60}
61
62/// Write all the data to the given file.
63///
64/// Returns `Errno::EPIPE` on EOF. NO-OP if data is empty.
65pub fn write_all<Fd: AsFd>(fd: Fd, data: &[u8]) -> Result<(), Errno> {
66    let mut nwrite = 0;
67
68    while nwrite < data.len() {
69        match retry_on_eintr(|| safe_write(&fd, &data[nwrite..]))? {
70            0 => return Err(Errno::EPIPE),
71            n => nwrite = nwrite.checked_add(n).ok_or(Errno::EOVERFLOW)?,
72        }
73    }
74
75    Ok(())
76}
77
78/// Super trait: AsFd + Read.
79pub trait ReadFd: AsFd + Read {}
80
81/// Super trait: AsFd + Write.
82pub trait WriteFd: AsFd + Write {}
83
84#[expect(clippy::disallowed_types)]
85impl ReadFd for std::fs::File {}
86impl ReadFd for Stdin {}
87impl ReadFd for SafeOwnedFd {}
88
89#[expect(clippy::disallowed_types)]
90impl WriteFd for std::fs::File {}
91impl WriteFd for SafeOwnedFd {}
92
93/// Copy all available data from one file to another.
94///
95/// Uses `nix::fcntl::sendfile64` and falls back to `std::io::copy`
96/// on errors `Err(Errno::EINVAL)` and `Err(Errno::ENOSYS)`.
97pub fn copy<Fd1, Fd2>(src: &mut Fd1, dst: &mut Fd2) -> Result<u64, Errno>
98where
99    Fd1: ReadFd,
100    Fd2: WriteFd,
101{
102    // sendfile() will transfer at most 0x7ffff000 (2,147,479,552) bytes,
103    // returning the number of bytes actually transferred. (This is true on
104    // both 32-bit and 64-bit systems.)
105    const MAX: usize = 0x7ffff000;
106
107    let mut ncopy = 0;
108    loop {
109        return match sendfile64(&dst, &src, None, MAX) {
110            Ok(0) => Ok(ncopy),
111            Ok(n) => {
112                let n = n.try_into().or(Err(Errno::EOVERFLOW))?;
113                ncopy = ncopy.checked_add(n).ok_or(Errno::EOVERFLOW)?;
114                continue;
115            }
116            Err(Errno::EINTR) => continue,
117            Err(Errno::EINVAL | Errno::ENOSYS) =>
118            {
119                #[expect(clippy::disallowed_methods)]
120                std::io::copy(src, dst).map_err(|err| err2no(&err))
121            }
122            Err(errno) => Err(errno),
123        };
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use std::io::{Seek, SeekFrom, Write as IoWrite};
130
131    use super::*;
132
133    fn tempfile_with(data: &[u8]) -> std::fs::File {
134        let mut f = tempfile::tempfile().unwrap();
135        f.write_all(data).unwrap();
136        f.seek(SeekFrom::Start(0)).unwrap();
137        f
138    }
139
140    #[test]
141    fn test_read_buf_1() {
142        let f = tempfile_with(b"hello");
143        let mut buf = [0u8; 5];
144        let n = read_buf(&f, &mut buf).unwrap();
145        assert_eq!(n, 5);
146        assert_eq!(&buf, b"hello");
147    }
148
149    #[test]
150    fn test_read_buf_2() {
151        let f = tempfile_with(b"hi");
152        let mut buf = [0u8; 10];
153        let n = read_buf(&f, &mut buf).unwrap();
154        assert_eq!(n, 2);
155        assert_eq!(&buf[..n], b"hi");
156    }
157
158    #[test]
159    fn test_read_buf_3() {
160        let f = tempfile_with(b"");
161        let mut buf = [0u8; 4];
162        let n = read_buf(&f, &mut buf).unwrap();
163        assert_eq!(n, 0);
164    }
165
166    #[test]
167    fn test_read_buf_4() {
168        let f = tempfile_with(b"abc");
169        let mut buf = [];
170        let n = read_buf(&f, &mut buf).unwrap();
171        assert_eq!(n, 0);
172    }
173
174    #[test]
175    fn test_read_all_1() {
176        let f = tempfile_with(b"syd rocks");
177        let data = read_all(&f).unwrap();
178        assert_eq!(data, b"syd rocks");
179    }
180
181    #[test]
182    fn test_read_all_2() {
183        let f = tempfile_with(b"");
184        let data = read_all(&f).unwrap();
185        assert!(data.is_empty());
186    }
187
188    #[test]
189    fn test_read_all_3() {
190        let payload = vec![0xffu8; 8192];
191        let f = tempfile_with(&payload);
192        let data = read_all(&f).unwrap();
193        assert_eq!(data, payload);
194    }
195
196    #[test]
197    fn test_write_all_1() {
198        let f = tempfile::tempfile().unwrap();
199        write_all(&f, b"hello world").unwrap();
200
201        let mut f = f;
202        f.seek(SeekFrom::Start(0)).unwrap();
203        let mut out = Vec::new();
204        std::io::Read::read_to_end(&mut f, &mut out).unwrap();
205        assert_eq!(out, b"hello world");
206    }
207
208    #[test]
209    fn test_write_all_2() {
210        let f = tempfile::tempfile().unwrap();
211        write_all(&f, b"").unwrap();
212
213        let mut f = f;
214        f.seek(SeekFrom::Start(0)).unwrap();
215        let mut out = Vec::new();
216        std::io::Read::read_to_end(&mut f, &mut out).unwrap();
217        assert!(out.is_empty());
218    }
219
220    #[test]
221    fn test_write_all_3() {
222        let payload = vec![0xabu8; 16384];
223        let f = tempfile::tempfile().unwrap();
224        write_all(&f, &payload).unwrap();
225
226        let mut f = f;
227        f.seek(SeekFrom::Start(0)).unwrap();
228        let mut out = Vec::new();
229        std::io::Read::read_to_end(&mut f, &mut out).unwrap();
230        assert_eq!(out, payload);
231    }
232
233    #[test]
234    fn test_copy_1() {
235        let mut src = tempfile_with(b"copy me");
236        let mut dst = tempfile::tempfile().unwrap();
237        let n = copy(&mut src, &mut dst).unwrap();
238        assert_eq!(n, 7);
239
240        dst.seek(SeekFrom::Start(0)).unwrap();
241        let mut out = Vec::new();
242        std::io::Read::read_to_end(&mut dst, &mut out).unwrap();
243        assert_eq!(out, b"copy me");
244    }
245
246    #[test]
247    fn test_copy_2() {
248        let mut src = tempfile_with(b"");
249        let mut dst = tempfile::tempfile().unwrap();
250        let n = copy(&mut src, &mut dst).unwrap();
251        assert_eq!(n, 0);
252    }
253
254    #[test]
255    fn test_copy_3() {
256        let payload = vec![0x42u8; 65536];
257        let mut src = tempfile_with(&payload);
258        let mut dst = tempfile::tempfile().unwrap();
259        let n = copy(&mut src, &mut dst).unwrap();
260        assert_eq!(n as usize, payload.len());
261
262        dst.seek(SeekFrom::Start(0)).unwrap();
263        let mut out = Vec::new();
264        std::io::Read::read_to_end(&mut dst, &mut out).unwrap();
265        assert_eq!(out, payload);
266    }
267
268    #[test]
269    fn test_readfd_1() {
270        let mut f = tempfile_with(b"trait test");
271        fn accept_readfd(r: &mut dyn ReadFd) -> Vec<u8> {
272            let mut buf = Vec::new();
273            r.read_to_end(&mut buf).unwrap();
274            buf
275        }
276        let data = accept_readfd(&mut f);
277        assert_eq!(data, b"trait test");
278    }
279
280    #[test]
281    fn test_writefd_1() {
282        let mut f = tempfile::tempfile().unwrap();
283        fn accept_writefd(w: &mut dyn WriteFd, data: &[u8]) {
284            w.write_all(data).unwrap();
285        }
286        accept_writefd(&mut f, b"trait write");
287
288        f.seek(SeekFrom::Start(0)).unwrap();
289        let mut out = Vec::new();
290        std::io::Read::read_to_end(&mut f, &mut out).unwrap();
291        assert_eq!(out, b"trait write");
292    }
293}