Skip to main content

btrfs_uapi/
resize.rs

1//! # Device resizing: growing or shrinking a device within a mounted filesystem
2//!
3//! Resizing adjusts how much of a block device's capacity btrfs uses, without
4//! unmounting.  A device can be grown up to its physical size, shrunk to the
5//! minimum space currently occupied, or set to an explicit byte count.
6
7use crate::raw::{btrfs_ioc_resize, btrfs_ioctl_vol_args};
8use nix::libc::c_char;
9use std::{
10    mem,
11    os::{fd::AsRawFd, unix::io::BorrowedFd},
12};
13
14/// The target size for a resize operation.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ResizeAmount {
17    /// Cancel an in-progress resize.
18    Cancel,
19    /// Grow the device to its maximum available size.
20    Max,
21    /// Set the device to exactly this many bytes.
22    Set(u64),
23    /// Add this many bytes to the current device size.
24    Add(u64),
25    /// Subtract this many bytes from the current device size.
26    Sub(u64),
27}
28
29impl ResizeAmount {
30    fn to_string(&self) -> String {
31        match self {
32            Self::Cancel => "cancel".to_owned(),
33            Self::Max => "max".to_owned(),
34            Self::Set(n) => n.to_string(),
35            Self::Add(n) => format!("+{n}"),
36            Self::Sub(n) => format!("-{n}"),
37        }
38    }
39}
40
41/// Arguments for a resize operation.
42///
43/// `devid` selects which device within the filesystem to resize. When `None`,
44/// the kernel defaults to device ID 1 (the first device).
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct ResizeArgs {
47    pub devid: Option<u64>,
48    pub amount: ResizeAmount,
49}
50
51impl ResizeArgs {
52    pub fn new(amount: ResizeAmount) -> Self {
53        Self {
54            devid: None,
55            amount,
56        }
57    }
58
59    pub fn with_devid(mut self, devid: u64) -> Self {
60        self.devid = Some(devid);
61        self
62    }
63
64    /// Format into the string that `BTRFS_IOC_RESIZE` expects in
65    /// `btrfs_ioctl_vol_args.name`: `[<devid>:]<amount>`.
66    fn format_name(&self) -> String {
67        let amount = self.amount.to_string();
68        match self.devid {
69            Some(devid) => format!("{devid}:{amount}"),
70            None => amount,
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    // --- ResizeAmount::to_string ---
80
81    #[test]
82    fn resize_amount_cancel() {
83        assert_eq!(ResizeAmount::Cancel.to_string(), "cancel");
84    }
85
86    #[test]
87    fn resize_amount_max() {
88        assert_eq!(ResizeAmount::Max.to_string(), "max");
89    }
90
91    #[test]
92    fn resize_amount_set() {
93        assert_eq!(ResizeAmount::Set(1073741824).to_string(), "1073741824");
94    }
95
96    #[test]
97    fn resize_amount_add() {
98        assert_eq!(ResizeAmount::Add(512000000).to_string(), "+512000000");
99    }
100
101    #[test]
102    fn resize_amount_sub() {
103        assert_eq!(ResizeAmount::Sub(256000000).to_string(), "-256000000");
104    }
105
106    // --- ResizeArgs builder + format_name ---
107
108    #[test]
109    fn resize_args_no_devid() {
110        let args = ResizeArgs::new(ResizeAmount::Max);
111        assert!(args.devid.is_none());
112        assert_eq!(args.format_name(), "max");
113    }
114
115    #[test]
116    fn resize_args_with_devid() {
117        let args = ResizeArgs::new(ResizeAmount::Add(1024)).with_devid(2);
118        assert_eq!(args.devid, Some(2));
119        assert_eq!(args.format_name(), "2:+1024");
120    }
121
122    #[test]
123    fn resize_args_set_with_devid() {
124        let args = ResizeArgs::new(ResizeAmount::Set(999)).with_devid(1);
125        assert_eq!(args.format_name(), "1:999");
126    }
127}
128
129/// Resize a device within the btrfs filesystem referred to by `fd`.
130///
131/// `fd` must be an open file descriptor to a directory on the mounted
132/// filesystem. Use [`ResizeArgs`] to specify the target device and amount.
133pub fn resize(fd: BorrowedFd, args: ResizeArgs) -> nix::Result<()> {
134    let name = args.format_name();
135    let name_bytes = name.as_bytes();
136
137    // BTRFS_PATH_NAME_MAX is 4087; the name field is [c_char; 4088].
138    // A well-formed resize string (devid + colon + u64 digits) is at most
139    // ~23 characters, so this can only fail if the caller constructs a
140    // pathological devid.
141    if name_bytes.len() >= 4088 {
142        return Err(nix::errno::Errno::EINVAL);
143    }
144
145    let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
146    for (i, &b) in name_bytes.iter().enumerate() {
147        raw.name[i] = b as c_char;
148    }
149
150    unsafe { btrfs_ioc_resize(fd.as_raw_fd(), &mut raw) }?;
151    Ok(())
152}