linuxutils_misc/
copyfilerange.rs1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7 fs::{self, File},
8 io::{self, BufRead},
9 os::unix::io::AsRawFd,
10 process::ExitCode,
11};
12
13#[derive(Parser)]
18#[command(name = "copyfilerange", about = "Copy byte ranges between files")]
19pub struct Args {
20 #[arg(short = 'r', long = "ranges")]
22 ranges_file: Option<String>,
23
24 #[arg(short = 'v', long)]
26 verbose: bool,
27
28 source: String,
30
31 destination: String,
33
34 #[arg(trailing_var_arg = true)]
36 ranges: Vec<String>,
37}
38
39struct Range {
40 src_off: u64,
41 dst_off: u64,
42 length: u64,
43}
44
45fn parse_range(
46 spec: &str,
47 last_src: u64,
48 last_dst: u64,
49) -> Result<Range, String> {
50 let parts: Vec<&str> = spec.splitn(3, ':').collect();
51
52 let src_off = if parts.is_empty() || parts[0].is_empty() {
53 last_src
54 } else {
55 parse_size(parts[0])?
56 };
57
58 let dst_off = if parts.len() < 2 || parts[1].is_empty() {
59 last_dst
60 } else {
61 parse_size(parts[1])?
62 };
63
64 let length = if parts.len() < 3 || parts[2].is_empty() {
65 0
66 } else {
67 parse_size(parts[2])?
68 };
69
70 Ok(Range {
71 src_off,
72 dst_off,
73 length,
74 })
75}
76
77fn parse_size(s: &str) -> Result<u64, String> {
78 let s = s.trim();
79 if s.is_empty() {
80 return Ok(0);
81 }
82 let (num_str, mult) = if let Some(n) = s.strip_suffix('G') {
84 (n, 1024 * 1024 * 1024)
85 } else if let Some(n) = s.strip_suffix('M') {
86 (n, 1024 * 1024)
87 } else if let Some(n) = s.strip_suffix('K') {
88 (n, 1024)
89 } else {
90 (s, 1)
91 };
92 num_str
93 .parse::<u64>()
94 .map(|n| n * mult)
95 .map_err(|_| format!("invalid number: {s}"))
96}
97
98fn do_copy_file_range(
99 src_fd: i32,
100 src_off: &mut i64,
101 dst_fd: i32,
102 dst_off: &mut i64,
103 len: usize,
104) -> io::Result<usize> {
105 let ret = unsafe {
106 libc::copy_file_range(src_fd, src_off, dst_fd, dst_off, len, 0)
107 };
108 if ret < 0 {
109 Err(io::Error::last_os_error())
110 } else {
111 Ok(ret as usize)
112 }
113}
114
115pub fn run(args: Args) -> ExitCode {
116 let src = match File::open(&args.source) {
117 Ok(f) => f,
118 Err(e) => {
119 eprintln!("copyfilerange: {}: {e}", args.source);
120 return ExitCode::FAILURE;
121 }
122 };
123
124 let dst = match File::options()
125 .write(true)
126 .create(true)
127 .truncate(false)
128 .open(&args.destination)
129 {
130 Ok(f) => f,
131 Err(e) => {
132 eprintln!("copyfilerange: {}: {e}", args.destination);
133 return ExitCode::FAILURE;
134 }
135 };
136
137 let src_size = fs::metadata(&args.source).map(|m| m.len()).unwrap_or(0);
138
139 let mut range_specs: Vec<String> = args.ranges.clone();
141 if let Some(ref path) = args.ranges_file {
142 match File::open(path) {
143 Ok(f) => {
144 for line in io::BufReader::new(f).lines().map_while(Result::ok)
145 {
146 let line = line.trim().to_string();
147 if !line.is_empty() {
148 range_specs.push(line);
149 }
150 }
151 }
152 Err(e) => {
153 eprintln!("copyfilerange: {path}: {e}");
154 return ExitCode::FAILURE;
155 }
156 }
157 }
158
159 if range_specs.is_empty() {
160 eprintln!("copyfilerange: no ranges specified");
161 return ExitCode::FAILURE;
162 }
163
164 let mut last_src: u64 = 0;
165 let mut last_dst: u64 = 0;
166
167 for spec in &range_specs {
168 let range = match parse_range(spec, last_src, last_dst) {
169 Ok(r) => r,
170 Err(e) => {
171 eprintln!("copyfilerange: invalid range '{spec}': {e}");
172 return ExitCode::FAILURE;
173 }
174 };
175
176 let copy_len = if range.length == 0 {
177 src_size.saturating_sub(range.src_off)
178 } else {
179 range.length
180 };
181
182 let mut src_off = range.src_off as i64;
183 let mut dst_off = range.dst_off as i64;
184 let mut remaining = copy_len as usize;
185
186 while remaining > 0 {
187 let chunk = remaining.min(1 << 30); match do_copy_file_range(
189 src.as_raw_fd(),
190 &mut src_off,
191 dst.as_raw_fd(),
192 &mut dst_off,
193 chunk,
194 ) {
195 Ok(0) => break,
196 Ok(n) => remaining -= n,
197 Err(e) => {
198 eprintln!("copyfilerange: copy failed: {e}");
199 return ExitCode::FAILURE;
200 }
201 }
202 }
203
204 if args.verbose {
205 let copied = copy_len as usize - remaining;
206 eprintln!(
207 "{}:{} -> {}:{} ({copied} bytes)",
208 args.source, range.src_off, args.destination, range.dst_off
209 );
210 }
211
212 last_src = src_off as u64;
213 last_dst = dst_off as u64;
214 }
215
216 ExitCode::SUCCESS
217}