Skip to main content

linuxutils_misc/
copyfilerange.rs

1use 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/// Copy byte ranges between files using copy_file_range(2).
14///
15/// Performs kernel-space file copies that can create reflinked files
16/// when the filesystem supports it. Range format: `src_off:dst_off:length`.
17#[derive(Parser)]
18#[command(name = "copyfilerange", about = "Copy byte ranges between files")]
19pub struct Args {
20    /// Read range specs from file (one per line)
21    #[arg(short = 'r', long = "ranges")]
22    ranges_file: Option<String>,
23
24    /// Print each range as it's copied
25    #[arg(short = 'v', long)]
26    verbose: bool,
27
28    /// Source file
29    source: String,
30
31    /// Destination file
32    destination: String,
33
34    /// Range specs: source_offset:destination_offset:length
35    #[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    // Support simple K/M/G suffixes
83    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    // Collect ranges
140    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); // 1 GiB max per call
188            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}