Skip to main content

btrfs_cli/inspect/
min_dev_size.rs

1use crate::{
2    RunContext, Runnable,
3    util::{human_bytes, open_path},
4};
5use anyhow::{Context, Result};
6use btrfs_disk::{
7    items::DeviceExtent,
8    reader,
9    tree::{KeyType, TreeBlock},
10};
11use clap::Parser;
12use std::{
13    fs::File,
14    io::{Read, Seek},
15    os::unix::io::AsFd,
16    path::PathBuf,
17};
18
19/// Print the minimum size a device can be shrunk to.
20///
21/// Returns the minimum size in bytes that the specified device can be
22/// resized to without losing data. The device id 1 is used by default.
23/// Requires CAP_SYS_ADMIN (unless --offline is used).
24#[derive(Parser, Debug)]
25#[allow(clippy::doc_markdown)]
26pub struct MinDevSizeCommand {
27    /// Specify the device id to query
28    #[arg(long = "id", default_value = "1")]
29    devid: u64,
30
31    /// Read directly from an unmounted device or image file instead of
32    /// a mounted filesystem. Does not require CAP_SYS_ADMIN.
33    #[clap(long)]
34    pub offline: bool,
35
36    /// Path to a file or directory on the btrfs filesystem, or a block
37    /// device / image file when --offline is used
38    path: PathBuf,
39}
40
41impl Runnable for MinDevSizeCommand {
42    fn run(&self, _ctx: &RunContext) -> Result<()> {
43        let size = if self.offline {
44            self.compute_offline()?
45        } else {
46            self.compute_online()?
47        };
48
49        println!("{} bytes ({})", size, human_bytes(size));
50        Ok(())
51    }
52}
53
54impl MinDevSizeCommand {
55    fn compute_online(&self) -> Result<u64> {
56        let file = open_path(&self.path)?;
57        btrfs_uapi::device::device_min_size(file.as_fd(), self.devid)
58            .with_context(|| {
59                format!(
60                    "failed to determine min device size for devid {} on '{}'",
61                    self.devid,
62                    self.path.display()
63                )
64            })
65    }
66
67    fn compute_offline(&self) -> Result<u64> {
68        let file = File::open(&self.path).with_context(|| {
69            format!("failed to open '{}'", self.path.display())
70        })?;
71
72        let mut open = reader::filesystem_open(file).with_context(|| {
73            format!(
74                "failed to open btrfs filesystem on '{}'",
75                self.path.display()
76            )
77        })?;
78
79        let dev_extents =
80            collect_dev_extents(&mut open.reader, &open.tree_roots, self.devid);
81
82        Ok(btrfs_uapi::device::compute_min_size(&dev_extents))
83    }
84}
85
86/// Walk the device tree to collect all device extents for a given devid.
87///
88/// Returns `(physical_start, length)` pairs in ascending physical order.
89fn collect_dev_extents<R: Read + Seek>(
90    block_reader: &mut reader::BlockReader<R>,
91    tree_roots: &std::collections::BTreeMap<u64, (u64, u64)>,
92    devid: u64,
93) -> Vec<(u64, u64)> {
94    let mut dev_extents: Vec<(u64, u64)> = Vec::new();
95
96    let dev_root = tree_roots
97        .get(&u64::from(btrfs_disk::raw::BTRFS_DEV_TREE_OBJECTID))
98        .map(|&(bytenr, _)| bytenr);
99
100    let Some(dev_root) = dev_root else {
101        return dev_extents;
102    };
103
104    let mut visitor = |_raw: &[u8], block: &TreeBlock| {
105        if let TreeBlock::Leaf { items, data, .. } = block {
106            for item in items {
107                if item.key.key_type != KeyType::DeviceExtent {
108                    continue;
109                }
110                if item.key.objectid != devid {
111                    continue;
112                }
113                let start =
114                    std::mem::size_of::<btrfs_disk::raw::btrfs_header>()
115                        + item.offset as usize;
116                let item_data = &data[start..][..item.size as usize];
117                if let Some(de) = DeviceExtent::parse(item_data) {
118                    dev_extents.push((item.key.offset, de.length));
119                }
120            }
121        }
122    };
123
124    let _ = reader::tree_walk_tolerant(
125        block_reader,
126        dev_root,
127        &mut visitor,
128        &mut |_, _| {},
129    );
130
131    // Ensure ascending physical order (tree walk is DFS by key, so this
132    // should already be sorted, but be safe).
133    dev_extents.sort_by_key(|&(start, _)| start);
134    dev_extents
135}