btrfs_transaction/filesystem.rs
1//! # In-memory filesystem state for a transaction session
2//!
3//! `Filesystem` is the central state object for modifying a btrfs filesystem. It
4//! wraps a `BlockReader` (from `btrfs-disk`), holds the parsed superblock, all
5//! tree root pointers, and tracks which blocks have been modified during the
6//! current transaction.
7//!
8//! Open a device or image with [`Filesystem::open`], then use the read/write
9//! methods to access tree blocks through `ExtentBuffer`.
10
11use crate::buffer::ExtentBuffer;
12use btrfs_disk::{
13 chunk::ChunkTreeCache,
14 items::DeviceItem,
15 reader::{self, BlockReader, OpenFilesystem},
16 superblock::{self, Superblock},
17};
18use std::{
19 collections::{BTreeMap, BTreeSet},
20 fs::File,
21 io::{self, Read, Seek, Write},
22};
23
24/// In-memory filesystem state for a transaction session.
25///
26/// Holds everything needed to read and write tree blocks: the block reader
27/// (with chunk cache for logical-to-physical resolution), the superblock,
28/// all tree root pointers, the current transaction generation, and the set
29/// of dirty (modified) block addresses.
30pub struct Filesystem<R> {
31 /// Block reader with fully populated chunk cache.
32 reader: BlockReader<R>,
33 /// Parsed superblock (updated in-memory during transactions).
34 pub superblock: Superblock,
35 /// Map of tree ID to root block logical address.
36 roots: BTreeMap<u64, u64>,
37 /// Snapshot of root bytenrs at transaction start. Used to detect which
38 /// trees had their root block change during the transaction.
39 original_roots: BTreeMap<u64, u64>,
40 /// Logical addresses of blocks modified in the current transaction.
41 /// `BTreeSet` gives sorted iteration in `flush_dirty` for sequential I/O.
42 dirty: BTreeSet<u64>,
43 /// Current transaction generation (superblock.generation + 1 during a
44 /// transaction, or superblock.generation when idle).
45 pub generation: u64,
46 /// Tree block size in bytes.
47 pub nodesize: u32,
48 /// Minimum I/O unit in bytes.
49 pub sectorsize: u32,
50 /// In-memory cache of extent buffers read or created during the transaction.
51 /// Keyed by logical address. This avoids re-reading blocks from disk and
52 /// ensures modifications are visible within the same transaction.
53 block_cache: BTreeMap<u64, ExtentBuffer>,
54 /// Logical addresses of blocks that have been written to stable storage
55 /// (via `flush_dirty` or `write_block`). A block in this set must be
56 /// COW'd before modification even if its generation matches the current
57 /// transaction, because the on-disk copy is now part of the committed
58 /// state and overwriting it would break crash consistency.
59 written: BTreeSet<u64>,
60 /// Override for the block-group-tree id used by
61 /// [`block_group_tree_id`](Self::block_group_tree_id). When `Some`,
62 /// callers see this id instead of the auto-detected one. Used by
63 /// the `convert-to-block-group-tree` path to pin allocator
64 /// metadata to the extent tree (id 2) while the new BGT (id 11)
65 /// is being built and is therefore only partially populated.
66 /// Should always be cleared via [`BgTreeOverrideGuard`] (RAII)
67 /// rather than written directly, so panics or early returns
68 /// cannot leak the override into normal operation.
69 bg_tree_override: Option<u64>,
70 /// Per-device superblock `dev_item` snapshots taken at open time.
71 /// The key is `devid`; the value is that device's local `dev_item`
72 /// (which encodes the device's identity: `devid`, `dev_uuid`,
73 /// per-device `bytes_used`, etc.). On commit, each device's
74 /// superblock is rewritten with these per-device fields preserved
75 /// so a multi-device filesystem doesn't get clobbered with the
76 /// primary device's identity.
77 per_device_dev_items: BTreeMap<u64, DeviceItem>,
78}
79
80impl<R: Read + Write + Seek> Filesystem<R> {
81 /// Open a btrfs filesystem from a readable+writable+seekable handle.
82 ///
83 /// Performs the full bootstrap sequence (superblock, chunk cache, root
84 /// tree), then wraps the result into an `Filesystem` ready for transactions.
85 ///
86 /// # Errors
87 ///
88 /// Returns an error if any I/O operation fails during bootstrap.
89 pub fn open(handle: R) -> io::Result<Self> {
90 let OpenFilesystem {
91 reader,
92 superblock,
93 tree_roots,
94 per_device_dev_items,
95 } = reader::filesystem_open(handle)?;
96
97 let generation = superblock.generation;
98 let nodesize = superblock.nodesize;
99 let sectorsize = superblock.sectorsize;
100
101 // Convert BTreeMap<u64, (u64, u64)> to BTreeMap<u64, u64> (tree_id -> root bytenr)
102 let mut roots: BTreeMap<u64, u64> = tree_roots
103 .into_iter()
104 .map(|(id, (bytenr, _offset))| (id, bytenr))
105 .collect();
106
107 // The root tree and chunk tree roots live in the superblock, not in
108 // ROOT_ITEM entries. Add them explicitly.
109 roots.insert(1, superblock.root);
110 roots.insert(3, superblock.chunk_root);
111
112 let original_roots = roots.clone();
113
114 Ok(Self {
115 reader,
116 superblock,
117 roots,
118 original_roots,
119 dirty: BTreeSet::new(),
120 generation,
121 nodesize,
122 sectorsize,
123 block_cache: BTreeMap::new(),
124 written: BTreeSet::new(),
125 bg_tree_override: None,
126 per_device_dev_items,
127 })
128 }
129
130 /// Open a btrfs filesystem using a specific superblock mirror.
131 ///
132 /// # Errors
133 ///
134 /// Returns an error if any I/O operation fails during bootstrap.
135 pub fn open_mirror(handle: R, mirror: u32) -> io::Result<Self> {
136 let OpenFilesystem {
137 reader,
138 superblock,
139 tree_roots,
140 per_device_dev_items,
141 } = reader::filesystem_open_mirror(handle, mirror)?;
142
143 let generation = superblock.generation;
144 let nodesize = superblock.nodesize;
145 let sectorsize = superblock.sectorsize;
146
147 let mut roots: BTreeMap<u64, u64> = tree_roots
148 .into_iter()
149 .map(|(id, (bytenr, _offset))| (id, bytenr))
150 .collect();
151
152 roots.insert(1, superblock.root);
153 roots.insert(3, superblock.chunk_root);
154
155 let original_roots = roots.clone();
156
157 Ok(Self {
158 reader,
159 superblock,
160 roots,
161 original_roots,
162 dirty: BTreeSet::new(),
163 generation,
164 nodesize,
165 sectorsize,
166 block_cache: BTreeMap::new(),
167 written: BTreeSet::new(),
168 bg_tree_override: None,
169 per_device_dev_items,
170 })
171 }
172
173 /// Open a multi-device btrfs filesystem from a `devid -> handle` map.
174 ///
175 /// Every device referenced by the filesystem's chunk tree must be
176 /// present in `devices`. Each device's superblock is read and
177 /// validated against the map key; all devices must share the same
178 /// `fsid`. The bootstrap fails with a clear error if any of these
179 /// invariants is violated.
180 ///
181 /// # Errors
182 ///
183 /// Returns an error if the device map is empty, any device's
184 /// superblock disagrees with its key or the primary's `fsid`, the
185 /// chunk tree references a devid not in the map, or any I/O fails.
186 pub fn open_multi(devices: BTreeMap<u64, R>) -> io::Result<Self> {
187 let OpenFilesystem {
188 reader,
189 superblock,
190 tree_roots,
191 per_device_dev_items,
192 } = reader::filesystem_open_multi(devices)?;
193
194 let generation = superblock.generation;
195 let nodesize = superblock.nodesize;
196 let sectorsize = superblock.sectorsize;
197
198 let mut roots: BTreeMap<u64, u64> = tree_roots
199 .into_iter()
200 .map(|(id, (bytenr, _offset))| (id, bytenr))
201 .collect();
202
203 roots.insert(1, superblock.root);
204 roots.insert(3, superblock.chunk_root);
205
206 let original_roots = roots.clone();
207
208 Ok(Self {
209 reader,
210 superblock,
211 roots,
212 original_roots,
213 dirty: BTreeSet::new(),
214 generation,
215 nodesize,
216 sectorsize,
217 block_cache: BTreeMap::new(),
218 written: BTreeSet::new(),
219 bg_tree_override: None,
220 per_device_dev_items,
221 })
222 }
223
224 /// Open a btrfs filesystem using a pre-built chunk cache.
225 ///
226 /// Skips the chunk tree walk entirely, using the provided cache for
227 /// logical-to-physical address resolution. This is the entry point
228 /// for recovery tools like `rescue chunk-recover --apply`, where the
229 /// on-disk chunk tree is damaged and the cache has been reconstructed
230 /// from a raw device scan.
231 ///
232 /// # Errors
233 ///
234 /// Returns an error if any I/O operation fails during bootstrap.
235 pub fn open_with_chunk_cache(
236 handle: R,
237 mirror: u32,
238 chunk_cache: ChunkTreeCache,
239 ) -> io::Result<Self> {
240 let OpenFilesystem {
241 reader,
242 superblock,
243 tree_roots,
244 per_device_dev_items,
245 } = reader::filesystem_open_with_cache(handle, mirror, chunk_cache)?;
246
247 let generation = superblock.generation;
248 let nodesize = superblock.nodesize;
249 let sectorsize = superblock.sectorsize;
250
251 let mut roots: BTreeMap<u64, u64> = tree_roots
252 .into_iter()
253 .map(|(id, (bytenr, _offset))| (id, bytenr))
254 .collect();
255
256 roots.insert(1, superblock.root);
257 roots.insert(3, superblock.chunk_root);
258
259 let original_roots = roots.clone();
260
261 Ok(Self {
262 reader,
263 superblock,
264 roots,
265 original_roots,
266 dirty: BTreeSet::new(),
267 generation,
268 nodesize,
269 sectorsize,
270 block_cache: BTreeMap::new(),
271 written: BTreeSet::new(),
272 bg_tree_override: None,
273 per_device_dev_items,
274 })
275 }
276
277 /// Read a tree block at the given logical address, returning an `ExtentBuffer`.
278 ///
279 /// If the block is already in the in-memory cache (e.g. it was COW'd or
280 /// previously read in this transaction), the cached version is returned
281 /// without hitting disk.
282 ///
283 /// # Errors
284 ///
285 /// Returns an error if the block cannot be read from disk.
286 pub fn read_block(&mut self, logical: u64) -> io::Result<ExtentBuffer> {
287 if let Some(eb) = self.block_cache.get(&logical) {
288 return Ok(eb.clone());
289 }
290 let data = self.reader.read_block(logical)?;
291 let eb = ExtentBuffer::from_raw(data, logical);
292 self.block_cache.insert(logical, eb.clone());
293 Ok(eb)
294 }
295
296 /// Write an extent buffer to disk and mark it dirty.
297 ///
298 /// The buffer's checksum is updated before writing. The block is also
299 /// stored in the in-memory cache so subsequent reads see the modification.
300 ///
301 /// # Errors
302 ///
303 /// Returns an error if the write fails.
304 pub fn write_block(&mut self, eb: &mut ExtentBuffer) -> io::Result<()> {
305 eb.update_checksum(self.superblock.csum_type);
306 self.reader.write_block(eb.logical(), eb.as_bytes())?;
307 self.dirty.insert(eb.logical());
308 self.written.insert(eb.logical());
309 self.block_cache.insert(eb.logical(), eb.clone());
310 Ok(())
311 }
312
313 /// Store an extent buffer in the cache and mark it dirty, without writing
314 /// to disk yet. The actual disk write happens at commit time.
315 pub fn mark_dirty(&mut self, eb: &ExtentBuffer) {
316 // Every modified block passes through here. Validate structural
317 // invariants in debug builds to catch corruption early.
318 debug_assert!(
319 eb.check().is_ok(),
320 "mark_dirty: block at {} failed validation: {}",
321 eb.logical(),
322 eb.check().unwrap_err(),
323 );
324 // The block's generation should match the current transaction.
325 // A dirty block from an older generation means COW was skipped.
326 debug_assert_eq!(
327 eb.generation(),
328 self.generation,
329 "mark_dirty: block at {} has generation {} but filesystem \
330 generation is {} (was COW skipped?)",
331 eb.logical(),
332 eb.generation(),
333 self.generation,
334 );
335 self.dirty.insert(eb.logical());
336 self.block_cache.insert(eb.logical(), eb.clone());
337 }
338
339 /// Insert a block into the in-memory cache without marking it dirty.
340 ///
341 /// This is for test code that needs to simulate blocks already present
342 /// on disk. Production code should use [`mark_dirty`](Self::mark_dirty)
343 /// (for newly modified blocks) or [`read_block`](Self::read_block)
344 /// (to read from disk).
345 #[doc(hidden)]
346 pub fn seed_cache(&mut self, eb: &ExtentBuffer) {
347 self.block_cache.insert(eb.logical(), eb.clone());
348 }
349
350 /// Return the root block logical address for the given tree ID.
351 #[must_use]
352 pub fn root_bytenr(&self, tree_id: u64) -> Option<u64> {
353 self.roots.get(&tree_id).copied()
354 }
355
356 /// Update the root block logical address for a tree.
357 pub fn set_root_bytenr(&mut self, tree_id: u64, bytenr: u64) {
358 self.roots.insert(tree_id, bytenr);
359 }
360
361 /// Read the root block of the given tree as an `ExtentBuffer`.
362 ///
363 /// # Errors
364 ///
365 /// Returns an error if the tree ID is unknown or the block cannot be read.
366 pub fn root_node(&mut self, tree_id: u64) -> io::Result<ExtentBuffer> {
367 let bytenr = self.root_bytenr(tree_id).ok_or_else(|| {
368 io::Error::new(
369 io::ErrorKind::NotFound,
370 format!("unknown tree ID {tree_id}"),
371 )
372 })?;
373 self.read_block(bytenr)
374 }
375
376 /// Return an iterator over all dirty block logical addresses.
377 pub fn dirty_blocks(&self) -> impl Iterator<Item = u64> + '_ {
378 self.dirty.iter().copied()
379 }
380
381 /// Return the number of dirty blocks.
382 #[must_use]
383 pub fn dirty_count(&self) -> usize {
384 self.dirty.len()
385 }
386
387 /// Check whether a block has been written to stable storage during
388 /// this transaction. Such blocks must be COW'd before modification
389 /// even if their generation matches the current transaction.
390 #[must_use]
391 pub fn is_written(&self, logical: u64) -> bool {
392 self.written.contains(&logical)
393 }
394
395 /// Clear the dirty and written sets (used after commit or abort).
396 pub fn clear_dirty(&mut self) {
397 self.dirty.clear();
398 self.written.clear();
399 }
400
401 /// Clear the block cache (used after commit or abort to free memory).
402 pub fn clear_cache(&mut self) {
403 self.block_cache.clear();
404 }
405
406 /// Return all tree root entries as `(tree_id, root_bytenr)` pairs.
407 pub fn tree_roots(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
408 self.roots.iter().map(|(&id, &bytenr)| (id, bytenr))
409 }
410
411 /// Flush all dirty blocks to disk.
412 ///
413 /// Iterates the dirty set, checksums each cached block, and writes it.
414 /// Blocks that are dirty but not in the cache are skipped (they were
415 /// already written by `write_block`).
416 ///
417 /// # Errors
418 ///
419 /// Returns an error if any write fails.
420 pub fn flush_dirty(&mut self) -> io::Result<()> {
421 /// `BTRFS_HEADER_FLAG_WRITTEN` (bit 0): the kernel requires this
422 /// flag on all tree blocks that have been committed to stable
423 /// storage. Must be set before computing the checksum.
424 const HEADER_FLAG_WRITTEN: u64 = 1 << 0;
425
426 let dirty: Vec<u64> = self.dirty.iter().copied().collect();
427 for logical in dirty {
428 if let Some(eb) = self.block_cache.get(&logical).cloned() {
429 let mut eb = eb;
430 // Validate before writing to stable storage. This is
431 // the last chance to catch corruption before it lands
432 // on disk.
433 debug_assert!(
434 eb.check().is_ok(),
435 "flush_dirty: block at {} failed validation before \
436 write: {}",
437 eb.logical(),
438 eb.check().unwrap_err(),
439 );
440 eb.set_flags(eb.flags() | HEADER_FLAG_WRITTEN);
441 eb.update_checksum(self.superblock.csum_type);
442 self.reader.write_block(eb.logical(), eb.as_bytes())?;
443 self.written.insert(eb.logical());
444 }
445 }
446 Ok(())
447 }
448
449 /// Return a mutable reference to the underlying block reader.
450 pub fn reader_mut(&mut self) -> &mut BlockReader<R> {
451 &mut self.reader
452 }
453
454 /// Return a reference to the underlying block reader.
455 #[must_use]
456 pub fn reader(&self) -> &BlockReader<R> {
457 &self.reader
458 }
459
460 /// Remove a tree root entry.
461 pub fn remove_root(&mut self, tree_id: u64) -> Option<u64> {
462 self.roots.remove(&tree_id)
463 }
464
465 /// Evict a block from the cache (e.g. after freeing it).
466 pub fn evict_block(&mut self, logical: u64) {
467 self.block_cache.remove(&logical);
468 self.dirty.remove(&logical);
469 }
470
471 /// Snapshot the current roots so we can detect changes at commit time.
472 ///
473 /// Called at transaction start to record the baseline state.
474 pub fn snapshot_roots(&mut self) {
475 self.original_roots = self.roots.clone();
476 }
477
478 /// Restore the roots map to the last snapshot. Used by
479 /// `Transaction::abort` to roll back in-memory `set_root_bytenr`
480 /// changes that pointed at COWed-but-never-written bytenrs.
481 pub fn restore_roots_from_snapshot(&mut self) {
482 self.roots = self.original_roots.clone();
483 }
484
485 /// Write the in-memory superblock to all 3 mirrors of every open
486 /// device, with each device's per-device `dev_item` spliced in.
487 ///
488 /// On a multi-device filesystem each device's superblock has its
489 /// own `dev_item` (`devid`, `dev_uuid`, per-device `bytes_used`, etc.).
490 /// Writing the primary device's superblock verbatim to a secondary
491 /// would corrupt the secondary's identity. This helper preserves
492 /// that per-device state by splicing the matching `dev_item` from
493 /// the snapshot taken at open time before serializing each
494 /// device's variant.
495 ///
496 /// # Errors
497 ///
498 /// Returns an error if any device referenced by the dev-item
499 /// snapshot is not open, or if any underlying write fails.
500 pub fn write_superblock_all_devices(&mut self) -> io::Result<()> {
501 // Collect the devids first so we don't hold a borrow on
502 // `self.reader` while iterating per-device serializations.
503 let devids: Vec<u64> = self.reader.devices().keys().copied().collect();
504 for devid in devids {
505 let mut sb_for_dev = self.superblock.clone();
506 if let Some(dev_item) = self.per_device_dev_items.get(&devid) {
507 sb_for_dev.dev_item = dev_item.clone();
508 }
509 let bytes = sb_for_dev.to_bytes();
510 let dev = self
511 .reader
512 .devices_mut()
513 .get_mut(&devid)
514 .expect("devid present in iteration but missing from map");
515 superblock::write_superblock_all_mirrors(dev, &bytes)?;
516 }
517 Ok(())
518 }
519
520 /// Flush pending writes via `Write::flush()` on every device.
521 ///
522 /// Flushes userspace write buffers on every open device. For
523 /// file-backed storage, use [`Filesystem<File>::sync`] instead,
524 /// which also calls fsync per device.
525 pub fn flush_writes(&mut self) -> io::Result<()> {
526 for dev in self.reader.devices_mut().values_mut() {
527 dev.flush()?;
528 }
529 Ok(())
530 }
531
532 /// Return tree IDs whose root block changed since the last snapshot.
533 ///
534 /// Compares current roots against the snapshot taken at transaction start.
535 /// Excludes tree IDs 1 (root tree) and 3 (chunk tree) since their root
536 /// pointers live in the superblock, not in root items.
537 #[must_use]
538 pub fn changed_roots(&self) -> Vec<(u64, u64, u8)> {
539 let mut changed = Vec::new();
540 for (&tree_id, ¤t_bytenr) in &self.roots {
541 // Root tree and chunk tree are updated via superblock, not root items
542 if tree_id == 1 || tree_id == 3 {
543 continue;
544 }
545 let original = self.original_roots.get(&tree_id).copied();
546 if original != Some(current_bytenr) {
547 // Look up the level from the cached block if available
548 let level = self
549 .block_cache
550 .get(¤t_bytenr)
551 .map_or(0, ExtentBuffer::level);
552 changed.push((tree_id, current_bytenr, level));
553 }
554 }
555 changed
556 }
557
558 /// Return the tree id that holds `BLOCK_GROUP_ITEM` records.
559 ///
560 /// When [`bg_tree_override`](Self::bg_tree_override_for_test) is
561 /// set (typically by the `convert-to-block-group-tree` path),
562 /// returns it verbatim. Otherwise auto-detects: returns 11
563 /// (`BLOCK_GROUP_TREE`) if a root for tree 11 is registered,
564 /// else 2 (`EXTENT_TREE`).
565 ///
566 /// All allocator and block-group-update code paths must consult
567 /// this accessor instead of duplicating the routing logic, so
568 /// that the override mechanism actually works for everything
569 /// that touches block-group state.
570 #[must_use]
571 pub fn block_group_tree_id(&self) -> u64 {
572 if let Some(id) = self.bg_tree_override {
573 return id;
574 }
575 if self.root_bytenr(11).is_some() {
576 11
577 } else {
578 2
579 }
580 }
581
582 /// Set the block-group-tree id override. Prefer
583 /// [`pin_block_group_tree`](Self::pin_block_group_tree) which
584 /// returns an RAII guard that clears the override on drop.
585 ///
586 /// Exposed primarily for unit tests of the routing primitive.
587 #[doc(hidden)]
588 pub fn bg_tree_override_for_test(&mut self, id: Option<u64>) {
589 self.bg_tree_override = id;
590 }
591
592 /// Pin [`block_group_tree_id`](Self::block_group_tree_id) to
593 /// the given tree id and return a guard that restores the
594 /// previous override (typically `None`) when dropped.
595 ///
596 /// Use this in conversion paths so that panics or `?`
597 /// early-returns cannot leave the override stuck on the wrong
598 /// value.
599 pub fn pin_block_group_tree(
600 &mut self,
601 id: u64,
602 ) -> BgTreeOverrideGuard<'_, R> {
603 let prev = self.bg_tree_override;
604 self.bg_tree_override = Some(id);
605 BgTreeOverrideGuard { fs: self, prev }
606 }
607}
608
609/// RAII guard that restores the previous block-group-tree
610/// override on drop. Created by
611/// [`Filesystem::pin_block_group_tree`].
612pub struct BgTreeOverrideGuard<'a, R> {
613 fs: &'a mut Filesystem<R>,
614 prev: Option<u64>,
615}
616
617impl<R> BgTreeOverrideGuard<'_, R> {
618 /// Borrow the underlying filesystem mutably for the duration
619 /// of the guard.
620 pub fn fs_mut(&mut self) -> &mut Filesystem<R> {
621 self.fs
622 }
623}
624
625impl<R> Drop for BgTreeOverrideGuard<'_, R> {
626 fn drop(&mut self) {
627 self.fs.bg_tree_override = self.prev;
628 }
629}
630
631impl Filesystem<File> {
632 /// Sync all data to stable storage on every device (fsync).
633 ///
634 /// Calls `File::sync_all()` on every open device handle, ensuring
635 /// all written data reaches stable storage. This should be called
636 /// after commit to guarantee durability.
637 pub fn sync(&mut self) -> io::Result<()> {
638 for dev in self.reader.devices_mut().values_mut() {
639 dev.sync_all()?;
640 }
641 Ok(())
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648
649 // Filesystem requires a real filesystem image to test meaningfully.
650 // These are basic structural tests; full integration tests will use
651 // temporary images created by btrfs-mkfs.
652
653 #[test]
654 fn dirty_tracking() {
655 // We can test the dirty set logic without a real filesystem
656 let mut dirty = BTreeSet::new();
657 dirty.insert(65536u64);
658 dirty.insert(131072);
659 assert_eq!(dirty.len(), 2);
660 assert!(dirty.contains(&65536));
661 dirty.clear();
662 assert!(dirty.is_empty());
663 }
664
665 #[test]
666 fn roots_map() {
667 let mut roots = BTreeMap::new();
668 roots.insert(1u64, 65536u64);
669 roots.insert(5, 131072);
670 assert_eq!(roots.get(&1), Some(&65536));
671 assert_eq!(roots.get(&5), Some(&131072));
672 assert_eq!(roots.get(&99), None);
673 }
674}