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