Skip to main content

btrfs_cli/
reflink.rs

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/// Toolbox for reflink operations: lightweight file copies that
8/// share data extents instead of copying bytes.
9#[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/// Lightweight file copy: data extents are shared between source
31/// and target instead of physically copied. Subsequent modifications
32/// are copy-on-write, so reading from the clone is fast and storage
33/// is only consumed once until the files diverge.
34///
35/// With no -r flags, the entire source file is cloned to the target
36/// (which is created or truncated as needed). With one or more
37/// -r RANGESPEC flags, only those byte ranges are cloned into the
38/// target at the specified destination offsets and the existing
39/// target contents outside those ranges are preserved.
40///
41/// RANGESPEC has three parts: SRCOFF:LENGTH:DESTOFF, where SRCOFF
42/// is the byte offset in the source file, LENGTH is the number of
43/// bytes (0 = up to end-of-source), and DESTOFF is the byte offset
44/// in the target file. All three values accept the size suffix
45/// k/m/g/t/p/e (case-insensitive). Offsets and length must be
46/// block-aligned (typically 4 KiB) except when the source range
47/// reaches end-of-file.
48#[derive(Parser, Debug)]
49#[allow(clippy::doc_markdown)]
50pub struct ReflinkCloneCommand {
51    /// Reflink only this range: SRCOFF:LENGTH:DESTOFF. May be
52    /// specified more than once; ranges are processed in order.
53    #[clap(short = 'r', long = "range", value_name = "RANGESPEC")]
54    ranges: Vec<String>,
55
56    /// Source file (read).
57    source: PathBuf,
58
59    /// Target file (created if missing, truncated when no -r is given).
60    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 only when cloning the whole file; explicit
82            // ranges target an existing layout and would lose data
83            // if we truncated underneath them.
84            .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            // length=0 → "to end of source file" per the kernel ABI.
97            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}