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}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use std::{
642 collections::HashMap,
643 time::{Duration, UNIX_EPOCH},
644 };
645 use uuid::Uuid;
646
647 fn test_item(root_id: u64, parent_id: u64) -> SubvolumeListItem {
648 SubvolumeListItem {
649 root_id,
650 parent_id,
651 dir_id: 0,
652 generation: 0,
653 flags: SubvolumeFlags::empty(),
654 uuid: Uuid::nil(),
655 parent_uuid: Uuid::nil(),
656 received_uuid: Uuid::nil(),
657 otransid: 0,
658 otime: UNIX_EPOCH,
659 name: String::new(),
660 }
661 }
662
663 #[test]
664 fn rle64_reads_little_endian() {
665 let buf = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
666 assert_eq!(rle64(&buf, 0), 0x0807060504030201);
667 }
668
669 #[test]
670 fn rle64_at_offset() {
671 let buf = [0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
672 assert_eq!(rle64(&buf, 2), 1);
673 }
674
675 #[test]
676 fn rle32_reads_little_endian() {
677 let buf = [0x78, 0x56, 0x34, 0x12];
678 assert_eq!(rle32(&buf, 0), 0x12345678);
679 }
680
681 #[test]
682 fn rle32_at_offset() {
683 let buf = [0x00, 0x00, 0x01, 0x00, 0x00, 0x00];
684 assert_eq!(rle32(&buf, 2), 1);
685 }
686
687 #[test]
688 fn timespec_zero_returns_epoch() {
689 assert_eq!(timespec_to_system_time(0, 0), UNIX_EPOCH);
690 }
691
692 #[test]
693 fn timespec_zero_sec_with_nonzero_nsec_returns_epoch() {
694 assert_eq!(timespec_to_system_time(0, 500_000_000), UNIX_EPOCH);
696 }
697
698 #[test]
699 fn timespec_nonzero_returns_correct_time() {
700 let t = timespec_to_system_time(1000, 500);
701 assert_eq!(t, UNIX_EPOCH + Duration::new(1000, 500));
702 }
703
704 #[test]
705 fn subvolume_flags_display_readonly() {
706 let flags = SubvolumeFlags::RDONLY;
707 assert_eq!(format!("{}", flags), "readonly");
708 }
709
710 #[test]
711 fn subvolume_flags_display_empty() {
712 let flags = SubvolumeFlags::empty();
713 assert_eq!(format!("{}", flags), "-");
714 }
715
716 #[test]
717 fn parse_root_ref_valid() {
718 let name = b"mysubvol";
720 let mut buf = Vec::new();
721 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);
725
726 let result = parse_root_ref(&buf);
727 assert!(result.is_some());
728 let (dir_id, parsed_name) = result.unwrap();
729 assert_eq!(dir_id, 42);
730 assert_eq!(parsed_name, "mysubvol");
731 }
732
733 #[test]
734 fn parse_root_ref_too_short_header() {
735 let buf = [0u8; 10];
737 assert!(parse_root_ref(&buf).is_none());
738 }
739
740 #[test]
741 fn parse_root_ref_too_short_name() {
742 let mut buf = vec![0u8; 18];
744 buf[16] = 10;
746 buf[17] = 0;
747 assert!(parse_root_ref(&buf).is_none());
748 }
749
750 #[test]
751 fn parse_root_ref_empty_name() {
752 let mut buf = Vec::new();
753 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);
758 assert!(result.is_some());
759 let (dir_id, parsed_name) = result.unwrap();
760 assert_eq!(dir_id, 100);
761 assert_eq!(parsed_name, "");
762 }
763
764 #[test]
765 fn build_full_path_single_subvol_parent_fs_tree() {
766 let items = vec![test_item(256, FS_TREE_OBJECTID)];
768 let segments = vec!["mysub".to_string()];
769 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
770 let mut cache = HashMap::new();
771
772 let path = build_full_path(
773 256,
774 FS_TREE_OBJECTID,
775 &id_to_idx,
776 &segments,
777 &items,
778 &mut cache,
779 );
780 assert_eq!(path, "mysub");
781 }
782
783 #[test]
784 fn build_full_path_nested_chain() {
785 let items = vec![
787 test_item(256, FS_TREE_OBJECTID),
788 test_item(257, 256),
789 test_item(258, 257),
790 ];
791 let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
792 let id_to_idx: HashMap<u64, usize> = [(256, 0), (257, 1), (258, 2)].into();
793 let mut cache = HashMap::new();
794
795 let path = build_full_path(
796 258,
797 FS_TREE_OBJECTID,
798 &id_to_idx,
799 &segments,
800 &items,
801 &mut cache,
802 );
803 assert_eq!(path, "A/B/C");
804 }
805
806 #[test]
807 fn build_full_path_stops_at_top_id() {
808 let items = vec![
812 test_item(256, FS_TREE_OBJECTID),
813 test_item(257, 256),
814 test_item(258, 257),
815 ];
816 let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
817 let id_to_idx: HashMap<u64, usize> = [(256, 0), (257, 1), (258, 2)].into();
818 let mut cache = HashMap::new();
819
820 let path = build_full_path(258, 257, &id_to_idx, &segments, &items, &mut cache);
821 assert_eq!(path, "C");
822
823 let path_b = build_full_path(257, 257, &id_to_idx, &segments, &items, &mut cache);
829 assert_eq!(path_b, "A/B");
833 }
834
835 #[test]
836 fn build_full_path_cycle_detection() {
837 let items = vec![test_item(256, 257), test_item(257, 256)];
839 let segments = vec!["A".to_string(), "B".to_string()];
840 let id_to_idx: HashMap<u64, usize> = [(256, 0), (257, 1)].into();
841 let mut cache = HashMap::new();
842
843 let _path = build_full_path(
845 256,
846 FS_TREE_OBJECTID,
847 &id_to_idx,
848 &segments,
849 &items,
850 &mut cache,
851 );
852 }
855
856 #[test]
857 fn build_full_path_cached_ancestor() {
858 let items = vec![
861 test_item(256, FS_TREE_OBJECTID),
862 test_item(257, 256),
863 test_item(258, 257),
864 ];
865 let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
866 let id_to_idx: HashMap<u64, usize> = [(256, 0), (257, 1), (258, 2)].into();
867 let mut cache = HashMap::new();
868 cache.insert(257, "A/B".to_string());
869
870 let path = build_full_path(
871 258,
872 FS_TREE_OBJECTID,
873 &id_to_idx,
874 &segments,
875 &items,
876 &mut cache,
877 );
878 assert_eq!(path, "A/B/C");
879 }
880
881 #[test]
882 fn build_full_path_unknown_parent() {
883 let items = vec![test_item(256, 999)];
885 let segments = vec!["orphan".to_string()];
886 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
887 let mut cache = HashMap::new();
888
889 let path = build_full_path(
890 256,
891 FS_TREE_OBJECTID,
892 &id_to_idx,
893 &segments,
894 &items,
895 &mut cache,
896 );
897 assert_eq!(path, "orphan");
898 }
899
900 #[test]
901 fn build_full_path_parent_id_zero() {
902 let items = vec![test_item(256, 0)];
904 let segments = vec!["noparent".to_string()];
905 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
906 let mut cache = HashMap::new();
907
908 let path = build_full_path(
909 256,
910 FS_TREE_OBJECTID,
911 &id_to_idx,
912 &segments,
913 &items,
914 &mut cache,
915 );
916 assert_eq!(path, "noparent");
917 }
918
919 #[test]
920 fn build_full_path_already_cached_target() {
921 let items = vec![test_item(256, FS_TREE_OBJECTID)];
923 let segments = vec!["A".to_string()];
924 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
925 let mut cache = HashMap::new();
926 cache.insert(256, "cached/path".to_string());
927
928 let path = build_full_path(
929 256,
930 FS_TREE_OBJECTID,
931 &id_to_idx,
932 &segments,
933 &items,
934 &mut cache,
935 );
936 assert_eq!(path, "cached/path");
937 }
938
939 #[test]
940 fn build_full_path_root_id_not_in_items() {
941 let items = vec![test_item(256, FS_TREE_OBJECTID)];
943 let segments = vec!["A".to_string()];
944 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
945 let mut cache = HashMap::new();
946
947 let path = build_full_path(
948 999,
949 FS_TREE_OBJECTID,
950 &id_to_idx,
951 &segments,
952 &items,
953 &mut cache,
954 );
955 assert_eq!(path, "");
956 }
957}