Skip to main content

coreutils_rs/dd/
core.rs

1use std::fs::{File, OpenOptions};
2use std::io::{self, Read, Seek, SeekFrom, Write};
3use std::time::Instant;
4
5/// Status output level for dd.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum StatusLevel {
8    /// Print transfer stats at end (default).
9    #[default]
10    Default,
11    /// No informational messages to stderr.
12    None,
13    /// Print periodic transfer stats (like GNU dd `status=progress`).
14    Progress,
15    /// Like default but also suppress error messages.
16    NoError,
17    /// Print record counts but suppress transfer speed/bytes line.
18    NoXfer,
19}
20
21/// Conversion flags for dd (`conv=` option).
22#[derive(Debug, Clone, Default)]
23pub struct DdConv {
24    /// Convert to lowercase.
25    pub lcase: bool,
26    /// Convert to uppercase.
27    pub ucase: bool,
28    /// Swap every pair of input bytes.
29    pub swab: bool,
30    /// Continue after read errors.
31    pub noerror: bool,
32    /// Do not truncate the output file.
33    pub notrunc: bool,
34    /// Pad every input block with NULs to ibs-size.
35    pub sync: bool,
36    /// Call fdatasync on output before finishing.
37    pub fdatasync: bool,
38    /// Call fsync on output before finishing.
39    pub fsync: bool,
40    /// Fail if the output file already exists.
41    pub excl: bool,
42    /// Do not create the output file.
43    pub nocreat: bool,
44    /// Convert fixed-length records to newline-terminated (unblock).
45    pub unblock: bool,
46    /// Convert newline-terminated records to fixed-length (block).
47    pub block: bool,
48}
49
50/// Input/output flags for dd (`iflag=`/`oflag=` options).
51#[derive(Debug, Clone, Default)]
52pub struct DdFlags {
53    pub append: bool,
54    pub direct: bool,
55    pub directory: bool,
56    pub dsync: bool,
57    pub sync: bool,
58    pub fullblock: bool,
59    pub nonblock: bool,
60    pub noatime: bool,
61    pub nocache: bool,
62    pub noctty: bool,
63    pub nofollow: bool,
64    pub count_bytes: bool,
65    pub skip_bytes: bool,
66}
67
68/// Configuration for a dd operation.
69#[derive(Debug, Clone)]
70pub struct DdConfig {
71    /// Input file path (None = stdin).
72    pub input: Option<String>,
73    /// Output file path (None = stdout).
74    pub output: Option<String>,
75    /// Input block size in bytes.
76    pub ibs: usize,
77    /// Output block size in bytes.
78    pub obs: usize,
79    /// Conversion block size (for block/unblock).
80    pub cbs: usize,
81    /// Copy only this many input blocks (None = unlimited).
82    pub count: Option<u64>,
83    /// Skip this many ibs-sized blocks at start of input.
84    pub skip: u64,
85    /// Skip this many obs-sized blocks at start of output.
86    pub seek: u64,
87    /// Conversion options.
88    pub conv: DdConv,
89    /// Status output level.
90    pub status: StatusLevel,
91    /// Input flags.
92    pub iflag: DdFlags,
93    /// Output flags.
94    pub oflag: DdFlags,
95}
96
97impl Default for DdConfig {
98    fn default() -> Self {
99        DdConfig {
100            input: None,
101            output: None,
102            ibs: 512,
103            obs: 512,
104            cbs: 0,
105            count: None,
106            skip: 0,
107            seek: 0,
108            conv: DdConv::default(),
109            status: StatusLevel::default(),
110            iflag: DdFlags::default(),
111            oflag: DdFlags::default(),
112        }
113    }
114}
115
116/// Statistics from a dd copy operation.
117#[derive(Debug, Clone, Default)]
118pub struct DdStats {
119    /// Number of full input blocks read.
120    pub records_in_full: u64,
121    /// Number of partial input blocks read.
122    pub records_in_partial: u64,
123    /// Number of full output blocks written.
124    pub records_out_full: u64,
125    /// Number of partial output blocks written.
126    pub records_out_partial: u64,
127    /// Total bytes copied.
128    pub bytes_copied: u64,
129}
130
131/// Parse a GNU dd SIZE string with optional suffix and `x` multiplier.
132///
133/// Suffix conventions (matching GNU dd):
134///   - Single letter = binary (powers of 1024): k/K, M, G, T, P, E
135///   - `xB` suffix = decimal (powers of 1000): kB, KB, MB, GB, TB, PB, EB
136///   - `xIB` suffix = explicit binary: KiB, MiB, GiB, TiB, PiB, EiB
137///   - Special: c (1), w (2), b (512)
138///
139/// The `x` operator multiplies terms and chains recursively,
140/// so `1x2x4` = 1 * (2 * 4) = 8.
141pub fn parse_size(s: &str) -> Result<u64, String> {
142    let s = s.trim();
143    if s.is_empty() {
144        return Err("empty size string".to_string());
145    }
146
147    // GNU dd supports 'x' as multiplication: e.g. "2x512", "1Mx2", "1x2x4"
148    // Split on first 'x' and recurse on the right side for chaining.
149    if let Some(pos) = s.find('x') {
150        let left = parse_size_single(&s[..pos])?;
151        let right = parse_size(&s[pos + 1..])?;
152        return left
153            .checked_mul(right)
154            .ok_or_else(|| format!("size overflow: {} * {}", left, right));
155    }
156
157    parse_size_single(s)
158}
159
160fn parse_size_single(s: &str) -> Result<u64, String> {
161    if s.is_empty() {
162        return Err("empty size string".to_string());
163    }
164
165    // Find where the numeric part ends
166    let num_end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
167
168    if num_end == 0 {
169        return Err(format!("invalid number: '{}'", s));
170    }
171
172    let num: u64 = s[..num_end]
173        .parse()
174        .map_err(|e| format!("invalid number '{}': {}", &s[..num_end], e))?;
175
176    let suffix = &s[num_end..];
177    // GNU dd suffix convention: single letter = binary (powers of 1024),
178    // xB suffix = decimal (powers of 1000), xIB suffix = binary (explicit).
179    let multiplier: u64 = match suffix {
180        "" => 1,
181        "c" => 1,
182        "w" => 2,
183        "b" => 512,
184        "k" | "K" => 1024,
185        "kB" | "KB" => 1000,
186        "KiB" => 1024,
187        "M" => 1_048_576,
188        "MB" => 1_000_000,
189        "MiB" => 1_048_576,
190        "G" => 1_073_741_824,
191        "GB" => 1_000_000_000,
192        "GiB" => 1_073_741_824,
193        "T" => 1_099_511_627_776,
194        "TB" => 1_000_000_000_000,
195        "TiB" => 1_099_511_627_776,
196        "P" => 1_125_899_906_842_624,
197        "PB" => 1_000_000_000_000_000,
198        "PiB" => 1_125_899_906_842_624,
199        "E" => 1_152_921_504_606_846_976,
200        "EB" => 1_000_000_000_000_000_000,
201        "EiB" => 1_152_921_504_606_846_976,
202        _ => return Err(format!("invalid suffix: '{}'", suffix)),
203    };
204
205    num.checked_mul(multiplier)
206        .ok_or_else(|| format!("size overflow: {} * {}", num, multiplier))
207}
208
209/// Parse dd command-line arguments (key=value pairs).
210pub fn parse_dd_args(args: &[String]) -> Result<DdConfig, String> {
211    let mut config = DdConfig::default();
212    let mut bs_set = false;
213
214    for arg in args {
215        if let Some((key, value)) = arg.split_once('=') {
216            match key {
217                "if" => config.input = Some(value.to_string()),
218                "of" => config.output = Some(value.to_string()),
219                "bs" => {
220                    let size = parse_size(value)? as usize;
221                    config.ibs = size;
222                    config.obs = size;
223                    bs_set = true;
224                }
225                "ibs" => {
226                    if !bs_set {
227                        config.ibs = parse_size(value)? as usize;
228                    }
229                }
230                "obs" => {
231                    if !bs_set {
232                        config.obs = parse_size(value)? as usize;
233                    }
234                }
235                "cbs" => config.cbs = parse_size(value)? as usize,
236                "count" => config.count = Some(parse_size(value)?),
237                "skip" => config.skip = parse_size(value)?,
238                "seek" => config.seek = parse_size(value)?,
239                "conv" => {
240                    for flag in value.split(',') {
241                        match flag {
242                            "lcase" => config.conv.lcase = true,
243                            "ucase" => config.conv.ucase = true,
244                            "swab" => config.conv.swab = true,
245                            "noerror" => config.conv.noerror = true,
246                            "notrunc" => config.conv.notrunc = true,
247                            "sync" => config.conv.sync = true,
248                            "fdatasync" => config.conv.fdatasync = true,
249                            "fsync" => config.conv.fsync = true,
250                            "excl" => config.conv.excl = true,
251                            "nocreat" => config.conv.nocreat = true,
252                            "block" => config.conv.block = true,
253                            "unblock" => config.conv.unblock = true,
254                            "" => {}
255                            _ => return Err(format!("invalid conversion: '{}'", flag)),
256                        }
257                    }
258                }
259                "iflag" => {
260                    for flag in value.split(',') {
261                        parse_flag(flag, &mut config.iflag)?;
262                    }
263                }
264                "oflag" => {
265                    for flag in value.split(',') {
266                        parse_flag(flag, &mut config.oflag)?;
267                    }
268                }
269                "status" => {
270                    config.status = match value {
271                        "none" => StatusLevel::None,
272                        "noxfer" => StatusLevel::NoXfer,
273                        "noerror" => StatusLevel::NoError,
274                        "progress" => StatusLevel::Progress,
275                        _ => return Err(format!("invalid status level: '{}'", value)),
276                    };
277                }
278                _ => return Err(format!("unrecognized operand: '{}'", arg)),
279            }
280        } else {
281            return Err(format!("unrecognized operand: '{}'", arg));
282        }
283    }
284
285    // Validate conflicting options
286    if config.conv.lcase && config.conv.ucase {
287        return Err("conv=lcase and conv=ucase are mutually exclusive".to_string());
288    }
289    if config.conv.excl && config.conv.nocreat {
290        return Err("conv=excl and conv=nocreat are mutually exclusive".to_string());
291    }
292
293    Ok(config)
294}
295
296/// Parse a single iflag/oflag value into the DdFlags struct.
297fn parse_flag(flag: &str, flags: &mut DdFlags) -> Result<(), String> {
298    match flag {
299        "append" => flags.append = true,
300        "direct" => flags.direct = true,
301        "directory" => flags.directory = true,
302        "dsync" => flags.dsync = true,
303        "sync" => flags.sync = true,
304        "fullblock" => flags.fullblock = true,
305        "nonblock" => flags.nonblock = true,
306        "noatime" => flags.noatime = true,
307        "nocache" => flags.nocache = true,
308        "noctty" => flags.noctty = true,
309        "nofollow" => flags.nofollow = true,
310        "count_bytes" => flags.count_bytes = true,
311        "skip_bytes" => flags.skip_bytes = true,
312        "" => {}
313        _ => return Err(format!("invalid flag: '{}'", flag)),
314    }
315    Ok(())
316}
317
318/// Read a full block from the reader, retrying on partial reads.
319/// Returns the number of bytes actually read (0 means EOF).
320fn read_full_block(reader: &mut dyn Read, buf: &mut [u8]) -> io::Result<usize> {
321    let mut total = 0;
322    while total < buf.len() {
323        match reader.read(&mut buf[total..]) {
324            Ok(0) => break,
325            Ok(n) => total += n,
326            Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
327            Err(e) => return Err(e),
328        }
329    }
330    Ok(total)
331}
332
333/// Apply conversion options to a data block in-place.
334pub fn apply_conversions(data: &mut [u8], conv: &DdConv) {
335    if conv.swab {
336        // Swap every pair of bytes using u64 word-at-a-time processing.
337        // Process 8 bytes at a time: rotate each u16 pair within the u64.
338        let (prefix, chunks, suffix) = unsafe { data.align_to_mut::<u64>() };
339        // Handle unaligned prefix bytes
340        let pairs_pre = prefix.len() / 2;
341        for i in 0..pairs_pre {
342            prefix.swap(i * 2, i * 2 + 1);
343        }
344        // Process aligned u64 chunks: swap adjacent bytes in each pair
345        // For each u64 AABBCCDD_EEFFGGHH, we want BBAADDCC_FFEEHHGG
346        // This is: ((x & 0xFF00FF00FF00FF00) >> 8) | ((x & 0x00FF00FF00FF00FF) << 8)
347        for w in chunks.iter_mut() {
348            let x = *w;
349            *w = ((x & 0xFF00FF00FF00FF00) >> 8) | ((x & 0x00FF00FF00FF00FF) << 8);
350        }
351        // Handle remaining suffix bytes
352        let pairs_suf = suffix.len() / 2;
353        for i in 0..pairs_suf {
354            suffix.swap(i * 2, i * 2 + 1);
355        }
356    }
357
358    if conv.lcase {
359        for b in data.iter_mut() {
360            b.make_ascii_lowercase();
361        }
362    } else if conv.ucase {
363        for b in data.iter_mut() {
364            b.make_ascii_uppercase();
365        }
366    }
367}
368
369/// Skip input blocks by reading and discarding them.
370fn skip_input(reader: &mut dyn Read, blocks: u64, block_size: usize) -> io::Result<()> {
371    let mut discard_buf = vec![0u8; block_size];
372    for _ in 0..blocks {
373        let n = read_full_block(reader, &mut discard_buf)?;
374        if n == 0 {
375            break;
376        }
377    }
378    Ok(())
379}
380
381/// Skip input by reading and discarding exactly `bytes` bytes.
382fn skip_input_bytes(reader: &mut dyn Read, bytes: u64) -> io::Result<()> {
383    let mut remaining = bytes;
384    let mut discard_buf = [0u8; 8192];
385    while remaining > 0 {
386        let chunk = std::cmp::min(remaining, discard_buf.len() as u64) as usize;
387        let n = reader.read(&mut discard_buf[..chunk])?;
388        if n == 0 {
389            break;
390        }
391        remaining -= n as u64;
392    }
393    Ok(())
394}
395
396/// Skip input blocks by seeking (for seekable file inputs).
397fn skip_input_seek(file: &mut File, blocks: u64, block_size: usize) -> io::Result<()> {
398    let offset = blocks * block_size as u64;
399    file.seek(SeekFrom::Start(offset))?;
400    Ok(())
401}
402
403/// Seek output by writing zero blocks (for non-seekable outputs) or using seek.
404fn seek_output(writer: &mut Box<dyn Write>, seek_blocks: u64, block_size: usize) -> io::Result<()> {
405    // Try to seek if the writer supports it. Since we use Box<dyn Write>,
406    // we write zero blocks for the general case.
407    let zero_block = vec![0u8; block_size];
408    for _ in 0..seek_blocks {
409        writer.write_all(&zero_block)?;
410    }
411    Ok(())
412}
413
414/// Seek output on a file using actual file seeking.
415fn seek_output_file(file: &mut File, seek_blocks: u64, block_size: usize) -> io::Result<()> {
416    let offset = seek_blocks * block_size as u64;
417    file.seek(SeekFrom::Start(offset))?;
418    Ok(())
419}
420
421/// Check if any data conversion options are enabled.
422#[cfg(target_os = "linux")]
423fn has_conversions(conv: &DdConv) -> bool {
424    conv.lcase || conv.ucase || conv.swab || conv.sync || conv.block || conv.unblock
425}
426
427/// Check if any iflag/oflag fields require the generic path.
428/// Note: noatime is excluded because the raw path already uses O_NOATIME.
429/// fullblock is excluded because the raw read loop already reads full blocks.
430#[cfg(target_os = "linux")]
431fn has_flags(flags: &DdFlags) -> bool {
432    flags.append
433        || flags.direct
434        || flags.directory
435        || flags.dsync
436        || flags.sync
437        || flags.nonblock
438        || flags.nocache
439        || flags.noctty
440        || flags.nofollow
441        || flags.count_bytes
442        || flags.skip_bytes
443}
444
445/// Raw-syscall fast path: when both input and output are file paths,
446/// ibs == obs, no conversions, and no iflag/oflag are set, bypass
447/// Box<dyn Read/Write> and use libc::read/write directly. Handles
448/// char devices (e.g. /dev/zero) that copy_file_range can't handle.
449#[cfg(target_os = "linux")]
450fn try_raw_dd(config: &DdConfig) -> Option<io::Result<DdStats>> {
451    if config.input.is_none() || config.output.is_none() {
452        return None;
453    }
454    if has_conversions(&config.conv) || config.ibs != config.obs {
455        return None;
456    }
457    // Bail out if any iflag/oflag is set — we don't apply open() flags here
458    if has_flags(&config.iflag) || has_flags(&config.oflag) {
459        return None;
460    }
461
462    let start_time = Instant::now();
463    let in_path = config.input.as_ref().unwrap();
464    let out_path = config.output.as_ref().unwrap();
465
466    // Build CStrings before opening any FDs to avoid leaks on interior NUL
467    let in_cstr = match std::ffi::CString::new(in_path.as_str()) {
468        Ok(c) => c,
469        Err(_) => {
470            return Some(Err(io::Error::new(
471                io::ErrorKind::InvalidInput,
472                format!("input path contains NUL byte: '{}'", in_path),
473            )));
474        }
475    };
476    let out_cstr = match std::ffi::CString::new(out_path.as_str()) {
477        Ok(c) => c,
478        Err(_) => {
479            return Some(Err(io::Error::new(
480                io::ErrorKind::InvalidInput,
481                format!("output path contains NUL byte: '{}'", out_path),
482            )));
483        }
484    };
485
486    // Open input: try O_NOATIME first (avoids atime updates, saves ~1 syscall on owned files).
487    // If EPERM (file not owned by us, common for /dev/* nodes), retry without O_NOATIME.
488    let in_fd = unsafe {
489        libc::open(
490            in_cstr.as_ptr(),
491            libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOATIME,
492        )
493    };
494    let in_fd = if in_fd < 0 {
495        let first_err = io::Error::last_os_error();
496        if first_err.raw_os_error() == Some(libc::EPERM) {
497            let fd = unsafe { libc::open(in_cstr.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
498            if fd < 0 {
499                return Some(Err(io::Error::last_os_error()));
500            }
501            fd
502        } else {
503            return Some(Err(first_err));
504        }
505    } else {
506        in_fd
507    };
508
509    // Open output (O_CLOEXEC prevents FD inheritance)
510    let mut oflags = libc::O_WRONLY | libc::O_CLOEXEC;
511    if config.conv.excl {
512        oflags |= libc::O_CREAT | libc::O_EXCL;
513    } else if config.conv.nocreat {
514        // don't create
515    } else {
516        oflags |= libc::O_CREAT;
517    }
518    if !config.conv.notrunc && !config.conv.excl {
519        oflags |= libc::O_TRUNC;
520    }
521
522    let out_fd = unsafe { libc::open(out_cstr.as_ptr(), oflags, 0o666 as libc::mode_t) };
523    if out_fd < 0 {
524        unsafe { libc::close(in_fd) };
525        return Some(Err(io::Error::last_os_error()));
526    }
527
528    // Hint kernel for sequential readahead — only for regular files.
529    // fadvise on char devices (e.g. /dev/zero) returns ESPIPE and wastes a syscall.
530    {
531        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
532        if unsafe { libc::fstat(in_fd, &mut stat) } == 0
533            && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
534        {
535            unsafe {
536                libc::posix_fadvise(in_fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL);
537            }
538        }
539    }
540
541    // Handle skip (seek input) — use checked_mul to prevent overflow
542    if config.skip > 0 {
543        let offset = match (config.skip as u64).checked_mul(config.ibs as u64) {
544            Some(o) if o <= i64::MAX as u64 => o as i64,
545            _ => {
546                unsafe {
547                    libc::close(in_fd);
548                    libc::close(out_fd);
549                }
550                return Some(Err(io::Error::new(
551                    io::ErrorKind::InvalidInput,
552                    "skip offset overflow",
553                )));
554            }
555        };
556        if unsafe { libc::lseek(in_fd, offset, libc::SEEK_SET) } < 0 {
557            // lseek failed (e.g. char device) — read and discard full blocks
558            let mut discard = vec![0u8; config.ibs];
559            'skip: for _ in 0..config.skip {
560                let mut skipped = 0usize;
561                while skipped < config.ibs {
562                    let n = unsafe {
563                        libc::read(
564                            in_fd,
565                            discard[skipped..].as_mut_ptr() as *mut _,
566                            config.ibs - skipped,
567                        )
568                    };
569                    if n > 0 {
570                        skipped += n as usize;
571                    } else if n == 0 {
572                        break 'skip; // EOF
573                    } else {
574                        let err = io::Error::last_os_error();
575                        if err.kind() == io::ErrorKind::Interrupted {
576                            continue;
577                        }
578                        // Non-EINTR error during skip — log and abort skip phase
579                        eprintln!("dd: error skipping input: {}", err);
580                        break 'skip;
581                    }
582                }
583            }
584        }
585    }
586
587    // Handle seek (seek output) — use checked_mul to prevent overflow
588    if config.seek > 0 {
589        let offset = match (config.seek as u64).checked_mul(config.obs as u64) {
590            Some(o) if o <= i64::MAX as u64 => o as i64,
591            _ => {
592                unsafe {
593                    libc::close(in_fd);
594                    libc::close(out_fd);
595                }
596                return Some(Err(io::Error::new(
597                    io::ErrorKind::InvalidInput,
598                    "seek offset overflow",
599                )));
600            }
601        };
602        if unsafe { libc::lseek(out_fd, offset, libc::SEEK_SET) } < 0 {
603            let err = io::Error::last_os_error();
604            unsafe {
605                libc::close(in_fd);
606                libc::close(out_fd);
607            }
608            return Some(Err(err));
609        }
610    }
611
612    let mut stats = DdStats::default();
613    let bs = config.ibs;
614    #[allow(clippy::uninit_vec)]
615    // SAFETY: The buffer is filled by libc::read before being passed to libc::write.
616    // No uninitialized data is ever consumed by user code or written to output —
617    // only `total_read` bytes are written, which are the bytes that libc::read populated.
618    let mut ibuf = unsafe {
619        let mut v = Vec::with_capacity(bs);
620        v.set_len(bs);
621        v
622    };
623    debug_assert_eq!(ibuf.len(), bs);
624    let count_limit = config.count;
625
626    loop {
627        if let Some(limit) = count_limit {
628            if stats.records_in_full + stats.records_in_partial >= limit {
629                break;
630            }
631        }
632
633        // Raw read — retry on EINTR, loop for full block
634        let mut total_read = 0usize;
635        let mut read_error = false;
636        while total_read < bs {
637            let ret = unsafe {
638                libc::read(
639                    in_fd,
640                    ibuf[total_read..].as_mut_ptr() as *mut _,
641                    bs - total_read,
642                )
643            };
644            if ret > 0 {
645                total_read += ret as usize;
646            } else if ret == 0 {
647                break; // EOF
648            } else {
649                let err = io::Error::last_os_error();
650                if err.kind() == io::ErrorKind::Interrupted {
651                    continue;
652                }
653                if config.conv.noerror {
654                    eprintln!("dd: error reading '{}': {}", in_path, err);
655                    read_error = true;
656                    break;
657                }
658                unsafe {
659                    libc::close(in_fd);
660                    libc::close(out_fd);
661                }
662                return Some(Err(err));
663            }
664        }
665
666        // conv=noerror: skip entire bad block (GNU behavior)
667        if read_error {
668            stats.records_in_partial += 1;
669            continue;
670        }
671
672        if total_read == 0 {
673            break;
674        }
675
676        if total_read == bs {
677            stats.records_in_full += 1;
678        } else {
679            stats.records_in_partial += 1;
680        }
681
682        // Raw write — retry on EINTR, treat write(0) as error
683        let mut written = 0usize;
684        debug_assert!(total_read <= ibuf.len());
685        while written < total_read {
686            debug_assert!(written <= ibuf.len());
687            let ret = unsafe {
688                libc::write(
689                    out_fd,
690                    ibuf[written..].as_ptr() as *const _,
691                    total_read - written,
692                )
693            };
694            if ret > 0 {
695                written += ret as usize;
696            } else if ret == 0 {
697                // write() returning 0 is abnormal — treat as error
698                unsafe {
699                    libc::close(in_fd);
700                    libc::close(out_fd);
701                }
702                return Some(Err(io::Error::new(
703                    io::ErrorKind::WriteZero,
704                    "write returned 0",
705                )));
706            } else {
707                let err = io::Error::last_os_error();
708                if err.kind() == io::ErrorKind::Interrupted {
709                    continue;
710                }
711                unsafe {
712                    libc::close(in_fd);
713                    libc::close(out_fd);
714                }
715                return Some(Err(err));
716            }
717        }
718
719        stats.bytes_copied += written as u64;
720        if written == bs {
721            stats.records_out_full += 1;
722        } else {
723            stats.records_out_partial += 1;
724        }
725    }
726
727    // fsync / fdatasync — propagate errors
728    if config.conv.fsync {
729        if unsafe { libc::fsync(out_fd) } < 0 {
730            let err = io::Error::last_os_error();
731            unsafe {
732                libc::close(in_fd);
733                libc::close(out_fd);
734            }
735            return Some(Err(err));
736        }
737    } else if config.conv.fdatasync {
738        if unsafe { libc::fdatasync(out_fd) } < 0 {
739            let err = io::Error::last_os_error();
740            unsafe {
741                libc::close(in_fd);
742                libc::close(out_fd);
743            }
744            return Some(Err(err));
745        }
746    }
747
748    unsafe { libc::close(in_fd) };
749    // Check close(out_fd) — on NFS, close can report deferred write errors
750    if unsafe { libc::close(out_fd) } < 0 {
751        return Some(Err(io::Error::last_os_error()));
752    }
753
754    if config.status != StatusLevel::None {
755        print_stats(&stats, start_time.elapsed(), config.status);
756    }
757
758    Some(Ok(stats))
759}
760
761/// Fast path: use copy_file_range when both input and output are files
762/// and no conversions are needed. This is zero-copy in the kernel.
763#[cfg(target_os = "linux")]
764fn try_copy_file_range_dd(config: &DdConfig) -> Option<io::Result<DdStats>> {
765    // Only usable when both are files, no conversions, and ibs == obs
766    if config.input.is_none() || config.output.is_none() {
767        return None;
768    }
769    if has_conversions(&config.conv) || config.ibs != config.obs {
770        return None;
771    }
772
773    let start_time = Instant::now();
774    let in_path = config.input.as_ref().unwrap();
775    let out_path = config.output.as_ref().unwrap();
776
777    let in_file = match File::open(in_path) {
778        Ok(f) => f,
779        Err(e) => return Some(Err(e)),
780    };
781
782    // Hint kernel for sequential readahead
783    {
784        use std::os::unix::io::AsRawFd;
785        unsafe {
786            libc::posix_fadvise(in_file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
787        }
788    }
789
790    let mut out_opts = OpenOptions::new();
791    out_opts.write(true);
792    if config.conv.excl {
793        out_opts.create_new(true);
794    } else if !config.conv.nocreat {
795        out_opts.create(true);
796    }
797    if !config.conv.notrunc && !config.conv.excl {
798        out_opts.truncate(true);
799    }
800
801    let out_file = match out_opts.open(out_path) {
802        Ok(f) => f,
803        Err(e) => return Some(Err(e)),
804    };
805
806    use std::os::unix::io::AsRawFd;
807    let in_fd = in_file.as_raw_fd();
808    let out_fd = out_file.as_raw_fd();
809
810    // Handle skip
811    let skip_bytes = config.skip * config.ibs as u64;
812    let seek_bytes = config.seek * config.obs as u64;
813    let mut in_off: i64 = skip_bytes as i64;
814    let mut out_off: i64 = seek_bytes as i64;
815
816    let mut stats = DdStats::default();
817    let block_size = config.ibs;
818
819    // Determine total bytes to copy
820    let total_to_copy = config.count.map(|count| count * block_size as u64);
821
822    let mut bytes_remaining = total_to_copy;
823    loop {
824        let chunk = match bytes_remaining {
825            Some(0) => break,
826            Some(r) => r.min(block_size as u64 * 1024) as usize, // copy in large chunks
827            None => block_size * 1024,
828        };
829
830        // SAFETY: in_fd and out_fd are valid file descriptors (files are open for the
831        // lifetime of this function). in_off and out_off are valid, aligned i64 pointers
832        // with no aliasing. The kernel updates offsets atomically. Return value is checked:
833        // negative = error, 0 = EOF, positive = bytes copied.
834        let ret = unsafe {
835            libc::syscall(
836                libc::SYS_copy_file_range,
837                in_fd,
838                &mut in_off as *mut i64,
839                out_fd,
840                &mut out_off as *mut i64,
841                chunk,
842                0u32,
843            )
844        };
845
846        if ret < 0 {
847            let err = io::Error::last_os_error();
848            if err.raw_os_error() == Some(libc::EINVAL)
849                || err.raw_os_error() == Some(libc::ENOSYS)
850                || err.raw_os_error() == Some(libc::EXDEV)
851            {
852                return None; // Fall back to regular copy
853            }
854            return Some(Err(err));
855        }
856        if ret == 0 {
857            break;
858        }
859
860        let copied = ret as u64;
861        stats.bytes_copied += copied;
862
863        // Track block stats
864        let full_blocks = copied / block_size as u64;
865        let partial = copied % block_size as u64;
866        stats.records_in_full += full_blocks;
867        stats.records_out_full += full_blocks;
868        if partial > 0 {
869            stats.records_in_partial += 1;
870            stats.records_out_partial += 1;
871        }
872
873        if let Some(ref mut r) = bytes_remaining {
874            *r = r.saturating_sub(copied);
875        }
876    }
877
878    // fsync / fdatasync
879    if config.conv.fsync {
880        if let Err(e) = out_file.sync_all() {
881            return Some(Err(e));
882        }
883    } else if config.conv.fdatasync {
884        if let Err(e) = out_file.sync_data() {
885            return Some(Err(e));
886        }
887    }
888
889    if config.status != StatusLevel::None {
890        print_stats(&stats, start_time.elapsed(), config.status);
891    }
892
893    Some(Ok(stats))
894}
895
896/// Perform the dd copy operation.
897pub fn dd_copy(config: &DdConfig) -> io::Result<DdStats> {
898    // Try zero-copy fast path on Linux (file-to-file)
899    #[cfg(target_os = "linux")]
900    {
901        if let Some(result) = try_copy_file_range_dd(config) {
902            return result;
903        }
904    }
905    // Raw syscall fast path: handles devices like /dev/zero where copy_file_range fails
906    #[cfg(target_os = "linux")]
907    {
908        if let Some(result) = try_raw_dd(config) {
909            return result;
910        }
911    }
912    let start_time = Instant::now();
913
914    // Only clone file handles when skip/seek are needed (avoids dup() syscalls otherwise)
915    let needs_input_seek = config.skip > 0;
916    let needs_output_seek = config.seek > 0;
917
918    let mut input_file: Option<File> = None;
919    let mut input: Box<dyn Read> = if let Some(ref path) = config.input {
920        let file = File::open(path)
921            .map_err(|e| io::Error::new(e.kind(), format!("failed to open '{}': {}", path, e)))?;
922        // Hint kernel for sequential readahead
923        #[cfg(target_os = "linux")]
924        {
925            use std::os::unix::io::AsRawFd;
926            unsafe {
927                libc::posix_fadvise(file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
928            }
929        }
930        if needs_input_seek {
931            input_file = Some(file.try_clone()?);
932        }
933        Box::new(file)
934    } else {
935        Box::new(io::stdin())
936    };
937
938    // Handle output file creation/opening
939    let mut output_file: Option<File> = None;
940    let mut output: Box<dyn Write> = if let Some(ref path) = config.output {
941        let mut opts = OpenOptions::new();
942        opts.write(true);
943
944        if config.conv.excl {
945            // excl: fail if file exists (create_new implies create)
946            opts.create_new(true);
947        } else if config.conv.nocreat {
948            // nocreat: do not create, file must exist
949            // Don't set create at all
950        } else {
951            opts.create(true);
952        }
953
954        if config.conv.notrunc {
955            opts.truncate(false);
956        } else if !config.conv.excl {
957            // Default: truncate (but not with excl since create_new starts fresh)
958            opts.truncate(true);
959        }
960
961        let file = opts
962            .open(path)
963            .map_err(|e| io::Error::new(e.kind(), format!("failed to open '{}': {}", path, e)))?;
964        if needs_output_seek || config.conv.fsync || config.conv.fdatasync {
965            // Clone for: (1) seek positioning (Box<dyn Write> can't seek directly),
966            // and (2) sync_all/sync_data at end. Safe because dup()-cloned fds
967            // share the same open file description.
968            output_file = Some(file.try_clone()?);
969        }
970        Box::new(file)
971    } else {
972        Box::new(io::stdout())
973    };
974
975    // Skip input — use seek() for file inputs to avoid reading and discarding data
976    if config.skip > 0 {
977        if config.iflag.skip_bytes {
978            // skip_bytes: skip N bytes, not N blocks
979            if let Some(ref mut f) = input_file {
980                f.seek(SeekFrom::Start(config.skip))?;
981                let seeked = f.try_clone()?;
982                input = Box::new(seeked);
983            } else {
984                skip_input_bytes(&mut input, config.skip)?;
985            }
986        } else if let Some(ref mut f) = input_file {
987            skip_input_seek(f, config.skip, config.ibs)?;
988            // Rebuild the input Box with a clone at the seeked position
989            let seeked = f.try_clone()?;
990            input = Box::new(seeked);
991        } else {
992            skip_input(&mut input, config.skip, config.ibs)?;
993        }
994    }
995
996    // Seek output blocks
997    if config.seek > 0 {
998        if let Some(ref mut f) = output_file {
999            seek_output_file(f, config.seek, config.obs)?;
1000            // Rebuild the output Box with a new clone at the seeked position
1001            let seeked = f.try_clone()?;
1002            output = Box::new(seeked);
1003        } else {
1004            seek_output(&mut output, config.seek, config.obs)?;
1005        }
1006    }
1007
1008    let mut stats = DdStats::default();
1009    let mut ibuf = vec![0u8; config.ibs];
1010    let mut obuf: Vec<u8> = Vec::with_capacity(config.obs);
1011    let mut unblock_buf: Vec<u8> = Vec::new();
1012    // For count_bytes mode, track total bytes read
1013    let mut bytes_read_total: u64 = 0;
1014
1015    loop {
1016        // Check count limit
1017        if let Some(count) = config.count {
1018            if config.iflag.count_bytes {
1019                if bytes_read_total >= count {
1020                    break;
1021                }
1022            } else if stats.records_in_full + stats.records_in_partial >= count {
1023                break;
1024            }
1025        }
1026
1027        // When count_bytes is active, limit the read to the remaining bytes
1028        let read_size = if config.iflag.count_bytes {
1029            if let Some(count) = config.count {
1030                let remaining = count.saturating_sub(bytes_read_total);
1031                std::cmp::min(config.ibs, remaining as usize)
1032            } else {
1033                config.ibs
1034            }
1035        } else {
1036            config.ibs
1037        };
1038        if read_size == 0 {
1039            break;
1040        }
1041
1042        // Read one input block
1043        let n = match read_full_block(&mut input, &mut ibuf[..read_size]) {
1044            Ok(n) => n,
1045            Err(e) => {
1046                if config.conv.noerror {
1047                    if config.status != StatusLevel::None {
1048                        eprintln!("dd: error reading input: {}", e);
1049                    }
1050                    // On noerror with sync, fill the entire block with NULs
1051                    if config.conv.sync {
1052                        ibuf.fill(0);
1053                        config.ibs
1054                    } else {
1055                        continue;
1056                    }
1057                } else {
1058                    return Err(e);
1059                }
1060            }
1061        };
1062
1063        if n == 0 {
1064            break;
1065        }
1066
1067        bytes_read_total += n as u64;
1068
1069        // Track full vs partial blocks
1070        if n == config.ibs {
1071            stats.records_in_full += 1;
1072        } else {
1073            stats.records_in_partial += 1;
1074            // Pad if conv=sync: spaces for block/unblock, NULs otherwise
1075            if config.conv.sync {
1076                let pad_byte = if config.conv.block || config.conv.unblock {
1077                    b' '
1078                } else {
1079                    0u8
1080                };
1081                ibuf[n..config.ibs].fill(pad_byte);
1082            }
1083        }
1084
1085        // Determine the data slice to use and apply conversions in-place
1086        let effective_len = if config.conv.sync { config.ibs } else { n };
1087        apply_conversions(&mut ibuf[..effective_len], &config.conv);
1088
1089        // Apply unblock conversion: split fixed-length records into
1090        // newline-terminated records with trailing spaces stripped
1091        let write_data: &[u8] = if config.conv.unblock && config.cbs > 0 {
1092            unblock_buf.clear();
1093            let data = &ibuf[..effective_len];
1094            let mut pos = 0;
1095            while pos < data.len() {
1096                let end = std::cmp::min(pos + config.cbs, data.len());
1097                let record = &data[pos..end];
1098                // Strip trailing spaces
1099                let trimmed_len = record
1100                    .iter()
1101                    .rposition(|&b| b != b' ')
1102                    .map(|p| p + 1)
1103                    .unwrap_or(0);
1104                unblock_buf.extend_from_slice(&record[..trimmed_len]);
1105                unblock_buf.push(b'\n');
1106                pos = end;
1107            }
1108            &unblock_buf
1109        } else {
1110            &ibuf[..effective_len]
1111        };
1112
1113        // Buffer output and flush when we have enough for a full output block.
1114        // Use efficient buffer management: write directly from ibuf when possible,
1115        // only buffer when ibs != obs.
1116        let wd_len = write_data.len();
1117        if config.ibs == config.obs && obuf.is_empty() && !config.conv.unblock {
1118            // Fast path: ibs == obs, write directly
1119            output.write_all(write_data)?;
1120            if wd_len == config.obs {
1121                stats.records_out_full += 1;
1122            } else {
1123                stats.records_out_partial += 1;
1124            }
1125            stats.bytes_copied += wd_len as u64;
1126            // Skip the drain loop below since we wrote directly
1127            continue;
1128        }
1129
1130        // Append write_data to obuf and drain full output blocks.
1131        // We write directly from write_data when possible to avoid copying
1132        // through obuf. Only buffer the remainder that doesn't fill a block.
1133        let obs = config.obs;
1134        let mut wd_off = 0;
1135
1136        // If obuf has leftover bytes, try to complete a full block
1137        if !obuf.is_empty() {
1138            let need = obs - obuf.len();
1139            if write_data.len() >= need {
1140                obuf.extend_from_slice(&write_data[..need]);
1141                output.write_all(&obuf)?;
1142                stats.records_out_full += 1;
1143                stats.bytes_copied += obs as u64;
1144                obuf.clear();
1145                wd_off = need;
1146            } else {
1147                obuf.extend_from_slice(write_data);
1148                wd_off = write_data.len();
1149            }
1150        }
1151
1152        // Write full blocks directly from write_data (zero-copy)
1153        let remaining_wd = &write_data[wd_off..];
1154        let full_blocks = remaining_wd.len() / obs;
1155        if full_blocks > 0 {
1156            let full_len = full_blocks * obs;
1157            output.write_all(&remaining_wd[..full_len])?;
1158            stats.records_out_full += full_blocks as u64;
1159            stats.bytes_copied += full_len as u64;
1160            wd_off += full_len;
1161        }
1162
1163        // Buffer any remaining partial block
1164        let leftover = &write_data[wd_off..];
1165        if !leftover.is_empty() {
1166            obuf.extend_from_slice(leftover);
1167        }
1168    }
1169
1170    // Flush remaining partial output block
1171    if !obuf.is_empty() {
1172        output.write_all(&obuf)?;
1173        stats.records_out_partial += 1;
1174        stats.bytes_copied += obuf.len() as u64;
1175    }
1176
1177    // Flush output
1178    output.flush()?;
1179
1180    // fsync / fdatasync (output_file is Some when seek or sync was requested)
1181    if let Some(ref f) = output_file {
1182        if config.conv.fsync {
1183            f.sync_all()?;
1184        } else if config.conv.fdatasync {
1185            f.sync_data()?;
1186        }
1187    }
1188
1189    let elapsed = start_time.elapsed();
1190
1191    // Print status
1192    if config.status != StatusLevel::None {
1193        print_stats(&stats, elapsed, config.status);
1194    }
1195
1196    Ok(stats)
1197}
1198
1199/// Print dd transfer statistics to stderr.
1200fn print_stats(stats: &DdStats, elapsed: std::time::Duration, status: StatusLevel) {
1201    eprintln!(
1202        "{}+{} records in",
1203        stats.records_in_full, stats.records_in_partial
1204    );
1205    eprintln!(
1206        "{}+{} records out",
1207        stats.records_out_full, stats.records_out_partial
1208    );
1209
1210    if status == StatusLevel::NoXfer {
1211        return;
1212    }
1213
1214    let secs = elapsed.as_secs_f64();
1215    if secs > 0.0 {
1216        let rate = stats.bytes_copied as f64 / secs;
1217        eprintln!(
1218            "{} bytes copied, {:.6} s, {}/s",
1219            stats.bytes_copied,
1220            secs,
1221            human_size(rate as u64)
1222        );
1223    } else {
1224        eprintln!("{} bytes copied", stats.bytes_copied);
1225    }
1226}
1227
1228/// Format a byte count as a human-readable string (e.g., "1.5 MB").
1229fn human_size(bytes: u64) -> String {
1230    const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB", "PB", "EB"];
1231    let mut size = bytes as f64;
1232    for &unit in UNITS {
1233        if size < 1000.0 {
1234            if size == size.floor() {
1235                return format!("{} {}", size as u64, unit);
1236            }
1237            return format!("{:.1} {}", size, unit);
1238        }
1239        size /= 1000.0;
1240    }
1241    format!("{:.1} EB", size * 1000.0)
1242}
1243
1244/// Print help message for dd.
1245pub fn print_help() {
1246    eprint!(
1247        "\
1248Usage: dd [OPERAND]...
1249  or:  dd OPTION
1250Copy a file, converting and formatting according to the operands.
1251
1252  bs=BYTES        read and write up to BYTES bytes at a time (default: 512)
1253  cbs=BYTES       convert BYTES bytes at a time
1254  conv=CONVS      convert the file as per the comma separated symbol list
1255  count=N         copy only N input blocks
1256  ibs=BYTES       read up to BYTES bytes at a time (default: 512)
1257  if=FILE         read from FILE instead of stdin
1258  iflag=FLAGS     read as per the comma separated symbol list
1259  obs=BYTES       write BYTES bytes at a time (default: 512)
1260  of=FILE         write to FILE instead of stdout
1261  oflag=FLAGS     write as per the comma separated symbol list
1262  seek=N          skip N obs-sized blocks at start of output
1263  skip=N          skip N ibs-sized blocks at start of input
1264  status=LEVEL    LEVEL of information to print to stderr;
1265                  'none' suppresses everything but error messages,
1266                  'noerror' suppresses the final transfer statistics,
1267                  'progress' shows periodic transfer statistics
1268
1269  BLOCKS and BYTES may be followed by the following multiplicative suffixes:
1270  c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000, M=1024*1024,
1271  GB=1000*1000*1000, GiB=1024*1024*1024, and so on for T, P, E.
1272
1273Each CONV symbol may be:
1274
1275  lcase     change upper case to lower case
1276  ucase     change lower case to upper case
1277  swab      swap every pair of input bytes
1278  sync      pad every input block with NULs to ibs-size
1279  noerror   continue after read errors
1280  notrunc   do not truncate the output file
1281  fdatasync physically write output file data before finishing
1282  fsync     likewise, but also write metadata
1283  excl      fail if the output file already exists
1284  nocreat   do not create the output file
1285
1286Each FLAG symbol may be:
1287
1288  append    append mode (makes sense only for output; conv=notrunc suggested)
1289  direct    use direct I/O for data
1290  directory fail unless a directory
1291  dsync     use synchronized I/O for data
1292  sync      likewise, but also for metadata
1293  fullblock accumulate full blocks of input (iflag only)
1294  nonblock  use non-blocking I/O
1295  noatime   do not update access time
1296  nocache   Request to drop cache
1297  noctty    do not assign controlling terminal from file
1298  nofollow  do not follow symlinks
1299  count_bytes  treat 'count=N' as a byte count (iflag only)
1300  skip_bytes   treat 'skip=N' as a byte count (iflag only)
1301
1302  --help     display this help and exit
1303  --version  output version information and exit
1304"
1305    );
1306}
1307
1308/// Print version information for dd.
1309pub fn print_version() {
1310    eprintln!("dd (fcoreutils) {}", env!("CARGO_PKG_VERSION"));
1311}