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