Skip to main content

btrfs_cli/rescue/
chunk_recover.rs

1use crate::{RunContext, Runnable};
2use anyhow::{Context, Result, bail};
3use btrfs_disk::items::{ChunkItem, DeviceItem};
4use clap::Parser;
5use std::{
6    fs::{File, OpenOptions},
7    path::PathBuf,
8};
9use uuid::Uuid;
10
11mod apply;
12mod reconstruct;
13mod report;
14mod scan;
15
16/// Recover the chunk tree by scanning the device for surviving chunk data.
17///
18/// Sweeps the raw device for tree blocks that belong to the chunk tree,
19/// extracts CHUNK_ITEM and DEV_ITEM records, resolves conflicts between
20/// duplicates found at different generations, and reports whether a
21/// coherent chunk tree can be reconstructed.
22///
23/// By default this is a read-only scan that reports whether recovery is
24/// possible. Use --apply to actually write the reconstructed chunk tree.
25#[derive(Parser, Debug)]
26pub struct RescueChunkRecoverCommand {
27    /// Assume an answer of 'yes' to all questions
28    #[clap(short = 'y', long)]
29    pub yes: bool,
30
31    /// Write the reconstructed chunk tree to disk
32    #[clap(long)]
33    pub apply: bool,
34
35    /// Device to recover
36    pub device: PathBuf,
37}
38
39impl Runnable for RescueChunkRecoverCommand {
40    fn run(&self, _ctx: &RunContext) -> Result<()> {
41        if crate::util::is_mounted(&self.device) {
42            bail!("{} is currently mounted", self.device.display());
43        }
44
45        let mut file = File::open(&self.device).with_context(|| {
46            format!("failed to open '{}'", self.device.display())
47        })?;
48
49        eprintln!("Phase 1: scanning device for chunk tree data...");
50        let scan_result = scan::scan_device(&mut file)?;
51
52        eprintln!("Phase 2: reconstructing chunk tree...");
53        let reconstruction = reconstruct::reconstruct(&scan_result)?;
54
55        report::print_report(&self.device, &scan_result, &reconstruction);
56
57        if !reconstruction.chunk_root_covered {
58            bail!(
59                "chunk root logical address {:#x} is not covered by any recovered chunk",
60                scan_result.chunk_root,
61            );
62        }
63
64        if !self.apply {
65            println!(
66                "\nDry run complete. Use --apply to write the reconstructed chunk tree.",
67            );
68            return Ok(());
69        }
70
71        if !self.yes {
72            eprint!(
73                "\nAbout to write reconstructed chunk tree ({} chunks, {} devices). Continue? [y/N] ",
74                reconstruction.chunks.len(),
75                reconstruction.devices.len(),
76            );
77            let mut answer = String::new();
78            std::io::stdin().read_line(&mut answer)?;
79            if !answer.trim().eq_ignore_ascii_case("y") {
80                bail!("aborted by user");
81            }
82        }
83
84        // Reopen with write access for the apply phase.
85        drop(file);
86        let file = OpenOptions::new()
87            .read(true)
88            .write(true)
89            .open(&self.device)
90            .with_context(|| {
91                format!(
92                    "failed to open '{}' for writing",
93                    self.device.display()
94                )
95            })?;
96
97        eprintln!("Phase 4: writing reconstructed chunk tree...");
98        apply::apply_chunk_tree(file, &scan_result, &reconstruction)?;
99
100        println!("Chunk tree successfully written.");
101        Ok(())
102    }
103}
104
105// --- Shared types ---
106
107/// Where a recovered record came from.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum RecordSource {
110    /// From the superblock's sys_chunk_array (bootstrap).
111    Bootstrap,
112    /// From a chunk-tree leaf found during the raw device scan.
113    ScannedLeaf {
114        /// Physical byte offset on device where the leaf was found.
115        bytenr: u64,
116        /// Generation of the leaf header.
117        generation: u64,
118    },
119}
120
121impl RecordSource {
122    fn is_bootstrap(self) -> bool {
123        matches!(self, Self::Bootstrap)
124    }
125}
126
127/// A recovered CHUNK_ITEM with provenance.
128#[derive(Debug, Clone)]
129pub struct ChunkRecord {
130    /// Logical start address of this chunk (the key offset).
131    pub logical: u64,
132    /// The parsed chunk item.
133    pub chunk: ChunkItem,
134    /// Where this record was found.
135    pub source: RecordSource,
136    /// Generation of the tree block containing this record.
137    pub generation: u64,
138}
139
140/// A recovered DEV_ITEM with provenance.
141#[derive(Debug, Clone)]
142pub struct DevRecord {
143    /// Device ID.
144    pub devid: u64,
145    /// The parsed device item.
146    pub device: DeviceItem,
147    /// Where this record was found.
148    pub source: RecordSource,
149    /// Generation of the tree block containing this record.
150    pub generation: u64,
151}
152
153/// A conflict that was resolved during reconstruction.
154#[derive(Debug)]
155pub enum Conflict {
156    /// Two DEV_ITEM records for the same devid.
157    DevItem {
158        devid: u64,
159        winner_gen: u64,
160        loser_gen: u64,
161    },
162    /// Two CHUNK_ITEM records for the same logical start.
163    ChunkItem {
164        logical: u64,
165        winner_gen: u64,
166        loser_gen: u64,
167        bootstrap_won: bool,
168    },
169}
170
171/// A non-fatal warning from reconstruction.
172#[derive(Debug)]
173pub enum Warning {
174    /// A chunk stripe references a devid with no corresponding DEV_ITEM.
175    DanglingStripeRef { logical: u64, devid: u64 },
176}
177
178/// Output of Phase 1: raw device scan.
179pub struct ScanResult {
180    pub fsid: Uuid,
181    pub metadata_uuid: Uuid,
182    pub has_metadata_uuid: bool,
183    pub nodesize: u32,
184    pub chunk_root: u64,
185    pub chunk_root_level: u8,
186    pub sb_generation: u64,
187    pub device_size: u64,
188    pub bytes_scanned: u64,
189    pub candidates_checked: u64,
190    pub valid_blocks: u64,
191    pub chunk_tree_leaves: u64,
192    pub chunk_records: Vec<ChunkRecord>,
193    pub dev_records: Vec<DevRecord>,
194}
195
196/// Output of Phase 2: chunk tree reconstruction.
197pub struct ReconstructionResult {
198    /// Deduplicated, conflict-resolved chunks sorted by logical start.
199    pub chunks: Vec<ChunkRecord>,
200    /// Deduplicated device records.
201    pub devices: Vec<DevRecord>,
202    /// Conflicts that were resolved.
203    pub conflicts: Vec<Conflict>,
204    /// Non-fatal warnings.
205    pub warnings: Vec<Warning>,
206    /// Whether the superblock's chunk_root logical address is covered.
207    pub chunk_root_covered: bool,
208}