1#![allow(unsafe_code)]
11
12use std::fs::File;
13use std::io;
14use std::os::fd::AsRawFd;
15
16use anyhow::{Context, Result};
17
18pub fn clone_file(src: &str, dst: &str) -> Result<()> {
20 let sf = File::open(src).with_context(|| format!("failed to open {}", src))?;
21 let df = File::create(dst).with_context(|| format!("failed to create {}", dst))?;
22 if reflink(&sf, &df).is_ok() {
23 return Ok(());
24 }
25 if sparse_copy(&sf, &df).is_ok() {
26 return Ok(());
27 }
28 drop(df); std::fs::copy(src, dst).with_context(|| format!("failed to copy {} -> {}", src, dst))?;
30 Ok(())
31}
32
33pub fn reflink_file(src: &str, dst: &str) -> io::Result<()> {
37 let sf = File::open(src)?;
38 let df = File::create(dst)?;
39 let r = reflink(&sf, &df);
40 if r.is_err() {
41 drop(df);
42 let _ = std::fs::remove_file(dst);
43 }
44 r
45}
46
47fn reflink(src: &File, dst: &File) -> io::Result<()> {
48 let ret = unsafe { libc::ioctl(dst.as_raw_fd(), libc::FICLONE, src.as_raw_fd()) };
50 if ret == 0 {
51 Ok(())
52 } else {
53 Err(io::Error::last_os_error())
54 }
55}
56
57fn sparse_copy(src: &File, dst: &File) -> io::Result<()> {
58 let len = src.metadata()?.len();
59 dst.set_len(len)?;
60 let (sfd, dfd) = (src.as_raw_fd(), dst.as_raw_fd());
61 let mut use_rw = false;
64 let mut buf = Vec::new();
65 let mut off: i64 = 0;
66 while (off as u64) < len {
67 let data = unsafe { libc::lseek(sfd, off, libc::SEEK_DATA) };
68 if data < 0 {
69 let err = io::Error::last_os_error();
70 if err.raw_os_error() == Some(libc::ENXIO) {
71 break; }
73 return Err(err); }
75 let end = unsafe { libc::lseek(sfd, data, libc::SEEK_HOLE) };
76 if end < 0 {
77 return Err(io::Error::last_os_error());
78 }
79 let mut pos = data;
80 while pos < end {
81 if use_rw {
82 if buf.is_empty() {
83 buf = vec![0u8; 1 << 20];
84 }
85 let want = ((end - pos) as usize).min(buf.len());
86 let n =
87 unsafe { libc::pread(sfd, buf.as_mut_ptr() as *mut libc::c_void, want, pos) };
88 if n <= 0 {
89 return Err(io::Error::last_os_error());
90 }
91 let n = n as i64;
92 let mut written: i64 = 0;
93 while written < n {
94 let w = unsafe {
95 libc::pwrite(
96 dfd,
97 buf.as_ptr().add(written as usize) as *const libc::c_void,
98 (n - written) as usize,
99 pos + written,
100 )
101 };
102 if w <= 0 {
103 return Err(io::Error::last_os_error());
104 }
105 written += w as i64;
106 }
107 pos += n;
108 continue;
109 }
110 let (mut s_off, mut d_off) = (pos, pos);
111 let n = unsafe {
112 libc::copy_file_range(sfd, &mut s_off, dfd, &mut d_off, (end - pos) as usize, 0)
113 };
114 if n < 0 {
115 let err = io::Error::last_os_error();
116 match err.raw_os_error() {
117 Some(libc::EXDEV) | Some(libc::EINVAL) | Some(libc::ENOSYS) => {
118 use_rw = true;
119 continue;
120 }
121 _ => return Err(err),
122 }
123 }
124 if n == 0 {
125 return Err(io::Error::new(
126 io::ErrorKind::UnexpectedEof,
127 "copy_file_range returned 0 before extent end",
128 ));
129 }
130 pos += n as i64;
131 }
132 off = end;
133 }
134 Ok(())
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use std::io::{Seek, SeekFrom, Write};
141 use std::os::unix::fs::MetadataExt;
142
143 const HOLE: u64 = 100 * 1024 * 1024;
144
145 #[test]
146 fn clone_preserves_content_and_holes() {
147 let dir = std::env::temp_dir().join(format!("vm-clone-test-{}", std::process::id()));
148 std::fs::create_dir_all(&dir).unwrap();
149 let src = dir.join("src.img");
150 let dst = dir.join("dst.img");
151
152 let mut f = File::create(&src).unwrap();
153 f.write_all(b"head data").unwrap();
154 f.seek(SeekFrom::Start(HOLE)).unwrap();
155 f.write_all(b"tail data").unwrap();
156 f.set_len(HOLE + 4096).unwrap();
157 drop(f);
158
159 clone_file(src.to_str().unwrap(), dst.to_str().unwrap()).unwrap();
163
164 assert_eq!(std::fs::read(&src).unwrap(), std::fs::read(&dst).unwrap());
165 let meta = std::fs::metadata(&dst).unwrap();
166 assert_eq!(meta.len(), HOLE + 4096);
167 let allocated = meta.blocks() * 512;
168 assert!(
169 allocated < 1024 * 1024,
170 "destination materialized holes: {} bytes allocated for {} apparent",
171 allocated,
172 meta.len()
173 );
174
175 std::fs::remove_dir_all(&dir).unwrap();
176 }
177
178 #[test]
179 fn clone_across_filesystems_stays_sparse() {
180 if !std::path::Path::new("/dev/shm").is_dir() {
183 return;
184 }
185 let dir = std::env::temp_dir().join(format!("vm-clone-x-{}", std::process::id()));
186 std::fs::create_dir_all(&dir).unwrap();
187 let src = dir.join("src.img");
188 let dst = format!("/dev/shm/vm-clone-x-{}.img", std::process::id());
189
190 let mut f = File::create(&src).unwrap();
191 f.write_all(b"head data").unwrap();
192 f.seek(SeekFrom::Start(HOLE)).unwrap();
193 f.write_all(b"tail data").unwrap();
194 f.set_len(HOLE + 4096).unwrap();
195 drop(f);
196
197 clone_file(src.to_str().unwrap(), &dst).unwrap();
198
199 assert_eq!(std::fs::read(&src).unwrap(), std::fs::read(&dst).unwrap());
200 let meta = std::fs::metadata(&dst).unwrap();
201 assert_eq!(meta.len(), HOLE + 4096);
202 let allocated = meta.blocks() * 512;
203 assert!(
204 allocated < 1024 * 1024,
205 "destination materialized holes: {} bytes allocated",
206 allocated
207 );
208
209 std::fs::remove_file(&dst).unwrap();
210 std::fs::remove_dir_all(&dir).unwrap();
211 }
212}