btrfs_cli/subvolume/
find_new.rs1use crate::{Format, Runnable};
2use anyhow::{Context, Result};
3use btrfs_uapi::{
4 filesystem::sync,
5 inode::ino_paths,
6 raw::{
7 BTRFS_EXTENT_DATA_KEY, BTRFS_FILE_EXTENT_INLINE,
8 BTRFS_FILE_EXTENT_PREALLOC, BTRFS_FILE_EXTENT_REG,
9 btrfs_file_extent_item,
10 },
11 subvolume::subvolume_info,
12 tree_search::{SearchKey, tree_search},
13};
14use clap::Parser;
15use std::{fs::File, mem, os::unix::io::AsFd, path::PathBuf};
16
17#[derive(Parser, Debug)]
22pub struct SubvolumeFindNewCommand {
23 path: PathBuf,
25
26 last_gen: u64,
28}
29
30fn rle64(buf: &[u8], off: usize) -> u64 {
31 u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
32}
33
34impl Runnable for SubvolumeFindNewCommand {
35 fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
36 let file = File::open(&self.path).with_context(|| {
37 format!("failed to open '{}'", self.path.display())
38 })?;
39
40 sync(file.as_fd()).with_context(|| {
42 format!("failed to sync '{}'", self.path.display())
43 })?;
44
45 let info = subvolume_info(file.as_fd()).with_context(|| {
47 format!(
48 "failed to get subvolume info for '{}'",
49 self.path.display()
50 )
51 })?;
52 let max_gen = info.generation;
53
54 let mut key = SearchKey::for_type(0, BTRFS_EXTENT_DATA_KEY as u32);
58 key.min_transid = self.last_gen;
59
60 let mut cache_ino: u64 = 0;
61 let mut cache_name: Option<String> = None;
62
63 tree_search(file.as_fd(), key, |hdr, data| {
64 let gen_off = mem::offset_of!(btrfs_file_extent_item, generation);
65 let type_off = mem::offset_of!(btrfs_file_extent_item, type_);
66 let compression_off = mem::offset_of!(btrfs_file_extent_item, compression);
67
68 if data.len() < type_off + 1 {
70 return Ok(());
71 }
72
73 let found_gen = rle64(data, gen_off);
74 if found_gen < self.last_gen {
75 return Ok(());
76 }
77
78 let extent_type = data[type_off];
79 let compressed = data.get(compression_off).copied().unwrap_or(0) != 0;
80
81 let (disk_start, disk_offset, len) =
82 if extent_type == BTRFS_FILE_EXTENT_REG as u8
83 || extent_type == BTRFS_FILE_EXTENT_PREALLOC as u8
84 {
85 let disk_bytenr_off = mem::offset_of!(btrfs_file_extent_item, disk_bytenr);
86 let offset_off = mem::offset_of!(btrfs_file_extent_item, offset);
87 let num_bytes_off = mem::offset_of!(btrfs_file_extent_item, num_bytes);
88
89 if data.len() < num_bytes_off + 8 {
90 return Ok(());
91 }
92 (
93 rle64(data, disk_bytenr_off),
94 rle64(data, offset_off),
95 rle64(data, num_bytes_off),
96 )
97 } else if extent_type == BTRFS_FILE_EXTENT_INLINE as u8 {
98 let ram_bytes_off = mem::offset_of!(btrfs_file_extent_item, ram_bytes);
99 if data.len() < ram_bytes_off + 8 {
100 return Ok(());
101 }
102 (0, 0, rle64(data, ram_bytes_off))
103 } else {
104 return Ok(());
105 };
106
107 let name = if hdr.objectid == cache_ino {
110 cache_name.as_deref().unwrap_or("unknown")
111 } else {
112 let resolved = match ino_paths(file.as_fd(), hdr.objectid) {
113 Ok(paths) if !paths.is_empty() => Some(paths.into_iter().next().unwrap()),
114 _ => None,
115 };
116 cache_ino = hdr.objectid;
117 cache_name = resolved;
118 cache_name.as_deref().unwrap_or("unknown")
119 };
120
121 let mut flags = String::new();
123 if compressed {
124 flags.push_str("COMPRESS");
125 }
126 if extent_type == BTRFS_FILE_EXTENT_PREALLOC as u8 {
127 if !flags.is_empty() {
128 flags.push('|');
129 }
130 flags.push_str("PREALLOC");
131 }
132 if extent_type == BTRFS_FILE_EXTENT_INLINE as u8 {
133 if !flags.is_empty() {
134 flags.push('|');
135 }
136 flags.push_str("INLINE");
137 }
138 if flags.is_empty() {
139 flags.push_str("NONE");
140 }
141
142 println!(
143 "inode {} file offset {} len {} disk start {} offset {} gen {} flags {flags} {name}",
144 hdr.objectid, hdr.offset, len, disk_start, disk_offset, found_gen,
145 );
146
147 Ok(())
148 })
149 .with_context(|| format!("tree search failed for '{}'", self.path.display()))?;
150
151 println!("transid marker was {max_gen}");
152
153 Ok(())
154 }
155}