1use crate::{
11 raw::{
12 BTRFS_DIR_ITEM_KEY, BTRFS_FIRST_FREE_OBJECTID, BTRFS_FS_TREE_OBJECTID,
13 BTRFS_LAST_FREE_OBJECTID, BTRFS_ROOT_BACKREF_KEY, BTRFS_ROOT_ITEM_KEY,
14 BTRFS_ROOT_TREE_DIR_OBJECTID, BTRFS_ROOT_TREE_OBJECTID,
15 BTRFS_SUBVOL_QGROUP_INHERIT, BTRFS_SUBVOL_RDONLY,
16 BTRFS_SUBVOL_SPEC_BY_ID, BTRFS_SUBVOL_SYNC_WAIT_FOR_ONE,
17 BTRFS_SUBVOL_SYNC_WAIT_FOR_QUEUED, btrfs_ioc_default_subvol,
18 btrfs_ioc_get_subvol_info, btrfs_ioc_ino_lookup,
19 btrfs_ioc_snap_create_v2, btrfs_ioc_snap_destroy_v2,
20 btrfs_ioc_subvol_create_v2, btrfs_ioc_subvol_getflags,
21 btrfs_ioc_subvol_setflags, btrfs_ioc_subvol_sync_wait,
22 btrfs_ioctl_get_subvol_info_args, btrfs_ioctl_ino_lookup_args,
23 btrfs_ioctl_subvol_wait, btrfs_ioctl_vol_args_v2, btrfs_qgroup_inherit,
24 },
25 tree_search::{SearchKey, tree_search},
26};
27use bitflags::bitflags;
28use nix::libc::c_char;
29use std::{
30 collections::HashMap,
31 ffi::CStr,
32 mem,
33 os::{fd::AsRawFd, unix::io::BorrowedFd},
34 time::{Duration, SystemTime, UNIX_EPOCH},
35};
36use uuid::Uuid;
37
38pub const FS_TREE_OBJECTID: u64 = BTRFS_FS_TREE_OBJECTID as u64;
42
43bitflags! {
44 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
47 pub struct SubvolumeFlags: u64 {
48 const RDONLY = BTRFS_SUBVOL_RDONLY as u64;
50 }
51}
52
53impl std::fmt::Display for SubvolumeFlags {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 if self.contains(SubvolumeFlags::RDONLY) {
56 write!(f, "readonly")
57 } else {
58 write!(f, "-")
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct SubvolumeInfo {
66 pub id: u64,
68 pub name: String,
70 pub parent_id: u64,
72 pub dir_id: u64,
74 pub generation: u64,
76 pub flags: SubvolumeFlags,
78 pub uuid: Uuid,
80 pub parent_uuid: Uuid,
82 pub received_uuid: Uuid,
84 pub ctransid: u64,
86 pub otransid: u64,
88 pub stransid: u64,
90 pub rtransid: u64,
92 pub ctime: SystemTime,
94 pub otime: SystemTime,
96 pub stime: SystemTime,
98 pub rtime: SystemTime,
100}
101
102#[derive(Debug, Clone)]
104pub struct SubvolumeListItem {
105 pub root_id: u64,
107 pub parent_id: u64,
109 pub dir_id: u64,
111 pub generation: u64,
113 pub flags: SubvolumeFlags,
115 pub uuid: Uuid,
117 pub parent_uuid: Uuid,
119 pub received_uuid: Uuid,
121 pub otransid: u64,
123 pub otime: SystemTime,
125 pub name: String,
131}
132
133#[allow(clippy::cast_possible_wrap)] fn set_v2_name(
137 args: &mut btrfs_ioctl_vol_args_v2,
138 name: &CStr,
139) -> nix::Result<()> {
140 let bytes = name.to_bytes(); let name_buf: &mut [c_char] = unsafe { &mut args.__bindgen_anon_2.name };
144 if bytes.len() >= name_buf.len() {
145 return Err(nix::errno::Errno::ENAMETOOLONG);
146 }
147 for (i, &b) in bytes.iter().enumerate() {
148 name_buf[i] = b as c_char;
149 }
150 Ok(())
151}
152
153fn build_qgroup_inherit(qgroups: &[u64]) -> Vec<u64> {
159 let base_size = mem::size_of::<btrfs_qgroup_inherit>();
160 let total_size = base_size + std::mem::size_of_val(qgroups);
161 let num_u64 = total_size.div_ceil(8);
162 let mut buf = vec![0u64; num_u64];
163
164 let inherit =
167 unsafe { &mut *buf.as_mut_ptr().cast::<btrfs_qgroup_inherit>() };
168 inherit.num_qgroups = qgroups.len() as u64;
169
170 if !qgroups.is_empty() {
172 let array = unsafe { inherit.qgroups.as_mut_slice(qgroups.len()) };
173 array.copy_from_slice(qgroups);
174 }
175
176 buf
177}
178
179fn set_qgroup_inherit(
183 args: &mut btrfs_ioctl_vol_args_v2,
184 buf: &[u64],
185 num_qgroups: usize,
186) {
187 args.flags |= u64::from(BTRFS_SUBVOL_QGROUP_INHERIT);
188 let base_size = mem::size_of::<btrfs_qgroup_inherit>();
189 let total_size = base_size + num_qgroups * mem::size_of::<u64>();
190 args.__bindgen_anon_1.__bindgen_anon_1.size = total_size as u64;
191 args.__bindgen_anon_1.__bindgen_anon_1.qgroup_inherit =
192 buf.as_ptr() as *mut btrfs_qgroup_inherit;
193}
194
195pub fn subvolume_create(
210 parent_fd: BorrowedFd,
211 name: &CStr,
212 qgroups: &[u64],
213) -> nix::Result<()> {
214 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
215 set_v2_name(&mut args, name)?;
216
217 let inherit_buf;
218 if !qgroups.is_empty() {
219 inherit_buf = build_qgroup_inherit(qgroups);
220 set_qgroup_inherit(&mut args, &inherit_buf, qgroups.len());
221 }
222
223 unsafe {
224 btrfs_ioc_subvol_create_v2(parent_fd.as_raw_fd(), &raw const args)
225 }?;
226 Ok(())
227}
228
229pub fn subvolume_delete(parent_fd: BorrowedFd, name: &CStr) -> nix::Result<()> {
246 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
247 set_v2_name(&mut args, name)?;
248 unsafe {
249 btrfs_ioc_snap_destroy_v2(parent_fd.as_raw_fd(), &raw const args)
250 }?;
251 Ok(())
252}
253
254pub fn subvolume_delete_by_id(
267 fd: BorrowedFd,
268 subvolid: u64,
269) -> nix::Result<()> {
270 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
271 args.flags = u64::from(BTRFS_SUBVOL_SPEC_BY_ID);
272 args.__bindgen_anon_2.subvolid = subvolid;
273 unsafe { btrfs_ioc_snap_destroy_v2(fd.as_raw_fd(), &raw const args) }?;
274 Ok(())
275}
276
277pub fn snapshot_create(
293 parent_fd: BorrowedFd,
294 source_fd: BorrowedFd,
295 name: &CStr,
296 readonly: bool,
297 qgroups: &[u64],
298) -> nix::Result<()> {
299 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
300 args.fd = i64::from(source_fd.as_raw_fd());
302 if readonly {
303 args.flags = u64::from(BTRFS_SUBVOL_RDONLY);
304 }
305 set_v2_name(&mut args, name)?;
306
307 let inherit_buf;
308 if !qgroups.is_empty() {
309 inherit_buf = build_qgroup_inherit(qgroups);
310 set_qgroup_inherit(&mut args, &inherit_buf, qgroups.len());
311 }
312
313 unsafe {
314 btrfs_ioc_snap_create_v2(parent_fd.as_raw_fd(), &raw const args)
315 }?;
316 Ok(())
317}
318
319pub fn subvolume_info(fd: BorrowedFd) -> nix::Result<SubvolumeInfo> {
328 subvolume_info_by_id(fd, 0)
329}
330
331pub fn subvolume_info_by_id(
344 fd: BorrowedFd,
345 rootid: u64,
346) -> nix::Result<SubvolumeInfo> {
347 let mut raw: btrfs_ioctl_get_subvol_info_args = unsafe { mem::zeroed() };
348 raw.treeid = rootid;
349 unsafe { btrfs_ioc_get_subvol_info(fd.as_raw_fd(), &raw mut raw) }?;
350
351 let name = unsafe { CStr::from_ptr(raw.name.as_ptr()) }
352 .to_string_lossy()
353 .into_owned();
354
355 Ok(SubvolumeInfo {
356 id: raw.treeid,
357 name,
358 parent_id: raw.parent_id,
359 dir_id: raw.dirid,
360 generation: raw.generation,
361 flags: SubvolumeFlags::from_bits_truncate(raw.flags),
362 uuid: Uuid::from_bytes(raw.uuid),
363 parent_uuid: Uuid::from_bytes(raw.parent_uuid),
364 received_uuid: Uuid::from_bytes(raw.received_uuid),
365 ctransid: raw.ctransid,
366 otransid: raw.otransid,
367 stransid: raw.stransid,
368 rtransid: raw.rtransid,
369 ctime: timespec_to_system_time(raw.ctime.sec, raw.ctime.nsec),
370 otime: timespec_to_system_time(raw.otime.sec, raw.otime.nsec),
371 stime: timespec_to_system_time(raw.stime.sec, raw.stime.nsec),
372 rtime: timespec_to_system_time(raw.rtime.sec, raw.rtime.nsec),
373 })
374}
375
376pub fn subvolume_flags_get(fd: BorrowedFd) -> nix::Result<SubvolumeFlags> {
382 let mut flags: u64 = 0;
383 unsafe { btrfs_ioc_subvol_getflags(fd.as_raw_fd(), &raw mut flags) }?;
384 Ok(SubvolumeFlags::from_bits_truncate(flags))
385}
386
387pub fn subvolume_flags_set(
396 fd: BorrowedFd,
397 flags: SubvolumeFlags,
398) -> nix::Result<()> {
399 let raw: u64 = flags.bits();
400 unsafe { btrfs_ioc_subvol_setflags(fd.as_raw_fd(), &raw const raw) }?;
401 Ok(())
402}
403
404pub fn subvolume_default_get(fd: BorrowedFd) -> nix::Result<u64> {
422 let mut default_id: Option<u64> = None;
423
424 tree_search(
425 fd,
426 SearchKey::for_objectid_range(
427 u64::from(BTRFS_ROOT_TREE_OBJECTID),
428 BTRFS_DIR_ITEM_KEY,
429 u64::from(BTRFS_ROOT_TREE_DIR_OBJECTID),
430 u64::from(BTRFS_ROOT_TREE_DIR_OBJECTID),
431 ),
432 |_hdr, data| {
433 use crate::raw::btrfs_dir_item;
434 use std::mem::{offset_of, size_of};
435
436 let header_size = size_of::<btrfs_dir_item>();
437 if data.len() < header_size {
438 return Ok(());
439 }
440 let name_off = offset_of!(btrfs_dir_item, name_len);
441 let name_len =
442 u16::from_le_bytes([data[name_off], data[name_off + 1]])
443 as usize;
444 if data.len() < header_size + name_len {
445 return Ok(());
446 }
447 let item_name = &data[header_size..header_size + name_len];
448 if item_name == b"default" {
449 let loc_off = offset_of!(btrfs_dir_item, location);
450 let target_id = u64::from_le_bytes(
451 data[loc_off..loc_off + 8].try_into().unwrap(),
452 );
453 default_id = Some(target_id);
454 }
455 Ok(())
456 },
457 )?;
458
459 Ok(default_id.unwrap_or(u64::from(BTRFS_FS_TREE_OBJECTID)))
460}
461
462pub fn subvolume_default_set(fd: BorrowedFd, subvolid: u64) -> nix::Result<()> {
471 unsafe { btrfs_ioc_default_subvol(fd.as_raw_fd(), &raw const subvolid) }?;
472 Ok(())
473}
474
475#[allow(clippy::cast_sign_loss)] pub fn subvolume_list(fd: BorrowedFd) -> nix::Result<Vec<SubvolumeListItem>> {
495 let mut items: Vec<SubvolumeListItem> = Vec::new();
496
497 tree_search(
498 fd,
499 SearchKey::for_objectid_range(
500 u64::from(BTRFS_ROOT_TREE_OBJECTID),
501 BTRFS_ROOT_ITEM_KEY,
502 u64::from(BTRFS_FIRST_FREE_OBJECTID),
503 BTRFS_LAST_FREE_OBJECTID as u64,
504 ),
505 |hdr, data| {
506 if let Some(item) = parse_root_item(hdr.objectid, data) {
507 items.push(item);
508 }
509 Ok(())
510 },
511 )?;
512
513 tree_search(
514 fd,
515 SearchKey::for_objectid_range(
516 u64::from(BTRFS_ROOT_TREE_OBJECTID),
517 BTRFS_ROOT_BACKREF_KEY,
518 u64::from(BTRFS_FIRST_FREE_OBJECTID),
519 BTRFS_LAST_FREE_OBJECTID as u64,
520 ),
521 |hdr, data| {
522 let root_id = hdr.objectid;
524 let parent_id = hdr.offset;
525
526 if let Some(item) = items.iter_mut().find(|i| i.root_id == root_id)
527 {
528 if item.parent_id == 0 {
536 item.parent_id = parent_id;
537 if let Some((dir_id, name)) = parse_root_ref(data) {
538 item.dir_id = dir_id;
539 item.name = name;
540 }
541 }
542 }
543 Ok(())
544 },
545 )?;
546
547 let top_id =
550 crate::inode::lookup_path_rootid(fd).unwrap_or(FS_TREE_OBJECTID);
551
552 resolve_full_paths(fd, &mut items, top_id);
553
554 Ok(items)
555}
556
557fn ino_lookup_dir_path(
565 fd: BorrowedFd,
566 parent_tree: u64,
567 dir_id: u64,
568) -> nix::Result<String> {
569 let mut args = btrfs_ioctl_ino_lookup_args {
570 treeid: parent_tree,
571 objectid: dir_id,
572 ..unsafe { mem::zeroed() }
573 };
574 unsafe { btrfs_ioc_ino_lookup(fd.as_raw_fd(), &raw mut args) }?;
577
578 let name_ptr: *const c_char = args.name.as_ptr();
580 let cstr = unsafe { CStr::from_ptr(name_ptr) };
582 Ok(cstr.to_string_lossy().into_owned())
583}
584
585fn resolve_full_paths(
596 fd: BorrowedFd,
597 items: &mut [SubvolumeListItem],
598 top_id: u64,
599) {
600 let id_to_idx: HashMap<u64, usize> = items
602 .iter()
603 .enumerate()
604 .map(|(i, item)| (item.root_id, i))
605 .collect();
606
607 let segments: Vec<String> = items
612 .iter()
613 .map(|item| {
614 if item.parent_id == 0 || item.name.is_empty() {
615 return item.name.clone();
616 }
617 match ino_lookup_dir_path(fd, item.parent_id, item.dir_id) {
618 Ok(prefix) => format!("{}{}", prefix, item.name),
619 Err(_) => item.name.clone(),
620 }
621 })
622 .collect();
623
624 let mut full_paths: HashMap<u64, String> = HashMap::new();
627 let root_ids: Vec<u64> = items.iter().map(|i| i.root_id).collect();
628 for root_id in root_ids {
629 build_full_path(
630 root_id,
631 top_id,
632 &id_to_idx,
633 &segments,
634 items,
635 &mut full_paths,
636 );
637 }
638
639 for item in items.iter_mut() {
640 if let Some(path) = full_paths.remove(&item.root_id) {
641 item.name = path;
642 }
643 }
644}
645
646fn build_full_path(
656 root_id: u64,
657 top_id: u64,
658 id_to_idx: &HashMap<u64, usize>,
659 segments: &[String],
660 items: &[SubvolumeListItem],
661 cache: &mut HashMap<u64, String>,
662) -> String {
663 let mut chain: Vec<u64> = Vec::new();
667 let mut visited: HashMap<u64, usize> = HashMap::new();
668 let mut cur = root_id;
669 loop {
670 if cache.contains_key(&cur) {
671 break;
672 }
673 if visited.contains_key(&cur) {
674 let cycle_start = visited[&cur];
677 chain.truncate(cycle_start);
678 break;
679 }
680 let Some(&idx) = id_to_idx.get(&cur) else {
681 break;
682 };
683 visited.insert(cur, chain.len());
684 chain.push(cur);
685 let parent = items[idx].parent_id;
686 if parent == 0
687 || parent == FS_TREE_OBJECTID
688 || parent == top_id
689 || !id_to_idx.contains_key(&parent)
690 {
691 break;
692 }
693 cur = parent;
694 }
695
696 for &id in chain.iter().rev() {
699 let Some(&idx) = id_to_idx.get(&id) else {
700 cache.insert(id, String::new());
701 continue;
702 };
703 let segment = &segments[idx];
704 let parent_id = items[idx].parent_id;
705
706 let full_path = if parent_id == 0
707 || parent_id == FS_TREE_OBJECTID
708 || parent_id == top_id
709 || !id_to_idx.contains_key(&parent_id)
710 {
711 segment.clone()
712 } else if let Some(parent_path) = cache.get(&parent_id) {
713 if parent_path.is_empty() {
714 segment.clone()
715 } else {
716 format!("{parent_path}/{segment}")
717 }
718 } else {
719 segment.clone()
720 };
721
722 cache.insert(id, full_path);
723 }
724
725 cache.get(&root_id).cloned().unwrap_or_default()
726}
727
728fn parse_root_item(root_id: u64, data: &[u8]) -> Option<SubvolumeListItem> {
730 let ri = btrfs_disk::items::RootItem::parse(data)?;
731 let flags = SubvolumeFlags::from_bits_truncate(ri.flags.bits());
732 let otime = timespec_to_system_time(ri.otime.sec, ri.otime.nsec);
733
734 Some(SubvolumeListItem {
735 root_id,
736 parent_id: 0,
737 dir_id: 0,
738 generation: ri.generation,
739 flags,
740 uuid: ri.uuid,
741 parent_uuid: ri.parent_uuid,
742 received_uuid: ri.received_uuid,
743 otransid: ri.otransid,
744 otime,
745 name: String::new(),
746 })
747}
748
749fn parse_root_ref(data: &[u8]) -> Option<(u64, String)> {
752 let rr = btrfs_disk::items::RootRef::parse(data)?;
753 let name = String::from_utf8_lossy(&rr.name).into_owned();
754 Some((rr.dirid, name))
755}
756
757fn timespec_to_system_time(sec: u64, nsec: u32) -> SystemTime {
762 if sec == 0 {
763 return UNIX_EPOCH;
764 }
765 UNIX_EPOCH + Duration::new(sec, nsec)
766}
767
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
770pub struct SubvolRootRef {
771 pub treeid: u64,
773 pub dirid: u64,
775}
776
777pub fn subvol_rootrefs(fd: BorrowedFd) -> nix::Result<Vec<SubvolRootRef>> {
791 use crate::raw::{
792 btrfs_ioc_get_subvol_rootref, btrfs_ioctl_get_subvol_rootref_args,
793 };
794
795 let mut results = Vec::new();
796 let mut min_treeid: u64 = 0;
797
798 loop {
799 let mut args: btrfs_ioctl_get_subvol_rootref_args =
800 unsafe { std::mem::zeroed() };
801 args.min_treeid = min_treeid;
802
803 let ret = unsafe {
804 btrfs_ioc_get_subvol_rootref(fd.as_raw_fd(), &raw mut args)
805 };
806
807 let overflow = match ret {
811 Ok(_) => false,
812 Err(nix::errno::Errno::EOVERFLOW) => true,
813 Err(e) => return Err(e),
814 };
815
816 let count = args.num_items as usize;
817 for i in 0..count {
818 let r = &args.rootref[i];
819 results.push(SubvolRootRef {
820 treeid: r.treeid,
821 dirid: r.dirid,
822 });
823 }
824
825 if !overflow || count == 0 {
826 break;
827 }
828
829 min_treeid = args.rootref[count - 1].treeid + 1;
831 }
832
833 Ok(results)
834}
835
836pub fn subvol_sync_wait_one(fd: BorrowedFd, subvolid: u64) -> nix::Result<()> {
848 let args = btrfs_ioctl_subvol_wait {
849 subvolid,
850 mode: BTRFS_SUBVOL_SYNC_WAIT_FOR_ONE,
851 count: 0,
852 };
853 match unsafe { btrfs_ioc_subvol_sync_wait(fd.as_raw_fd(), &raw const args) }
854 {
855 Ok(_) | Err(nix::errno::Errno::ENOENT) => Ok(()),
856 Err(e) => Err(e),
857 }
858}
859
860pub fn subvol_sync_wait_all(fd: BorrowedFd) -> nix::Result<()> {
870 let args = btrfs_ioctl_subvol_wait {
871 subvolid: 0,
872 mode: BTRFS_SUBVOL_SYNC_WAIT_FOR_QUEUED,
873 count: 0,
874 };
875 unsafe { btrfs_ioc_subvol_sync_wait(fd.as_raw_fd(), &raw const args) }?;
876 Ok(())
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882 use std::{
883 collections::HashMap,
884 time::{Duration, UNIX_EPOCH},
885 };
886 use uuid::Uuid;
887
888 fn test_item(root_id: u64, parent_id: u64) -> SubvolumeListItem {
889 SubvolumeListItem {
890 root_id,
891 parent_id,
892 dir_id: 0,
893 generation: 0,
894 flags: SubvolumeFlags::empty(),
895 uuid: Uuid::nil(),
896 parent_uuid: Uuid::nil(),
897 received_uuid: Uuid::nil(),
898 otransid: 0,
899 otime: UNIX_EPOCH,
900 name: String::new(),
901 }
902 }
903
904 #[test]
905 fn timespec_zero_returns_epoch() {
906 assert_eq!(timespec_to_system_time(0, 0), UNIX_EPOCH);
907 }
908
909 #[test]
910 fn timespec_zero_sec_with_nonzero_nsec_returns_epoch() {
911 assert_eq!(timespec_to_system_time(0, 500_000_000), UNIX_EPOCH);
913 }
914
915 #[test]
916 fn timespec_nonzero_returns_correct_time() {
917 let t = timespec_to_system_time(1000, 500);
918 assert_eq!(t, UNIX_EPOCH + Duration::new(1000, 500));
919 }
920
921 #[test]
922 fn subvolume_flags_display_readonly() {
923 let flags = SubvolumeFlags::RDONLY;
924 assert_eq!(format!("{}", flags), "readonly");
925 }
926
927 #[test]
928 fn subvolume_flags_display_empty() {
929 let flags = SubvolumeFlags::empty();
930 assert_eq!(format!("{}", flags), "-");
931 }
932
933 #[test]
934 fn parse_root_ref_valid() {
935 let name = b"mysubvol";
937 let mut buf = Vec::new();
938 buf.extend_from_slice(&42u64.to_le_bytes()); buf.extend_from_slice(&1u64.to_le_bytes()); buf.extend_from_slice(&(name.len() as u16).to_le_bytes()); buf.extend_from_slice(name);
942
943 let result = parse_root_ref(&buf);
944 assert!(result.is_some());
945 let (dir_id, parsed_name) = result.unwrap();
946 assert_eq!(dir_id, 42);
947 assert_eq!(parsed_name, "mysubvol");
948 }
949
950 #[test]
951 fn parse_root_ref_too_short_header() {
952 let buf = [0u8; 10];
954 assert!(parse_root_ref(&buf).is_none());
955 }
956
957 #[test]
958 fn parse_root_ref_truncated_name() {
959 let mut buf = vec![0u8; 18];
962 buf[16] = 10; buf[17] = 0;
964 let result = parse_root_ref(&buf);
965 assert!(result.is_some());
966 let (_, name) = result.unwrap();
967 assert!(name.is_empty());
968 }
969
970 #[test]
971 fn parse_root_ref_empty_name() {
972 let mut buf = Vec::new();
973 buf.extend_from_slice(&100u64.to_le_bytes()); buf.extend_from_slice(&0u64.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); let result = parse_root_ref(&buf);
978 assert!(result.is_some());
979 let (dir_id, parsed_name) = result.unwrap();
980 assert_eq!(dir_id, 100);
981 assert_eq!(parsed_name, "");
982 }
983
984 #[test]
985 fn build_full_path_single_subvol_parent_fs_tree() {
986 let items = vec![test_item(256, FS_TREE_OBJECTID)];
988 let segments = vec!["mysub".to_string()];
989 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
990 let mut cache = HashMap::new();
991
992 let path = build_full_path(
993 256,
994 FS_TREE_OBJECTID,
995 &id_to_idx,
996 &segments,
997 &items,
998 &mut cache,
999 );
1000 assert_eq!(path, "mysub");
1001 }
1002
1003 #[test]
1004 fn build_full_path_nested_chain() {
1005 let items = vec![
1007 test_item(256, FS_TREE_OBJECTID),
1008 test_item(257, 256),
1009 test_item(258, 257),
1010 ];
1011 let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1012 let id_to_idx: HashMap<u64, usize> =
1013 [(256, 0), (257, 1), (258, 2)].into();
1014 let mut cache = HashMap::new();
1015
1016 let path = build_full_path(
1017 258,
1018 FS_TREE_OBJECTID,
1019 &id_to_idx,
1020 &segments,
1021 &items,
1022 &mut cache,
1023 );
1024 assert_eq!(path, "A/B/C");
1025 }
1026
1027 #[test]
1028 fn build_full_path_stops_at_top_id() {
1029 let items = vec![
1033 test_item(256, FS_TREE_OBJECTID),
1034 test_item(257, 256),
1035 test_item(258, 257),
1036 ];
1037 let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1038 let id_to_idx: HashMap<u64, usize> =
1039 [(256, 0), (257, 1), (258, 2)].into();
1040 let mut cache = HashMap::new();
1041
1042 let path = build_full_path(
1043 258, 257, &id_to_idx, &segments, &items, &mut cache,
1044 );
1045 assert_eq!(path, "C");
1046
1047 let path_b = build_full_path(
1053 257, 257, &id_to_idx, &segments, &items, &mut cache,
1054 );
1055 assert_eq!(path_b, "A/B");
1059 }
1060
1061 #[test]
1062 fn build_full_path_cycle_detection() {
1063 let items = vec![test_item(256, 257), test_item(257, 256)];
1065 let segments = vec!["A".to_string(), "B".to_string()];
1066 let id_to_idx: HashMap<u64, usize> = [(256, 0), (257, 1)].into();
1067 let mut cache = HashMap::new();
1068
1069 let _path = build_full_path(
1071 256,
1072 FS_TREE_OBJECTID,
1073 &id_to_idx,
1074 &segments,
1075 &items,
1076 &mut cache,
1077 );
1078 }
1081
1082 #[test]
1083 fn build_full_path_cached_ancestor() {
1084 let items = vec![
1087 test_item(256, FS_TREE_OBJECTID),
1088 test_item(257, 256),
1089 test_item(258, 257),
1090 ];
1091 let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1092 let id_to_idx: HashMap<u64, usize> =
1093 [(256, 0), (257, 1), (258, 2)].into();
1094 let mut cache = HashMap::new();
1095 cache.insert(257, "A/B".to_string());
1096
1097 let path = build_full_path(
1098 258,
1099 FS_TREE_OBJECTID,
1100 &id_to_idx,
1101 &segments,
1102 &items,
1103 &mut cache,
1104 );
1105 assert_eq!(path, "A/B/C");
1106 }
1107
1108 #[test]
1109 fn build_full_path_unknown_parent() {
1110 let items = vec![test_item(256, 999)];
1112 let segments = vec!["orphan".to_string()];
1113 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
1114 let mut cache = HashMap::new();
1115
1116 let path = build_full_path(
1117 256,
1118 FS_TREE_OBJECTID,
1119 &id_to_idx,
1120 &segments,
1121 &items,
1122 &mut cache,
1123 );
1124 assert_eq!(path, "orphan");
1125 }
1126
1127 #[test]
1128 fn build_full_path_parent_id_zero() {
1129 let items = vec![test_item(256, 0)];
1131 let segments = vec!["noparent".to_string()];
1132 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
1133 let mut cache = HashMap::new();
1134
1135 let path = build_full_path(
1136 256,
1137 FS_TREE_OBJECTID,
1138 &id_to_idx,
1139 &segments,
1140 &items,
1141 &mut cache,
1142 );
1143 assert_eq!(path, "noparent");
1144 }
1145
1146 #[test]
1147 fn build_full_path_already_cached_target() {
1148 let items = vec![test_item(256, FS_TREE_OBJECTID)];
1150 let segments = vec!["A".to_string()];
1151 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
1152 let mut cache = HashMap::new();
1153 cache.insert(256, "cached/path".to_string());
1154
1155 let path = build_full_path(
1156 256,
1157 FS_TREE_OBJECTID,
1158 &id_to_idx,
1159 &segments,
1160 &items,
1161 &mut cache,
1162 );
1163 assert_eq!(path, "cached/path");
1164 }
1165
1166 #[test]
1167 fn build_full_path_root_id_not_in_items() {
1168 let items = vec![test_item(256, FS_TREE_OBJECTID)];
1170 let segments = vec!["A".to_string()];
1171 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
1172 let mut cache = HashMap::new();
1173
1174 let path = build_full_path(
1175 999,
1176 FS_TREE_OBJECTID,
1177 &id_to_idx,
1178 &segments,
1179 &items,
1180 &mut cache,
1181 );
1182 assert_eq!(path, "");
1183 }
1184}