use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StatusLevel {
#[default]
Default,
None,
Progress,
NoError,
NoXfer,
}
#[derive(Debug, Clone, Default)]
pub struct DdConv {
pub lcase: bool,
pub ucase: bool,
pub swab: bool,
pub noerror: bool,
pub notrunc: bool,
pub sync: bool,
pub fdatasync: bool,
pub fsync: bool,
pub excl: bool,
pub nocreat: bool,
pub unblock: bool,
pub block: bool,
}
#[derive(Debug, Clone, Default)]
pub struct DdFlags {
pub append: bool,
pub direct: bool,
pub directory: bool,
pub dsync: bool,
pub sync: bool,
pub fullblock: bool,
pub nonblock: bool,
pub noatime: bool,
pub nocache: bool,
pub noctty: bool,
pub nofollow: bool,
pub count_bytes: bool,
pub skip_bytes: bool,
}
#[derive(Debug, Clone)]
pub struct DdConfig {
pub input: Option<String>,
pub output: Option<String>,
pub ibs: usize,
pub obs: usize,
pub cbs: usize,
pub count: Option<u64>,
pub skip: u64,
pub seek: u64,
pub conv: DdConv,
pub status: StatusLevel,
pub iflag: DdFlags,
pub oflag: DdFlags,
}
impl Default for DdConfig {
fn default() -> Self {
DdConfig {
input: None,
output: None,
ibs: 512,
obs: 512,
cbs: 0,
count: None,
skip: 0,
seek: 0,
conv: DdConv::default(),
status: StatusLevel::default(),
iflag: DdFlags::default(),
oflag: DdFlags::default(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DdStats {
pub records_in_full: u64,
pub records_in_partial: u64,
pub records_out_full: u64,
pub records_out_partial: u64,
pub bytes_copied: u64,
}
pub fn parse_size(s: &str) -> Result<u64, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty size string".to_string());
}
if let Some(pos) = s.find('x') {
let left = parse_size_single(&s[..pos])?;
let right = parse_size(&s[pos + 1..])?;
return left
.checked_mul(right)
.ok_or_else(|| format!("size overflow: {} * {}", left, right));
}
parse_size_single(s)
}
fn parse_size_single(s: &str) -> Result<u64, String> {
if s.is_empty() {
return Err("empty size string".to_string());
}
let num_end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
if num_end == 0 {
return Err(format!("invalid number: '{}'", s));
}
let num: u64 = s[..num_end]
.parse()
.map_err(|e| format!("invalid number '{}': {}", &s[..num_end], e))?;
let suffix = &s[num_end..];
let multiplier: u64 = match suffix {
"" => 1,
"c" => 1,
"w" => 2,
"b" => 512,
"k" | "K" => 1024,
"kB" | "KB" => 1000,
"KiB" => 1024,
"M" => 1_048_576,
"MB" => 1_000_000,
"MiB" => 1_048_576,
"G" => 1_073_741_824,
"GB" => 1_000_000_000,
"GiB" => 1_073_741_824,
"T" => 1_099_511_627_776,
"TB" => 1_000_000_000_000,
"TiB" => 1_099_511_627_776,
"P" => 1_125_899_906_842_624,
"PB" => 1_000_000_000_000_000,
"PiB" => 1_125_899_906_842_624,
"E" => 1_152_921_504_606_846_976,
"EB" => 1_000_000_000_000_000_000,
"EiB" => 1_152_921_504_606_846_976,
_ => return Err(format!("invalid suffix: '{}'", suffix)),
};
num.checked_mul(multiplier)
.ok_or_else(|| format!("size overflow: {} * {}", num, multiplier))
}
pub fn parse_dd_args(args: &[String]) -> Result<DdConfig, String> {
let mut config = DdConfig::default();
let mut bs_set = false;
for arg in args {
if let Some((key, value)) = arg.split_once('=') {
match key {
"if" => config.input = Some(value.to_string()),
"of" => config.output = Some(value.to_string()),
"bs" => {
let size = parse_size(value)? as usize;
config.ibs = size;
config.obs = size;
bs_set = true;
}
"ibs" => {
if !bs_set {
config.ibs = parse_size(value)? as usize;
}
}
"obs" => {
if !bs_set {
config.obs = parse_size(value)? as usize;
}
}
"cbs" => config.cbs = parse_size(value)? as usize,
"count" => config.count = Some(parse_size(value)?),
"skip" => config.skip = parse_size(value)?,
"seek" => config.seek = parse_size(value)?,
"conv" => {
for flag in value.split(',') {
match flag {
"lcase" => config.conv.lcase = true,
"ucase" => config.conv.ucase = true,
"swab" => config.conv.swab = true,
"noerror" => config.conv.noerror = true,
"notrunc" => config.conv.notrunc = true,
"sync" => config.conv.sync = true,
"fdatasync" => config.conv.fdatasync = true,
"fsync" => config.conv.fsync = true,
"excl" => config.conv.excl = true,
"nocreat" => config.conv.nocreat = true,
"block" => config.conv.block = true,
"unblock" => config.conv.unblock = true,
"" => {}
_ => return Err(format!("invalid conversion: '{}'", flag)),
}
}
}
"iflag" => {
for flag in value.split(',') {
parse_flag(flag, &mut config.iflag)?;
}
}
"oflag" => {
for flag in value.split(',') {
parse_flag(flag, &mut config.oflag)?;
}
}
"status" => {
config.status = match value {
"none" => StatusLevel::None,
"noxfer" => StatusLevel::NoXfer,
"noerror" => StatusLevel::NoError,
"progress" => StatusLevel::Progress,
_ => return Err(format!("invalid status level: '{}'", value)),
};
}
_ => return Err(format!("unrecognized operand: '{}'", arg)),
}
} else {
return Err(format!("unrecognized operand: '{}'", arg));
}
}
if config.conv.lcase && config.conv.ucase {
return Err("conv=lcase and conv=ucase are mutually exclusive".to_string());
}
if config.conv.excl && config.conv.nocreat {
return Err("conv=excl and conv=nocreat are mutually exclusive".to_string());
}
Ok(config)
}
fn parse_flag(flag: &str, flags: &mut DdFlags) -> Result<(), String> {
match flag {
"append" => flags.append = true,
"direct" => flags.direct = true,
"directory" => flags.directory = true,
"dsync" => flags.dsync = true,
"sync" => flags.sync = true,
"fullblock" => flags.fullblock = true,
"nonblock" => flags.nonblock = true,
"noatime" => flags.noatime = true,
"nocache" => flags.nocache = true,
"noctty" => flags.noctty = true,
"nofollow" => flags.nofollow = true,
"count_bytes" => flags.count_bytes = true,
"skip_bytes" => flags.skip_bytes = true,
"" => {}
_ => return Err(format!("invalid flag: '{}'", flag)),
}
Ok(())
}
fn read_full_block(reader: &mut dyn Read, buf: &mut [u8]) -> io::Result<usize> {
let mut total = 0;
while total < buf.len() {
match reader.read(&mut buf[total..]) {
Ok(0) => break,
Ok(n) => total += n,
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(total)
}
pub fn apply_conversions(data: &mut [u8], conv: &DdConv) {
if conv.swab {
let (prefix, chunks, suffix) = unsafe { data.align_to_mut::<u64>() };
let pairs_pre = prefix.len() / 2;
for i in 0..pairs_pre {
prefix.swap(i * 2, i * 2 + 1);
}
for w in chunks.iter_mut() {
let x = *w;
*w = ((x & 0xFF00FF00FF00FF00) >> 8) | ((x & 0x00FF00FF00FF00FF) << 8);
}
let pairs_suf = suffix.len() / 2;
for i in 0..pairs_suf {
suffix.swap(i * 2, i * 2 + 1);
}
}
if conv.lcase {
for b in data.iter_mut() {
b.make_ascii_lowercase();
}
} else if conv.ucase {
for b in data.iter_mut() {
b.make_ascii_uppercase();
}
}
}
fn skip_input(reader: &mut dyn Read, blocks: u64, block_size: usize) -> io::Result<()> {
let mut discard_buf = vec![0u8; block_size];
for _ in 0..blocks {
let n = read_full_block(reader, &mut discard_buf)?;
if n == 0 {
break;
}
}
Ok(())
}
fn skip_input_bytes(reader: &mut dyn Read, bytes: u64) -> io::Result<()> {
let mut remaining = bytes;
let mut discard_buf = [0u8; 8192];
while remaining > 0 {
let chunk = std::cmp::min(remaining, discard_buf.len() as u64) as usize;
let n = reader.read(&mut discard_buf[..chunk])?;
if n == 0 {
break;
}
remaining -= n as u64;
}
Ok(())
}
fn skip_input_seek(file: &mut File, blocks: u64, block_size: usize) -> io::Result<()> {
let offset = blocks * block_size as u64;
file.seek(SeekFrom::Start(offset))?;
Ok(())
}
fn seek_output(writer: &mut Box<dyn Write>, seek_blocks: u64, block_size: usize) -> io::Result<()> {
let zero_block = vec![0u8; block_size];
for _ in 0..seek_blocks {
writer.write_all(&zero_block)?;
}
Ok(())
}
fn seek_output_file(file: &mut File, seek_blocks: u64, block_size: usize) -> io::Result<()> {
let offset = seek_blocks * block_size as u64;
file.seek(SeekFrom::Start(offset))?;
Ok(())
}
#[cfg(target_os = "linux")]
fn has_conversions(conv: &DdConv) -> bool {
conv.lcase || conv.ucase || conv.swab || conv.sync || conv.block || conv.unblock
}
#[cfg(target_os = "linux")]
fn has_flags(flags: &DdFlags) -> bool {
flags.append
|| flags.direct
|| flags.directory
|| flags.dsync
|| flags.sync
|| flags.nonblock
|| flags.nocache
|| flags.noctty
|| flags.nofollow
|| flags.count_bytes
|| flags.skip_bytes
}
#[cfg(target_os = "linux")]
fn try_raw_dd(config: &DdConfig) -> Option<io::Result<DdStats>> {
if config.input.is_none() || config.output.is_none() {
return None;
}
if has_conversions(&config.conv) || config.ibs != config.obs {
return None;
}
if has_flags(&config.iflag) || has_flags(&config.oflag) {
return None;
}
let start_time = Instant::now();
let in_path = config.input.as_ref().unwrap();
let out_path = config.output.as_ref().unwrap();
let in_cstr = match std::ffi::CString::new(in_path.as_str()) {
Ok(c) => c,
Err(_) => {
return Some(Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("input path contains NUL byte: '{}'", in_path),
)));
}
};
let out_cstr = match std::ffi::CString::new(out_path.as_str()) {
Ok(c) => c,
Err(_) => {
return Some(Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("output path contains NUL byte: '{}'", out_path),
)));
}
};
let in_fd = unsafe {
libc::open(
in_cstr.as_ptr(),
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOATIME,
)
};
let in_fd = if in_fd < 0 {
let first_err = io::Error::last_os_error();
if first_err.raw_os_error() == Some(libc::EPERM) {
let fd = unsafe { libc::open(in_cstr.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
if fd < 0 {
return Some(Err(io::Error::last_os_error()));
}
fd
} else {
return Some(Err(first_err));
}
} else {
in_fd
};
let mut oflags = libc::O_WRONLY | libc::O_CLOEXEC;
if config.conv.excl {
oflags |= libc::O_CREAT | libc::O_EXCL;
} else if config.conv.nocreat {
} else {
oflags |= libc::O_CREAT;
}
if !config.conv.notrunc && !config.conv.excl {
oflags |= libc::O_TRUNC;
}
let out_fd = unsafe { libc::open(out_cstr.as_ptr(), oflags, 0o666 as libc::mode_t) };
if out_fd < 0 {
unsafe { libc::close(in_fd) };
return Some(Err(io::Error::last_os_error()));
}
{
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(in_fd, &mut stat) } == 0
&& (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
{
unsafe {
libc::posix_fadvise(in_fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL);
}
}
}
if config.skip > 0 {
let offset = match (config.skip as u64).checked_mul(config.ibs as u64) {
Some(o) if o <= i64::MAX as u64 => o as i64,
_ => {
unsafe {
libc::close(in_fd);
libc::close(out_fd);
}
return Some(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"skip offset overflow",
)));
}
};
if unsafe { libc::lseek(in_fd, offset, libc::SEEK_SET) } < 0 {
let mut discard = vec![0u8; config.ibs];
'skip: for _ in 0..config.skip {
let mut skipped = 0usize;
while skipped < config.ibs {
let n = unsafe {
libc::read(
in_fd,
discard[skipped..].as_mut_ptr() as *mut _,
config.ibs - skipped,
)
};
if n > 0 {
skipped += n as usize;
} else if n == 0 {
break 'skip; } else {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::Interrupted {
continue;
}
eprintln!("dd: error skipping input: {}", err);
break 'skip;
}
}
}
}
}
if config.seek > 0 {
let offset = match (config.seek as u64).checked_mul(config.obs as u64) {
Some(o) if o <= i64::MAX as u64 => o as i64,
_ => {
unsafe {
libc::close(in_fd);
libc::close(out_fd);
}
return Some(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"seek offset overflow",
)));
}
};
if unsafe { libc::lseek(out_fd, offset, libc::SEEK_SET) } < 0 {
let err = io::Error::last_os_error();
unsafe {
libc::close(in_fd);
libc::close(out_fd);
}
return Some(Err(err));
}
}
let mut stats = DdStats::default();
let bs = config.ibs;
#[allow(clippy::uninit_vec)]
let mut ibuf = unsafe {
let mut v = Vec::with_capacity(bs);
v.set_len(bs);
v
};
debug_assert_eq!(ibuf.len(), bs);
let count_limit = config.count;
loop {
if let Some(limit) = count_limit {
if stats.records_in_full + stats.records_in_partial >= limit {
break;
}
}
let mut total_read = 0usize;
let mut read_error = false;
while total_read < bs {
let ret = unsafe {
libc::read(
in_fd,
ibuf[total_read..].as_mut_ptr() as *mut _,
bs - total_read,
)
};
if ret > 0 {
total_read += ret as usize;
} else if ret == 0 {
break; } else {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::Interrupted {
continue;
}
if config.conv.noerror {
eprintln!("dd: error reading '{}': {}", in_path, err);
read_error = true;
break;
}
unsafe {
libc::close(in_fd);
libc::close(out_fd);
}
return Some(Err(err));
}
}
if read_error {
stats.records_in_partial += 1;
continue;
}
if total_read == 0 {
break;
}
if total_read == bs {
stats.records_in_full += 1;
} else {
stats.records_in_partial += 1;
}
let mut written = 0usize;
debug_assert!(total_read <= ibuf.len());
while written < total_read {
debug_assert!(written <= ibuf.len());
let ret = unsafe {
libc::write(
out_fd,
ibuf[written..].as_ptr() as *const _,
total_read - written,
)
};
if ret > 0 {
written += ret as usize;
} else if ret == 0 {
unsafe {
libc::close(in_fd);
libc::close(out_fd);
}
return Some(Err(io::Error::new(
io::ErrorKind::WriteZero,
"write returned 0",
)));
} else {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::Interrupted {
continue;
}
unsafe {
libc::close(in_fd);
libc::close(out_fd);
}
return Some(Err(err));
}
}
stats.bytes_copied += written as u64;
if written == bs {
stats.records_out_full += 1;
} else {
stats.records_out_partial += 1;
}
}
if config.conv.fsync {
if unsafe { libc::fsync(out_fd) } < 0 {
let err = io::Error::last_os_error();
unsafe {
libc::close(in_fd);
libc::close(out_fd);
}
return Some(Err(err));
}
} else if config.conv.fdatasync {
if unsafe { libc::fdatasync(out_fd) } < 0 {
let err = io::Error::last_os_error();
unsafe {
libc::close(in_fd);
libc::close(out_fd);
}
return Some(Err(err));
}
}
unsafe { libc::close(in_fd) };
if unsafe { libc::close(out_fd) } < 0 {
return Some(Err(io::Error::last_os_error()));
}
if config.status != StatusLevel::None {
print_stats(&stats, start_time.elapsed(), config.status);
}
Some(Ok(stats))
}
#[cfg(target_os = "linux")]
fn try_copy_file_range_dd(config: &DdConfig) -> Option<io::Result<DdStats>> {
if config.input.is_none() || config.output.is_none() {
return None;
}
if has_conversions(&config.conv) || config.ibs != config.obs {
return None;
}
let start_time = Instant::now();
let in_path = config.input.as_ref().unwrap();
let out_path = config.output.as_ref().unwrap();
let in_file = match File::open(in_path) {
Ok(f) => f,
Err(e) => return Some(Err(e)),
};
{
use std::os::unix::io::AsRawFd;
unsafe {
libc::posix_fadvise(in_file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
}
}
let mut out_opts = OpenOptions::new();
out_opts.write(true);
if config.conv.excl {
out_opts.create_new(true);
} else if !config.conv.nocreat {
out_opts.create(true);
}
if !config.conv.notrunc && !config.conv.excl {
out_opts.truncate(true);
}
let out_file = match out_opts.open(out_path) {
Ok(f) => f,
Err(e) => return Some(Err(e)),
};
use std::os::unix::io::AsRawFd;
let in_fd = in_file.as_raw_fd();
let out_fd = out_file.as_raw_fd();
let skip_bytes = config.skip * config.ibs as u64;
let seek_bytes = config.seek * config.obs as u64;
let mut in_off: i64 = skip_bytes as i64;
let mut out_off: i64 = seek_bytes as i64;
let mut stats = DdStats::default();
let block_size = config.ibs;
let total_to_copy = config.count.map(|count| count * block_size as u64);
let mut bytes_remaining = total_to_copy;
loop {
let chunk = match bytes_remaining {
Some(0) => break,
Some(r) => r.min(block_size as u64 * 1024) as usize, None => block_size * 1024,
};
let ret = unsafe {
libc::syscall(
libc::SYS_copy_file_range,
in_fd,
&mut in_off as *mut i64,
out_fd,
&mut out_off as *mut i64,
chunk,
0u32,
)
};
if ret < 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINVAL)
|| err.raw_os_error() == Some(libc::ENOSYS)
|| err.raw_os_error() == Some(libc::EXDEV)
{
return None; }
return Some(Err(err));
}
if ret == 0 {
break;
}
let copied = ret as u64;
stats.bytes_copied += copied;
let full_blocks = copied / block_size as u64;
let partial = copied % block_size as u64;
stats.records_in_full += full_blocks;
stats.records_out_full += full_blocks;
if partial > 0 {
stats.records_in_partial += 1;
stats.records_out_partial += 1;
}
if let Some(ref mut r) = bytes_remaining {
*r = r.saturating_sub(copied);
}
}
if config.conv.fsync {
if let Err(e) = out_file.sync_all() {
return Some(Err(e));
}
} else if config.conv.fdatasync {
if let Err(e) = out_file.sync_data() {
return Some(Err(e));
}
}
if config.status != StatusLevel::None {
print_stats(&stats, start_time.elapsed(), config.status);
}
Some(Ok(stats))
}
pub fn dd_copy(config: &DdConfig) -> io::Result<DdStats> {
#[cfg(target_os = "linux")]
{
if let Some(result) = try_copy_file_range_dd(config) {
return result;
}
}
#[cfg(target_os = "linux")]
{
if let Some(result) = try_raw_dd(config) {
return result;
}
}
let start_time = Instant::now();
let needs_input_seek = config.skip > 0;
let needs_output_seek = config.seek > 0;
let mut input_file: Option<File> = None;
let mut input: Box<dyn Read> = if let Some(ref path) = config.input {
let file = File::open(path)
.map_err(|e| io::Error::new(e.kind(), format!("failed to open '{}': {}", path, e)))?;
#[cfg(target_os = "linux")]
{
use std::os::unix::io::AsRawFd;
unsafe {
libc::posix_fadvise(file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
}
}
if needs_input_seek {
input_file = Some(file.try_clone()?);
}
Box::new(file)
} else {
Box::new(io::stdin())
};
let mut output_file: Option<File> = None;
let mut output: Box<dyn Write> = if let Some(ref path) = config.output {
let mut opts = OpenOptions::new();
opts.write(true);
if config.conv.excl {
opts.create_new(true);
} else if config.conv.nocreat {
} else {
opts.create(true);
}
if config.conv.notrunc {
opts.truncate(false);
} else if !config.conv.excl {
opts.truncate(true);
}
let file = opts
.open(path)
.map_err(|e| io::Error::new(e.kind(), format!("failed to open '{}': {}", path, e)))?;
if needs_output_seek || config.conv.fsync || config.conv.fdatasync {
output_file = Some(file.try_clone()?);
}
Box::new(file)
} else {
Box::new(io::stdout())
};
if config.skip > 0 {
if config.iflag.skip_bytes {
if let Some(ref mut f) = input_file {
f.seek(SeekFrom::Start(config.skip))?;
let seeked = f.try_clone()?;
input = Box::new(seeked);
} else {
skip_input_bytes(&mut input, config.skip)?;
}
} else if let Some(ref mut f) = input_file {
skip_input_seek(f, config.skip, config.ibs)?;
let seeked = f.try_clone()?;
input = Box::new(seeked);
} else {
skip_input(&mut input, config.skip, config.ibs)?;
}
}
if config.seek > 0 {
if let Some(ref mut f) = output_file {
seek_output_file(f, config.seek, config.obs)?;
let seeked = f.try_clone()?;
output = Box::new(seeked);
} else {
seek_output(&mut output, config.seek, config.obs)?;
}
}
let mut stats = DdStats::default();
let mut ibuf = vec![0u8; config.ibs];
let mut obuf: Vec<u8> = Vec::with_capacity(config.obs);
let mut unblock_buf: Vec<u8> = Vec::new();
let mut bytes_read_total: u64 = 0;
loop {
if let Some(count) = config.count {
if config.iflag.count_bytes {
if bytes_read_total >= count {
break;
}
} else if stats.records_in_full + stats.records_in_partial >= count {
break;
}
}
let read_size = if config.iflag.count_bytes {
if let Some(count) = config.count {
let remaining = count.saturating_sub(bytes_read_total);
std::cmp::min(config.ibs, remaining as usize)
} else {
config.ibs
}
} else {
config.ibs
};
if read_size == 0 {
break;
}
let n = match read_full_block(&mut input, &mut ibuf[..read_size]) {
Ok(n) => n,
Err(e) => {
if config.conv.noerror {
if config.status != StatusLevel::None {
eprintln!("dd: error reading input: {}", e);
}
if config.conv.sync {
ibuf.fill(0);
config.ibs
} else {
continue;
}
} else {
return Err(e);
}
}
};
if n == 0 {
break;
}
bytes_read_total += n as u64;
if n == config.ibs {
stats.records_in_full += 1;
} else {
stats.records_in_partial += 1;
if config.conv.sync {
let pad_byte = if config.conv.block || config.conv.unblock {
b' '
} else {
0u8
};
ibuf[n..config.ibs].fill(pad_byte);
}
}
let effective_len = if config.conv.sync { config.ibs } else { n };
apply_conversions(&mut ibuf[..effective_len], &config.conv);
let write_data: &[u8] = if config.conv.unblock && config.cbs > 0 {
unblock_buf.clear();
let data = &ibuf[..effective_len];
let mut pos = 0;
while pos < data.len() {
let end = std::cmp::min(pos + config.cbs, data.len());
let record = &data[pos..end];
let trimmed_len = record
.iter()
.rposition(|&b| b != b' ')
.map(|p| p + 1)
.unwrap_or(0);
unblock_buf.extend_from_slice(&record[..trimmed_len]);
unblock_buf.push(b'\n');
pos = end;
}
&unblock_buf
} else {
&ibuf[..effective_len]
};
let wd_len = write_data.len();
if config.ibs == config.obs && obuf.is_empty() && !config.conv.unblock {
output.write_all(write_data)?;
if wd_len == config.obs {
stats.records_out_full += 1;
} else {
stats.records_out_partial += 1;
}
stats.bytes_copied += wd_len as u64;
continue;
}
let obs = config.obs;
let mut wd_off = 0;
if !obuf.is_empty() {
let need = obs - obuf.len();
if write_data.len() >= need {
obuf.extend_from_slice(&write_data[..need]);
output.write_all(&obuf)?;
stats.records_out_full += 1;
stats.bytes_copied += obs as u64;
obuf.clear();
wd_off = need;
} else {
obuf.extend_from_slice(write_data);
wd_off = write_data.len();
}
}
let remaining_wd = &write_data[wd_off..];
let full_blocks = remaining_wd.len() / obs;
if full_blocks > 0 {
let full_len = full_blocks * obs;
output.write_all(&remaining_wd[..full_len])?;
stats.records_out_full += full_blocks as u64;
stats.bytes_copied += full_len as u64;
wd_off += full_len;
}
let leftover = &write_data[wd_off..];
if !leftover.is_empty() {
obuf.extend_from_slice(leftover);
}
}
if !obuf.is_empty() {
output.write_all(&obuf)?;
stats.records_out_partial += 1;
stats.bytes_copied += obuf.len() as u64;
}
output.flush()?;
if let Some(ref f) = output_file {
if config.conv.fsync {
f.sync_all()?;
} else if config.conv.fdatasync {
f.sync_data()?;
}
}
let elapsed = start_time.elapsed();
if config.status != StatusLevel::None {
print_stats(&stats, elapsed, config.status);
}
Ok(stats)
}
fn print_stats(stats: &DdStats, elapsed: std::time::Duration, status: StatusLevel) {
eprintln!(
"{}+{} records in",
stats.records_in_full, stats.records_in_partial
);
eprintln!(
"{}+{} records out",
stats.records_out_full, stats.records_out_partial
);
if status == StatusLevel::NoXfer {
return;
}
let secs = elapsed.as_secs_f64();
if secs > 0.0 {
let rate = stats.bytes_copied as f64 / secs;
eprintln!(
"{} bytes copied, {:.6} s, {}/s",
stats.bytes_copied,
secs,
human_size(rate as u64)
);
} else {
eprintln!("{} bytes copied", stats.bytes_copied);
}
}
fn human_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB", "PB", "EB"];
let mut size = bytes as f64;
for &unit in UNITS {
if size < 1000.0 {
if size == size.floor() {
return format!("{} {}", size as u64, unit);
}
return format!("{:.1} {}", size, unit);
}
size /= 1000.0;
}
format!("{:.1} EB", size * 1000.0)
}
pub fn print_help() {
eprint!(
"\
Usage: dd [OPERAND]...
or: dd OPTION
Copy a file, converting and formatting according to the operands.
bs=BYTES read and write up to BYTES bytes at a time (default: 512)
cbs=BYTES convert BYTES bytes at a time
conv=CONVS convert the file as per the comma separated symbol list
count=N copy only N input blocks
ibs=BYTES read up to BYTES bytes at a time (default: 512)
if=FILE read from FILE instead of stdin
iflag=FLAGS read as per the comma separated symbol list
obs=BYTES write BYTES bytes at a time (default: 512)
of=FILE write to FILE instead of stdout
oflag=FLAGS write as per the comma separated symbol list
seek=N skip N obs-sized blocks at start of output
skip=N skip N ibs-sized blocks at start of input
status=LEVEL LEVEL of information to print to stderr;
'none' suppresses everything but error messages,
'noerror' suppresses the final transfer statistics,
'progress' shows periodic transfer statistics
BLOCKS and BYTES may be followed by the following multiplicative suffixes:
c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000, M=1024*1024,
GB=1000*1000*1000, GiB=1024*1024*1024, and so on for T, P, E.
Each CONV symbol may be:
lcase change upper case to lower case
ucase change lower case to upper case
swab swap every pair of input bytes
sync pad every input block with NULs to ibs-size
noerror continue after read errors
notrunc do not truncate the output file
fdatasync physically write output file data before finishing
fsync likewise, but also write metadata
excl fail if the output file already exists
nocreat do not create the output file
Each FLAG symbol may be:
append append mode (makes sense only for output; conv=notrunc suggested)
direct use direct I/O for data
directory fail unless a directory
dsync use synchronized I/O for data
sync likewise, but also for metadata
fullblock accumulate full blocks of input (iflag only)
nonblock use non-blocking I/O
noatime do not update access time
nocache Request to drop cache
noctty do not assign controlling terminal from file
nofollow do not follow symlinks
count_bytes treat 'count=N' as a byte count (iflag only)
skip_bytes treat 'skip=N' as a byte count (iflag only)
--help display this help and exit
--version output version information and exit
"
);
}
pub fn print_version() {
eprintln!("dd (fcoreutils) {}", env!("CARGO_PKG_VERSION"));
}