Skip to main content

btrfs_uapi/
filesystem.rs

1//! # Filesystem-level operations: metadata, sync, label, and resize
2//!
3//! Operations that apply to a btrfs filesystem as a whole rather than to any
4//! individual device or subvolume: querying filesystem info (UUID, device count,
5//! node size), syncing pending writes to disk, reading/writing the
6//! human-readable label, and resizing a device within the filesystem.
7
8use crate::raw::{
9    BTRFS_FS_INFO_FLAG_GENERATION, BTRFS_LABEL_SIZE, btrfs_ioc_fs_info,
10    btrfs_ioc_get_fslabel, btrfs_ioc_resize, btrfs_ioc_set_fslabel,
11    btrfs_ioc_start_sync, btrfs_ioc_sync, btrfs_ioc_wait_sync,
12    btrfs_ioctl_fs_info_args, btrfs_ioctl_vol_args,
13};
14use nix::libc::c_char;
15use std::{
16    ffi::{CStr, CString},
17    mem,
18    os::{fd::AsRawFd, unix::io::BorrowedFd},
19};
20use uuid::Uuid;
21
22/// Information about a mounted btrfs filesystem, as returned by
23/// `BTRFS_IOC_FS_INFO`.
24#[derive(Debug, Clone)]
25pub struct FilesystemInfo {
26    /// Filesystem UUID.
27    pub uuid: Uuid,
28    /// Number of devices in the filesystem.
29    pub num_devices: u64,
30    /// Highest device ID in the filesystem.
31    pub max_id: u64,
32    /// B-tree node size in bytes.
33    pub nodesize: u32,
34    /// Sector size in bytes.
35    pub sectorsize: u32,
36    /// Generation number of the filesystem.
37    pub generation: u64,
38}
39
40/// Query information about the btrfs filesystem referred to by `fd`.
41pub fn filesystem_info(fd: BorrowedFd) -> nix::Result<FilesystemInfo> {
42    let mut raw: btrfs_ioctl_fs_info_args = unsafe { mem::zeroed() };
43    raw.flags = u64::from(BTRFS_FS_INFO_FLAG_GENERATION);
44    unsafe { btrfs_ioc_fs_info(fd.as_raw_fd(), &raw mut raw) }?;
45
46    Ok(FilesystemInfo {
47        uuid: Uuid::from_bytes(raw.fsid),
48        num_devices: raw.num_devices,
49        max_id: raw.max_id,
50        nodesize: raw.nodesize,
51        sectorsize: raw.sectorsize,
52        generation: raw.generation,
53    })
54}
55
56/// Force a sync on the btrfs filesystem referred to by `fd` and wait for it
57/// to complete.
58pub fn sync(fd: BorrowedFd) -> nix::Result<()> {
59    unsafe { btrfs_ioc_sync(fd.as_raw_fd()) }?;
60    Ok(())
61}
62
63/// Asynchronously start a sync on the btrfs filesystem referred to by `fd`.
64///
65/// Returns the transaction ID of the initiated sync, which can be passed to
66/// `wait_sync` to block until it completes.
67pub fn start_sync(fd: BorrowedFd) -> nix::Result<u64> {
68    let mut transid: u64 = 0;
69    unsafe { btrfs_ioc_start_sync(fd.as_raw_fd(), &raw mut transid) }?;
70    Ok(transid)
71}
72
73/// Wait for a previously started transaction to complete.
74///
75/// `transid` is the transaction ID returned by `start_sync`. Pass zero to
76/// wait for the current transaction.
77pub fn wait_sync(fd: BorrowedFd, transid: u64) -> nix::Result<()> {
78    unsafe { btrfs_ioc_wait_sync(fd.as_raw_fd(), &raw const transid) }?;
79    Ok(())
80}
81
82/// Read the label of the btrfs filesystem referred to by `fd`.
83///
84/// Returns the label as a [`CString`]. An empty string means no label is set.
85pub fn label_get(fd: BorrowedFd) -> nix::Result<CString> {
86    let mut buf = [0i8; BTRFS_LABEL_SIZE as usize];
87    unsafe { btrfs_ioc_get_fslabel(fd.as_raw_fd(), &raw mut buf) }?;
88    let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
89    // CStr::to_owned() copies the bytes into a freshly allocated CString,
90    // which is safe to return after `buf` goes out of scope.
91    Ok(cstr.to_owned())
92}
93
94/// Set the label of the btrfs filesystem referred to by `fd`.
95///
96/// The label must be shorter than 256 bytes (not counting the null terminator).
97/// Further validation (e.g. rejecting labels that contain `/`) is left to the
98/// kernel.
99///
100/// Errors: EINVAL if the label is 256 bytes or longer (checked before the
101/// ioctl).  EPERM without `CAP_SYS_ADMIN`.
102pub fn label_set(fd: BorrowedFd, label: &CStr) -> nix::Result<()> {
103    let bytes = label.to_bytes();
104    if bytes.len() >= BTRFS_LABEL_SIZE as usize {
105        return Err(nix::errno::Errno::EINVAL);
106    }
107    let mut buf = [0i8; BTRFS_LABEL_SIZE as usize];
108    for (i, &b) in bytes.iter().enumerate() {
109        buf[i] = b as c_char;
110    }
111    unsafe { btrfs_ioc_set_fslabel(fd.as_raw_fd(), &raw const buf) }?;
112    Ok(())
113}
114
115/// The target size for a resize operation.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum ResizeAmount {
118    /// Cancel an in-progress resize.
119    Cancel,
120    /// Grow the device to its maximum available size.
121    Max,
122    /// Set the device to exactly this many bytes.
123    Set(u64),
124    /// Add this many bytes to the current device size.
125    Add(u64),
126    /// Subtract this many bytes from the current device size.
127    Sub(u64),
128}
129
130impl std::fmt::Display for ResizeAmount {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            Self::Cancel => f.write_str("cancel"),
134            Self::Max => f.write_str("max"),
135            Self::Set(n) => write!(f, "{n}"),
136            Self::Add(n) => write!(f, "+{n}"),
137            Self::Sub(n) => write!(f, "-{n}"),
138        }
139    }
140}
141
142/// Arguments for a resize operation.
143///
144/// `devid` selects which device within the filesystem to resize. When `None`,
145/// the kernel defaults to device ID 1 (the first device).
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct ResizeArgs {
148    pub devid: Option<u64>,
149    pub amount: ResizeAmount,
150}
151
152impl ResizeArgs {
153    #[must_use]
154    pub fn new(amount: ResizeAmount) -> Self {
155        Self {
156            devid: None,
157            amount,
158        }
159    }
160
161    #[must_use]
162    pub fn with_devid(mut self, devid: u64) -> Self {
163        self.devid = Some(devid);
164        self
165    }
166
167    /// Format into the string that `BTRFS_IOC_RESIZE` expects in
168    /// `btrfs_ioctl_vol_args.name`: `[<devid>:]<amount>`.
169    fn format_name(&self) -> String {
170        let amount = self.amount.to_string();
171        match self.devid {
172            Some(devid) => format!("{devid}:{amount}"),
173            None => amount,
174        }
175    }
176}
177
178/// Resize a device within the btrfs filesystem referred to by `fd`.
179///
180/// `fd` must be an open file descriptor to a directory on the mounted
181/// filesystem. Use [`ResizeArgs`] to specify the target device and amount.
182pub fn resize(fd: BorrowedFd, args: ResizeArgs) -> nix::Result<()> {
183    let name = args.format_name();
184    let name_bytes = name.as_bytes();
185
186    // BTRFS_PATH_NAME_MAX is 4087; the name field is [c_char; 4088].
187    // A well-formed resize string (devid + colon + u64 digits) is at most
188    // ~23 characters, so this can only fail if the caller constructs a
189    // pathological devid.
190    if name_bytes.len() >= 4088 {
191        return Err(nix::errno::Errno::EINVAL);
192    }
193
194    let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
195    for (i, &b) in name_bytes.iter().enumerate() {
196        raw.name[i] = b as c_char;
197    }
198
199    unsafe { btrfs_ioc_resize(fd.as_raw_fd(), &raw const raw) }?;
200    Ok(())
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    // --- ResizeAmount::to_string ---
208
209    #[test]
210    fn resize_amount_cancel() {
211        assert_eq!(ResizeAmount::Cancel.to_string(), "cancel");
212    }
213
214    #[test]
215    fn resize_amount_max() {
216        assert_eq!(ResizeAmount::Max.to_string(), "max");
217    }
218
219    #[test]
220    fn resize_amount_set() {
221        assert_eq!(ResizeAmount::Set(1073741824).to_string(), "1073741824");
222    }
223
224    #[test]
225    fn resize_amount_add() {
226        assert_eq!(ResizeAmount::Add(512000000).to_string(), "+512000000");
227    }
228
229    #[test]
230    fn resize_amount_sub() {
231        assert_eq!(ResizeAmount::Sub(256000000).to_string(), "-256000000");
232    }
233
234    // --- ResizeArgs builder + format_name ---
235
236    #[test]
237    fn resize_args_no_devid() {
238        let args = ResizeArgs::new(ResizeAmount::Max);
239        assert!(args.devid.is_none());
240        assert_eq!(args.format_name(), "max");
241    }
242
243    #[test]
244    fn resize_args_with_devid() {
245        let args = ResizeArgs::new(ResizeAmount::Add(1024)).with_devid(2);
246        assert_eq!(args.devid, Some(2));
247        assert_eq!(args.format_name(), "2:+1024");
248    }
249
250    #[test]
251    fn resize_args_set_with_devid() {
252        let args = ResizeArgs::new(ResizeAmount::Set(999)).with_devid(1);
253        assert_eq!(args.format_name(), "1:999");
254    }
255}