1use crate::{Format, Runnable};
2use anyhow::{Context, Result, bail};
3use btrfs_uapi::{
4 raw::{
5 BTRFS_BLOCK_GROUP_PROFILE_MASK, BTRFS_CHUNK_ITEM_KEY,
6 BTRFS_CHUNK_TREE_OBJECTID, BTRFS_EXTENT_DATA_KEY,
7 BTRFS_FIRST_CHUNK_TREE_OBJECTID, btrfs_chunk, btrfs_file_extent_item,
8 btrfs_stripe,
9 },
10 tree_search::{SearchKey, tree_search},
11 util::read_le_u64,
12};
13use clap::Parser;
14use std::{
15 fs::File,
16 mem,
17 os::unix::io::{AsFd, AsRawFd},
18 path::PathBuf,
19};
20
21#[derive(Parser, Debug)]
29pub struct MapSwapfileCommand {
30 #[arg(short = 'r', long)]
32 resume_offset: bool,
33
34 path: PathBuf,
36}
37
38impl Runnable for MapSwapfileCommand {
39 fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
40 let file = File::open(&self.path).with_context(|| {
41 format!("cannot open '{}'", self.path.display())
42 })?;
43
44 validate_file(&file, &self.path)?;
45
46 let fd = file.as_fd();
47 let chunks = read_chunk_tree(fd)?;
48
49 let tree_id = btrfs_uapi::inode::lookup_path_rootid(fd)
50 .context("cannot lookup parent subvolume")?;
51
52 let stat = nix::sys::stat::fstat(&file).context("cannot fstat file")?;
53
54 let physical_start =
55 map_physical_start(fd, tree_id, stat.st_ino, &chunks)?;
56
57 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
58 if self.resume_offset {
59 println!("{}", physical_start / page_size);
60 } else {
61 println!("Physical start: {:12}", physical_start);
62 println!("Resume offset: {:12}", physical_start / page_size);
63 }
64
65 Ok(())
66 }
67}
68
69fn validate_file(file: &File, path: &std::path::Path) -> Result<()> {
72 let stfs = nix::sys::statfs::fstatfs(file)
73 .with_context(|| format!("cannot statfs '{}'", path.display()))?;
74 if stfs.filesystem_type() != nix::sys::statfs::BTRFS_SUPER_MAGIC {
75 bail!("not a file on btrfs");
76 }
77
78 let stat = nix::sys::stat::fstat(file)
79 .with_context(|| format!("cannot fstat '{}'", path.display()))?;
80 if stat.st_mode & libc::S_IFMT != libc::S_IFREG {
81 bail!("not a regular file");
82 }
83
84 let mut flags: libc::c_long = 0;
85 let ret = unsafe {
86 libc::ioctl(file.as_raw_fd(), libc::FS_IOC_GETFLAGS, &mut flags)
87 };
88 if ret == -1 {
89 bail!(
90 "cannot verify file flags: {}",
91 std::io::Error::last_os_error()
92 );
93 }
94 const FS_NOCOW_FL: libc::c_long = 0x0080_0000;
95 const FS_COMPR_FL: libc::c_long = 0x0000_0004;
96 if flags & FS_NOCOW_FL == 0 {
97 bail!("file is not NOCOW");
98 }
99 if flags & FS_COMPR_FL != 0 {
100 bail!("file has COMPR attribute");
101 }
102
103 Ok(())
104}
105
106struct Chunk {
108 offset: u64,
109 length: u64,
110 stripe_len: u64,
111 type_flags: u64,
112 num_stripes: usize,
113 stripes: Vec<(u64, u64)>,
114}
115
116const CHUNK_LENGTH_OFF: usize = mem::offset_of!(btrfs_chunk, length);
117const CHUNK_STRIPE_LEN_OFF: usize = mem::offset_of!(btrfs_chunk, stripe_len);
118const CHUNK_TYPE_OFF: usize = mem::offset_of!(btrfs_chunk, type_);
119const CHUNK_NUM_STRIPES_OFF: usize = mem::offset_of!(btrfs_chunk, num_stripes);
120const CHUNK_FIRST_STRIPE_OFF: usize = mem::offset_of!(btrfs_chunk, stripe);
121const STRIPE_SIZE: usize = mem::size_of::<btrfs_stripe>();
122const STRIPE_DEVID_OFF: usize = mem::offset_of!(btrfs_stripe, devid);
123const STRIPE_OFFSET_OFF: usize = mem::offset_of!(btrfs_stripe, offset);
124
125fn read_le_u16(data: &[u8], off: usize) -> u16 {
126 u16::from_le_bytes(data[off..off + 2].try_into().unwrap())
127}
128
129fn read_chunk_tree(fd: std::os::unix::io::BorrowedFd) -> Result<Vec<Chunk>> {
131 let mut chunks = Vec::new();
132
133 tree_search(
134 fd,
135 SearchKey::for_objectid_range(
136 u64::from(BTRFS_CHUNK_TREE_OBJECTID),
137 BTRFS_CHUNK_ITEM_KEY,
138 u64::from(BTRFS_FIRST_CHUNK_TREE_OBJECTID),
139 u64::from(BTRFS_FIRST_CHUNK_TREE_OBJECTID),
140 ),
141 |hdr, data| {
142 let min_len = CHUNK_FIRST_STRIPE_OFF + STRIPE_SIZE;
143 if data.len() < min_len {
144 return Ok(());
145 }
146 let num_stripes = read_le_u16(data, CHUNK_NUM_STRIPES_OFF) as usize;
147 let expected_len =
148 CHUNK_FIRST_STRIPE_OFF + num_stripes * STRIPE_SIZE;
149 if data.len() < expected_len || num_stripes == 0 {
150 return Ok(());
151 }
152
153 let stripes = (0..num_stripes)
154 .map(|i| {
155 let s = CHUNK_FIRST_STRIPE_OFF + i * STRIPE_SIZE;
156 (
157 read_le_u64(data, s + STRIPE_DEVID_OFF),
158 read_le_u64(data, s + STRIPE_OFFSET_OFF),
159 )
160 })
161 .collect();
162
163 chunks.push(Chunk {
164 offset: hdr.offset,
165 length: read_le_u64(data, CHUNK_LENGTH_OFF),
166 stripe_len: read_le_u64(data, CHUNK_STRIPE_LEN_OFF),
167 type_flags: read_le_u64(data, CHUNK_TYPE_OFF),
168 num_stripes,
169 stripes,
170 });
171 Ok(())
172 },
173 )
174 .context("failed to read chunk tree")?;
175
176 Ok(chunks)
177}
178
179fn find_chunk(chunks: &[Chunk], logical: u64) -> Option<&Chunk> {
181 chunks
182 .binary_search_by(|c| {
183 if logical < c.offset {
184 std::cmp::Ordering::Greater
185 } else if logical >= c.offset + c.length {
186 std::cmp::Ordering::Less
187 } else {
188 std::cmp::Ordering::Equal
189 }
190 })
191 .ok()
192 .map(|i| &chunks[i])
193}
194
195struct FileExtent {
197 logical_offset: u64,
198 num_stripes: usize,
199 stripe_len: u64,
200 stripe_devid: u64,
201 stripe_physical: u64,
202 chunk_offset: u64,
203}
204
205const EXTENT_TYPE_OFF: usize = mem::offset_of!(btrfs_file_extent_item, type_);
206const EXTENT_COMPRESSION_OFF: usize =
207 mem::offset_of!(btrfs_file_extent_item, compression);
208const EXTENT_ENCRYPTION_OFF: usize =
209 mem::offset_of!(btrfs_file_extent_item, encryption);
210const EXTENT_OTHER_ENCODING_OFF: usize =
211 mem::offset_of!(btrfs_file_extent_item, other_encoding);
212const EXTENT_DISK_BYTENR_OFF: usize =
213 mem::offset_of!(btrfs_file_extent_item, disk_bytenr);
214
215fn map_physical_start(
217 fd: std::os::unix::io::BorrowedFd,
218 tree_id: u64,
219 ino: u64,
220 chunks: &[Chunk],
221) -> Result<u64> {
222 let mut extents: Vec<FileExtent> = Vec::new();
224 let mut error: Option<String> = None;
225
226 tree_search(
227 fd,
228 SearchKey {
229 tree_id,
230 min_objectid: ino,
231 max_objectid: ino,
232 min_type: BTRFS_EXTENT_DATA_KEY,
233 max_type: BTRFS_EXTENT_DATA_KEY,
234 min_offset: 0,
235 max_offset: u64::MAX,
236 min_transid: 0,
237 max_transid: u64::MAX,
238 },
239 |_hdr, data| {
240 if error.is_some() {
241 return Ok(());
242 }
243 if data.len() < mem::size_of::<btrfs_file_extent_item>() {
244 return Ok(());
245 }
246
247 let extent_type = data[EXTENT_TYPE_OFF];
248 if extent_type != 1 && extent_type != 2 {
250 error = Some(if extent_type == 0 {
251 "file with inline extent".to_string()
252 } else {
253 format!("unknown extent type: {extent_type}")
254 });
255 return Ok(());
256 }
257
258 let logical_offset = read_le_u64(data, EXTENT_DISK_BYTENR_OFF);
259 if logical_offset == 0 {
260 error = Some("file with holes".to_string());
261 return Ok(());
262 }
263
264 if data[EXTENT_COMPRESSION_OFF] != 0 {
265 error = Some(format!(
266 "compressed extent: {}",
267 data[EXTENT_COMPRESSION_OFF]
268 ));
269 return Ok(());
270 }
271 if data[EXTENT_ENCRYPTION_OFF] != 0 {
272 error = Some(format!(
273 "file with encryption: {}",
274 data[EXTENT_ENCRYPTION_OFF]
275 ));
276 return Ok(());
277 }
278 let other_encoding = read_le_u16(data, EXTENT_OTHER_ENCODING_OFF);
279 if other_encoding != 0 {
280 error =
281 Some(format!("file with other_encoding: {other_encoding}"));
282 return Ok(());
283 }
284
285 let chunk = match find_chunk(chunks, logical_offset) {
286 Some(c) => c,
287 None => {
288 error = Some(format!(
289 "cannot find chunk containing {logical_offset}"
290 ));
291 return Ok(());
292 }
293 };
294
295 if chunk.type_flags & u64::from(BTRFS_BLOCK_GROUP_PROFILE_MASK) != 0
296 {
297 error = Some(format!(
298 "unsupported block group profile: {:#x}",
299 chunk.type_flags
300 & u64::from(BTRFS_BLOCK_GROUP_PROFILE_MASK)
301 ));
302 return Ok(());
303 }
304
305 extents.push(FileExtent {
306 logical_offset,
307 num_stripes: chunk.num_stripes,
308 stripe_len: chunk.stripe_len,
309 stripe_devid: chunk.stripes[0].0,
310 stripe_physical: chunk.stripes[0].1,
311 chunk_offset: chunk.offset,
312 });
313
314 Ok(())
315 },
316 )
317 .context("failed to search extent data")?;
318
319 if let Some(err) = error {
320 bail!("{err}");
321 }
322 if extents.is_empty() {
323 bail!("file has no extents");
324 }
325
326 let first_devid = extents[0].stripe_devid;
328 for ext in &extents[1..] {
329 if ext.stripe_devid != first_devid {
330 bail!("file stored on multiple devices");
331 }
332 }
333
334 let ext = &extents[0];
336 let offset = ext.logical_offset - ext.chunk_offset;
339 let stripe_nr = offset / ext.stripe_len;
340 let stripe_offset = offset - stripe_nr * ext.stripe_len;
341 let physical_start = ext.stripe_physical
342 + (stripe_nr / ext.num_stripes as u64) * ext.stripe_len
343 + stripe_offset;
344
345 Ok(physical_start)
346}