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`.
41///
42/// # Errors
43///
44/// Returns `Err` if the ioctl fails.
45pub fn filesystem_info(fd: BorrowedFd) -> nix::Result<FilesystemInfo> {
46    let mut raw: btrfs_ioctl_fs_info_args = unsafe { mem::zeroed() };
47    raw.flags = u64::from(BTRFS_FS_INFO_FLAG_GENERATION);
48    unsafe { btrfs_ioc_fs_info(fd.as_raw_fd(), &raw mut raw) }?;
49
50    Ok(FilesystemInfo {
51        uuid: Uuid::from_bytes(raw.fsid),
52        num_devices: raw.num_devices,
53        max_id: raw.max_id,
54        nodesize: raw.nodesize,
55        sectorsize: raw.sectorsize,
56        generation: raw.generation,
57    })
58}
59
60/// Force a sync on the btrfs filesystem referred to by `fd` and wait for it
61/// to complete.
62///
63/// # Errors
64///
65/// Returns `Err` if the ioctl fails.
66pub fn sync(fd: BorrowedFd) -> nix::Result<()> {
67    unsafe { btrfs_ioc_sync(fd.as_raw_fd()) }?;
68    Ok(())
69}
70
71/// Asynchronously start a sync on the btrfs filesystem referred to by `fd`.
72///
73/// Returns the transaction ID of the initiated sync, which can be passed to
74/// `wait_sync` to block until it completes.
75///
76/// # Errors
77///
78/// Returns `Err` if the ioctl fails.
79pub fn start_sync(fd: BorrowedFd) -> nix::Result<u64> {
80    let mut transid: u64 = 0;
81    unsafe { btrfs_ioc_start_sync(fd.as_raw_fd(), &raw mut transid) }?;
82    Ok(transid)
83}
84
85/// Wait for a previously started transaction to complete.
86///
87/// `transid` is the transaction ID returned by `start_sync`. Pass zero to
88/// wait for the current transaction.
89///
90/// # Errors
91///
92/// Returns `Err` if the ioctl fails.
93pub fn wait_sync(fd: BorrowedFd, transid: u64) -> nix::Result<()> {
94    unsafe { btrfs_ioc_wait_sync(fd.as_raw_fd(), &raw const transid) }?;
95    Ok(())
96}
97
98/// Read the label of the btrfs filesystem referred to by `fd`.
99///
100/// Returns the label as a [`CString`]. An empty string means no label is set.
101///
102/// # Errors
103///
104/// Returns `Err` if the ioctl fails.
105pub fn label_get(fd: BorrowedFd) -> nix::Result<CString> {
106    let mut buf = [0i8; BTRFS_LABEL_SIZE as usize];
107    unsafe { btrfs_ioc_get_fslabel(fd.as_raw_fd(), &raw mut buf) }?;
108    let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
109    // CStr::to_owned() copies the bytes into a freshly allocated CString,
110    // which is safe to return after `buf` goes out of scope.
111    Ok(cstr.to_owned())
112}
113
114/// Set the label of the btrfs filesystem referred to by `fd`.
115///
116/// The label must be shorter than 256 bytes (not counting the null terminator).
117/// Further validation (e.g. rejecting labels that contain `/`) is left to the
118/// kernel.
119///
120/// Errors: EINVAL if the label is 256 bytes or longer (checked before the
121/// ioctl).  `EPERM` without `CAP_SYS_ADMIN`.
122///
123/// # Errors
124///
125/// Returns `Err` if the label is too long or the ioctl fails.
126#[allow(clippy::cast_possible_wrap)] // ASCII bytes always fit in c_char
127pub fn label_set(fd: BorrowedFd, label: &CStr) -> nix::Result<()> {
128    let bytes = label.to_bytes();
129    if bytes.len() >= BTRFS_LABEL_SIZE as usize {
130        return Err(nix::errno::Errno::EINVAL);
131    }
132    let mut buf = [0i8; BTRFS_LABEL_SIZE as usize];
133    for (i, &b) in bytes.iter().enumerate() {
134        buf[i] = b as c_char;
135    }
136    unsafe { btrfs_ioc_set_fslabel(fd.as_raw_fd(), &raw const buf) }?;
137    Ok(())
138}
139
140/// The target size for a resize operation.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum ResizeAmount {
143    /// Cancel an in-progress resize.
144    Cancel,
145    /// Grow the device to its maximum available size.
146    Max,
147    /// Set the device to exactly this many bytes.
148    Set(u64),
149    /// Add this many bytes to the current device size.
150    Add(u64),
151    /// Subtract this many bytes from the current device size.
152    Sub(u64),
153}
154
155impl std::fmt::Display for ResizeAmount {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        match self {
158            Self::Cancel => f.write_str("cancel"),
159            Self::Max => f.write_str("max"),
160            Self::Set(n) => write!(f, "{n}"),
161            Self::Add(n) => write!(f, "+{n}"),
162            Self::Sub(n) => write!(f, "-{n}"),
163        }
164    }
165}
166
167/// Arguments for a resize operation.
168///
169/// `devid` selects which device within the filesystem to resize. When `None`,
170/// the kernel defaults to device ID 1 (the first device).
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct ResizeArgs {
173    /// Device within the filesystem to resize, identified by btrfs device ID.
174    /// When `None`, the kernel defaults to device ID 1 (the first device).
175    pub devid: Option<u64>,
176    /// How much to grow, shrink, or set the device size to.
177    pub amount: ResizeAmount,
178}
179
180impl ResizeArgs {
181    /// Create a new `ResizeArgs` targeting the default device (ID 1) with the
182    /// given resize amount.
183    #[must_use]
184    pub fn new(amount: ResizeAmount) -> Self {
185        Self {
186            devid: None,
187            amount,
188        }
189    }
190
191    /// Set the target device ID for the resize operation.
192    #[must_use]
193    pub fn with_devid(mut self, devid: u64) -> Self {
194        self.devid = Some(devid);
195        self
196    }
197
198    /// Format into the string that `BTRFS_IOC_RESIZE` expects in
199    /// `btrfs_ioctl_vol_args.name`: `[<devid>:]<amount>`.
200    fn format_name(&self) -> String {
201        let amount = self.amount.to_string();
202        match self.devid {
203            Some(devid) => format!("{devid}:{amount}"),
204            None => amount,
205        }
206    }
207}
208
209/// Resize a device within the btrfs filesystem referred to by `fd`.
210///
211/// `fd` must be an open file descriptor to a directory on the mounted
212/// filesystem. Use [`ResizeArgs`] to specify the target device and amount.
213///
214/// # Errors
215///
216/// Returns `Err` if the resize ioctl fails.
217#[allow(clippy::cast_possible_wrap)] // ASCII bytes always fit in c_char
218pub fn resize(fd: BorrowedFd, args: ResizeArgs) -> nix::Result<()> {
219    let name = args.format_name();
220    let name_bytes = name.as_bytes();
221
222    // BTRFS_PATH_NAME_MAX is 4087; the name field is [c_char; 4088].
223    // A well-formed resize string (devid + colon + u64 digits) is at most
224    // ~23 characters, so this can only fail if the caller constructs a
225    // pathological devid.
226    if name_bytes.len() >= 4088 {
227        return Err(nix::errno::Errno::EINVAL);
228    }
229
230    let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
231    for (i, &b) in name_bytes.iter().enumerate() {
232        raw.name[i] = b as c_char;
233    }
234
235    unsafe { btrfs_ioc_resize(fd.as_raw_fd(), &raw const raw) }?;
236    Ok(())
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    // --- ResizeAmount::to_string ---
244
245    #[test]
246    fn resize_amount_cancel() {
247        assert_eq!(ResizeAmount::Cancel.to_string(), "cancel");
248    }
249
250    #[test]
251    fn resize_amount_max() {
252        assert_eq!(ResizeAmount::Max.to_string(), "max");
253    }
254
255    #[test]
256    fn resize_amount_set() {
257        assert_eq!(ResizeAmount::Set(1073741824).to_string(), "1073741824");
258    }
259
260    #[test]
261    fn resize_amount_add() {
262        assert_eq!(ResizeAmount::Add(512000000).to_string(), "+512000000");
263    }
264
265    #[test]
266    fn resize_amount_sub() {
267        assert_eq!(ResizeAmount::Sub(256000000).to_string(), "-256000000");
268    }
269
270    // --- ResizeArgs builder + format_name ---
271
272    #[test]
273    fn resize_args_no_devid() {
274        let args = ResizeArgs::new(ResizeAmount::Max);
275        assert!(args.devid.is_none());
276        assert_eq!(args.format_name(), "max");
277    }
278
279    #[test]
280    fn resize_args_with_devid() {
281        let args = ResizeArgs::new(ResizeAmount::Add(1024)).with_devid(2);
282        assert_eq!(args.devid, Some(2));
283        assert_eq!(args.format_name(), "2:+1024");
284    }
285
286    #[test]
287    fn resize_args_set_with_devid() {
288        let args = ResizeArgs::new(ResizeAmount::Set(999)).with_devid(1);
289        assert_eq!(args.format_name(), "1:999");
290    }
291}
292
293/// Check whether a device path appears as a mount source in `/proc/mounts`.
294///
295/// Canonicalizes the given path and compares it against each mount source.
296/// Symlinks and relative paths are handled correctly.
297///
298/// # Errors
299///
300/// Returns an error if the path cannot be canonicalized or `/proc/mounts`
301/// cannot be read.
302pub fn is_mounted(device: &std::path::Path) -> std::io::Result<bool> {
303    let canonical = std::fs::canonicalize(device)?;
304    let contents = std::fs::read_to_string("/proc/mounts")?;
305    Ok(contents.lines().any(|line| {
306        line.split_whitespace()
307            .next()
308            .and_then(|src| std::fs::canonicalize(src).ok())
309            .is_some_and(|src_canon| src_canon == canonical)
310    }))
311}