Skip to main content

btrfs_uapi/
send_receive.rs

1//! # Send and receive: generating and applying btrfs send streams
2//!
3//! The send side wraps `BTRFS_IOC_SEND`, which produces a binary stream
4//! representing the contents of a read-only subvolume (or the delta between a
5//! parent and child snapshot).
6//!
7//! The receive side wraps the ioctls used when applying a send stream:
8//! marking a subvolume as received (`SET_RECEIVED_SUBVOL`), cloning extents
9//! between files (`CLONE_RANGE`), writing pre-compressed data
10//! (`ENCODED_WRITE`), and searching the UUID tree to locate subvolumes by
11//! their UUID or received UUID.
12
13use crate::{
14    raw::{
15        self, btrfs_ioc_clone_range, btrfs_ioc_encoded_read,
16        btrfs_ioc_encoded_write, btrfs_ioc_send, btrfs_ioc_set_received_subvol,
17        btrfs_ioctl_clone_range_args, btrfs_ioctl_encoded_io_args,
18        btrfs_ioctl_received_subvol_args, btrfs_ioctl_send_args,
19    },
20    tree_search::{SearchKey, tree_search},
21};
22use bitflags::bitflags;
23use std::os::fd::{AsRawFd, BorrowedFd, RawFd};
24use uuid::Uuid;
25
26bitflags! {
27    /// Flags for the send ioctl.
28    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29    pub struct SendFlags: u64 {
30        /// Do not include file data in the stream (metadata only).
31        const NO_FILE_DATA = raw::BTRFS_SEND_FLAG_NO_FILE_DATA as u64;
32        /// Omit the stream header (for multi-subvolume sends).
33        const OMIT_STREAM_HEADER = raw::BTRFS_SEND_FLAG_OMIT_STREAM_HEADER as u64;
34        /// Omit the end-cmd marker (for multi-subvolume sends).
35        const OMIT_END_CMD = raw::BTRFS_SEND_FLAG_OMIT_END_CMD as u64;
36        /// Request a specific protocol version (set the version field).
37        const VERSION = raw::BTRFS_SEND_FLAG_VERSION as u64;
38        /// Send compressed data directly without decompressing.
39        const COMPRESSED = raw::BTRFS_SEND_FLAG_COMPRESSED as u64;
40    }
41}
42
43/// Invoke `BTRFS_IOC_SEND` on the given subvolume.
44///
45/// The kernel writes the send stream to `send_fd` (the write end of a pipe).
46/// The caller is responsible for reading from the read end of the pipe,
47/// typically in a separate thread.
48///
49/// `clone_sources` is a list of root IDs that the kernel may reference for
50/// clone operations in the stream. `parent_root` is the root ID of the parent
51/// snapshot for incremental sends, or `0` for a full send.
52pub fn send(
53    subvol_fd: BorrowedFd<'_>,
54    send_fd: RawFd,
55    parent_root: u64,
56    clone_sources: &mut [u64],
57    flags: SendFlags,
58    version: u32,
59) -> nix::Result<()> {
60    let mut args: btrfs_ioctl_send_args = unsafe { std::mem::zeroed() };
61    args.send_fd = send_fd as i64;
62    args.parent_root = parent_root;
63    args.clone_sources_count = clone_sources.len() as u64;
64    args.clone_sources = if clone_sources.is_empty() {
65        std::ptr::null_mut()
66    } else {
67        clone_sources.as_mut_ptr()
68    };
69    args.flags = flags.bits();
70    args.version = version;
71
72    // SAFETY: args is fully initialized, clone_sources points to valid memory
73    // that outlives the ioctl call, and subvol_fd is a valid borrowed fd.
74    unsafe {
75        btrfs_ioc_send(subvol_fd.as_raw_fd(), &args)?;
76    }
77
78    Ok(())
79}
80
81/// Result of searching the UUID tree for a subvolume.
82#[derive(Debug, Clone)]
83pub struct SubvolumeSearchResult {
84    /// The root ID (subvolume ID) found in the UUID tree.
85    pub root_id: u64,
86}
87
88/// Mark a subvolume as received by setting its received UUID and stransid.
89///
90/// After applying a send stream, this ioctl records the sender's UUID and
91/// transaction ID so that future incremental sends can use this subvolume as
92/// a reference. Returns the receive transaction ID assigned by the kernel.
93pub fn received_subvol_set(
94    fd: BorrowedFd<'_>,
95    uuid: &Uuid,
96    stransid: u64,
97) -> nix::Result<u64> {
98    let mut args: btrfs_ioctl_received_subvol_args =
99        unsafe { std::mem::zeroed() };
100
101    let uuid_bytes = uuid.as_bytes();
102    // uuid field is [c_char; 16]; copy byte-by-byte.
103    for (i, &b) in uuid_bytes.iter().enumerate() {
104        args.uuid[i] = b as std::os::raw::c_char;
105    }
106    args.stransid = stransid;
107
108    // SAFETY: args is fully initialized, fd is a valid borrowed fd to a subvolume.
109    unsafe {
110        btrfs_ioc_set_received_subvol(fd.as_raw_fd(), &mut args)?;
111    }
112
113    Ok(args.rtransid)
114}
115
116/// Clone a range of bytes from one file to another using `BTRFS_IOC_CLONE_RANGE`.
117///
118/// Both files must be on the same btrfs filesystem. The destination file
119/// descriptor `dest_fd` is the ioctl target.
120///
121/// Errors: EXDEV if source and destination are on different filesystems.
122/// EINVAL if the range is not sector-aligned or extends beyond EOF.
123/// ETXTBSY if the destination file is a swap file.
124pub fn clone_range(
125    dest_fd: BorrowedFd<'_>,
126    src_fd: BorrowedFd<'_>,
127    src_offset: u64,
128    src_length: u64,
129    dest_offset: u64,
130) -> nix::Result<()> {
131    let args = btrfs_ioctl_clone_range_args {
132        src_fd: src_fd.as_raw_fd() as i64,
133        src_offset,
134        src_length,
135        dest_offset,
136    };
137
138    // SAFETY: args is fully initialized, both fds are valid.
139    unsafe {
140        btrfs_ioc_clone_range(dest_fd.as_raw_fd(), &args)?;
141    }
142
143    Ok(())
144}
145
146/// Write pre-compressed data to a file using `BTRFS_IOC_ENCODED_WRITE`.
147///
148/// This passes compressed data directly to the filesystem without
149/// decompression, which is more efficient than decompressing and writing.
150///
151/// Errors: ENOTTY on kernels that do not support encoded writes (pre-5.18).
152/// EINVAL if the compression type, alignment, or lengths are not accepted.
153/// ENOSPC if the filesystem has no room for the encoded extent.  Callers
154/// should fall back to manual decompression + pwrite for any of these.
155#[allow(clippy::too_many_arguments)]
156pub fn encoded_write(
157    fd: BorrowedFd<'_>,
158    data: &[u8],
159    offset: u64,
160    unencoded_file_len: u64,
161    unencoded_len: u64,
162    unencoded_offset: u64,
163    compression: u32,
164    encryption: u32,
165) -> nix::Result<()> {
166    let iov = nix::libc::iovec {
167        iov_base: data.as_ptr() as *mut _,
168        iov_len: data.len(),
169    };
170
171    let mut args: btrfs_ioctl_encoded_io_args = unsafe { std::mem::zeroed() };
172    args.iov = &iov as *const _ as *mut _;
173    args.iovcnt = 1;
174    args.offset = offset as i64;
175    args.len = unencoded_file_len;
176    args.unencoded_len = unencoded_len;
177    args.unencoded_offset = unencoded_offset;
178    args.compression = compression;
179    args.encryption = encryption;
180
181    // SAFETY: args.iov points to a stack-allocated iovec whose iov_base
182    // references `data` which outlives this call. The ioctl reads from the
183    // iov buffers and writes encoded data to the file.
184    unsafe {
185        btrfs_ioc_encoded_write(fd.as_raw_fd(), &args)?;
186    }
187
188    Ok(())
189}
190
191/// Metadata returned by [`encoded_read`] describing how the data is encoded.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub struct EncodedReadResult {
194    /// File offset of the extent.
195    pub offset: u64,
196    /// Length of the extent in the file (after decompression).
197    pub unencoded_file_len: u64,
198    /// Unencoded (decompressed) length of the data.
199    pub unencoded_len: u64,
200    /// Offset within the unencoded data where the file content starts.
201    pub unencoded_offset: u64,
202    /// Compression type (0 = none, 1 = zlib, 2 = lzo, 3 = zstd).
203    pub compression: u32,
204    /// Encryption type (currently always 0).
205    pub encryption: u32,
206    /// Number of bytes actually read into the buffer.
207    pub bytes_read: usize,
208}
209
210/// Read compressed (encoded) data from a file using `BTRFS_IOC_ENCODED_READ`.
211///
212/// This reads the raw compressed extent data without decompressing it,
213/// which is the counterpart to [`encoded_write`]. The caller provides a
214/// buffer to receive the data; the returned [`EncodedReadResult`] describes
215/// the encoding and how many bytes were read.
216///
217/// Errors: ENOTTY on kernels that do not support encoded reads (pre-5.18).
218/// EINVAL if the offset or length are not accepted.
219pub fn encoded_read(
220    fd: BorrowedFd<'_>,
221    buf: &mut [u8],
222    offset: u64,
223    len: u64,
224) -> nix::Result<EncodedReadResult> {
225    let iov = nix::libc::iovec {
226        iov_base: buf.as_mut_ptr() as *mut _,
227        iov_len: buf.len(),
228    };
229
230    let mut args: btrfs_ioctl_encoded_io_args = unsafe { std::mem::zeroed() };
231    args.iov = &iov as *const _ as *mut _;
232    args.iovcnt = 1;
233    args.offset = offset as i64;
234    args.len = len;
235
236    // SAFETY: args.iov points to a stack-allocated iovec whose iov_base
237    // references `buf` which outlives this call. The ioctl writes encoded
238    // data into the iov buffers.
239    let ret = unsafe { btrfs_ioc_encoded_read(fd.as_raw_fd(), &mut args) }?;
240
241    Ok(EncodedReadResult {
242        offset: args.offset as u64,
243        unencoded_file_len: args.len,
244        unencoded_len: args.unencoded_len,
245        unencoded_offset: args.unencoded_offset,
246        compression: args.compression,
247        encryption: args.encryption,
248        bytes_read: ret as usize,
249    })
250}
251
252/// Search the UUID tree for a subvolume by its UUID.
253///
254/// Returns the root ID of the matching subvolume, or `Errno::ENOENT` if not
255/// found.
256pub fn subvolume_search_by_uuid(
257    fd: BorrowedFd<'_>,
258    uuid: &Uuid,
259) -> nix::Result<u64> {
260    search_uuid_tree(fd, uuid, raw::BTRFS_UUID_KEY_SUBVOL)
261}
262
263/// Search the UUID tree for a subvolume by its received UUID.
264///
265/// Returns the root ID of the matching subvolume, or `Errno::ENOENT` if not
266/// found.
267pub fn subvolume_search_by_received_uuid(
268    fd: BorrowedFd<'_>,
269    uuid: &Uuid,
270) -> nix::Result<u64> {
271    search_uuid_tree(fd, uuid, raw::BTRFS_UUID_KEY_RECEIVED_SUBVOL)
272}
273
274/// Internal: search the UUID tree for a given key type.
275///
276/// The UUID tree encodes UUIDs as a compound key: objectid = LE u64 from
277/// bytes [0..8], offset = LE u64 from bytes [8..16]. The item type selects
278/// whether we are looking for regular UUIDs or received UUIDs. The data
279/// payload is a single LE u64 root ID.
280fn search_uuid_tree(
281    fd: BorrowedFd<'_>,
282    uuid: &Uuid,
283    item_type: u32,
284) -> nix::Result<u64> {
285    let bytes = uuid.as_bytes();
286    let objectid = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
287    let offset = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
288
289    let mut key =
290        SearchKey::for_type(raw::BTRFS_UUID_TREE_OBJECTID as u64, item_type);
291    key.min_objectid = objectid;
292    key.max_objectid = objectid;
293    key.min_offset = offset;
294    key.max_offset = offset;
295
296    let mut result: Option<u64> = None;
297
298    tree_search(fd, key, |_hdr, data| {
299        if data.len() >= 8 {
300            result = Some(u64::from_le_bytes(data[0..8].try_into().unwrap()));
301        }
302        Ok(())
303    })?;
304
305    result.ok_or(nix::errno::Errno::ENOENT)
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn send_flags_no_file_data() {
314        let flags = SendFlags::NO_FILE_DATA;
315        assert!(flags.contains(SendFlags::NO_FILE_DATA));
316        assert!(!flags.contains(SendFlags::COMPRESSED));
317    }
318
319    #[test]
320    fn send_flags_combine() {
321        let flags = SendFlags::NO_FILE_DATA | SendFlags::COMPRESSED;
322        assert!(flags.contains(SendFlags::NO_FILE_DATA));
323        assert!(flags.contains(SendFlags::COMPRESSED));
324        assert!(!flags.contains(SendFlags::VERSION));
325    }
326
327    #[test]
328    fn send_flags_empty() {
329        let flags = SendFlags::empty();
330        assert!(flags.is_empty());
331        assert_eq!(flags.bits(), 0);
332    }
333
334    #[test]
335    fn send_flags_debug() {
336        let flags = SendFlags::OMIT_STREAM_HEADER | SendFlags::OMIT_END_CMD;
337        let s = format!("{flags:?}");
338        assert!(s.contains("OMIT_STREAM_HEADER"), "debug: {s}");
339        assert!(s.contains("OMIT_END_CMD"), "debug: {s}");
340    }
341
342    #[test]
343    fn encoded_read_result_equality() {
344        let a = EncodedReadResult {
345            offset: 0,
346            unencoded_file_len: 4096,
347            unencoded_len: 4096,
348            unencoded_offset: 0,
349            compression: 0,
350            encryption: 0,
351            bytes_read: 4096,
352        };
353        let b = a;
354        assert_eq!(a, b);
355    }
356
357    #[test]
358    fn encoded_read_result_debug() {
359        let r = EncodedReadResult {
360            offset: 0,
361            unencoded_file_len: 4096,
362            unencoded_len: 8192,
363            unencoded_offset: 0,
364            compression: 3,
365            encryption: 0,
366            bytes_read: 1024,
367        };
368        let s = format!("{r:?}");
369        assert!(s.contains("compression: 3"), "debug: {s}");
370        assert!(s.contains("bytes_read: 1024"), "debug: {s}");
371    }
372
373    #[test]
374    fn subvolume_search_result_debug() {
375        let r = SubvolumeSearchResult { root_id: 256 };
376        let s = format!("{r:?}");
377        assert!(s.contains("256"), "debug: {s}");
378    }
379}