Skip to main content

btrfs_disk/
reader.rs

1//! # Block device reader with logical-to-physical address resolution
2//!
3//! Provides `BlockReader` which reads btrfs tree blocks by logical address,
4//! resolving them through the chunk tree cache. Also provides `filesystem_open`
5//! which bootstraps a complete `BlockReader` from a raw block device or image.
6
7use crate::{
8    chunk::{
9        self, ChunkTreeCache, ParityKind, ParityPlan, ParityRow,
10        StripePlacement, WritePlan,
11    },
12    raid56, raw,
13    superblock::{self, Superblock},
14    tree::{KeyType, TreeBlock},
15};
16use bytes::Buf;
17use std::{
18    collections::BTreeMap,
19    io::{self, Read, Seek, SeekFrom, Write},
20    mem,
21    sync::Arc,
22};
23
24/// Pluggable cache for parsed [`TreeBlock`]s, consulted by
25/// [`BlockReader::read_tree_block`].
26///
27/// The trait is `Send + Sync` and uses `&self` methods so a single cache
28/// can be shared across threads (and, eventually, across multiple
29/// `BlockReader` instances in a reader pool). Implementations use
30/// interior mutability — typically a `Mutex<lru::LruCache<...>>` or
31/// similar.
32///
33/// `btrfs-disk` does not provide an LRU implementation: keeping the
34/// crate dependency-light is more important than convenience here.
35/// The `btrfs-fs` crate ships an `LruTreeBlockCache` that most
36/// embedders will use.
37pub trait TreeBlockCache: Send + Sync {
38    /// Look up a tree block by logical address. Returns `None` on miss.
39    fn get(&self, addr: u64) -> Option<Arc<TreeBlock>>;
40
41    /// Insert a freshly-read tree block into the cache. Implementations
42    /// decide eviction policy.
43    fn put(&self, addr: u64, block: Arc<TreeBlock>);
44
45    /// Drop a single entry. Used by writers (e.g. the transaction crate
46    /// during `CoW`) to invalidate stale blocks.
47    fn invalidate(&self, addr: u64);
48}
49
50/// A block reader that resolves logical addresses through a chunk cache.
51///
52/// Holds one I/O handle per device, keyed by `devid`. For single-device
53/// filesystems the map has a single entry. For RAID1 / RAID1C3 / RAID1C4
54/// / RAID10 / DUP, each stripe's `devid` (from the chunk cache) is used
55/// to look up the handle. SINGLE and DUP work with a one-entry map.
56pub struct BlockReader<R> {
57    devices: BTreeMap<u64, R>,
58    nodesize: u32,
59    chunk_cache: ChunkTreeCache,
60    /// Optional shared tree-block cache. `None` means no caching (the
61    /// default — preserves existing behaviour for one-shot readers).
62    /// Wired up by [`BlockReader::with_cache`].
63    cache: Option<Arc<dyn TreeBlockCache>>,
64}
65
66impl<R> BlockReader<R> {
67    /// Create a single-device block reader.
68    ///
69    /// `devid` is the device id (`superblock.dev_item.devid`) under which
70    /// this handle is registered. Stripe lookups for this device must
71    /// resolve to this devid.
72    pub fn new(
73        handle: R,
74        devid: u64,
75        nodesize: u32,
76        chunk_cache: ChunkTreeCache,
77    ) -> Self {
78        let mut devices = BTreeMap::new();
79        devices.insert(devid, handle);
80        Self {
81            devices,
82            nodesize,
83            chunk_cache,
84            cache: None,
85        }
86    }
87
88    /// Attach a [`TreeBlockCache`] to this reader.
89    ///
90    /// Subsequent calls to [`BlockReader::read_tree_block`] will consult
91    /// the cache before reading from disk and populate it on miss.
92    /// Cache hits return the stored `Arc<TreeBlock>` unchanged. Detach
93    /// by passing `None`.
94    pub fn set_cache(&mut self, cache: Option<Arc<dyn TreeBlockCache>>) {
95        self.cache = cache;
96    }
97
98    /// Builder-style helper: attach a cache and return the reader.
99    #[must_use]
100    pub fn with_cache(mut self, cache: Arc<dyn TreeBlockCache>) -> Self {
101        self.cache = Some(cache);
102        self
103    }
104
105    /// Create a multi-device block reader.
106    ///
107    /// `devices` maps each device id to its I/O handle. Every devid
108    /// referenced by the chunk cache must be present.
109    #[must_use]
110    pub fn new_multi(
111        devices: BTreeMap<u64, R>,
112        nodesize: u32,
113        chunk_cache: ChunkTreeCache,
114    ) -> Self {
115        Self {
116            devices,
117            nodesize,
118            chunk_cache,
119            cache: None,
120        }
121    }
122
123    /// Return the per-devid handle map.
124    #[must_use]
125    pub fn devices(&self) -> &BTreeMap<u64, R> {
126        &self.devices
127    }
128
129    /// Return the per-devid handle map mutably.
130    ///
131    /// Used by transaction commit / sync / flush paths that need to
132    /// flush every device. For ordinary reads/writes prefer
133    /// [`read_block`](Self::read_block), [`read_data`](Self::read_data),
134    /// or [`write_block`](Self::write_block) which route by devid via
135    /// the chunk cache.
136    pub fn devices_mut(&mut self) -> &mut BTreeMap<u64, R> {
137        &mut self.devices
138    }
139
140    /// Return the underlying handle for a single-device filesystem.
141    ///
142    /// Convenience for offline tools (`btrfs-tune`,
143    /// `btrfs filesystem resize` on a regular file) that operate on
144    /// one device at a time and need raw file access (e.g. `set_len`
145    /// or full-file scans).
146    ///
147    /// # Panics
148    ///
149    /// Panics if more than one device is open. Multi-device callers
150    /// must use [`devices_mut`](Self::devices_mut) and route by
151    /// devid explicitly.
152    pub fn single_device_mut(&mut self) -> &mut R {
153        assert_eq!(
154            self.devices.len(),
155            1,
156            "single_device_mut: filesystem has {} devices, not 1",
157            self.devices.len(),
158        );
159        self.devices.values_mut().next().unwrap()
160    }
161}
162
163impl<R: Read + Seek> BlockReader<R> {
164    /// Read raw bytes at a logical address, resolving to physical via the chunk cache.
165    ///
166    /// # Errors
167    ///
168    /// Returns an error if the logical address is unmapped, the resolved
169    /// device id is not in the handle map, or the underlying read fails.
170    pub fn read_block(&mut self, logical: u64) -> io::Result<Vec<u8>> {
171        // Tree blocks are always nodesize <= stripe_len, so a single
172        // block lives entirely in one row. Use plan_read so striped
173        // profiles (RAID0 / RAID10 / RAID5 / RAID6) route to the
174        // correct device column; mirrored/SINGLE chunks return one
175        // placement on stripes[0] either way.
176        self.read_data(logical, self.nodesize as usize)
177    }
178
179    /// Read and parse a tree block at a logical address.
180    ///
181    /// If a [`TreeBlockCache`] is attached the cache is consulted first
182    /// and populated on miss; the returned `Arc` either points at the
183    /// cached entry or at a freshly-parsed block.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if the logical address is unmapped or the
188    /// underlying read fails.
189    pub fn read_tree_block(
190        &mut self,
191        logical: u64,
192    ) -> io::Result<Arc<TreeBlock>> {
193        if let Some(cache) = self.cache.as_ref() {
194            if let Some(hit) = cache.get(logical) {
195                return Ok(hit);
196            }
197        }
198        let buf = self.read_block(logical)?;
199        let block = Arc::new(TreeBlock::parse(&buf));
200        if let Some(cache) = self.cache.as_ref() {
201            cache.put(logical, Arc::clone(&block));
202        }
203        Ok(block)
204    }
205
206    /// Return a reference to the chunk cache.
207    #[must_use]
208    pub fn chunk_cache(&self) -> &ChunkTreeCache {
209        &self.chunk_cache
210    }
211
212    /// Return a mutable reference to the chunk cache.
213    pub fn chunk_cache_mut(&mut self) -> &mut ChunkTreeCache {
214        &mut self.chunk_cache
215    }
216
217    /// Return the nodesize.
218    #[must_use]
219    pub fn nodesize(&self) -> u32 {
220        self.nodesize
221    }
222
223    /// Read arbitrary data at a logical address (not limited to nodesize).
224    ///
225    /// Unlike `read_block` which always reads `nodesize` bytes, this reads
226    /// exactly `len` bytes. Used for reading file data extents.
227    ///
228    /// Uses [`ChunkTreeCache::plan_read`](crate::chunk::ChunkTreeCache::plan_read)
229    /// internally so reads on striped profiles (RAID0 / RAID10 /
230    /// RAID5 / RAID6) that span multiple rows assemble the bytes from
231    /// the correct devices in order. RAID5/RAID6 reads route to the
232    /// data column owning each row's bytes, ignoring parity (degraded
233    /// reads are out of scope).
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if the logical address is unmapped, the request
238    /// extends past the chunk, or the underlying read fails.
239    pub fn read_data(
240        &mut self,
241        logical: u64,
242        len: usize,
243    ) -> io::Result<Vec<u8>> {
244        let placements =
245            self.chunk_cache.plan_read(logical, len).ok_or_else(|| {
246                io::Error::new(
247                    io::ErrorKind::NotFound,
248                    format!(
249                        "logical address {logical} not mapped or unsupported profile"
250                    ),
251                )
252            })?;
253        let mut buf = vec![0u8; len];
254        for p in placements {
255            let dev = self.device_handle_mut(p.devid)?;
256            dev.seek(SeekFrom::Start(p.physical))?;
257            dev.read_exact(&mut buf[p.buf_offset..p.buf_offset + p.len])?;
258        }
259        Ok(buf)
260    }
261
262    /// Look up a device handle by `devid`. Returns a clear error if the
263    /// chunk cache references a device that was not opened.
264    fn device_handle_mut(&mut self, devid: u64) -> io::Result<&mut R> {
265        self.devices.get_mut(&devid).ok_or_else(|| {
266            io::Error::new(
267                io::ErrorKind::NotFound,
268                format!(
269                    "device {devid} not open (referenced by the chunk cache)"
270                ),
271            )
272        })
273    }
274}
275
276impl<R: Read + Write + Seek> BlockReader<R> {
277    /// Write raw bytes to a logical address, routing to the correct
278    /// per-device locations based on the chunk's RAID profile.
279    ///
280    /// Uses [`ChunkTreeCache::plan_write`](crate::chunk::ChunkTreeCache::plan_write)
281    /// internally. For DUP / RAID1 / RAID1C3 / RAID1C4 every mirror
282    /// receives the same bytes; for RAID0 each row goes to exactly one
283    /// device; for RAID10 each row goes to one mirror pair; for
284    /// RAID5/RAID6 the executor prereads every data column slot of
285    /// every touched row, mixes in caller bytes, computes parity, then
286    /// writes data + parity. Writes larger than `stripe_len` on a
287    /// striped profile are split into per-row segments automatically.
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if the logical address is unmapped, the
292    /// request extends past the chunk, any referenced device is not
293    /// open, or any underlying read/write fails.
294    pub fn write_block(&mut self, logical: u64, buf: &[u8]) -> io::Result<()> {
295        let plan = self.chunk_cache.plan_write(logical, buf.len()).ok_or_else(
296            || {
297                io::Error::new(
298                    io::ErrorKind::NotFound,
299                    format!(
300                        "logical address {logical} not mapped or unsupported profile"
301                    ),
302                )
303            },
304        )?;
305        match plan {
306            WritePlan::Plain(placements) => {
307                self.execute_plain_plan(buf, &placements)
308            }
309            WritePlan::Parity(plan) => self.execute_parity_plan(buf, &plan),
310        }
311    }
312
313    fn execute_plain_plan(
314        &mut self,
315        buf: &[u8],
316        placements: &[StripePlacement],
317    ) -> io::Result<()> {
318        for p in placements {
319            let dev = self.device_handle_mut(p.devid)?;
320            dev.seek(SeekFrom::Start(p.physical))?;
321            dev.write_all(&buf[p.buf_offset..p.buf_offset + p.len])?;
322        }
323        Ok(())
324    }
325
326    /// Run the read-modify-write cycle for a RAID5/RAID6 write.
327    ///
328    /// For every row in the plan: preread each data column slot into a
329    /// scratch buffer, overlay any caller bytes, compute P (and Q for
330    /// RAID6) over the assembled scratches, then issue the data writes
331    /// (the overlaid byte ranges only) and the parity writes (full
332    /// `stripe_len` slots).
333    fn execute_parity_plan(
334        &mut self,
335        buf: &[u8],
336        plan: &ParityPlan,
337    ) -> io::Result<()> {
338        let stripe_len = plan.stripe_len as usize;
339        for row in &plan.rows {
340            self.execute_parity_row(buf, stripe_len, row)?;
341        }
342        Ok(())
343    }
344
345    fn execute_parity_row(
346        &mut self,
347        buf: &[u8],
348        stripe_len: usize,
349        row: &ParityRow,
350    ) -> io::Result<()> {
351        // Preread every data column slot of the row into its own scratch.
352        let mut scratch: Vec<Vec<u8>> =
353            Vec::with_capacity(row.data_columns.len());
354        for col in &row.data_columns {
355            let mut slot = vec![0u8; stripe_len];
356            let dev = self.device_handle_mut(col.devid)?;
357            dev.seek(SeekFrom::Start(col.physical))?;
358            dev.read_exact(&mut slot)?;
359            scratch.push(slot);
360        }
361        // Overlay caller bytes onto each touched slot.
362        for (col, slot) in row.data_columns.iter().zip(scratch.iter_mut()) {
363            if let Some(ov) = &col.overlay {
364                let dst_start = ov.slot_offset as usize;
365                let dst_end = dst_start + ov.len as usize;
366                let src_start = ov.buf_offset;
367                let src_end = src_start + ov.len as usize;
368                slot[dst_start..dst_end]
369                    .copy_from_slice(&buf[src_start..src_end]);
370            }
371        }
372        // Compute parity from the assembled (post-overlay) data slots.
373        let stripe_refs: Vec<&[u8]> =
374            scratch.iter().map(Vec::as_slice).collect();
375        let want_q = row.parity_targets.iter().any(|t| t.kind == ParityKind::Q);
376        let (p_buf, q_buf) = if want_q {
377            let (p, q) = raid56::compute_p_q(&stripe_refs);
378            (p, Some(q))
379        } else {
380            (raid56::compute_p(&stripe_refs), None)
381        };
382        // Issue the data writes (overlaid byte ranges only).
383        for col in &row.data_columns {
384            let Some(ov) = &col.overlay else { continue };
385            let dev = self.device_handle_mut(col.devid)?;
386            dev.seek(SeekFrom::Start(
387                col.physical + u64::from(ov.slot_offset),
388            ))?;
389            let src_start = ov.buf_offset;
390            let src_end = src_start + ov.len as usize;
391            dev.write_all(&buf[src_start..src_end])?;
392        }
393        // Issue the parity writes (full stripe_len slots).
394        for target in &row.parity_targets {
395            let bytes = match target.kind {
396                ParityKind::P => &p_buf,
397                ParityKind::Q => {
398                    q_buf.as_ref().expect("Q target without Q computation")
399                }
400            };
401            let dev = self.device_handle_mut(target.devid)?;
402            dev.seek(SeekFrom::Start(target.physical))?;
403            dev.write_all(bytes)?;
404        }
405        Ok(())
406    }
407}
408
409/// Result of opening a btrfs filesystem from a block device or image.
410pub struct OpenFilesystem<R> {
411    /// Block reader with fully populated chunk cache.
412    pub reader: BlockReader<R>,
413    /// Parsed primary-device superblock.
414    pub superblock: Superblock,
415    /// Map of tree ID -> (root block logical address, key offset), from the root tree.
416    pub tree_roots: BTreeMap<u64, (u64, u64)>,
417    /// Per-device `dev_item` snapshots taken at open time. One entry
418    /// per opened device (always at least the primary). The transaction
419    /// crate uses these to splice the correct per-device identity into
420    /// the superblock when writing it back during commit, so a
421    /// multi-device filesystem doesn't get clobbered with the primary
422    /// device's `dev_item`.
423    pub per_device_dev_items: BTreeMap<u64, crate::items::DeviceItem>,
424}
425
426/// Open a btrfs filesystem by bootstrapping from the superblock.
427///
428/// This performs the full bootstrap sequence:
429/// 1. Read the superblock (mirror 0)
430/// 2. Seed the chunk cache from the `sys_chunk_array`
431/// 3. Read the full chunk tree to complete the cache
432/// 4. Read the root tree to collect all tree root pointers
433///
434/// # Errors
435///
436/// Returns an error if any I/O operation fails during bootstrap.
437pub fn filesystem_open<R: Read + Seek>(
438    reader: R,
439) -> io::Result<OpenFilesystem<R>> {
440    filesystem_open_mirror(reader, 0)
441}
442
443/// Open a btrfs filesystem using a specific superblock mirror (0, 1, or 2).
444///
445/// # Errors
446///
447/// Returns an error if any I/O operation fails during bootstrap.
448pub fn filesystem_open_mirror<R: Read + Seek>(
449    reader: R,
450    mirror: u32,
451) -> io::Result<OpenFilesystem<R>> {
452    let mut reader = reader;
453
454    // Step 1: read the superblock
455    let sb = superblock::read_superblock(&mut reader, mirror)?;
456    let primary_devid = sb.dev_item.devid;
457    let mut per_device_dev_items = BTreeMap::new();
458    per_device_dev_items.insert(primary_devid, sb.dev_item.clone());
459
460    // Step 2: seed chunk cache from sys_chunk_array
461    let chunk_cache = chunk::seed_from_sys_chunk_array(
462        &sb.sys_chunk_array,
463        sb.sys_chunk_array_size,
464    );
465
466    let mut block_reader =
467        BlockReader::new(reader, primary_devid, sb.nodesize, chunk_cache);
468
469    // Step 3: read the full chunk tree to complete the cache
470    read_chunk_tree(&mut block_reader, sb.chunk_root)?;
471
472    // Step 4: read the root tree to collect tree roots
473    let tree_roots = read_root_tree(&mut block_reader, sb.root)?;
474
475    Ok(OpenFilesystem {
476        reader: block_reader,
477        superblock: sb,
478        tree_roots,
479        per_device_dev_items,
480    })
481}
482
483/// Open a multi-device btrfs filesystem from a map of `devid -> handle`.
484///
485/// All devices in the filesystem must be present in the map; the
486/// bootstrap fails with a clear error if the chunk tree references a
487/// devid that is not in the map. Each device's superblock is read and
488/// its `dev_item.devid` is verified against the map key, and all
489/// devices' `fsid` must match.
490///
491/// The "primary" superblock used for filesystem-wide fields (root,
492/// `chunk_root`, generation, etc.) is the one with the lowest devid.
493///
494/// # Errors
495///
496/// Returns an error if any superblock cannot be read, any device's
497/// superblock disagrees with its map key or with the primary's fsid,
498/// or the chunk tree references a devid not in the map.
499///
500/// # Panics
501///
502/// Panics if the in-memory device map is empty after the initial
503/// non-empty check (an internal invariant violation, not a runtime
504/// possibility).
505pub fn filesystem_open_multi<R: Read + Seek>(
506    devices: BTreeMap<u64, R>,
507) -> io::Result<OpenFilesystem<R>> {
508    if devices.is_empty() {
509        return Err(io::Error::other(
510            "filesystem_open_multi: device map is empty",
511        ));
512    }
513    let mut devices = devices;
514
515    // Step 1: read each device's superblock and validate identity.
516    let mut per_device_dev_items: BTreeMap<u64, crate::items::DeviceItem> =
517        BTreeMap::new();
518    let mut superblocks: BTreeMap<u64, Superblock> = BTreeMap::new();
519    for (&devid, dev) in &mut devices {
520        let sb = superblock::read_superblock(dev, 0)?;
521        if sb.dev_item.devid != devid {
522            return Err(io::Error::other(format!(
523                "device map key {devid} doesn't match superblock dev_item.devid {}",
524                sb.dev_item.devid,
525            )));
526        }
527        per_device_dev_items.insert(devid, sb.dev_item.clone());
528        superblocks.insert(devid, sb);
529    }
530
531    // Step 2: pick the lowest-devid superblock as authoritative for
532    // filesystem-wide fields, and validate fsid consistency.
533    let primary_sb = superblocks.values().next().unwrap().clone();
534    for (devid, sb) in &superblocks {
535        if sb.fsid != primary_sb.fsid {
536            return Err(io::Error::other(format!(
537                "device {devid} fsid {} differs from primary fsid {}",
538                sb.fsid, primary_sb.fsid,
539            )));
540        }
541    }
542
543    // Step 3: seed chunk cache from primary's sys_chunk_array.
544    let chunk_cache = chunk::seed_from_sys_chunk_array(
545        &primary_sb.sys_chunk_array,
546        primary_sb.sys_chunk_array_size,
547    );
548
549    let mut block_reader =
550        BlockReader::new_multi(devices, primary_sb.nodesize, chunk_cache);
551
552    // Step 4: read the full chunk tree to complete the cache.
553    read_chunk_tree(&mut block_reader, primary_sb.chunk_root)?;
554
555    // Step 5: validate every devid the chunk cache references is open.
556    let mut referenced: std::collections::BTreeSet<u64> =
557        std::collections::BTreeSet::new();
558    for mapping in block_reader.chunk_cache().iter() {
559        for stripe in &mapping.stripes {
560            referenced.insert(stripe.devid);
561        }
562    }
563    for devid in &referenced {
564        if !block_reader.devices().contains_key(devid) {
565            return Err(io::Error::other(format!(
566                "chunk tree references devid {devid} but no handle was provided"
567            )));
568        }
569    }
570
571    // Step 6: read the root tree.
572    let tree_roots = read_root_tree(&mut block_reader, primary_sb.root)?;
573
574    Ok(OpenFilesystem {
575        reader: block_reader,
576        superblock: primary_sb,
577        tree_roots,
578        per_device_dev_items,
579    })
580}
581
582/// Open a btrfs filesystem using a pre-built chunk cache.
583///
584/// Skips the chunk tree walk entirely, using the provided cache for
585/// all logical-to-physical address resolution. This is the entry point
586/// for `rescue chunk-recover --apply`, where the on-disk chunk tree is
587/// damaged and the cache has been reconstructed from a raw device scan.
588///
589/// The root tree is still read normally (it becomes accessible once the
590/// correct chunk mappings are in place).
591///
592/// # Errors
593///
594/// Returns an error if the superblock read or root tree walk fails.
595pub fn filesystem_open_with_cache<R: Read + Seek>(
596    reader: R,
597    mirror: u32,
598    chunk_cache: ChunkTreeCache,
599) -> io::Result<OpenFilesystem<R>> {
600    let mut reader = reader;
601    let sb = superblock::read_superblock(&mut reader, mirror)?;
602    let primary_devid = sb.dev_item.devid;
603    let mut per_device_dev_items = BTreeMap::new();
604    per_device_dev_items.insert(primary_devid, sb.dev_item.clone());
605    let mut block_reader =
606        BlockReader::new(reader, primary_devid, sb.nodesize, chunk_cache);
607    let tree_roots = read_root_tree(&mut block_reader, sb.root)?;
608
609    Ok(OpenFilesystem {
610        reader: block_reader,
611        superblock: sb,
612        tree_roots,
613        per_device_dev_items,
614    })
615}
616
617/// Recursively read the chunk tree to populate the chunk cache.
618///
619/// Starting from the given root block, walks all leaves and inserts any
620/// `CHUNK_ITEM` entries that are not already present in the cache.
621///
622/// # Errors
623///
624/// Returns an error if any tree block read fails.
625pub fn read_chunk_tree<R: Read + Seek>(
626    reader: &mut BlockReader<R>,
627    root_logical: u64,
628) -> io::Result<()> {
629    let block = reader.read_tree_block(root_logical)?;
630
631    match &*block {
632        TreeBlock::Leaf { items, data, .. } => {
633            for item in items {
634                if item.key.key_type != KeyType::ChunkItem {
635                    continue;
636                }
637                let item_data = &data[mem::size_of::<raw::btrfs_header>()
638                    + item.offset as usize..];
639                if let Some((mapping, _)) =
640                    chunk::parse_chunk_item(item_data, item.key.offset)
641                {
642                    // Only insert if not already in cache (sys_chunk_array may
643                    // have already seeded some entries)
644                    if reader.chunk_cache.lookup(mapping.logical).is_none() {
645                        reader.chunk_cache.insert(mapping);
646                    }
647                }
648            }
649        }
650        TreeBlock::Node { ptrs, .. } => {
651            for ptr in ptrs {
652                read_chunk_tree(reader, ptr.blockptr)?;
653            }
654        }
655    }
656
657    Ok(())
658}
659
660/// Read the root tree to collect all tree root pointers.
661///
662/// Returns a map of `tree_id` (objectid) -> `(root_bytenr, key_offset)`.
663///
664/// # Errors
665///
666/// Returns an error if any tree block read fails.
667pub fn read_root_tree<R: Read + Seek>(
668    reader: &mut BlockReader<R>,
669    root_logical: u64,
670) -> io::Result<BTreeMap<u64, (u64, u64)>> {
671    let mut tree_roots = BTreeMap::new();
672    collect_root_items(reader, root_logical, &mut tree_roots)?;
673    Ok(tree_roots)
674}
675
676/// Tree traversal order.
677#[derive(Debug, Clone, Copy, PartialEq, Eq)]
678pub enum Traversal {
679    /// Breadth-first: print all nodes at level N, then N-1, down to leaves.
680    Bfs,
681    /// Depth-first: print a node, then recursively its children.
682    Dfs,
683}
684
685/// Walk a tree starting at `root_logical`, calling `visitor` for each block.
686///
687/// # Errors
688///
689/// Returns an error if any tree block cannot be read.
690pub fn tree_walk<R: Read + Seek>(
691    reader: &mut BlockReader<R>,
692    root_logical: u64,
693    traversal: Traversal,
694    visitor: &mut dyn FnMut(&TreeBlock),
695) -> io::Result<()> {
696    match traversal {
697        Traversal::Bfs => tree_walk_bfs(reader, root_logical, visitor),
698        Traversal::Dfs => tree_walk_dfs(reader, root_logical, visitor),
699    }
700}
701
702fn tree_walk_dfs<R: Read + Seek>(
703    reader: &mut BlockReader<R>,
704    logical: u64,
705    visitor: &mut dyn FnMut(&TreeBlock),
706) -> io::Result<()> {
707    let block = reader.read_tree_block(logical)?;
708    visitor(&block);
709
710    if let TreeBlock::Node { ptrs, .. } = &*block {
711        for ptr in ptrs {
712            tree_walk_dfs(reader, ptr.blockptr, visitor)?;
713        }
714    }
715
716    Ok(())
717}
718
719fn tree_walk_bfs<R: Read + Seek>(
720    reader: &mut BlockReader<R>,
721    root_logical: u64,
722    visitor: &mut dyn FnMut(&TreeBlock),
723) -> io::Result<()> {
724    let root_block = reader.read_tree_block(root_logical)?;
725    let root_level = root_block.header().level;
726    visitor(&root_block);
727
728    let mut current_level_ptrs: Vec<u64> = match &*root_block {
729        TreeBlock::Node { ptrs, .. } => {
730            ptrs.iter().map(|p| p.blockptr).collect()
731        }
732        TreeBlock::Leaf { .. } => return Ok(()),
733    };
734
735    for _level in (0..root_level).rev() {
736        let mut next_level_ptrs = Vec::new();
737
738        for logical in &current_level_ptrs {
739            let block = reader.read_tree_block(*logical)?;
740            visitor(&block);
741
742            if let TreeBlock::Node { ptrs, .. } = &*block {
743                next_level_ptrs.extend(ptrs.iter().map(|p| p.blockptr));
744            }
745        }
746
747        current_level_ptrs = next_level_ptrs;
748    }
749
750    Ok(())
751}
752
753/// Walk a tree, continuing past individual block read errors.
754///
755/// Unlike [`tree_walk`], this does not stop when a child block cannot be read.
756/// Instead, it calls `on_error` with the logical address and the I/O error,
757/// then continues with remaining siblings. The root block failure still
758/// propagates since there is nothing to walk.
759///
760/// # Errors
761///
762/// Returns an error only if the root block itself cannot be read.
763pub fn tree_walk_tolerant<R: Read + Seek>(
764    reader: &mut BlockReader<R>,
765    root_logical: u64,
766    visitor: &mut dyn FnMut(&[u8], &TreeBlock),
767    on_error: &mut dyn FnMut(u64, &io::Error),
768) -> io::Result<()> {
769    let buf = reader.read_block(root_logical)?;
770    let block = TreeBlock::parse(&buf);
771    visitor(&buf, &block);
772
773    if let TreeBlock::Node { ptrs, .. } = &block {
774        for ptr in ptrs {
775            tree_walk_tolerant_dfs(reader, ptr.blockptr, visitor, on_error);
776        }
777    }
778
779    Ok(())
780}
781
782fn tree_walk_tolerant_dfs<R: Read + Seek>(
783    reader: &mut BlockReader<R>,
784    logical: u64,
785    visitor: &mut dyn FnMut(&[u8], &TreeBlock),
786    on_error: &mut dyn FnMut(u64, &io::Error),
787) {
788    let buf = match reader.read_block(logical) {
789        Ok(b) => b,
790        Err(e) => {
791            on_error(logical, &e);
792            return;
793        }
794    };
795    let block = TreeBlock::parse(&buf);
796    visitor(&buf, &block);
797
798    if let TreeBlock::Node { ptrs, .. } = &block {
799        for ptr in ptrs {
800            tree_walk_tolerant_dfs(reader, ptr.blockptr, visitor, on_error);
801        }
802    }
803}
804
805/// Walk a tree (DFS), allowing the visitor to modify each block in place.
806///
807/// The visitor receives the raw block buffer and the parsed `TreeBlock`. If it
808/// returns `true`, the block is re-checksummed using `csum_type` and written
809/// back to disk. This is used by operations that need to patch tree block
810/// headers or items (e.g. fsid rewrite, repair).
811///
812/// # Errors
813///
814/// Returns an error if the root block cannot be read or any write fails.
815pub fn tree_walk_mut<R: Read + Write + Seek>(
816    reader: &mut BlockReader<R>,
817    root_logical: u64,
818    csum_type: superblock::ChecksumType,
819    visitor: &mut dyn FnMut(&mut Vec<u8>, &TreeBlock) -> bool,
820) -> io::Result<()> {
821    let mut buf = reader.read_block(root_logical)?;
822    let block = TreeBlock::parse(&buf);
823
824    let child_ptrs: Vec<u64> = if let TreeBlock::Node { ptrs, .. } = &block {
825        ptrs.iter().map(|p| p.blockptr).collect()
826    } else {
827        Vec::new()
828    };
829
830    if visitor(&mut buf, &block) {
831        crate::util::csum_tree_block(&mut buf, csum_type);
832        reader.write_block(root_logical, &buf)?;
833    }
834
835    for ptr in child_ptrs {
836        tree_walk_mut(reader, ptr, csum_type, visitor)?;
837    }
838
839    Ok(())
840}
841
842/// Read a single block and call `visitor` (and optionally walk children with `follow`).
843///
844/// # Errors
845///
846/// Returns an error if any tree block cannot be read.
847pub fn block_visit<R: Read + Seek>(
848    reader: &mut BlockReader<R>,
849    logical: u64,
850    follow: bool,
851    traversal: Traversal,
852    visitor: &mut dyn FnMut(&TreeBlock),
853) -> io::Result<()> {
854    if follow {
855        tree_walk(reader, logical, traversal, visitor)
856    } else {
857        let block = reader.read_tree_block(logical)?;
858        visitor(&block);
859        Ok(())
860    }
861}
862
863/// Statistics collected by walking all blocks of a single B-tree.
864#[derive(Debug, Clone)]
865pub struct TreeStats {
866    /// Total number of tree blocks (nodes and leaves).
867    pub total_nodes: u64,
868    /// Total bytes occupied by tree blocks (`total_nodes × nodesize`).
869    pub total_bytes: u64,
870    /// Total bytes of inline file data (non-zero only when `find_inline` is true).
871    pub total_inline: u64,
872    /// Number of non-contiguous jumps between sibling block addresses.
873    pub total_seeks: u64,
874    /// Seeks where the next sibling is at a higher address.
875    pub forward_seeks: u64,
876    /// Seeks where the next sibling is at a lower address.
877    pub backward_seeks: u64,
878    /// Sum of all seek distances in bytes.
879    pub total_seek_len: u64,
880    /// Largest single seek distance in bytes.
881    pub max_seek_len: u64,
882    /// Number of contiguous block runs (clusters) counted between seeks.
883    pub total_clusters: u64,
884    /// Sum of all cluster sizes in bytes.
885    pub total_cluster_size: u64,
886    /// Smallest cluster size in bytes (`u64::MAX` if no seeks occurred).
887    pub min_cluster_size: u64,
888    /// Largest cluster size in bytes (initialised to `nodesize`).
889    pub max_cluster_size: u64,
890    /// Lowest block bytenr seen during the walk.
891    pub lowest_bytenr: u64,
892    /// Highest block bytenr seen during the walk.
893    pub highest_bytenr: u64,
894    /// Number of blocks at each level: index 0 = leaves, higher = internal.
895    pub node_counts: Vec<u64>,
896    /// Tree height (number of levels, root level + 1).
897    pub levels: u8,
898}
899
900/// Walk `root_logical` collecting [`TreeStats`].
901///
902/// When `find_inline` is true the walk also counts inline extent data bytes
903/// (relevant for subvolume / FS trees which contain `EXTENT_DATA` items).
904///
905/// # Errors
906///
907/// Returns an error if any tree block cannot be read.
908pub fn tree_stats_collect<R: Read + Seek>(
909    reader: &mut BlockReader<R>,
910    root_logical: u64,
911    find_inline: bool,
912) -> io::Result<TreeStats> {
913    let root_block = reader.read_tree_block(root_logical)?;
914    let nodesize = u64::from(reader.nodesize());
915    let root_level = root_block.header().level;
916    let root_bytenr = root_block.header().bytenr;
917
918    let mut stats = TreeStats {
919        total_nodes: 0,
920        total_bytes: 0,
921        total_inline: 0,
922        total_seeks: 0,
923        forward_seeks: 0,
924        backward_seeks: 0,
925        total_seek_len: 0,
926        max_seek_len: 0,
927        total_clusters: 0,
928        total_cluster_size: 0,
929        min_cluster_size: u64::MAX,
930        max_cluster_size: nodesize,
931        lowest_bytenr: root_bytenr,
932        highest_bytenr: root_bytenr,
933        node_counts: vec![0u64; root_level as usize + 1],
934        levels: root_level + 1,
935    };
936
937    walk_stats(reader, &root_block, &mut stats, find_inline, nodesize)?;
938    Ok(stats)
939}
940
941/// Recursively walk a tree block, accumulating stats.
942fn walk_stats<R: Read + Seek>(
943    reader: &mut BlockReader<R>,
944    block: &TreeBlock,
945    stats: &mut TreeStats,
946    find_inline: bool,
947    nodesize: u64,
948) -> io::Result<()> {
949    let level = block.header().level;
950    let bytenr = block.header().bytenr;
951
952    stats.total_nodes += 1;
953    stats.total_bytes += nodesize;
954    if (level as usize) < stats.node_counts.len() {
955        stats.node_counts[level as usize] += 1;
956    }
957    if bytenr < stats.lowest_bytenr {
958        stats.lowest_bytenr = bytenr;
959    }
960    if bytenr > stats.highest_bytenr {
961        stats.highest_bytenr = bytenr;
962    }
963
964    match block {
965        TreeBlock::Leaf { items, data, .. } => {
966            if find_inline {
967                let type_off =
968                    mem::offset_of!(raw::btrfs_file_extent_item, type_);
969                let inline_hdr_size =
970                    mem::offset_of!(raw::btrfs_file_extent_item, disk_bytenr);
971                let header_size = mem::size_of::<raw::btrfs_header>();
972                for item in items {
973                    if item.key.key_type != KeyType::ExtentData {
974                        continue;
975                    }
976                    let start = header_size + item.offset as usize;
977                    if start + type_off >= data.len() {
978                        continue;
979                    }
980                    // BTRFS_FILE_EXTENT_INLINE == 0
981                    if data[start + type_off] == 0
982                        && item.size as usize > inline_hdr_size
983                    {
984                        stats.total_inline +=
985                            u64::from(item.size) - inline_hdr_size as u64;
986                    }
987                }
988            }
989        }
990        TreeBlock::Node { ptrs, .. } => {
991            let mut last_block = bytenr;
992            let mut cluster_size = nodesize;
993
994            for ptr in ptrs {
995                let child = reader.read_tree_block(ptr.blockptr)?;
996                walk_stats(reader, &child, stats, find_inline, nodesize)?;
997
998                let cur = ptr.blockptr;
999                if last_block + nodesize == cur {
1000                    cluster_size += nodesize;
1001                } else {
1002                    let distance = cur.abs_diff(last_block + nodesize);
1003                    stats.total_seeks += 1;
1004                    stats.total_seek_len += distance;
1005                    if distance > stats.max_seek_len {
1006                        stats.max_seek_len = distance;
1007                    }
1008                    if cur > last_block + nodesize {
1009                        stats.forward_seeks += 1;
1010                    } else {
1011                        stats.backward_seeks += 1;
1012                    }
1013                    if cluster_size != nodesize {
1014                        stats.total_clusters += 1;
1015                        stats.total_cluster_size += cluster_size;
1016                        if cluster_size < stats.min_cluster_size {
1017                            stats.min_cluster_size = cluster_size;
1018                        }
1019                        if cluster_size > stats.max_cluster_size {
1020                            stats.max_cluster_size = cluster_size;
1021                        }
1022                    }
1023                    cluster_size = nodesize;
1024                }
1025                last_block = cur;
1026            }
1027        }
1028    }
1029
1030    Ok(())
1031}
1032
1033fn collect_root_items<R: Read + Seek>(
1034    reader: &mut BlockReader<R>,
1035    logical: u64,
1036    tree_roots: &mut BTreeMap<u64, (u64, u64)>,
1037) -> io::Result<()> {
1038    let block = reader.read_tree_block(logical)?;
1039
1040    match &*block {
1041        TreeBlock::Leaf { items, data, .. } => {
1042            // ROOT_ITEM contains btrfs_root_item; the bytenr field gives
1043            // the root block of that tree.
1044            let header_size = mem::size_of::<raw::btrfs_header>();
1045            let root_item_bytenr_offset = {
1046                // bytenr is after inode (160 bytes) + generation (8) + root_dirid (8)
1047                // = offset 176 in btrfs_root_item
1048                mem::offset_of!(raw::btrfs_root_item, bytenr)
1049            };
1050
1051            for item in items {
1052                if item.key.key_type != KeyType::RootItem {
1053                    continue;
1054                }
1055                let item_start = header_size + item.offset as usize;
1056                if item_start + root_item_bytenr_offset + 8 > data.len() {
1057                    continue;
1058                }
1059                let mut buf = &data[item_start + root_item_bytenr_offset..];
1060                let bytenr = buf.get_u64_le();
1061                if bytenr != 0 {
1062                    tree_roots
1063                        .insert(item.key.objectid, (bytenr, item.key.offset));
1064                }
1065            }
1066        }
1067        TreeBlock::Node { ptrs, .. } => {
1068            for ptr in ptrs {
1069                collect_root_items(reader, ptr.blockptr, tree_roots)?;
1070            }
1071        }
1072    }
1073
1074    Ok(())
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079    use super::*;
1080    use crate::chunk::{ChunkMapping, Stripe};
1081    use std::io::Cursor;
1082    use uuid::Uuid;
1083
1084    /// Build a per-device cursor backed by a `len`-byte zero-filled
1085    /// `Vec<u8>` for use as a fake device.
1086    fn make_device(len: usize) -> Cursor<Vec<u8>> {
1087        Cursor::new(vec![0u8; len])
1088    }
1089
1090    /// Build a chunk mapping with arbitrary stripes. Each entry is
1091    /// `(devid, physical_offset)`. The profile defaults to SINGLE for
1092    /// one-stripe mappings and RAID1 for multi-stripe (matches what
1093    /// the old `resolve_all`-based tests assumed).
1094    fn make_mapping(
1095        logical: u64,
1096        length: u64,
1097        stripes: &[(u64, u64)],
1098    ) -> ChunkMapping {
1099        let chunk_type = if stripes.len() == 1 {
1100            0 // SINGLE
1101        } else {
1102            // Pick a profile that fans out to all stripes regardless of
1103            // count: DUP when stripes share a devid, otherwise
1104            // RAID1 / RAID1C3 / RAID1C4 by mirror count.
1105            let same_devid = stripes.iter().all(|s| s.0 == stripes[0].0);
1106            if same_devid {
1107                u64::from(raw::BTRFS_BLOCK_GROUP_DUP)
1108            } else {
1109                u64::from(match stripes.len() {
1110                    3 => raw::BTRFS_BLOCK_GROUP_RAID1C3,
1111                    4 => raw::BTRFS_BLOCK_GROUP_RAID1C4,
1112                    // Default: RAID1 (covers 2 stripes and any
1113                    // unexpected count — every stripe gets the bytes).
1114                    _ => raw::BTRFS_BLOCK_GROUP_RAID1,
1115                })
1116            }
1117        };
1118        ChunkMapping {
1119            logical,
1120            length,
1121            stripe_len: 65536,
1122            chunk_type,
1123            num_stripes: stripes.len() as u16,
1124            sub_stripes: 0,
1125            stripes: stripes
1126                .iter()
1127                .map(|&(devid, offset)| Stripe {
1128                    devid,
1129                    offset,
1130                    dev_uuid: Uuid::nil(),
1131                })
1132                .collect(),
1133        }
1134    }
1135
1136    /// Build a `BlockReader` over the supplied per-devid cursors with
1137    /// the given chunk cache contents and a 4 KiB nodesize.
1138    fn make_reader(
1139        devices: &[(u64, usize)],
1140        mappings: &[ChunkMapping],
1141    ) -> BlockReader<Cursor<Vec<u8>>> {
1142        let mut handles = BTreeMap::new();
1143        for &(devid, len) in devices {
1144            handles.insert(devid, make_device(len));
1145        }
1146        let mut cache = ChunkTreeCache::default();
1147        for m in mappings {
1148            cache.insert(m.clone());
1149        }
1150        BlockReader::new_multi(handles, 4096, cache)
1151    }
1152
1153    #[test]
1154    fn read_block_routes_to_correct_devid() {
1155        // Two devices, each carrying distinguishable bytes at the
1156        // physical offset that the chunk mapping resolves the logical
1157        // address to. resolve picks stripe[0]; we put devid=2 first
1158        // in the stripe list to verify routing follows that, not the
1159        // numerically-lowest devid.
1160        let mapping =
1161            make_mapping(1_000_000, 4096, &[(2, 50_000), (1, 20_000)]);
1162        let mut reader = make_reader(&[(1, 100_000), (2, 100_000)], &[mapping]);
1163
1164        // Seed each device's cursor with a recognizable byte at the
1165        // physical offset we'll route to.
1166        reader.devices_mut().get_mut(&1).unwrap().get_mut()
1167            [20_000..20_000 + 4096]
1168            .fill(0xAA);
1169        reader.devices_mut().get_mut(&2).unwrap().get_mut()
1170            [50_000..50_000 + 4096]
1171            .fill(0xBB);
1172
1173        // resolve picks stripe[0] = (devid=2, phys=50_000), so the
1174        // read returns 0xBB bytes.
1175        let buf = reader.read_block(1_000_000).expect("read_block");
1176        assert_eq!(buf.len(), 4096);
1177        assert!(buf.iter().all(|&b| b == 0xBB), "expected all 0xBB");
1178    }
1179
1180    #[test]
1181    fn read_data_routes_to_correct_devid() {
1182        let mapping = make_mapping(1_000_000, 4096, &[(5, 8000)]);
1183        let mut reader = make_reader(&[(5, 100_000)], &[mapping]);
1184        reader.devices_mut().get_mut(&5).unwrap().get_mut()[8000..8000 + 100]
1185            .fill(0xCC);
1186
1187        let buf = reader.read_data(1_000_000, 100).expect("read_data");
1188        assert_eq!(buf, vec![0xCC; 100]);
1189    }
1190
1191    #[test]
1192    fn write_block_fans_out_to_all_stripes() {
1193        // RAID1: 2 devices, write_block must update both at the
1194        // resolved physical offsets, leaving everything else zero.
1195        let mapping = make_mapping(2_000_000, 4096, &[(1, 1000), (2, 7000)]);
1196        let mut reader = make_reader(&[(1, 100_000), (2, 100_000)], &[mapping]);
1197
1198        let payload = vec![0xDDu8; 4096];
1199        reader
1200            .write_block(2_000_000, &payload)
1201            .expect("write_block");
1202
1203        let dev1 = reader.devices().get(&1).unwrap().get_ref();
1204        let dev2 = reader.devices().get(&2).unwrap().get_ref();
1205        assert_eq!(&dev1[1000..1000 + 4096], &payload[..]);
1206        assert_eq!(&dev2[7000..7000 + 4096], &payload[..]);
1207        // Untouched regions still zero on both devices.
1208        assert!(dev1[..1000].iter().all(|&b| b == 0));
1209        assert!(dev1[1000 + 4096..].iter().all(|&b| b == 0));
1210        assert!(dev2[..7000].iter().all(|&b| b == 0));
1211        assert!(dev2[7000 + 4096..].iter().all(|&b| b == 0));
1212    }
1213
1214    #[test]
1215    fn write_block_fans_out_to_dup_same_devid() {
1216        // DUP profile: both copies on the same device, at distinct
1217        // physical offsets. write_block must hit both.
1218        let mapping = make_mapping(3_000_000, 4096, &[(1, 1000), (1, 50_000)]);
1219        let mut reader = make_reader(&[(1, 100_000)], &[mapping]);
1220
1221        let payload = vec![0xEEu8; 4096];
1222        reader
1223            .write_block(3_000_000, &payload)
1224            .expect("write_block");
1225
1226        let dev = reader.devices().get(&1).unwrap().get_ref();
1227        assert_eq!(&dev[1000..1000 + 4096], &payload[..]);
1228        assert_eq!(&dev[50_000..50_000 + 4096], &payload[..]);
1229    }
1230
1231    #[test]
1232    fn write_block_three_devices_raid1c3() {
1233        let mapping = make_mapping(4_000_000, 4096, &[(1, 0), (2, 0), (3, 0)]);
1234        let mut reader =
1235            make_reader(&[(1, 8192), (2, 8192), (3, 8192)], &[mapping]);
1236
1237        let payload = vec![0xFFu8; 4096];
1238        reader
1239            .write_block(4_000_000, &payload)
1240            .expect("write_block");
1241
1242        for &devid in &[1u64, 2, 3] {
1243            let dev = reader.devices().get(&devid).unwrap().get_ref();
1244            assert_eq!(
1245                &dev[..4096],
1246                &payload[..],
1247                "devid {devid} mirror missing"
1248            );
1249        }
1250    }
1251
1252    #[test]
1253    fn read_block_missing_devid_errors() {
1254        // Chunk cache references devid 9, but the handle map only has
1255        // devids 1 and 2. Reads must surface a clear error rather than
1256        // panicking or silently mis-routing.
1257        let mapping = make_mapping(5_000_000, 4096, &[(9, 0)]);
1258        let mut reader = make_reader(&[(1, 8192), (2, 8192)], &[mapping]);
1259
1260        let err = reader.read_block(5_000_000).unwrap_err();
1261        assert_eq!(err.kind(), io::ErrorKind::NotFound);
1262        assert!(
1263            err.to_string().contains("device 9"),
1264            "expected error to mention devid 9, got: {err}"
1265        );
1266    }
1267
1268    #[test]
1269    fn write_block_missing_devid_errors() {
1270        let mapping = make_mapping(5_000_000, 4096, &[(1, 0), (9, 0)]);
1271        let mut reader = make_reader(&[(1, 8192)], &[mapping]);
1272
1273        let err = reader.write_block(5_000_000, &[0u8; 4096]).unwrap_err();
1274        assert_eq!(err.kind(), io::ErrorKind::NotFound);
1275        assert!(err.to_string().contains("device 9"));
1276    }
1277
1278    #[test]
1279    fn read_block_unmapped_logical_errors() {
1280        // No chunk for this logical address.
1281        let mut reader = make_reader(&[(1, 8192)], &[]);
1282        let err = reader.read_block(1_000_000).unwrap_err();
1283        assert_eq!(err.kind(), io::ErrorKind::NotFound);
1284        assert!(err.to_string().contains("not mapped"));
1285    }
1286
1287    #[test]
1288    fn new_single_inserts_under_supplied_devid() {
1289        // BlockReader::new wraps the handle under the explicit devid
1290        // so multi-device callers can mix it with other handles later.
1291        let cursor = make_device(8192);
1292        let cache = ChunkTreeCache::default();
1293        let reader = BlockReader::new(cursor, 7, 4096, cache);
1294        assert_eq!(reader.devices().len(), 1);
1295        assert!(reader.devices().contains_key(&7));
1296    }
1297
1298    #[test]
1299    fn new_multi_with_disjoint_devids() {
1300        // Sparse devid map (1 and 5 only — devid 3 was removed).
1301        // Both reads and writes should route correctly.
1302        let mapping = make_mapping(0, 4096, &[(1, 100), (5, 200)]);
1303        let mut reader = make_reader(&[(1, 8192), (5, 8192)], &[mapping]);
1304
1305        let payload = vec![0x77u8; 4096];
1306        reader.write_block(0, &payload).expect("write_block");
1307        let dev1 = reader.devices().get(&1).unwrap().get_ref();
1308        let dev5 = reader.devices().get(&5).unwrap().get_ref();
1309        assert_eq!(&dev1[100..100 + 4096], &payload[..]);
1310        assert_eq!(&dev5[200..200 + 4096], &payload[..]);
1311    }
1312
1313    #[test]
1314    #[should_panic(expected = "filesystem has 2 devices")]
1315    fn single_device_mut_panics_on_multi_device() {
1316        let mapping = make_mapping(0, 4096, &[(1, 0), (2, 0)]);
1317        let mut reader = make_reader(&[(1, 4096), (2, 4096)], &[mapping]);
1318        let _ = reader.single_device_mut();
1319    }
1320
1321    #[test]
1322    fn single_device_mut_returns_handle_for_single_device() {
1323        let mapping = make_mapping(0, 4096, &[(1, 0)]);
1324        let mut reader = make_reader(&[(1, 8192)], &[mapping]);
1325        // Verify it doesn't panic and we can use the returned handle.
1326        // Write a marker byte and confirm via the map view.
1327        reader.single_device_mut().get_mut()[42] = 0x99;
1328        assert_eq!(reader.devices().get(&1).unwrap().get_ref()[42], 0x99);
1329    }
1330}