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_write, btrfs_ioc_send,
16        btrfs_ioc_set_received_subvol, btrfs_ioctl_clone_range_args,
17        btrfs_ioctl_encoded_io_args, btrfs_ioctl_received_subvol_args,
18        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.
121pub fn clone_range(
122    dest_fd: BorrowedFd<'_>,
123    src_fd: BorrowedFd<'_>,
124    src_offset: u64,
125    src_length: u64,
126    dest_offset: u64,
127) -> nix::Result<()> {
128    let args = btrfs_ioctl_clone_range_args {
129        src_fd: src_fd.as_raw_fd() as i64,
130        src_offset,
131        src_length,
132        dest_offset,
133    };
134
135    // SAFETY: args is fully initialized, both fds are valid.
136    unsafe {
137        btrfs_ioc_clone_range(dest_fd.as_raw_fd() as c_int, &args)?;
138    }
139
140    Ok(())
141}
142
143/// Write pre-compressed data to a file using `BTRFS_IOC_ENCODED_WRITE`.
144///
145/// This passes compressed data directly to the filesystem without
146/// decompression, which is more efficient than decompressing and writing.
147/// The kernel may reject the call with `ENOTTY` (old kernel), `EINVAL`
148/// (unsupported parameters), or `ENOSPC`; callers should fall back to
149/// manual decompression + pwrite in those cases.
150#[allow(clippy::too_many_arguments)]
151pub fn encoded_write(
152    fd: BorrowedFd<'_>,
153    data: &[u8],
154    offset: u64,
155    unencoded_file_len: u64,
156    unencoded_len: u64,
157    unencoded_offset: u64,
158    compression: u32,
159    encryption: u32,
160) -> nix::Result<()> {
161    let iov = nix::libc::iovec {
162        iov_base: data.as_ptr() as *mut _,
163        iov_len: data.len(),
164    };
165
166    let mut args: btrfs_ioctl_encoded_io_args = unsafe { std::mem::zeroed() };
167    args.iov = &iov as *const _ as *mut _;
168    args.iovcnt = 1;
169    args.offset = offset as i64;
170    args.len = unencoded_file_len;
171    args.unencoded_len = unencoded_len;
172    args.unencoded_offset = unencoded_offset;
173    args.compression = compression;
174    args.encryption = encryption;
175
176    // SAFETY: args.iov points to a stack-allocated iovec whose iov_base
177    // references `data` which outlives this call. The ioctl reads from the
178    // iov buffers and writes encoded data to the file.
179    unsafe {
180        btrfs_ioc_encoded_write(fd.as_raw_fd() as c_int, &args)?;
181    }
182
183    Ok(())
184}
185
186/// Search the UUID tree for a subvolume by its UUID.
187///
188/// Returns the root ID of the matching subvolume, or `Errno::ENOENT` if not
189/// found.
190pub fn subvolume_search_by_uuid(
191    fd: BorrowedFd<'_>,
192    uuid: &Uuid,
193) -> nix::Result<u64> {
194    search_uuid_tree(fd, uuid, raw::BTRFS_UUID_KEY_SUBVOL as u32)
195}
196
197/// Search the UUID tree for a subvolume by its received UUID.
198///
199/// Returns the root ID of the matching subvolume, or `Errno::ENOENT` if not
200/// found.
201pub fn subvolume_search_by_received_uuid(
202    fd: BorrowedFd<'_>,
203    uuid: &Uuid,
204) -> nix::Result<u64> {
205    search_uuid_tree(fd, uuid, raw::BTRFS_UUID_KEY_RECEIVED_SUBVOL as u32)
206}
207
208/// Internal: search the UUID tree for a given key type.
209///
210/// The UUID tree encodes UUIDs as a compound key: objectid = LE u64 from
211/// bytes [0..8], offset = LE u64 from bytes [8..16]. The item type selects
212/// whether we are looking for regular UUIDs or received UUIDs. The data
213/// payload is a single LE u64 root ID.
214fn search_uuid_tree(
215    fd: BorrowedFd<'_>,
216    uuid: &Uuid,
217    item_type: u32,
218) -> nix::Result<u64> {
219    let bytes = uuid.as_bytes();
220    let objectid = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
221    let offset = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
222
223    let mut key =
224        SearchKey::for_type(raw::BTRFS_UUID_TREE_OBJECTID as u64, item_type);
225    key.min_objectid = objectid;
226    key.max_objectid = objectid;
227    key.min_offset = offset;
228    key.max_offset = offset;
229
230    let mut result: Option<u64> = None;
231
232    tree_search(fd, key, |_hdr, data| {
233        if data.len() >= 8 {
234            result = Some(u64::from_le_bytes(data[0..8].try_into().unwrap()));
235        }
236        Ok(())
237    })?;
238
239    result.ok_or(nix::errno::Errno::ENOENT)
240}