Skip to content

Commit c7e2efc

Browse files
committed
feat: refactor stdio handling
1 parent 94b06d9 commit c7e2efc

9 files changed

Lines changed: 126 additions & 100 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/cat/src/cat.rs

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -480,44 +480,29 @@ fn get_input_type(path: &OsString) -> CatResult<InputType> {
480480
/// Writes handle to stdout with no configuration. This allows a
481481
/// simple memory copy.
482482
fn print_fast<R: FdReadable>(handle: &mut InputHandle<R>) -> CatResult<()> {
483-
let stdout = io::stdout();
484-
#[cfg(any(target_os = "linux", target_os = "android"))]
485-
let mut stdout = stdout;
486483
#[cfg(any(target_os = "linux", target_os = "android"))]
487484
{
488485
// If we're on Linux or Android, try to use the splice() system call
489486
// for faster writing. If it works, we're done.
490-
if !splice::write_fast_using_splice(handle, &mut stdout)? {
487+
if !splice::write_fast_using_splice(handle, &mut io::stdout())? {
491488
return Ok(());
492489
}
493490
}
494491
// If we're not on Linux or Android, or the splice() call failed,
495492
// fall back on slower writing.
496-
print_unbuffered(handle, stdout)
493+
print_unbuffered(handle)
497494
}
498495

499496
#[cfg_attr(any(target_os = "linux", target_os = "android"), inline(never))] // splice fast-path does not require this allocation
500-
fn print_unbuffered<R: FdReadable>(
501-
handle: &mut InputHandle<R>,
502-
stdout: io::Stdout,
503-
) -> CatResult<()> {
504-
#[cfg(any(unix, target_os = "wasi"))]
505-
let mut stdout = uucore::io::RawWriter(stdout); // use raw syscall to remove buffering
506-
#[cfg(not(any(unix, target_os = "wasi")))]
507-
let mut stdout = stdout.lock();
497+
fn print_unbuffered<R: FdReadable>(handle: &mut InputHandle<R>) -> CatResult<()> {
498+
let mut stdout = uucore::stdio::stdout_raw();
508499
let mut buf = [0; 1024 * 64];
509500
loop {
510501
match handle.reader.read(&mut buf) {
511502
Ok(0) => return Ok(()),
512-
Ok(n) => {
513-
stdout
514-
.write_all(&buf[..n])
515-
.inspect_err(handle_broken_pipe)?;
516-
// cannot use rustix::io on Windows
517-
// really bad workaround for unbuffered write <https://github.com/uutils/coreutils/issues/12188>
518-
#[cfg(not(any(unix, target_os = "wasi")))]
519-
stdout.flush().inspect_err(handle_broken_pipe)?;
520-
}
503+
Ok(n) => stdout
504+
.write_all(&buf[..n])
505+
.inspect_err(handle_broken_pipe)?,
521506
Err(e) if e.kind() != ErrorKind::Interrupted => return Err(e.into()),
522507
_ => {}
523508
}

src/uu/dd/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@ doctest = false
2020

2121
[dependencies]
2222
clap = { workspace = true }
23+
fluent = { workspace = true }
2324
gcd = { workspace = true }
2425
libc = { workspace = true }
26+
rustix = { workspace = true }
27+
thiserror = { workspace = true }
2528
uucore = { workspace = true, features = [
2629
"format",
2730
"parser-size",
2831
"quoting-style",
2932
"fs",
3033
"signals",
3134
] }
32-
thiserror = { workspace = true }
33-
fluent = { workspace = true }
3435

3536
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
3637
nix = { workspace = true, features = ["fs", "signal"] }

src/uu/dd/src/dd.rs

Lines changed: 35 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use progress::ProgUpdateType;
2525
use progress::{ProgUpdate, ReadStat, StatusLevel, WriteStat, gen_prog_updater};
2626
#[cfg(target_os = "linux")]
2727
use progress::{check_and_reset_sigusr1, install_sigusr1_handler};
28-
use uucore::io::OwnedFileDescriptorOrHandle;
28+
use uucore::stdio::{StdinRaw, StdoutRaw};
2929
use uucore::translate;
3030

3131
use std::cmp;
@@ -37,15 +37,12 @@ use std::fs::{File, OpenOptions};
3737
use std::io::{self, Read, Seek, SeekFrom, Write};
3838
#[cfg(any(target_os = "linux", target_os = "android"))]
3939
use std::os::fd::AsFd;
40+
#[cfg(unix)]
41+
use std::os::unix::fs::FileTypeExt;
4042
#[cfg(any(target_os = "linux", target_os = "android"))]
4143
use std::os::unix::fs::OpenOptionsExt;
42-
#[cfg(unix)]
43-
use std::os::unix::{
44-
fs::FileTypeExt,
45-
io::{AsRawFd, FromRawFd},
46-
};
4744
#[cfg(windows)]
48-
use std::os::windows::{fs::MetadataExt, io::AsHandle};
45+
use std::os::windows::fs::MetadataExt;
4946
use std::path::Path;
5047
use std::sync::atomic::AtomicU8;
5148
use std::sync::{Arc, atomic::Ordering::Relaxed, mpsc};
@@ -178,20 +175,20 @@ impl Num {
178175
}
179176
}
180177

181-
/// Read and discard `n` bytes from `reader` using a buffer of size `buf_size`.
178+
/// Read and discard `n` bytes from `file` using a buffer of size `buf_size`.
182179
///
183180
/// This is more efficient than `io::copy` with `BufReader` because it reads
184181
/// directly in `buf_size`-sized chunks, matching GNU dd's behavior.
185182
/// Returns the total number of bytes actually read.
186-
fn read_and_discard<R: Read>(reader: &mut R, n: u64, buf_size: usize) -> io::Result<u64> {
183+
fn read_and_discard(file: &mut File, n: u64, buf_size: usize) -> io::Result<u64> {
187184
// todo: consider splice()ing to /dev/null on Linux
188185
let mut buf = Vec::with_capacity(buf_size);
189186
let mut total = 0u64;
190187
let mut remaining = n;
191188
while remaining > 0 {
192189
let to_read = cmp::min(remaining, buf_size as u64);
193190
buf.clear();
194-
match reader.by_ref().take(to_read).read_to_end(&mut buf) {
191+
match file.take(to_read).read_to_end(&mut buf) {
195192
Ok(0) => break, // EOF
196193
Ok(bytes_read) => {
197194
total += bytes_read as u64;
@@ -207,44 +204,25 @@ fn read_and_discard<R: Read>(reader: &mut R, n: u64, buf_size: usize) -> io::Res
207204

208205
/// Data sources.
209206
///
210-
/// Use [`Source::stdin_as_file`] if available to enable more
207+
/// Use [`Source::StdinRaw`] if available to enable more
211208
/// fine-grained access to reading from stdin.
212209
enum Source {
213-
/// Input from stdin.
214-
#[cfg(not(unix))]
215-
Stdin(io::Stdin),
216-
217210
/// Input from a file.
218211
File(File),
219212

220213
/// Input from stdin, opened from its file descriptor.
221-
#[cfg(unix)]
222-
StdinFile(File),
214+
StdinRaw(StdinRaw),
223215

224216
/// Input from a named pipe, also known as a FIFO.
225217
#[cfg(unix)]
226218
Fifo(File),
227219
}
228220

229221
impl Source {
230-
/// Create a source from stdin using its raw file descriptor.
231-
///
232-
/// This returns an instance of the `Source::StdinFile` variant,
233-
/// using the raw file descriptor of [`io::Stdin`] to create
234-
/// the [`File`] parameter. You can use this instead of
235-
/// `Source::Stdin` to allow reading from stdin without consuming
236-
/// the entire contents of stdin when this process terminates.
237-
#[cfg(unix)]
238-
fn stdin_as_file() -> Self {
239-
let fd = io::stdin().as_raw_fd();
240-
let f = unsafe { File::from_raw_fd(fd) };
241-
Self::StdinFile(f)
242-
}
243-
244222
fn skip(&mut self, n: u64, ibs: usize) -> io::Result<u64> {
245223
match self {
246224
#[cfg(not(unix))]
247-
Self::Stdin(stdin) => {
225+
Self::StdinRaw(stdin) => {
248226
let m = read_and_discard(stdin, n, ibs)?;
249227
if m < n {
250228
show_error!(
@@ -255,8 +233,8 @@ impl Source {
255233
Ok(m)
256234
}
257235
#[cfg(unix)]
258-
Self::StdinFile(f) => {
259-
if let Ok(Some(len)) = try_get_len_of_block_device(f)
236+
Self::StdinRaw(stdin) => {
237+
if let Ok(Some(len)) = try_get_len_of_block_device(stdin)
260238
&& len < n
261239
{
262240
// GNU compatibility:
@@ -269,9 +247,9 @@ impl Source {
269247
return Ok(len);
270248
}
271249
// Get file length before seeking to avoid race condition
272-
let file_len = f.metadata().as_ref().map_or(u64::MAX, Metadata::len);
250+
let file_len = stdin.metadata().as_ref().map_or(u64::MAX, Metadata::len);
273251
// Try seek first; fall back to read if not seekable
274-
match n.try_into().ok().map(|n| f.seek(SeekFrom::Current(n))) {
252+
match n.try_into().ok().map(|n| stdin.seek(SeekFrom::Current(n))) {
275253
Some(Ok(pos)) => {
276254
if pos > file_len {
277255
show_error!(
@@ -284,7 +262,7 @@ impl Source {
284262
// ESPIPE means the file descriptor is not seekable (e.g., a pipe),
285263
// so fall back to reading and discarding bytes using ibs-sized buffer
286264
Some(Err(e)) if e.raw_os_error() == Some(libc::ESPIPE) => {
287-
let m = read_and_discard(f, n, ibs)?;
265+
let m = read_and_discard(stdin, n, ibs)?;
288266
if m < n {
289267
show_error!(
290268
"{}",
@@ -331,11 +309,8 @@ impl Source {
331309
impl Read for Source {
332310
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
333311
match self {
334-
#[cfg(not(unix))]
335-
Self::Stdin(stdin) => stdin.read(buf),
336312
Self::File(f) => f.read(buf),
337-
#[cfg(unix)]
338-
Self::StdinFile(f) => f.read(buf),
313+
Self::StdinRaw(f) => f.read(buf),
339314
#[cfg(unix)]
340315
Self::Fifo(f) => f.read(buf),
341316
}
@@ -359,10 +334,11 @@ struct Input<'a> {
359334
impl<'a> Input<'a> {
360335
/// Instantiate this struct with stdin as a source.
361336
fn new_stdin(settings: &'a Settings) -> UResult<Self> {
337+
let stdin = uucore::stdio::stdin_raw();
338+
362339
#[cfg(not(unix))]
363340
let mut src = {
364-
let f = File::from(io::stdin().as_handle().try_clone_to_owned()?);
365-
let is_file = if let Ok(metadata) = f.metadata() {
341+
let is_file = if let Ok(metadata) = stdin.metadata() {
366342
// this hack is needed as there is no other way on windows
367343
// to differentiate between the case where `seek` works
368344
// on a file handle or not. i.e. when the handle is no real
@@ -373,15 +349,15 @@ impl<'a> Input<'a> {
373349
false
374350
};
375351
if is_file {
376-
Source::File(f)
352+
Source::File(stdin.try_clone()?)
377353
} else {
378-
Source::Stdin(io::stdin())
354+
Source::StdinRaw(stdin)
379355
}
380356
};
381357
#[cfg(unix)]
382-
let mut src = Source::stdin_as_file();
358+
let mut src = Source::StdinRaw(stdin);
383359
#[cfg(unix)]
384-
if let Source::StdinFile(f) = &src
360+
if let Source::StdinRaw(f) = &src
385361
&& settings.iflags.directory
386362
&& !f.metadata()?.is_dir()
387363
{
@@ -599,7 +575,7 @@ enum Density {
599575
/// Data destinations.
600576
enum Dest {
601577
/// Output to stdout.
602-
Stdout(File),
578+
StdoutRaw(StdoutRaw),
603579

604580
/// Output to a file.
605581
///
@@ -619,7 +595,7 @@ enum Dest {
619595
impl Dest {
620596
fn fsync(&mut self) -> io::Result<()> {
621597
match self {
622-
Self::Stdout(stdout) => stdout.flush(),
598+
Self::StdoutRaw(stdout) => stdout.flush(),
623599
Self::File(f, _) => {
624600
f.flush()?;
625601
f.sync_all()
@@ -636,7 +612,7 @@ impl Dest {
636612

637613
fn fdatasync(&mut self) -> io::Result<()> {
638614
match self {
639-
Self::Stdout(stdout) => stdout.flush(),
615+
Self::StdoutRaw(stdout) => stdout.flush(),
640616
Self::File(f, _) => {
641617
f.flush()?;
642618
f.sync_data()
@@ -654,7 +630,7 @@ impl Dest {
654630
#[cfg_attr(not(unix), allow(unused_variables))]
655631
fn seek(&mut self, n: u64, obs: usize) -> io::Result<u64> {
656632
match self {
657-
Self::Stdout(stdout) => io::copy(&mut io::repeat(0).take(n), stdout),
633+
Self::StdoutRaw(stdout) => io::copy(&mut io::repeat(0).take(n), &mut **stdout),
658634
Self::File(f, _) => {
659635
#[cfg(unix)]
660636
if let Ok(Some(len)) = try_get_len_of_block_device(f)
@@ -790,7 +766,7 @@ impl Write for Dest {
790766
Err(e) => Err(e),
791767
}
792768
}
793-
Self::Stdout(stdout) => stdout.write(buf),
769+
Self::StdoutRaw(stdout) => stdout.write(buf),
794770
#[cfg(unix)]
795771
Self::Fifo(f) => f.write(buf),
796772
#[cfg(unix)]
@@ -800,7 +776,7 @@ impl Write for Dest {
800776

801777
fn flush(&mut self) -> io::Result<()> {
802778
match self {
803-
Self::Stdout(stdout) => stdout.flush(),
779+
Self::StdoutRaw(stdout) => stdout.flush(),
804780
Self::File(f, _) => f.flush(),
805781
#[cfg(unix)]
806782
Self::Fifo(f) => f.flush(),
@@ -827,8 +803,8 @@ struct Output<'a> {
827803
impl<'a> Output<'a> {
828804
/// Instantiate this struct with stdout as a destination.
829805
fn new_stdout(settings: &'a Settings) -> UResult<Self> {
830-
let fx = OwnedFileDescriptorOrHandle::from(io::stdout())?;
831-
let mut dst = Dest::Stdout(fx.into_file());
806+
let stdout = uucore::stdio::stdout_raw();
807+
let mut dst = Dest::StdoutRaw(stdout);
832808
dst.seek(settings.seek, settings.obs)
833809
.map_err_context(|| translate!("dd-error-write-error"))?;
834810
Ok(Self { dst, settings })
@@ -888,16 +864,17 @@ impl<'a> Output<'a> {
888864
/// already opened by the system (stdout) and has a state
889865
/// (current position) that shall be used.
890866
fn new_file_from_stdout(settings: &'a Settings) -> UResult<Self> {
891-
let fx = OwnedFileDescriptorOrHandle::from(io::stdout())?;
867+
let stdout = uucore::stdio::stdout_raw();
892868
#[cfg(any(target_os = "linux", target_os = "android"))]
893869
if let Some(libc_flags) = make_linux_oflags(&settings.oflags) {
894870
nix::fcntl::fcntl(
895-
fx.as_raw().as_fd(),
871+
stdout.as_fd(),
896872
FcntlArg::F_SETFL(OFlag::from_bits_retain(libc_flags)),
897873
)?;
898874
}
899875

900-
Self::prepare_file(fx.into_file(), settings)
876+
// TODO: avoid cloning the underlying file handle
877+
Self::prepare_file(stdout.try_clone()?, settings)
901878
}
902879

903880
/// Instantiate this struct with the given named pipe as a destination.

src/uucore/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jiff = { workspace = true, optional = true, features = [
3636
"tzdb-concatenated",
3737
] }
3838
rustc-hash = { workspace = true }
39-
rustix = { workspace = true, features = ["fs", "pipe", "process"] }
39+
rustix = { workspace = true, features = ["fs", "pipe", "process", "stdio"] }
4040
time = { workspace = true, optional = true, features = [
4141
"formatting",
4242
"local-offset",

src/uucore/src/lib/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub use crate::mods::locale;
3232
pub use crate::mods::os;
3333
pub use crate::mods::panic;
3434
pub use crate::mods::posix;
35+
pub use crate::mods::stdio;
3536

3637
// * feature-gated modules
3738
#[cfg(feature = "backup-control")]

src/uucore/src/lib/mods.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ pub mod locale;
1414
pub mod os;
1515
pub mod panic;
1616
pub mod posix;
17+
pub mod stdio;

src/uucore/src/lib/mods/io.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,6 @@ impl<T: AsFd> io::Write for RawWriter<T> {
4242
Ok(())
4343
}
4444
}
45-
46-
// io::write_all but no buffering (deprecated?)
47-
#[inline]
48-
#[cfg(any(unix, target_os = "wasi"))]
49-
pub fn write_all_raw(output: impl AsFd, buf: &[u8]) -> io::Result<()> {
50-
let mut written = 0;
51-
let len = buf.len();
52-
while written < len {
53-
match rustix::io::write(&output, &buf[written..]) {
54-
Ok(n) => written += n,
55-
Err(e) if e.kind() != io::ErrorKind::Interrupted => return Err(e.into()),
56-
_ => {}
57-
}
58-
}
59-
Ok(())
60-
}
61-
6245
/// abstraction wrapper for native file handle / file descriptor
6346
// todo: remove clone introducing additional syscall dependency
6447
pub struct OwnedFileDescriptorOrHandle {

0 commit comments

Comments
 (0)