1use std::fs::{File, OpenOptions};
2use std::io::{self, Read, Seek, SeekFrom, Write};
3use std::time::Instant;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum StatusLevel {
8 #[default]
10 Default,
11 None,
13 Progress,
15 NoError,
17}
18
19#[derive(Debug, Clone, Default)]
21pub struct DdConv {
22 pub lcase: bool,
24 pub ucase: bool,
26 pub swab: bool,
28 pub noerror: bool,
30 pub notrunc: bool,
32 pub sync: bool,
34 pub fdatasync: bool,
36 pub fsync: bool,
38 pub excl: bool,
40 pub nocreat: bool,
42}
43
44#[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#[derive(Debug, Clone)]
64pub struct DdConfig {
65 pub input: Option<String>,
67 pub output: Option<String>,
69 pub ibs: usize,
71 pub obs: usize,
73 pub count: Option<u64>,
75 pub skip: u64,
77 pub seek: u64,
79 pub conv: DdConv,
81 pub status: StatusLevel,
83 pub iflag: DdFlags,
85 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#[derive(Debug, Clone, Default)]
109pub struct DdStats {
110 pub records_in_full: u64,
112 pub records_in_partial: u64,
114 pub records_out_full: u64,
116 pub records_out_partial: u64,
118 pub bytes_copied: u64,
120}
121
122pub 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 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
173pub 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 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
256fn 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
278fn 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
293pub fn apply_conversions(data: &mut [u8], conv: &DdConv) {
295 if conv.swab {
296 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
314fn 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
326fn 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
333fn seek_output(writer: &mut Box<dyn Write>, seek_blocks: u64, block_size: usize) -> io::Result<()> {
335 let zero_block = vec![0u8; block_size];
338 for _ in 0..seek_blocks {
339 writer.write_all(&zero_block)?;
340 }
341 Ok(())
342}
343
344fn 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#[cfg(target_os = "linux")]
353fn has_conversions(conv: &DdConv) -> bool {
354 conv.lcase || conv.ucase || conv.swab || conv.sync
355}
356
357#[cfg(target_os = "linux")]
360fn try_copy_file_range_dd(config: &DdConfig) -> Option<io::Result<DdStats>> {
361 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 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 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, None => block_size * 1024,
416 };
417
418 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; }
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 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 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
484pub fn dd_copy(config: &DdConfig) -> io::Result<DdStats> {
486 #[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 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 opts.create_new(true);
515 } else if config.conv.nocreat {
516 } else {
519 opts.create(true);
520 }
521
522 if config.conv.notrunc {
523 opts.truncate(false);
524 } else if !config.conv.excl {
525 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 if config.skip > 0 {
540 if let Some(ref mut f) = input_file {
541 skip_input_seek(f, config.skip, config.ibs)?;
542 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 if config.seek > 0 {
552 if let Some(ref mut f) = output_file {
553 seek_output_file(f, config.seek, config.obs)?;
554 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 if let Some(count) = config.count {
569 if stats.records_in_full + stats.records_in_partial >= count {
570 break;
571 }
572 }
573
574 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 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 if n == config.ibs {
601 stats.records_in_full += 1;
602 } else {
603 stats.records_in_partial += 1;
604 if config.conv.sync {
606 ibuf[n..].fill(0);
607 }
608 }
609
610 let effective_len = if config.conv.sync { config.ibs } else { n };
612 apply_conversions(&mut ibuf[..effective_len], &config.conv);
613
614 if config.ibs == config.obs && obuf.is_empty() {
618 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 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 let remaining = obuf.len() - consumed;
641 if remaining > 0 {
642 obuf.copy_within(consumed.., 0);
643 }
644 obuf.truncate(remaining);
645 }
646 }
647
648 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 output.flush()?;
657
658 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 if config.status != StatusLevel::None {
673 print_stats(&stats, elapsed);
674 }
675
676 Ok(stats)
677}
678
679fn 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
704fn 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
720pub 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
784pub fn print_version() {
786 eprintln!("dd (fcoreutils) {}", env!("CARGO_PKG_VERSION"));
787}