1use crate::{
11 field_size,
12 raw::{
13 BTRFS_DIR_ITEM_KEY, BTRFS_FIRST_FREE_OBJECTID, BTRFS_FS_TREE_OBJECTID,
14 BTRFS_LAST_FREE_OBJECTID, BTRFS_ROOT_BACKREF_KEY, BTRFS_ROOT_ITEM_KEY,
15 BTRFS_ROOT_TREE_DIR_OBJECTID, BTRFS_ROOT_TREE_OBJECTID, BTRFS_SUBVOL_RDONLY,
16 btrfs_ioc_default_subvol, btrfs_ioc_get_subvol_info, btrfs_ioc_ino_lookup,
17 btrfs_ioc_snap_create_v2, btrfs_ioc_snap_destroy_v2, btrfs_ioc_subvol_create_v2,
18 btrfs_ioc_subvol_getflags, btrfs_ioc_subvol_setflags, btrfs_ioctl_get_subvol_info_args,
19 btrfs_ioctl_ino_lookup_args, btrfs_ioctl_vol_args_v2, btrfs_root_item, btrfs_timespec,
20 },
21 tree_search::{SearchKey, tree_search},
22};
23use bitflags::bitflags;
24use nix::libc::c_char;
25use std::{
26 collections::HashMap,
27 ffi::CStr,
28 mem,
29 os::{fd::AsRawFd, unix::io::BorrowedFd},
30 time::{Duration, SystemTime, UNIX_EPOCH},
31};
32use uuid::Uuid;
33
34pub const FS_TREE_OBJECTID: u64 = BTRFS_FS_TREE_OBJECTID as u64;
38
39bitflags! {
40 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
43 pub struct SubvolumeFlags: u64 {
44 const RDONLY = 1 << 1;
46 }
47}
48
49impl std::fmt::Display for SubvolumeFlags {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 if self.contains(SubvolumeFlags::RDONLY) {
52 write!(f, "readonly")
53 } else {
54 write!(f, "-")
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct SubvolumeInfo {
62 pub id: u64,
64 pub name: String,
66 pub parent_id: u64,
68 pub dir_id: u64,
70 pub generation: u64,
72 pub flags: SubvolumeFlags,
74 pub uuid: Uuid,
76 pub parent_uuid: Uuid,
78 pub received_uuid: Uuid,
80 pub ctransid: u64,
82 pub otransid: u64,
84 pub stransid: u64,
86 pub rtransid: u64,
88 pub ctime: SystemTime,
90 pub otime: SystemTime,
92 pub stime: SystemTime,
94 pub rtime: SystemTime,
96}
97
98#[derive(Debug, Clone)]
100pub struct SubvolumeListItem {
101 pub root_id: u64,
103 pub parent_id: u64,
105 pub dir_id: u64,
107 pub generation: u64,
109 pub flags: SubvolumeFlags,
111 pub uuid: Uuid,
113 pub parent_uuid: Uuid,
115 pub received_uuid: Uuid,
117 pub otransid: u64,
119 pub otime: SystemTime,
121 pub name: String,
127}
128
129fn set_v2_name(args: &mut btrfs_ioctl_vol_args_v2, name: &CStr) -> nix::Result<()> {
132 let bytes = name.to_bytes(); let name_buf: &mut [c_char] = unsafe { &mut args.__bindgen_anon_2.name };
136 if bytes.len() >= name_buf.len() {
137 return Err(nix::errno::Errno::ENAMETOOLONG);
138 }
139 for (i, &b) in bytes.iter().enumerate() {
140 name_buf[i] = b as c_char;
141 }
142 Ok(())
143}
144
145pub fn subvolume_create(parent_fd: BorrowedFd, name: &CStr) -> nix::Result<()> {
151 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
152 set_v2_name(&mut args, name)?;
153 unsafe { btrfs_ioc_subvol_create_v2(parent_fd.as_raw_fd(), &args) }?;
154 Ok(())
155}
156
157pub fn subvolume_delete(parent_fd: BorrowedFd, name: &CStr) -> nix::Result<()> {
162 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
163 set_v2_name(&mut args, name)?;
164 unsafe { btrfs_ioc_snap_destroy_v2(parent_fd.as_raw_fd(), &args) }?;
165 Ok(())
166}
167
168pub fn snapshot_create(
174 parent_fd: BorrowedFd,
175 source_fd: BorrowedFd,
176 name: &CStr,
177 readonly: bool,
178) -> nix::Result<()> {
179 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
180 args.fd = source_fd.as_raw_fd() as i64;
182 if readonly {
183 args.flags = BTRFS_SUBVOL_RDONLY as u64;
184 }
185 set_v2_name(&mut args, name)?;
186 unsafe { btrfs_ioc_snap_create_v2(parent_fd.as_raw_fd(), &args) }?;
187 Ok(())
188}
189
190pub fn subvolume_info(fd: BorrowedFd) -> nix::Result<SubvolumeInfo> {
195 let mut raw: btrfs_ioctl_get_subvol_info_args = unsafe { mem::zeroed() };
196 unsafe { btrfs_ioc_get_subvol_info(fd.as_raw_fd(), &mut raw) }?;
197
198 let name = unsafe { CStr::from_ptr(raw.name.as_ptr()) }
199 .to_string_lossy()
200 .into_owned();
201
202 Ok(SubvolumeInfo {
203 id: raw.treeid,
204 name,
205 parent_id: raw.parent_id,
206 dir_id: raw.dirid,
207 generation: raw.generation,
208 flags: SubvolumeFlags::from_bits_truncate(raw.flags),
209 uuid: Uuid::from_bytes(raw.uuid),
210 parent_uuid: Uuid::from_bytes(raw.parent_uuid),
211 received_uuid: Uuid::from_bytes(raw.received_uuid),
212 ctransid: raw.ctransid,
213 otransid: raw.otransid,
214 stransid: raw.stransid,
215 rtransid: raw.rtransid,
216 ctime: ioctl_timespec_to_system_time(raw.ctime.sec, raw.ctime.nsec),
217 otime: ioctl_timespec_to_system_time(raw.otime.sec, raw.otime.nsec),
218 stime: ioctl_timespec_to_system_time(raw.stime.sec, raw.stime.nsec),
219 rtime: ioctl_timespec_to_system_time(raw.rtime.sec, raw.rtime.nsec),
220 })
221}
222
223pub fn subvolume_flags_get(fd: BorrowedFd) -> nix::Result<SubvolumeFlags> {
225 let mut flags: u64 = 0;
226 unsafe { btrfs_ioc_subvol_getflags(fd.as_raw_fd(), &mut flags) }?;
227 Ok(SubvolumeFlags::from_bits_truncate(flags))
228}
229
230pub fn subvolume_flags_set(fd: BorrowedFd, flags: SubvolumeFlags) -> nix::Result<()> {
235 let raw: u64 = flags.bits();
236 unsafe { btrfs_ioc_subvol_setflags(fd.as_raw_fd(), &raw) }?;
237 Ok(())
238}
239
240pub fn subvolume_default_get(fd: BorrowedFd) -> nix::Result<u64> {
249 let mut default_id: Option<u64> = None;
250
251 tree_search(
252 fd,
253 SearchKey::for_objectid_range(
254 BTRFS_ROOT_TREE_OBJECTID as u64,
255 BTRFS_DIR_ITEM_KEY as u32,
256 BTRFS_ROOT_TREE_DIR_OBJECTID as u64,
257 BTRFS_ROOT_TREE_DIR_OBJECTID as u64,
258 ),
259 |_hdr, data| {
260 use crate::raw::btrfs_dir_item;
261 use std::mem::{offset_of, size_of};
262
263 let header_size = size_of::<btrfs_dir_item>();
264 if data.len() < header_size {
265 return Ok(());
266 }
267 let name_off = offset_of!(btrfs_dir_item, name_len);
268 let name_len = u16::from_le_bytes([data[name_off], data[name_off + 1]]) as usize;
269 if data.len() < header_size + name_len {
270 return Ok(());
271 }
272 let item_name = &data[header_size..header_size + name_len];
273 if item_name == b"default" {
274 let loc_off = offset_of!(btrfs_dir_item, location);
275 let target_id = u64::from_le_bytes(data[loc_off..loc_off + 8].try_into().unwrap());
276 default_id = Some(target_id);
277 }
278 Ok(())
279 },
280 )?;
281
282 Ok(default_id.unwrap_or(BTRFS_FS_TREE_OBJECTID as u64))
283}
284
285pub fn subvolume_default_set(fd: BorrowedFd, subvolid: u64) -> nix::Result<()> {
290 unsafe { btrfs_ioc_default_subvol(fd.as_raw_fd(), &subvolid) }?;
291 Ok(())
292}
293
294pub fn subvolume_list(fd: BorrowedFd) -> nix::Result<Vec<SubvolumeListItem>> {
307 let mut items: Vec<SubvolumeListItem> = Vec::new();
308
309 tree_search(
310 fd,
311 SearchKey::for_objectid_range(
312 BTRFS_ROOT_TREE_OBJECTID as u64,
313 BTRFS_ROOT_ITEM_KEY as u32,
314 BTRFS_FIRST_FREE_OBJECTID as u64,
315 BTRFS_LAST_FREE_OBJECTID as u64,
316 ),
317 |hdr, data| {
318 if let Some(item) = parse_root_item(hdr.objectid, data) {
319 items.push(item);
320 }
321 Ok(())
322 },
323 )?;
324
325 tree_search(
326 fd,
327 SearchKey::for_objectid_range(
328 BTRFS_ROOT_TREE_OBJECTID as u64,
329 BTRFS_ROOT_BACKREF_KEY as u32,
330 BTRFS_FIRST_FREE_OBJECTID as u64,
331 BTRFS_LAST_FREE_OBJECTID as u64,
332 ),
333 |hdr, data| {
334 let root_id = hdr.objectid;
336 let parent_id = hdr.offset;
337
338 if let Some(item) = items.iter_mut().find(|i| i.root_id == root_id) {
339 if item.parent_id == 0 {
347 item.parent_id = parent_id;
348 if let Some((dir_id, name)) = parse_root_ref(data) {
349 item.dir_id = dir_id;
350 item.name = name;
351 }
352 }
353 }
354 Ok(())
355 },
356 )?;
357
358 let top_id = crate::inode::lookup_path_rootid(fd).unwrap_or(FS_TREE_OBJECTID);
361
362 resolve_full_paths(fd, &mut items, top_id)?;
363
364 Ok(items)
365}
366
367fn ino_lookup_dir_path(fd: BorrowedFd, parent_tree: u64, dir_id: u64) -> nix::Result<String> {
375 let mut args = btrfs_ioctl_ino_lookup_args {
376 treeid: parent_tree,
377 objectid: dir_id,
378 ..unsafe { mem::zeroed() }
379 };
380 unsafe { btrfs_ioc_ino_lookup(fd.as_raw_fd(), &mut args) }?;
383
384 let name_ptr: *const c_char = args.name.as_ptr();
386 let cstr = unsafe { CStr::from_ptr(name_ptr) };
388 Ok(cstr.to_string_lossy().into_owned())
389}
390
391fn resolve_full_paths(
402 fd: BorrowedFd,
403 items: &mut Vec<SubvolumeListItem>,
404 top_id: u64,
405) -> nix::Result<()> {
406 let id_to_idx: HashMap<u64, usize> = items
408 .iter()
409 .enumerate()
410 .map(|(i, item)| (item.root_id, i))
411 .collect();
412
413 let segments: Vec<String> = items
418 .iter()
419 .map(|item| {
420 if item.parent_id == 0 || item.name.is_empty() {
421 return item.name.clone();
422 }
423 match ino_lookup_dir_path(fd, item.parent_id, item.dir_id) {
424 Ok(prefix) => format!("{}{}", prefix, item.name),
425 Err(_) => item.name.clone(),
426 }
427 })
428 .collect();
429
430 let mut full_paths: HashMap<u64, String> = HashMap::new();
433 let root_ids: Vec<u64> = items.iter().map(|i| i.root_id).collect();
434 for root_id in root_ids {
435 build_full_path(
436 root_id,
437 top_id,
438 &id_to_idx,
439 &segments,
440 items,
441 &mut full_paths,
442 );
443 }
444
445 for item in items.iter_mut() {
446 if let Some(path) = full_paths.remove(&item.root_id) {
447 item.name = path;
448 }
449 }
450
451 Ok(())
452}
453
454fn build_full_path(
464 root_id: u64,
465 top_id: u64,
466 id_to_idx: &HashMap<u64, usize>,
467 segments: &[String],
468 items: &[SubvolumeListItem],
469 cache: &mut HashMap<u64, String>,
470) -> String {
471 let mut chain: Vec<u64> = Vec::new();
475 let mut visited: HashMap<u64, usize> = HashMap::new();
476 let mut cur = root_id;
477 loop {
478 if cache.contains_key(&cur) {
479 break;
480 }
481 if visited.contains_key(&cur) {
482 let cycle_start = visited[&cur];
485 chain.truncate(cycle_start);
486 break;
487 }
488 let Some(&idx) = id_to_idx.get(&cur) else {
489 break;
490 };
491 visited.insert(cur, chain.len());
492 chain.push(cur);
493 let parent = items[idx].parent_id;
494 if parent == 0
495 || parent == FS_TREE_OBJECTID
496 || parent == top_id
497 || !id_to_idx.contains_key(&parent)
498 {
499 break;
500 }
501 cur = parent;
502 }
503
504 for &id in chain.iter().rev() {
507 let Some(&idx) = id_to_idx.get(&id) else {
508 cache.insert(id, String::new());
509 continue;
510 };
511 let segment = &segments[idx];
512 let parent_id = items[idx].parent_id;
513
514 let full_path = if parent_id == 0
515 || parent_id == FS_TREE_OBJECTID
516 || parent_id == top_id
517 || !id_to_idx.contains_key(&parent_id)
518 {
519 segment.clone()
520 } else if let Some(parent_path) = cache.get(&parent_id) {
521 if parent_path.is_empty() {
522 segment.clone()
523 } else {
524 format!("{}/{}", parent_path, segment)
525 }
526 } else {
527 segment.clone()
528 };
529
530 cache.insert(id, full_path);
531 }
532
533 cache.get(&root_id).cloned().unwrap_or_default()
534}
535
536fn parse_root_item(root_id: u64, data: &[u8]) -> Option<SubvolumeListItem> {
538 use std::mem::offset_of;
539
540 let legacy_boundary = offset_of!(btrfs_root_item, generation_v2);
543 if data.len() < legacy_boundary {
544 return None;
545 }
546
547 let generation = rle64(data, offset_of!(btrfs_root_item, generation));
548 let flags_raw = rle64(data, offset_of!(btrfs_root_item, flags));
549 let flags = SubvolumeFlags::from_bits_truncate(flags_raw);
550
551 let otime_nsec = offset_of!(btrfs_root_item, otime) + offset_of!(btrfs_timespec, nsec);
553 let (uuid, parent_uuid, received_uuid, otransid, otime) =
554 if data.len() >= otime_nsec + field_size!(btrfs_timespec, nsec) {
555 let off_uuid = offset_of!(btrfs_root_item, uuid);
556 let off_parent = offset_of!(btrfs_root_item, parent_uuid);
557 let off_received = offset_of!(btrfs_root_item, received_uuid);
558 let uuid_size = field_size!(btrfs_root_item, uuid);
559 let uuid = Uuid::from_bytes(data[off_uuid..off_uuid + uuid_size].try_into().unwrap());
560 let parent_uuid =
561 Uuid::from_bytes(data[off_parent..off_parent + uuid_size].try_into().unwrap());
562 let received_uuid = Uuid::from_bytes(
563 data[off_received..off_received + uuid_size]
564 .try_into()
565 .unwrap(),
566 );
567 let otransid = rle64(data, offset_of!(btrfs_root_item, otransid));
568 let otime_sec = offset_of!(btrfs_root_item, otime);
569 let otime = timespec_to_system_time(rle64(data, otime_sec), rle32(data, otime_nsec));
570 (uuid, parent_uuid, received_uuid, otransid, otime)
571 } else {
572 (Uuid::nil(), Uuid::nil(), Uuid::nil(), 0, UNIX_EPOCH)
573 };
574
575 Some(SubvolumeListItem {
576 root_id,
577 parent_id: 0,
578 dir_id: 0,
579 generation,
580 flags,
581 uuid,
582 parent_uuid,
583 received_uuid,
584 otransid,
585 otime,
586 name: String::new(),
587 })
588}
589
590fn parse_root_ref(data: &[u8]) -> Option<(u64, String)> {
593 use crate::raw::btrfs_root_ref;
594 use std::mem::{offset_of, size_of};
595
596 let header_size = size_of::<btrfs_root_ref>();
597 if data.len() < header_size {
598 return None;
599 }
600 let dir_id = rle64(data, offset_of!(btrfs_root_ref, dirid));
601 let name_off = offset_of!(btrfs_root_ref, name_len);
602 let name_len = u16::from_le_bytes([data[name_off], data[name_off + 1]]) as usize;
603 if data.len() < header_size + name_len {
604 return None;
605 }
606 let name = String::from_utf8_lossy(&data[header_size..header_size + name_len]).into_owned();
607 Some((dir_id, name))
608}
609
610#[inline]
611fn rle64(buf: &[u8], off: usize) -> u64 {
612 u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
613}
614
615#[inline]
616fn rle32(buf: &[u8], off: usize) -> u32 {
617 u32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
618}
619
620fn timespec_to_system_time(sec: u64, nsec: u32) -> SystemTime {
623 if sec == 0 {
624 return UNIX_EPOCH;
625 }
626 UNIX_EPOCH + Duration::new(sec, nsec)
627}
628
629fn ioctl_timespec_to_system_time(sec: u64, nsec: u32) -> SystemTime {
632 if sec == 0 {
633 return UNIX_EPOCH;
634 }
635 UNIX_EPOCH + Duration::new(sec, nsec)
636}