1use crate::{CommandGroup, RunContext, Runnable, util::parse_size_with_suffix};
2use anyhow::{Context, Result};
3use btrfs_uapi::reflink;
4use clap::Parser;
5use std::{fs::OpenOptions, os::fd::AsFd, path::PathBuf};
6
7#[derive(Parser, Debug)]
10#[allow(clippy::doc_markdown)]
11#[clap(arg_required_else_help = true)]
12pub struct ReflinkCommand {
13 #[clap(subcommand)]
14 pub subcommand: ReflinkSubcommand,
15}
16
17impl CommandGroup for ReflinkCommand {
18 fn leaf(&self) -> &dyn Runnable {
19 match &self.subcommand {
20 ReflinkSubcommand::Clone(cmd) => cmd,
21 }
22 }
23}
24
25#[derive(Parser, Debug)]
26pub enum ReflinkSubcommand {
27 Clone(ReflinkCloneCommand),
28}
29
30#[derive(Parser, Debug)]
49#[allow(clippy::doc_markdown)]
50pub struct ReflinkCloneCommand {
51 #[clap(short = 'r', long = "range", value_name = "RANGESPEC")]
54 ranges: Vec<String>,
55
56 source: PathBuf,
58
59 target: PathBuf,
61}
62
63impl Runnable for ReflinkCloneCommand {
64 fn run(&self, ctx: &RunContext) -> Result<()> {
65 let ranges: Vec<RangeSpec> = self
66 .ranges
67 .iter()
68 .map(|s| RangeSpec::parse(s))
69 .collect::<Result<_>>()?;
70
71 let source = OpenOptions::new()
72 .read(true)
73 .open(&self.source)
74 .with_context(|| {
75 format!("opening source {}", self.source.display())
76 })?;
77 let target = OpenOptions::new()
78 .read(true)
79 .write(true)
80 .create(true)
81 .truncate(ranges.is_empty())
85 .open(&self.target)
86 .with_context(|| {
87 format!("opening target {}", self.target.display())
88 })?;
89
90 if !ctx.quiet {
91 println!("Source: {}", self.source.display());
92 println!("Target: {}", self.target.display());
93 }
94
95 if ranges.is_empty() {
96 reflink::clone_range(source.as_fd(), 0, 0, target.as_fd(), 0)
98 .context("cloning entire source file")?;
99 } else {
100 for r in &ranges {
101 if !ctx.quiet {
102 println!(
103 "Range: {}:{}:{}",
104 r.src_offset, r.length, r.dest_offset
105 );
106 }
107 reflink::clone_range(
108 source.as_fd(),
109 r.src_offset,
110 r.length,
111 target.as_fd(),
112 r.dest_offset,
113 )
114 .with_context(|| {
115 format!(
116 "cloning {}:{}:{}",
117 r.src_offset, r.length, r.dest_offset
118 )
119 })?;
120 }
121 }
122 Ok(())
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127struct RangeSpec {
128 src_offset: u64,
129 length: u64,
130 dest_offset: u64,
131}
132
133impl RangeSpec {
134 fn parse(s: &str) -> Result<Self> {
135 let parts: Vec<&str> = s.split(':').collect();
136 if parts.len() != 3 {
137 anyhow::bail!(
138 "range spec {s:?} must have three colon-separated parts: SRCOFF:LENGTH:DESTOFF"
139 );
140 }
141 let src_offset = parse_size_with_suffix(parts[0])
142 .with_context(|| format!("parsing SRCOFF {:?}", parts[0]))?;
143 let length = parse_size_with_suffix(parts[1])
144 .with_context(|| format!("parsing LENGTH {:?}", parts[1]))?;
145 let dest_offset = parse_size_with_suffix(parts[2])
146 .with_context(|| format!("parsing DESTOFF {:?}", parts[2]))?;
147 Ok(Self {
148 src_offset,
149 length,
150 dest_offset,
151 })
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn range_spec_parses_decimal_triple() {
161 let r = RangeSpec::parse("0:4096:8192").unwrap();
162 assert_eq!(
163 r,
164 RangeSpec {
165 src_offset: 0,
166 length: 4096,
167 dest_offset: 8192,
168 }
169 );
170 }
171
172 #[test]
173 fn range_spec_accepts_size_suffixes() {
174 let r = RangeSpec::parse("1k:4M:2G").unwrap();
175 assert_eq!(
176 r,
177 RangeSpec {
178 src_offset: 1024,
179 length: 4 * 1024 * 1024,
180 dest_offset: 2 * 1024 * 1024 * 1024,
181 }
182 );
183 }
184
185 #[test]
186 fn range_spec_length_zero_is_eof_sentinel() {
187 let r = RangeSpec::parse("0:0:0").unwrap();
188 assert_eq!(r.length, 0);
189 }
190
191 #[test]
192 fn range_spec_rejects_wrong_arity() {
193 assert!(RangeSpec::parse("0:4096").is_err());
194 assert!(RangeSpec::parse("0:4096:8192:extra").is_err());
195 assert!(RangeSpec::parse("").is_err());
196 }
197
198 #[test]
199 fn range_spec_rejects_unknown_suffix() {
200 assert!(RangeSpec::parse("1x:4096:0").is_err());
201 }
202}