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_ioc_fs_info, btrfs_ioc_get_fslabel,
10    btrfs_ioc_resize, btrfs_ioc_set_fslabel, btrfs_ioc_start_sync,
11    btrfs_ioc_sync, btrfs_ioc_wait_sync, btrfs_ioctl_fs_info_args,
12    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 = BTRFS_FS_INFO_FLAG_GENERATION as u64;
44    unsafe { btrfs_ioc_fs_info(fd.as_raw_fd(), &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(), &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(), &transid) }?;
79    Ok(())
80}
81
82/// Maximum label length including the null terminator (BTRFS_LABEL_SIZE).
83const BTRFS_LABEL_SIZE: usize = crate::raw::BTRFS_LABEL_SIZE as usize;
84
85/// Read the label of the btrfs filesystem referred to by `fd`.
86///
87/// Returns the label as a [`CString`]. An empty string means no label is set.
88pub fn label_get(fd: BorrowedFd) -> nix::Result<CString> {
89    let mut buf = [0i8; BTRFS_LABEL_SIZE];
90    unsafe { btrfs_ioc_get_fslabel(fd.as_raw_fd(), &mut buf) }?;
91    let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
92    // CStr::to_owned() copies the bytes into a freshly allocated CString,
93    // which is safe to return after `buf` goes out of scope.
94    Ok(cstr.to_owned())
95}
96
97/// Set the label of the btrfs filesystem referred to by `fd`.
98///
99/// The label must be shorter than 256 bytes (not counting the null terminator).
100/// Further validation (e.g. rejecting labels that contain `/`) is left to the
101/// kernel.
102pub fn label_set(fd: BorrowedFd, label: &CStr) -> nix::Result<()> {
103    let bytes = label.to_bytes();
104    if bytes.len() >= BTRFS_LABEL_SIZE {
105        return Err(nix::errno::Errno::EINVAL);
106    }
107    let mut buf = [0i8; BTRFS_LABEL_SIZE];
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(), &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 ResizeAmount {
131    fn to_string(&self) -> String {
132        match self {
133            Self::Cancel => "cancel".to_owned(),
134            Self::Max => "max".to_owned(),
135            Self::Set(n) => n.to_string(),
136            Self::Add(n) => format!("+{n}"),
137            Self::Sub(n) => format!("-{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    pub fn new(amount: ResizeAmount) -> Self {
154        Self {
155            devid: None,
156            amount,
157        }
158    }
159
160    pub fn with_devid(mut self, devid: u64) -> Self {
161        self.devid = Some(devid);
162        self
163    }
164
165    /// Format into the string that `BTRFS_IOC_RESIZE` expects in
166    /// `btrfs_ioctl_vol_args.name`: `[<devid>:]<amount>`.
167    fn format_name(&self) -> String {
168        let amount = self.amount.to_string();
169        match self.devid {
170            Some(devid) => format!("{devid}:{amount}"),
171            None => amount,
172        }
173    }
174}
175
176/// Resize a device within the btrfs filesystem referred to by `fd`.
177///
178/// `fd` must be an open file descriptor to a directory on the mounted
179/// filesystem. Use [`ResizeArgs`] to specify the target device and amount.
180pub fn resize(fd: BorrowedFd, args: ResizeArgs) -> nix::Result<()> {
181    let name = args.format_name();
182    let name_bytes = name.as_bytes();
183
184    // BTRFS_PATH_NAME_MAX is 4087; the name field is [c_char; 4088].
185    // A well-formed resize string (devid + colon + u64 digits) is at most
186    // ~23 characters, so this can only fail if the caller constructs a
187    // pathological devid.
188    if name_bytes.len() >= 4088 {
189        return Err(nix::errno::Errno::EINVAL);
190    }
191
192    let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
193    for (i, &b) in name_bytes.iter().enumerate() {
194        raw.name[i] = b as c_char;
195    }
196
197    unsafe { btrfs_ioc_resize(fd.as_raw_fd(), &mut raw) }?;
198    Ok(())
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    // --- ResizeAmount::to_string ---
206
207    #[test]
208    fn resize_amount_cancel() {
209        assert_eq!(ResizeAmount::Cancel.to_string(), "cancel");
210    }
211
212    #[test]
213    fn resize_amount_max() {
214        assert_eq!(ResizeAmount::Max.to_string(), "max");
215    }
216
217    #[test]
218    fn resize_amount_set() {
219        assert_eq!(ResizeAmount::Set(1073741824).to_string(), "1073741824");
220    }
221
222    #[test]
223    fn resize_amount_add() {
224        assert_eq!(ResizeAmount::Add(512000000).to_string(), "+512000000");
225    }
226
227    #[test]
228    fn resize_amount_sub() {
229        assert_eq!(ResizeAmount::Sub(256000000).to_string(), "-256000000");
230    }
231
232    // --- ResizeArgs builder + format_name ---
233
234    #[test]
235    fn resize_args_no_devid() {
236        let args = ResizeArgs::new(ResizeAmount::Max);
237        assert!(args.devid.is_none());
238        assert_eq!(args.format_name(), "max");
239    }
240
241    #[test]
242    fn resize_args_with_devid() {
243        let args = ResizeArgs::new(ResizeAmount::Add(1024)).with_devid(2);
244        assert_eq!(args.devid, Some(2));
245        assert_eq!(args.format_name(), "2:+1024");
246    }
247
248    #[test]
249    fn resize_args_set_with_devid() {
250        let args = ResizeArgs::new(ResizeAmount::Set(999)).with_devid(1);
251        assert_eq!(args.format_name(), "1:999");
252    }
253}