Skip to content

Commit d3ac1c9

Browse files
committed
dd: refactor stdio handling
1 parent f65350a commit d3ac1c9

4 files changed

Lines changed: 121 additions & 60 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/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, features = ["stdio"] }
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: 36 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ mod datastructures;
1212
mod numbers;
1313
mod parseargs;
1414
mod progress;
15+
mod stdio;
1516

1617
use crate::bufferedoutput::BufferedOutput;
18+
use crate::stdio::{StdinRaw, StdoutRaw};
1719
use blocks::conv_block_unblock_helper;
1820
use datastructures::{ConversionMode, IConvFlags, IFlags, OConvFlags, OFlags, options};
1921
#[cfg(any(target_os = "linux", target_os = "android"))]
@@ -25,7 +27,6 @@ use progress::ProgUpdateType;
2527
use progress::{ProgUpdate, ReadStat, StatusLevel, WriteStat, gen_prog_updater};
2628
#[cfg(target_os = "linux")]
2729
use progress::{check_and_reset_sigusr1, install_sigusr1_handler};
28-
use uucore::io::OwnedFileDescriptorOrHandle;
2930
use uucore::translate;
3031

3132
use std::cmp;
@@ -37,15 +38,12 @@ use std::fs::{File, OpenOptions};
3738
use std::io::{self, Read, Seek, SeekFrom, Write};
3839
#[cfg(any(target_os = "linux", target_os = "android"))]
3940
use std::os::fd::AsFd;
41+
#[cfg(unix)]
42+
use std::os::unix::fs::FileTypeExt;
4043
#[cfg(any(target_os = "linux", target_os = "android"))]
4144
use std::os::unix::fs::OpenOptionsExt;
42-
#[cfg(unix)]
43-
use std::os::unix::{
44-
fs::FileTypeExt,
45-
io::{AsRawFd, FromRawFd},
46-
};
4745
#[cfg(windows)]
48-
use std::os::windows::{fs::MetadataExt, io::AsHandle};
46+
use std::os::windows::fs::MetadataExt;
4947
use std::path::Path;
5048
use std::sync::atomic::AtomicU8;
5149
use std::sync::{Arc, atomic::Ordering::Relaxed, mpsc};
@@ -178,20 +176,20 @@ impl Num {
178176
}
179177
}
180178

