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}
18
19/// Conversion flags for dd (`conv=` option).
20#[derive(Debug, Clone, Default)]
21pub struct DdConv {
22    /// Convert to lowercase.
23    pub lcase: bool,
24    /// Convert to uppercase.
25    pub ucase: bool,
26    /// Swap every pair of input bytes.
27    pub swab: bool,
28    /// Continue after read errors.
29    pub noerror: bool,
30    /// Do not truncate the output file.
31    pub notrunc: bool,
32    /// Pad every input block with NULs to ibs-size.
33    pub sync: bool,
34    /// Call fdatasync on output before finishing.
35    pub fdatasync: bool,
36    /// Call fsync on output before finishing.
37    pub fsync: bool,
38    /// Fail if the output file already exists.
39    pub excl: bool,
40    /// Do not create the output file.
41    pub nocreat: bool,
42}
43
44/// Input/output flags for dd (`iflag=`/`oflag=` options).
45#[derive(Debug, Clone, Default)]
46pub struct DdFlags {
47    pub append: bool,
48    pub direct: bool,
49    pub directory: bool,
50    pub dsync: bool,
51    pub sync: bool,
52    pub fullblock: bool,
53    pub nonblock: bool,
54    pub noatime: bool,
55    pub nocache: bool,
56    pub noctty: bool,
57    pub nofollow: bool,
58    pub count_bytes: bool,
59    pub skip_bytes: bool,
60}
61
62/// Configuration for a dd operation.
63#[derive(Debug, Clone)]
64pub struct DdConfig {
65    /// Input file path (None = stdin).
66    pub input: Option<String>,
67    /// Output file path (None = stdout).
68    pub output: Option<String>,
69    /// Input block size in bytes.
70    pub ibs: usize,
71    /// Output block size in bytes.
72    pub obs: usize,
73    /// Copy only this many input blocks (None = unlimited).
74    pub count: Option<u64>,
75    /// Skip this many ibs-sized blocks at start of input.
76    pub skip: u64,
77    /// Skip this many obs-sized blocks at start of output.
78    pub seek: u64,
79    /// Conversion options.
80    pub conv: DdConv,
81    /// Status output level.
82    pub status: StatusLevel,
83    /// Input flags.
84    pub iflag: DdFlags,
85    /// Output flags.
86    pub oflag: DdFlags,
87}
88
89impl Default for DdConfig {
90    fn default() -> Self {
91        DdConfig {
92            input: None,
93            output: None,
94            ibs: 512,
95            obs: 512,
96            count: None,
97            skip: 0,
98            seek: 0,
99            conv: DdConv::default(),
100            status: StatusLevel::default(),
101            iflag: DdFlags::default(),
102            oflag: DdFlags::default(),
103        }
104    }
105}
106
107/// Statistics from a dd copy operation.
108#[derive(Debug, Clone, Default)]
109pub struct DdStats {
110    /// Number of full input blocks read.
111    pub records_in_full: u64,
112    /// Number of partial input blocks read.
113    pub records_in_partial: u64,
114    /// Number of full output blocks written.
115    pub records_out_full: u64,
116    /// Number of partial output blocks written.
117    pub records_out_partial: u64,
118    /// Total bytes copied.
119    pub bytes_copied: u64,
120}
121
122/// Parse a SIZE string with optional suffix.
123///
124/// Supported suffixes: c (1), w (2), b (512),
125/// K/kB (1000), KiB/k (1024),
126/// M/MB (1000^2), MiB (1024^2),
127/// G/GB (1000^3), GiB (1024^3),
128/// T/TB (1000^4), TiB (1024^4),
129/// P/PB (1000^5), PiB (1024^5),
130/// E/EB (1000^6), EiB (1024^6).
131pub fn parse_size(s: &str) -> Result<u64, String> {
132    let s = s.trim();
133    if s.is_empty() {
134        return Err("empty size string".to_string());
135    }
136
137    // Find where the numeric part ends
138    let num_end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
139
140    if num_end == 0 {
141        return Err(format!("invalid number: '{}'", s));
142    }
143
144    let num: u64 = s[..num_end]
145        .parse()
146        .map_err(|e| format!("invalid number '{}': {}", &s[..num_end], e))?;
147
148    let suffix = &s[num_end..];
149    let multiplier: u64 = match suffix {
150        "" => 1,
151        "c" => 1,
152        "w" => 2,
153        "b" => 512,
154        "K" | "kB" => 1000,
155        "KiB" | "k" => 1024,
156        "M" | "MB" => 1_000_000,
157        "MiB" => 1_048_576,
158        "G" | "GB" => 1_000_000_000,
159        "GiB" => 1_073_741_824,
160        "T" | "TB" => 1_000_000_000_000,
161        "TiB" => 1_099_511_627_776,
162        "P" | "PB" => 1_000_000_000_000_000,
163        "PiB" => 1_125_899_906_842_624,
164        "E" | "EB" => 1_000_000_000_000_000_000,
165        "EiB" => 1_152_921_504_606_846_976,
166        _ => return Err(format!("invalid suffix: '{}'", suffix)),
167    };
168
169    num.checked_mul(multiplier)
170        .ok_or_else(|| format!("size overflow: {} * {}", num, multiplier))
171}
172
173/// Parse dd command-line arguments (key=value pairs).
174pub fn parse_dd_args(args: &[String]) -> Result<DdConfig, String> {
175    let mut config = DdConfig::default();
176    let mut bs_set = false;
177
178    for arg in args {
179        if let Some((key, value)) = arg.split_once('=') {
180            match key {
181                "if" => config.input = Some(value.to_string()),
182                "of" => config.output = Some(value.to_string()),
183                "bs" => {
184                    let size = parse_size(value)? as usize;
185                    config.ibs = size;
186                    config.obs = size;
187                    bs_set = true;
188                }
189                "ibs" => {
190                    if !bs_set {
191                        config.ibs = parse_size(value)? as usize;
192                    }
193                }
194                "obs" => {
195                    if !bs_set {
196                        config.obs = parse_size(value)? as usize;
197                    }
198                }
199                "count" => config.count = Some(parse_size(value)?),
200                "skip" => config.skip = parse_size(value)?,
201                "seek" => config.seek = parse_size(value)?,
202                "conv" => {
203                    for flag in value.split(',') {
204                        match flag {
205                            "lcase" => config.conv.lcase = true,
206                            "ucase" => config.conv.ucase = true,
207                            "swab" => config.conv.swab = true,
208                            "noerror" => config.conv.noerror = true,
209                            "notrunc" => config.conv.notrunc = true,
210                            "sync" => config.conv.sync = true,
211                            "fdatasync" => config.conv.fdatasync = true,
212                            "fsync" => config.conv.fsync = true,
213                            "excl" => config.conv.excl = true,
214                            "nocreat" => config.conv.nocreat = true,
215                            "" => {}
216                            _ => return Err(format!("invalid conversion: '{}'", flag)),
217                        }
218                    }
219                }
220                "iflag" => {
221                    for flag in value.split(',') {
222                        parse_flag(flag, &mut config.iflag)?;
223                    }
224                }
225                "oflag" => {
226                    for flag in value.split(',') {
227                        parse_flag(flag, &mut config.oflag)?;
228                    }
229                }
230                "status" => {
231                    config.status = match value {
232                        "none" => StatusLevel::None,
233                        "noerror" => StatusLevel::NoError,
234                        "progress" => StatusLevel::Progress,
235                        _ => return Err(format!("invalid status level: '{}'", value)),
236                    };
237                }
238                _ => return Err(format!("unrecognized operand: '{}'", arg)),
239            }
240        } else {
241            return Err(format!("unrecognized operand: '{}'", arg));
242        }
243    }
244
245    // Validate conflicting options
246    if config.conv.lcase && config.conv.ucase {
247        return Err("conv=lcase and conv=ucase are mutually exclusive".to_string());
248    }
249    if config.conv.excl && config.conv.nocreat {
250        return Err("conv=excl and conv=nocreat are mutually exclusive".to_string());
251    }
252
253    Ok(config)
254}
255
256/// Parse a single iflag/oflag value into the DdFlags struct.
257fn parse_flag(flag: &str, flags: &mut DdFlags) -> Result<(), String> {
258    match flag {
259        "append" => flags.append = true,
260        "direct" => flags.direct = true,
261        "directory" => flags.directory = true,
262        "dsync" => flags.dsync = true,
263        "sync" => flags.sync = true,
264        "fullblock" => flags.fullblock = true,
265        "nonblock" => flags.nonblock = true,
266        "noatime" => flags.noatime = true,
267        "nocache" => flags.nocache = true,
268        "noctty" => flags.noctty = true,
269        "nofollow" => flags.nofollow = true,
270        "count_bytes" => flags.count_bytes = true,
271        "skip_bytes" => flags.skip_bytes = true,
272        "" => {}
273        _ => return Err(format!("invalid flag: '{}'", flag)),
274    }
275    Ok(())
276}
277
278/// Read a full block from the reader, retrying on partial reads.
279/// Returns the number of bytes actually read (0 means EOF).
280fn read_full_block(reader: &mut dyn Read, buf: &mut [u8]) -> io::Result<usize> {
281    let mut total = 0;
282    while total < buf.len() {
283        match reader.read(&mut buf[total..]) {
284            Ok(0) => break,
285            Ok(n) => total += n,
286            Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
287            Err(e) => return Err(e),
288        }
289    }
290    Ok(total)
291}
292
293/// Apply conversion options to a data block in-place.
294pub fn apply_conversions(data: &mut [u8], conv: &DdConv) {
295    if conv.swab {
296        // Swap every pair of bytes
297        let pairs = data.len() / 2;
298        for i in 0..pairs {
299            data.swap(i * 2, i * 2 + 1);
300        }
301    }
302
303    if conv.lcase {
304        for b in data.iter_mut() {
305            b.make_ascii_lowercase();
306        }
307    } else if conv.ucase {
308        for b in data.iter_mut() {
309            b.make_ascii_uppercase();
310        }
311    }
312}
313
314/// Skip input blocks by reading and discarding them.
315fn skip_input(reader: &mut dyn Read, blocks: u64, block_size: usize) -> io::Result<()> {
316    let mut discard_buf = vec![0u8; block_size];
317    for _ in 0..blocks {
318        let n = read_full_block(reader, &mut discard_buf)?;
319        if n == 0 {
320            break;
321        }
322    }
323    Ok(())
324}
325
326/// Skip input blocks by seeking (for seekable file inputs).
327fn skip_input_seek(file: &mut File, blocks: u64, block_size: usize) -> io::Result<()> {
328    let offset = blocks * block_size as u64;
329    file.seek(SeekFrom::Start(offset))?;
330    Ok(())
331}
332
333/// Seek output by writing zero blocks (for non-seekable outputs) or using seek.
334fn seek_output(writer: &mut Box<dyn Write>, seek_blocks: u64, block_size: usize) -> io::Result<()> {
335    // Try to seek if the writer supports it. Since we use Box<dyn Write>,
336    // we write zero blocks for the general case.
337    let zero_block = vec![0u8; block_size];
338    for _ in 0..seek_blocks {
339        writer.write_all(&zero_block)?;
340    }
341    Ok(())
342}
343
344/// Seek output on a file using actual file seeking.
345fn seek_output_file(file: &mut File, seek_blocks: u64, block_size: usize) -> io::Result<()> {
346    let offset = seek_blocks * block_size as u64;
347    file.seek(SeekFrom::Start(offset))?;
348    Ok(())
349}
350
351/// Check if any data conversion options are enabled.
352#[cfg(target_os = "linux")]
353fn has_conversions(conv: &DdConv) -> bool {
354    conv.lcase || conv.ucase || conv.swab || conv.sync
355}
356
357/// Fast path: use copy_file_range when both input and output are files
358/// and no conversions are needed. This is zero-copy in the kernel.
359#[cfg(target_os = "linux")]
360fn try_copy_file_range_dd(config: &DdConfig) -> Option<io::Result<DdStats>> {
361    // Only usable when both are files, no conversions, and ibs == obs
362    if config.input.is_none() || config.output.is_none() {
363        return None;
364    }
365    if has_conversions(&config.conv) || config.ibs != config.obs {
366        return None;
367    }
368
369    let start_time = Instant::now();
370    let in_path = config.input.as_ref().unwrap();
371    let out_path = config.output.as_ref().unwrap();
372
373    let in_file = match File::open(in_path) {
374        Ok(f) => f,
375        Err(e) => return Some(Err(e)),
376    };
377
378    let mut out_opts = OpenOptions::new();
379    out_opts.write(true);
380    if config.conv.excl {
381        out_opts.create_new(true);
382    } else if !config.conv.nocreat {
383        out_opts.create(true);
384    }
385    if !config.conv.notrunc && !config.conv.excl {
386        out_opts.truncate(true);
387    }
388
389    let out_file = match out_opts.open(out_path) {
390        Ok(f) => f,
391        Err(e) => return Some(Err(e)),
392    };
393
394    use std::os::unix::io::AsRawFd;
395    let in_fd = in_file.as_raw_fd();
396    let out_fd = out_file.as_raw_fd();
397
398    // Handle skip
399    let skip_bytes = config.skip * config.ibs as u64;
400    let seek_bytes = config.seek * config.obs as u64;
401    let mut in_off: i64 = skip_bytes as i64;
402    let mut out_off: i64 = seek_bytes as i64;
403
404    let mut stats = DdStats::default();
405    let block_size = config.ibs;
406
407    // Determine total bytes to copy
408    let total_to_copy = config.count.map(|count| count * block_size as u64);
409
410    let mut bytes_remaining = total_to_copy;
411    loop {
412        let chunk = match bytes_remaining {
413            Some(0) => break,
414            Some(r) => r.min(block_size as u64 * 1024) as usize, // copy in large chunks
415            None => block_size * 1024,
416        };
417
418        // SAFETY: in_fd and out_fd are valid file descriptors (files are open for the
419        // lifetime of this function). in_off and out_off are valid, aligned i64 pointers
420        // with no aliasing. The kernel updates offsets atomically. Return value is checked:
421        // negative = error, 0 = EOF, positive = bytes copied.
422        let ret = unsafe {
423            libc::syscall(
424                libc::SYS_copy_file_range,
425                in_fd,
426                &mut in_off as *mut i64,
427                out_fd,
428                &mut out_off as *mut i64,
429                chunk,
430                0u32,
431            )
432        };
433
434        if ret < 0 {
435            let err = io::Error::last_os_error();
436            if err.raw_os_error() == Some(libc::EINVAL)
437                || err.raw_os_error() == Some(libc::ENOSYS)
438                || err.raw_os_error() == Some(libc::EXDEV)
439            {
440                return None; // Fall back to regular copy
441            }
442            return Some(Err(err));
443        }
444        if ret == 0 {
445            break;
446        }
447
448        let copied = ret as u64;
449        stats.bytes_copied += copied;
450
451        // Track block stats
452        let full_blocks = copied / block_size as u64;
453        let partial = copied % block_size as u64;
454        stats.records_in_full += full_blocks;
455        stats.records_out_full += full_blocks;
456        if partial > 0 {
457            stats.records_in_partial += 1;
458            stats.records_out_partial += 1;
459        }
460
461        if let Some(ref mut r) = bytes_remaining {
462            *r = r.saturating_sub(copied);
463        }
464    }
465
466    // fsync / fdatasync
467    if config.conv.fsync {
468        if let Err(e) = out_file.sync_all() {
469            return Some(Err(e));
470        }
471    } else if config.conv.fdatasync {
472        if let Err(e) = out_file.sync_data() {
473            return Some(Err(e));
474        }
475    }
476
477    if config.status != StatusLevel::None {
478        print_stats(&stats, start_time.elapsed());
479    }
480
481    Some(Ok(stats))
482}
483
484/// Perform the dd copy operation.
485pub fn dd_copy(config: &DdConfig) -> io::Result<DdStats> {
486    // Try zero-copy fast path on Linux
487    #[cfg(target_os = "linux")]
488    {
489        if let Some(result) = try_copy_file_range_dd(config) {
490            return result;
491        }
492    }
493    let start_time = Instant::now();
494
495    let mut input_file: Option<File> = None;
496    let mut input: Box<dyn Read> = if let Some(ref path) = config.input {
497        let file = File::open(path)
498            .map_err(|e| io::Error::new(e.kind(), format!("failed to open '{}': {}", path, e)))?;
499        input_file = Some(file.try_clone()?);
500        Box::new(file)
501    } else {
502        Box::new(io::stdin())
503    };
504
505    // Handle output file creation/opening
506    let have_output_file = config.output.is_some();
507    let mut output_file: Option<File> = None;
508    let mut output: Box<dyn Write> = if let Some(ref path) = config.output {
509        let mut opts = OpenOptions::new();
510        opts.write(true);
511
512        if config.conv.excl {
513            // excl: fail if file exists (create_new implies create)
514            opts.create_new(true);
515        } else if config.conv.nocreat {
516            // nocreat: do not create, file must exist
517            // Don't set create at all
518        } else {
519            opts.create(true);
520        }
521
522        if config.conv.notrunc {
523            opts.truncate(false);
524        } else if !config.conv.excl {
525            // Default: truncate (but not with excl since create_new starts fresh)
526            opts.truncate(true);
527        }
528
529        let file = opts
530            .open(path)
531            .map_err(|e| io::Error::new(e.kind(), format!("failed to open '{}': {}", path, e)))?;
532        output_file = Some(file.try_clone()?);
533        Box::new(file)
534    } else {
535        Box::new(io::stdout())
536    };
537
538    // Skip input blocks — use seek() for file inputs to avoid reading and discarding data
539    if config.skip > 0 {
540        if let Some(ref mut f) = input_file {
541            skip_input_seek(f, config.skip, config.ibs)?;
542            // Rebuild the input Box with a clone at the seeked position
543            let seeked = f.try_clone()?;
544            input = Box::new(seeked);
545        } else {
546            skip_input(&mut input, config.skip, config.ibs)?;
547        }
548    }
549
550    // Seek output blocks
551    if config.seek > 0 {
552        if let Some(ref mut f) = output_file {
553            seek_output_file(f, config.seek, config.obs)?;
554            // Rebuild the output Box with a new clone at the seeked position
555            let seeked = f.try_clone()?;
556            output = Box::new(seeked);
557        } else {
558            seek_output(&mut output, config.seek, config.obs)?;
559        }
560    }
561
562    let mut stats = DdStats::default();
563    let mut ibuf = vec![0u8; config.ibs];
564    let mut obuf: Vec<u8> = Vec::with_capacity(config.obs);
565
566    loop {
567        // Check count limit
568        if let Some(count) = config.count {
569            if stats.records_in_full + stats.records_in_partial >= count {
570                break;
571            }
572        }
573
574        // Read one input block
575        let n = match read_full_block(&mut input, &mut ibuf) {
576            Ok(n) => n,
577            Err(e) => {
578                if config.conv.noerror {
579                    if config.status != StatusLevel::None {
580                        eprintln!("dd: error reading input: {}", e);
581                    }
582                    // On noerror with sync, fill the entire block with NULs
583                    if config.conv.sync {
584                        ibuf.fill(0);
585                        config.ibs
586                    } else {
587                        continue;
588                    }
589                } else {
590                    return Err(e);
591                }
592            }
593        };
594
595        if n == 0 {
596            break;
597        }
598
599        // Track full vs partial blocks
600        if n == config.ibs {
601            stats.records_in_full += 1;
602        } else {
603            stats.records_in_partial += 1;
604            // Pad with NULs if conv=sync
605            if config.conv.sync {
606                ibuf[n..].fill(0);
607            }
608        }
609
610        // Determine the data slice to use and apply conversions in-place
611        let effective_len = if config.conv.sync { config.ibs } else { n };
612        apply_conversions(&mut ibuf[..effective_len], &config.conv);
613
614        // Buffer output and flush when we have enough for a full output block.
615        // Use efficient buffer management: write directly from ibuf when possible,
616        // only buffer when ibs != obs.
617        if config.ibs == config.obs && obuf.is_empty() {
618            // Fast path: ibs == obs, write directly
619            output.write_all(&ibuf[..effective_len])?;
620            if effective_len == config.obs {
621                stats.records_out_full += 1;
622            } else {
623                stats.records_out_partial += 1;
624            }
625            stats.bytes_copied += effective_len as u64;
626            // Skip the drain loop below since we wrote directly
627            continue;
628        }
629
630        obuf.extend_from_slice(&ibuf[..effective_len]);
631        let mut consumed = 0;
632        while obuf.len() - consumed >= config.obs {
633            output.write_all(&obuf[consumed..consumed + config.obs])?;
634            stats.records_out_full += 1;
635            stats.bytes_copied += config.obs as u64;
636            consumed += config.obs;
637        }
638        if consumed > 0 {
639            // Shift remaining bytes to front (more efficient than drain for large buffers)
640            let remaining = obuf.len() - consumed;
641            if remaining > 0 {
642                obuf.copy_within(consumed.., 0);
643            }
644            obuf.truncate(remaining);
645        }
646    }
647
648    // Flush remaining partial output block
649    if !obuf.is_empty() {
650        output.write_all(&obuf)?;
651        stats.records_out_partial += 1;
652        stats.bytes_copied += obuf.len() as u64;
653    }
654
655    // Flush output
656    output.flush()?;
657
658    // fsync / fdatasync
659    if have_output_file {
660        if let Some(ref f) = output_file {
661            if config.conv.fsync {
662                f.sync_all()?;
663            } else if config.conv.fdatasync {
664                f.sync_data()?;
665            }
666        }
667    }
668
669    let elapsed = start_time.elapsed();
670
671    // Print status
672    if config.status != StatusLevel::None {
673        print_stats(&stats, elapsed);
674    }
675
676    Ok(stats)
677}
678
679/// Print dd transfer statistics to stderr.
680fn print_stats(stats: &DdStats, elapsed: std::time::Duration) {
681    eprintln!(
682        "{}+{} records in",
683        stats.records_in_full, stats.records_in_partial
684    );
685    eprintln!(
686        "{}+{} records out",
687        stats.records_out_full, stats.records_out_partial
688    );
689
690    let secs = elapsed.as_secs_f64();
691    if secs > 0.0 {
692        let rate = stats.bytes_copied as f64 / secs;
693        eprintln!(
694            "{} bytes copied, {:.6} s, {}/s",
695            stats.bytes_copied,
696            secs,
697            human_size(rate as u64)
698        );
699    } else {
700        eprintln!("{} bytes copied", stats.bytes_copied);
701    }
702}
703
704/// Format a byte count as a human-readable string (e.g., "1.5 MB").
705fn human_size(bytes: u64) -> String {
706    const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB", "PB", "EB"];
707    let mut size = bytes as f64;
708    for &unit in UNITS {
709        if size < 1000.0 {
710            if size == size.floor() {
711                return format!("{} {}", size as u64, unit);
712            }
713            return format!("{:.1} {}", size, unit);
714        }
715        size /= 1000.0;
716    }
717    format!("{:.1} EB", size * 1000.0)
718}
719
720/// Print help message for dd.
721pub fn print_help() {
722    eprint!(
723        "\
724Usage: dd [OPERAND]...
725  or:  dd OPTION
726Copy a file, converting and formatting according to the operands.
727
728  bs=BYTES        read and write up to BYTES bytes at a time (default: 512)
729  cbs=BYTES       convert BYTES bytes at a time
730  conv=CONVS      convert the file as per the comma separated symbol list
731  count=N         copy only N input blocks
732  ibs=BYTES       read up to BYTES bytes at a time (default: 512)
733  if=FILE         read from FILE instead of stdin
734  iflag=FLAGS     read as per the comma separated symbol list
735  obs=BYTES       write BYTES bytes at a time (default: 512)
736  of=FILE         write to FILE instead of stdout
737  oflag=FLAGS     write as per the comma separated symbol list
738  seek=N          skip N obs-sized blocks at start of output
739  skip=N          skip N ibs-sized blocks at start of input
740  status=LEVEL    LEVEL of information to print to stderr;
741                  'none' suppresses everything but error messages,
742                  'noerror' suppresses the final transfer statistics,
743                  'progress' shows periodic transfer statistics
744
745  BLOCKS and BYTES may be followed by the following multiplicative suffixes:
746  c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000, M=1024*1024,
747  GB=1000*1000*1000, GiB=1024*1024*1024, and so on for T, P, E.
748
749Each CONV symbol may be:
750
751  lcase     change upper case to lower case
752  ucase     change lower case to upper case
753  swab      swap every pair of input bytes
754  sync      pad every input block with NULs to ibs-size
755  noerror   continue after read errors
756  notrunc   do not truncate the output file
757  fdatasync physically write output file data before finishing
758  fsync     likewise, but also write metadata
759  excl      fail if the output file already exists
760  nocreat   do not create the output file
761
762Each FLAG symbol may be:
763
764  append    append mode (makes sense only for output; conv=notrunc suggested)
765  direct    use direct I/O for data
766  directory fail unless a directory
767  dsync     use synchronized I/O for data
768  sync      likewise, but also for metadata
769  fullblock accumulate full blocks of input (iflag only)
770  nonblock  use non-blocking I/O
771  noatime   do not update access time
772  nocache   Request to drop cache
773  noctty    do not assign controlling terminal from file
774  nofollow  do not follow symlinks
775  count_bytes  treat 'count=N' as a byte count (iflag only)
776  skip_bytes   treat 'skip=N' as a byte count (iflag only)
777
778  --help     display this help and exit
779  --version  output version information and exit
780"
781    );
782}
783
784/// Print version information for dd.
785pub fn print_version() {
786    eprintln!("dd (fcoreutils) {}", env!("CARGO_PKG_VERSION"));
787}