use linuxutils_common::man::ManContent;
pub const MAN: ManContent = ManContent::empty();
use clap::Parser;
use std::{
fs::{self, File},
io::{self, BufRead},
os::unix::io::AsRawFd,
process::ExitCode,
};
#[derive(Parser)]
#[command(name = "copyfilerange", about = "Copy byte ranges between files")]
pub struct Args {
#[arg(short = 'r', long = "ranges")]
ranges_file: Option<String>,
#[arg(short = 'v', long)]
verbose: bool,
source: String,
destination: String,
#[arg(trailing_var_arg = true)]
ranges: Vec<String>,
}
struct Range {
src_off: u64,
dst_off: u64,
length: u64,
}
fn parse_range(
spec: &str,
last_src: u64,
last_dst: u64,
) -> Result<Range, String> {
let parts: Vec<&str> = spec.splitn(3, ':').collect();
let src_off = if parts.is_empty() || parts[0].is_empty() {
last_src
} else {
parse_size(parts[0])?
};
let dst_off = if parts.len() < 2 || parts[1].is_empty() {
last_dst
} else {
parse_size(parts[1])?
};
let length = if parts.len() < 3 || parts[2].is_empty() {
0
} else {
parse_size(parts[2])?
};
Ok(Range {
src_off,
dst_off,
length,
})
}
fn parse_size(s: &str) -> Result<u64, String> {
let s = s.trim();
if s.is_empty() {
return Ok(0);
}
let (num_str, mult) = if let Some(n) = s.strip_suffix('G') {
(n, 1024 * 1024 * 1024)
} else if let Some(n) = s.strip_suffix('M') {
(n, 1024 * 1024)
} else if let Some(n) = s.strip_suffix('K') {
(n, 1024)
} else {
(s, 1)
};
num_str
.parse::<u64>()
.map(|n| n * mult)
.map_err(|_| format!("invalid number: {s}"))
}
fn do_copy_file_range(
src_fd: i32,
src_off: &mut i64,
dst_fd: i32,
dst_off: &mut i64,
len: usize,
) -> io::Result<usize> {
let ret = unsafe {
libc::copy_file_range(src_fd, src_off, dst_fd, dst_off, len, 0)
};
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(ret as usize)
}
}
pub fn run(args: Args) -> ExitCode {
let src = match File::open(&args.source) {
Ok(f) => f,
Err(e) => {
eprintln!("copyfilerange: {}: {e}", args.source);
return ExitCode::FAILURE;
}
};
let dst = match File::options()
.write(true)
.create(true)
.truncate(false)
.open(&args.destination)
{
Ok(f) => f,
Err(e) => {
eprintln!("copyfilerange: {}: {e}", args.destination);
return ExitCode::FAILURE;
}
};
let src_size = fs::metadata(&args.source).map(|m| m.len()).unwrap_or(0);
let mut range_specs: Vec<String> = args.ranges.clone();
if let Some(ref path) = args.ranges_file {
match File::open(path) {
Ok(f) => {
for line in io::BufReader::new(f).lines().map_while(Result::ok)
{
let line = line.trim().to_string();
if !line.is_empty() {
range_specs.push(line);
}
}
}
Err(e) => {
eprintln!("copyfilerange: {path}: {e}");
return ExitCode::FAILURE;
}
}
}
if range_specs.is_empty() {
eprintln!("copyfilerange: no ranges specified");
return ExitCode::FAILURE;
}
let mut last_src: u64 = 0;
let mut last_dst: u64 = 0;
for spec in &range_specs {
let range = match parse_range(spec, last_src, last_dst) {
Ok(r) => r,
Err(e) => {
eprintln!("copyfilerange: invalid range '{spec}': {e}");
return ExitCode::FAILURE;
}
};
let copy_len = if range.length == 0 {
src_size.saturating_sub(range.src_off)
} else {
range.length
};
let mut src_off = range.src_off as i64;
let mut dst_off = range.dst_off as i64;
let mut remaining = copy_len as usize;
while remaining > 0 {
let chunk = remaining.min(1 << 30); match do_copy_file_range(
src.as_raw_fd(),
&mut src_off,
dst.as_raw_fd(),
&mut dst_off,
chunk,
) {
Ok(0) => break,
Ok(n) => remaining -= n,
Err(e) => {
eprintln!("copyfilerange: copy failed: {e}");
return ExitCode::FAILURE;
}
}
}
if args.verbose {
let copied = copy_len as usize - remaining;
eprintln!(
"{}:{} -> {}:{} ({copied} bytes)",
args.source, range.src_off, args.destination, range.dst_off
);
}
last_src = src_off as u64;
last_dst = dst_off as u64;
}
ExitCode::SUCCESS
}