181-
/// Read and discard `n` bytes from `reader` using a buffer of size `buf_size`.
179+
/// Read and discard `n` bytes from `file` using a buffer of size `buf_size`.
182180
///
183181
/// This is more efficient than `io::copy` with `BufReader` because it reads
184182
/// directly in `buf_size`-sized chunks, matching GNU dd's behavior.
185183
/// 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> {
184+
fn read_and_discard(file: &mut File, n: u64, buf_size: usize) -> io::Result<u64> {
187185
// todo: consider splice()ing to /dev/null on Linux
188186
let mut buf = Vec::with_capacity(buf_size);
189187
let mut total = 0u64;
190188
let mut remaining = n;
191189
while remaining > 0 {
192190
let to_read = cmp::min(remaining, buf_size as u64);
193191
buf.clear();
194-
match reader.by_ref().take(to_read).read_to_end(&mut buf) {
192+
match file.take(to_read).read_to_end(&mut buf) {
195193
Ok(0) => break, // EOF
196194
Ok(bytes_read) => {
197195
total += bytes_read as u64;
@@ -207,44 +205,25 @@ fn read_and_discard<R: Read>(reader: &mut R, n: u64, buf_size: usize) -> io::Res
207205

208206
/// Data sources.
209207
///
210-
/// Use [`Source::stdin_as_file`] if available to enable more
208+
/// Use [`Source::StdinRaw`] if available to enable more
211209
/// fine-grained access to reading from stdin.
212210
enum Source {
213-
/// Input from stdin.
214-
#[cfg(not(unix))]
215-
Stdin(io::Stdin),
216-
217211
/// Input from a file.
218212
File(File),
219213

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

224217
/// Input from a named pipe, also known as a FIFO.
225218
#[cfg(unix)]
226219
Fifo(File),
227220
}
228221

229222
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-
244223
fn skip(&mut self, n: u64, ibs: usize) -> io::Result<u64> {
245224
match self {
246225
#[cfg(not(unix))]
247-
Self::Stdin(stdin) => {
226+
Self::StdinRaw(stdin) => {
248227
let m = read_and_discard(stdin, n, ibs)?;
249228
if m < n {
250229
show_error!(
@@ -255,8 +234,8 @@ impl Source {
255234
Ok(m)
256235
}
257236
#[cfg(unix)]
258-
Self::StdinFile(f) => {
259-
if let Ok(Some(len)) = try_get_len_of_block_device(f)
237+
Self::StdinRaw(stdin) => {
238+
if let Ok(Some(len)) = try_get_len_of_block_device(stdin)
260239
&& len < n
261240
{
262241
// GNU compatibility:
@@ -269,9 +248,9 @@ impl Source {
269248
return Ok(len);
270249
}
271250
// Get file length before seeking to avoid race condition
272-
let file_len = f.metadata().as_ref().map_or(u64::MAX, Metadata::len);
251+
let file_len = stdin.metadata().as_ref().map_or(u64::MAX, Metadata::len);
273252
// Try seek first; fall back to read if not seekable
274-
match n.try_into().ok().map(|n| f.seek(SeekFrom::Current(n))) {
253+
match n.try_into().ok().map(|n| stdin.seek(SeekFrom::Current(n))) {
275254
Some(Ok(pos)) => {
276255
if pos > file_len {
277256
show_error!(
@@ -284,7 +263,7 @@ impl Source {
284263
// ESPIPE means the file descriptor is not seekable (e.g., a pipe),
285264
// so fall back to reading and discarding bytes using ibs-sized buffer
286265
Some(Err(e)) if e.raw_os_error() == Some(libc::ESPIPE) => {
287-
let m = read_and_discard(f, n, ibs)?;
266+
let m = read_and_discard(stdin, n, ibs)?;
288267
if m < n {
289268
show_error!(
290269
"{}",
@@ -331,11 +310,8 @@ impl Source {
331310
impl Read for Source {
332311
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
333312
match self {
334-
#[cfg(not(unix))]
335-
Self::Stdin(stdin) => stdin.read(buf),
336313
Self::File(f) => f.read(buf),
337-
#[cfg(unix)]
338-
Self::StdinFile(f) => f.read(buf),
314+
Self::StdinRaw(f) => f.read(buf),
339315
#[cfg(unix)]
340316
Self::Fifo(f) => f.read(buf),
341317
}
@@ -359,10 +335,11 @@ struct Input<'a> {
359335
impl<'a> Input<'a> {
360336
/// Instantiate this struct with stdin as a source.
361337
fn new_stdin(settings: &'a Settings) -> UResult<Self> {
338+
let stdin = StdinRaw::new();
339+
362340
#[cfg(not(unix))]
363341
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() {
342+
let is_file = if let Ok(metadata) = stdin.metadata() {
366343
// this hack is needed as there is no other way on windows
367344
// to differentiate between the case where `seek` works
368345
// on a file handle or not. i.e. when the handle is no real
@@ -373,15 +350,15 @@ impl<'a> Input<'a> {
373350
false
374351
};
375352
if is_file {
376-
Source::File(f)
353+
Source::File(stdin.try_clone()?)
377354
} else {
378-
Source::Stdin(io::stdin())
355+
Source::StdinRaw(stdin)
379356
}
380357
};
381358
#[cfg(unix)]
382-
let mut src = Source::stdin_as_file();
359+
let mut src = Source::StdinRaw(stdin);
383360
#[cfg(unix)]
384-
if let Source::StdinFile(f) = &src
361+
if let Source::StdinRaw(f) = &src
385362
&& settings.iflags.directory
386363
&& !f.metadata()?.is_dir()
387364
{
@@ -599,7 +576,7 @@ enum Density {
599576
/// Data destinations.
600577
enum Dest {
601578
/// Output to stdout.
602-
Stdout(File),
579+
StdoutRaw(StdoutRaw),
603580

604581
/// Output to a file.
605582
///
@@ -619,7 +596,7 @@ enum Dest {
619596
impl Dest {
620597
fn fsync(&mut self) -> io::Result<()> {
621598
match self {
622-
Self::Stdout(stdout) => stdout.flush(),
599+
Self::StdoutRaw(stdout) => stdout.flush(),
623600
Self::File(f, _) => {
624601
f.flush()?;
625602
f.sync_all()
@@ -636,7 +613,7 @@ impl Dest {
636613

637614
fn fdatasync(&mut self) -> io::Result<()> {
638615
match self {
639-
Self::Stdout(stdout) => stdout.flush(),
616+
Self::StdoutRaw(stdout) => stdout.flush(),
640617
Self::File(f, _) => {
641618
f.flush()?;
642619
f.sync_data()
@@ -654,7 +631,7 @@ impl Dest {
654631
#[cfg_attr(not(unix), allow(unused_variables))]
655632
fn seek(&mut self, n: u64, obs: usize) -> io::Result<u64> {
656633
match self {
657-
Self::Stdout(stdout) => io::copy(&mut io::repeat(0).take(n), stdout),
634+
Self::StdoutRaw(stdout) => io::copy(&mut io::repeat(0).take(n), &mut **stdout),
658635
Self::File(f, _) => {
659636
#[cfg(unix)]
660637
if let Ok(Some(len)) = try_get_len_of_block_device(f)
@@ -790,7 +767,7 @@ impl Write for Dest {
790767
Err(e) => Err(e),
791768
}
792769
}
793-
Self::Stdout(stdout) => stdout.write(buf),
770+
Self::StdoutRaw(stdout) => stdout.write(buf),
794771
#[cfg(unix)]
795772
Self::Fifo(f) => f.write(buf),
796773
#[cfg(unix)]
@@ -800,7 +777,7 @@ impl Write for Dest {
800777

801778
fn flush(&mut self) -> io::Result<()> {
802779
match self {
803-
Self::Stdout(stdout) => stdout.flush(),
780+
Self::StdoutRaw(stdout) => stdout.flush(),
804781
Self::File(f, _) => f.flush(),
805782
#[cfg(unix)]
806783
Self::Fifo(f) => f.flush(),
@@ -827,8 +804,8 @@ struct Output<'a> {
827804
impl<'a> Output<'a> {
828805
/// Instantiate this struct with stdout as a destination.
829806
fn new_stdout(settings: &'a Settings) -> UResult<Self> {
830-
let fx = OwnedFileDescriptorOrHandle::from(io::stdout())?;
831-
let mut dst = Dest::Stdout(fx.into_file());
807+
let stdout = StdoutRaw::new();
808+
let mut dst = Dest::StdoutRaw(stdout);
832809
dst.seek(settings.seek, settings.obs)
833810
.map_err_context(|| translate!("dd-error-write-error"))?;
834811
Ok(Self { dst, settings })
@@ -888,16 +865,17 @@ impl<'a> Output<'a> {
888865
/// already opened by the system (stdout) and has a state
889866
/// (current position) that shall be used.
890867
fn new_file_from_stdout(settings: &'a Settings) -> UResult<Self> {
891-
let fx = OwnedFileDescriptorOrHandle::from(io::stdout())?;
868+
let stdout = StdoutRaw::new();
892869
#[cfg(any(target_os = "linux", target_os = "android"))]
893870
if let Some(libc_flags) = make_linux_oflags(&settings.oflags) {
894871
nix::fcntl::fcntl(
895-
fx.as_raw().as_fd(),
872+
stdout.as_fd(),
896873
FcntlArg::F_SETFL(OFlag::from_bits_retain(libc_flags)),
897874
)?;
898875
}
899876

900-
Self::prepare_file(fx.into_file(), settings)
877+
// TODO: avoid cloning the underlying file descriptor here
878+
Self::prepare_file(stdout.try_clone()?, settings)
901879
}
902880

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

src/uu/dd/src/stdio.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
//! Abstractions for raw access to stdin and stdout, without buffering.
7+
8+
use core::ops::{Deref, DerefMut};
9+
#[cfg(windows)]
10+
use std::io::{stdin, stdout};
11+
#[cfg(windows)]
12+
use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _};
13+
use std::{fs::File, mem::ManuallyDrop};
14+
15+
pub(crate) struct StdinRaw(ManuallyDrop<File>);
16+
17+
impl StdinRaw {
18+
#[cfg(not(windows))]
19+
pub fn new() -> Self {
20+
// SAFETY: We ensure that the file descriptor is never closed by
21+
// wrapping the `File` in `ManuallyDrop`.
22+
let fd = unsafe { rustix::stdio::take_stdin() };
23+
let f = File::from(fd);
24+
Self(ManuallyDrop::new(f))
25+
}
26+
27+
#[cfg(windows)]
28+
pub fn new() -> Self {
29+
let handle = stdin().as_raw_handle();
30+
let f = unsafe { File::from_raw_handle(handle) };
31+
Self(ManuallyDrop::new(f))
32+
}
33+
}
34+
35+
impl Deref for StdinRaw {
36+
type Target = File;
37+
38+
fn deref(&self) -> &Self::Target {
39+
&self.0
40+
}
41+
}
42+
43+
impl DerefMut for StdinRaw {
44+
fn deref_mut(&mut self) -> &mut Self::Target {
45+
&mut self.0
46+
}
47+
}
48+
49+
pub(crate) struct StdoutRaw(ManuallyDrop<File>);
50+
51+
impl StdoutRaw {
52+
#[cfg(not(windows))]
53+
pub fn new() -> Self {
54+
// SAFETY: We ensure that the file descriptor is never closed by
55+
// wrapping the `File` in `ManuallyDrop`.
56+
let fd = unsafe { rustix::stdio::take_stdout() };
57+
let f = File::from(fd);
58+
Self(ManuallyDrop::new(f))
59+
}
60+
61+
#[cfg(windows)]
62+
pub fn new() -> Self {
63+
let handle = stdout().as_raw_handle();
64+
let f = unsafe { File::from_raw_handle(handle) };
65+
Self(ManuallyDrop::new(f))
66+
}
67+
}
68+
69+
impl Deref for StdoutRaw {
70+
type Target = File;
71+
72+
fn deref(&self) -> &Self::Target {
73+
&self.0
74+
}
75+
}
76+
77+
impl DerefMut for StdoutRaw {
78+
fn deref_mut(&mut self) -> &mut Self::Target {
79+
&mut self.0
80+
}
81+
}

0 commit comments

Comments
 (0)