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,
16 BTRFS_SUBVOL_QGROUP_INHERIT, BTRFS_SUBVOL_RDONLY,
17 BTRFS_SUBVOL_SPEC_BY_ID, BTRFS_SUBVOL_SYNC_WAIT_FOR_ONE,
18 BTRFS_SUBVOL_SYNC_WAIT_FOR_QUEUED, btrfs_ioc_default_subvol,
19 btrfs_ioc_get_subvol_info, btrfs_ioc_ino_lookup,
20 btrfs_ioc_snap_create_v2, btrfs_ioc_snap_destroy_v2,
21 btrfs_ioc_subvol_create_v2, btrfs_ioc_subvol_getflags,
22 btrfs_ioc_subvol_setflags, btrfs_ioc_subvol_sync_wait,
23 btrfs_ioctl_get_subvol_info_args, btrfs_ioctl_ino_lookup_args,
24 btrfs_ioctl_subvol_wait, btrfs_ioctl_vol_args_v2, btrfs_qgroup_inherit,
25 btrfs_root_item, btrfs_timespec,
26 },
27 tree_search::{SearchKey, tree_search},
28 util::{read_le_u32, read_le_u64},
29};
30use bitflags::bitflags;
31use nix::libc::c_char;
32use std::{
33 collections::HashMap,
34 ffi::CStr,
35 mem,
36 os::{fd::AsRawFd, unix::io::BorrowedFd},
37 time::{Duration, SystemTime, UNIX_EPOCH},
38};
39use uuid::Uuid;
40
41pub const FS_TREE_OBJECTID: u64 = BTRFS_FS_TREE_OBJECTID as u64;
45
46bitflags! {
47 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
50 pub struct SubvolumeFlags: u64 {
51 const RDONLY = BTRFS_SUBVOL_RDONLY as u64;
53 }
54}
55
56impl std::fmt::Display for SubvolumeFlags {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 if self.contains(SubvolumeFlags::RDONLY) {
59 write!(f, "readonly")
60 } else {
61 write!(f, "-")
62 }
63 }
64}
65
66#[derive(Debug, Clone)]
68pub struct SubvolumeInfo {
69 pub id: u64,
71 pub name: String,
73 pub parent_id: u64,
75 pub dir_id: u64,
77 pub generation: u64,
79 pub flags: SubvolumeFlags,
81 pub uuid: Uuid,
83 pub parent_uuid: Uuid,
85 pub received_uuid: Uuid,
87 pub ctransid: u64,
89 pub otransid: u64,
91 pub stransid: u64,
93 pub rtransid: u64,
95 pub ctime: SystemTime,
97 pub otime: SystemTime,
99 pub stime: SystemTime,
101 pub rtime: SystemTime,
103}
104
105#[derive(Debug, Clone)]
107pub struct SubvolumeListItem {
108 pub root_id: u64,
110 pub parent_id: u64,
112 pub dir_id: u64,
114 pub generation: u64,
116 pub flags: SubvolumeFlags,
118 pub uuid: Uuid,
120 pub parent_uuid: Uuid,
122 pub received_uuid: Uuid,
124 pub otransid: u64,
126 pub otime: SystemTime,
128 pub name: String,
134}
135
136fn set_v2_name(
139 args: &mut btrfs_ioctl_vol_args_v2,
140 name: &CStr,
141) -> nix::Result<()> {
142 let bytes = name.to_bytes(); let name_buf: &mut [c_char] = unsafe { &mut args.__bindgen_anon_2.name };
146 if bytes.len() >= name_buf.len() {
147 return Err(nix::errno::Errno::ENAMETOOLONG);
148 }
149 for (i, &b) in bytes.iter().enumerate() {
150 name_buf[i] = b as c_char;
151 }
152 Ok(())
153}
154
155fn build_qgroup_inherit(qgroups: &[u64]) -> Vec<u64> {
161 let base_size = mem::size_of::<btrfs_qgroup_inherit>();
162 let total_size = base_size + std::mem::size_of_val(qgroups);
163 let num_u64 = total_size.div_ceil(8);
164 let mut buf = vec![0u64; num_u64];
165
166 let inherit =
169 unsafe { &mut *(buf.as_mut_ptr() as *mut btrfs_qgroup_inherit) };
170 inherit.num_qgroups = qgroups.len() as u64;
171
172 if !qgroups.is_empty() {
174 let array = unsafe { inherit.qgroups.as_mut_slice(qgroups.len()) };
175 array.copy_from_slice(qgroups);
176 }
177
178 buf
179}
180
181fn set_qgroup_inherit(
185 args: &mut btrfs_ioctl_vol_args_v2,
186 buf: &[u64],
187 num_qgroups: usize,
188) {
189 args.flags |= BTRFS_SUBVOL_QGROUP_INHERIT as u64;
190 let base_size = mem::size_of::<btrfs_qgroup_inherit>();
191 let total_size = base_size + num_qgroups * mem::size_of::<u64>();
192 args.__bindgen_anon_1.__bindgen_anon_1.size = total_size as u64;
193 args.__bindgen_anon_1.__bindgen_anon_1.qgroup_inherit =
194 buf.as_ptr() as *mut btrfs_qgroup_inherit;
195}
196
197pub fn subvolume_create(
208 parent_fd: BorrowedFd,
209 name: &CStr,
210 qgroups: &[u64],
211) -> nix::Result<()> {
212 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
213 set_v2_name(&mut args, name)?;
214
215 let inherit_buf;
216 if !qgroups.is_empty() {
217 inherit_buf = build_qgroup_inherit(qgroups);
218 set_qgroup_inherit(&mut args, &inherit_buf, qgroups.len());
219 }
220
221 unsafe { btrfs_ioc_subvol_create_v2(parent_fd.as_raw_fd(), &args) }?;
222 Ok(())
223}
224
225pub fn subvolume_delete(parent_fd: BorrowedFd, name: &CStr) -> nix::Result<()> {
238 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
239 set_v2_name(&mut args, name)?;
240 unsafe { btrfs_ioc_snap_destroy_v2(parent_fd.as_raw_fd(), &args) }?;
241 Ok(())
242}
243
244pub fn subvolume_delete_by_id(
253 fd: BorrowedFd,
254 subvolid: u64,
255) -> nix::Result<()> {
256 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
257 args.flags = BTRFS_SUBVOL_SPEC_BY_ID as u64;
258 args.__bindgen_anon_2.subvolid = subvolid;
259 unsafe { btrfs_ioc_snap_destroy_v2(fd.as_raw_fd(), &args) }?;
260 Ok(())
261}
262
263pub fn snapshot_create(
275 parent_fd: BorrowedFd,
276 source_fd: BorrowedFd,
277 name: &CStr,
278 readonly: bool,
279 qgroups: &[u64],
280) -> nix::Result<()> {
281 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
282 args.fd = source_fd.as_raw_fd() as i64;
284 if readonly {
285 args.flags = BTRFS_SUBVOL_RDONLY as u64;
286 }
287 set_v2_name(&mut args, name)?;
288
289 let inherit_buf;
290 if !qgroups.is_empty() {
291 inherit_buf = build_qgroup_inherit(qgroups);
292 set_qgroup_inherit(&mut args, &inherit_buf, qgroups.len());
293 }
294
295 unsafe { btrfs_ioc_snap_create_v2(parent_fd.as_raw_fd(), &args) }?;
296 Ok(())
297}
298
299pub fn subvolume_info(fd: BorrowedFd) -> nix::Result<SubvolumeInfo> {
304 subvolume_info_by_id(fd, 0)
305}
306
307pub fn subvolume_info_by_id(
316 fd: BorrowedFd,
317 rootid: u64,
318) -> nix::Result<SubvolumeInfo> {
319 let mut raw: btrfs_ioctl_get_subvol_info_args = unsafe { mem::zeroed() };
320 raw.treeid = rootid;
321 unsafe { btrfs_ioc_get_subvol_info(fd.as_raw_fd(), &mut raw) }?;
322
323 let name = unsafe { CStr::from_ptr(raw.name.as_ptr()) }
324 .to_string_lossy()
325 .into_owned();
326
327 Ok(SubvolumeInfo {
328 id: raw.treeid,
329 name,
330 parent_id: raw.parent_id,
331 dir_id: raw.dirid,
332 generation: raw.generation,
333 flags: SubvolumeFlags::from_bits_truncate(raw.flags),
334 uuid: Uuid::from_bytes(raw.uuid),
335 parent_uuid: Uuid::from_bytes(raw.parent_uuid),
336 received_uuid: Uuid::from_bytes(raw.received_uuid),
337 ctransid: raw.ctransid,
338 otransid: raw.otransid,
339 stransid: raw.stransid,
340 rtransid: raw.rtransid,
341 ctime: ioctl_timespec_to_system_time(raw.ctime.sec, raw.ctime.nsec),
342 otime: ioctl_timespec_to_system_time(raw.otime.sec, raw.otime.nsec),
343 stime: ioctl_timespec_to_system_time(raw.stime.sec, raw.stime.nsec),
344 rtime: ioctl_timespec_to_system_time(raw.rtime.sec, raw.rtime.nsec),
345 })
346}
347
348pub fn subvolume_flags_get(fd: BorrowedFd) -> nix::Result<SubvolumeFlags> {
350 let mut flags: u64 = 0;
351 unsafe { btrfs_ioc_subvol_getflags(fd.as_raw_fd(), &mut flags) }?;
352 Ok(SubvolumeFlags::from_bits_truncate(flags))
353}
354
355pub fn subvolume_flags_set(
360 fd: BorrowedFd,
361 flags: SubvolumeFlags,
362) -> nix::Result<()> {
363 let raw: u64 = flags.bits();
364 unsafe { btrfs_ioc_subvol_setflags(fd.as_raw_fd(), &raw) }?;
365 Ok(())
366}
367
368pub fn subvolume_default_get(fd: BorrowedFd) -> nix::Result<u64> {
377 let mut default_id: Option<u64> = None;
378
379 tree_search(
380 fd,
381 SearchKey::for_objectid_range(
382 BTRFS_ROOT_TREE_OBJECTID as u64,
383 BTRFS_DIR_ITEM_KEY,
384 BTRFS_ROOT_TREE_DIR_OBJECTID as u64,
385 BTRFS_ROOT_TREE_DIR_OBJECTID as u64,
386 ),
387 |_hdr, data| {
388 use crate::raw::btrfs_dir_item;
389 use std::mem::{offset_of, size_of};
390
391 let header_size = size_of::<btrfs_dir_item>();
392 if data.len() < header_size {
393 return Ok(());
394 }
395 let name_off = offset_of!(btrfs_dir_item, name_len);
396 let name_len =
397 u16::from_le_bytes([data[name_off], data[name_off + 1]])
398 as usize;
399 if data.len() < header_size + name_len {
400 return Ok(());
401 }
402 let item_name = &data[header_size..header_size + name_len];
403 if item_name == b"default" {
404 let loc_off = offset_of!(btrfs_dir_item, location);
405 let target_id = u64::from_le_bytes(
406 data[loc_off..loc_off + 8].try_into().unwrap(),
407 );
408 default_id = Some(target_id);
409 }
410 Ok(())
411 },
412 )?;
413
414 Ok(default_id.unwrap_or(BTRFS_FS_TREE_OBJECTID as u64))
415}
416
417pub fn subvolume_default_set(fd: BorrowedFd, subvolid: u64) -> nix::Result<()> {
422 unsafe { btrfs_ioc_default_subvol(fd.as_raw_fd(), &subvolid) }?;
423 Ok(())
424}
425
426pub fn subvolume_list(fd: BorrowedFd) -> nix::Result<Vec<SubvolumeListItem>> {
441 let mut items: Vec<SubvolumeListItem> = Vec::new();
442
443 tree_search(
444 fd,
445 SearchKey::for_objectid_range(
446 BTRFS_ROOT_TREE_OBJECTID as u64,
447 BTRFS_ROOT_ITEM_KEY,
448 BTRFS_FIRST_FREE_OBJECTID as u64,
449 BTRFS_LAST_FREE_OBJECTID as u64,
450 ),
451 |hdr, data| {
452 if let Some(item) = parse_root_item(hdr.objectid, data) {
453 items.push(item);
454 }
455 Ok(())
456 },
457 )?;
458
459 tree_search(
460 fd,
461 SearchKey::for_objectid_range(
462 BTRFS_ROOT_TREE_OBJECTID as u64,
463 BTRFS_ROOT_BACKREF_KEY,
464 BTRFS_FIRST_FREE_OBJECTID as u64,
465 BTRFS_LAST_FREE_OBJECTID as u64,
466 ),
467 |hdr, data| {
468 let root_id = hdr.objectid;
470 let parent_id = hdr.offset;
471
472 if let Some(item) = items.iter_mut().find(|i| i.root_id == root_id)
473 {
474 if item.parent_id == 0 {
482 item.parent_id = parent_id;
483 if let Some((dir_id, name)) = parse_root_ref(data) {
484 item.dir_id = dir_id;
485 item.name = name;
486 }
487 }
488 }
489 Ok(())
490 },
491 )?;
492
493 let top_id =
496 crate::inode::lookup_path_rootid(fd).unwrap_or(FS_TREE_OBJECTID);
497
498 resolve_full_paths(fd, &mut items, top_id)?;
499
500 Ok(items)
501}
502
503fn ino_lookup_dir_path(
511 fd: BorrowedFd,
512 parent_tree: u64,
513 dir_id: u64,
514) -> nix::Result<String> {
515 let mut args = btrfs_ioctl_ino_lookup_args {
516 treeid: parent_tree,
517 objectid: dir_id,
518 ..unsafe { mem::zeroed() }
519 };
520 unsafe { btrfs_ioc_ino_lookup(fd.as_raw_fd(), &mut args) }?;
523
524 let name_ptr: *const c_char = args.name.as_ptr();
526 let cstr = unsafe { CStr::from_ptr(name_ptr) };
528 Ok(cstr.to_string_lossy().into_owned())
529}
530
531fn resolve_full_paths(
542 fd: BorrowedFd,
543 items: &mut [SubvolumeListItem],
544 top_id: u64,
545) -> nix::Result<()> {
546 let id_to_idx: HashMap<u64, usize> = items
548 .iter()
549 .enumerate()
550 .map(|(i, item)| (item.root_id, i))
551 .collect();
552
553 let segments: Vec<String> = items
558 .iter()
559 .map(|item| {
560 if item.parent_id == 0 || item.name.is_empty() {
561 return item.name.clone();
562 }
563 match ino_lookup_dir_path(fd, item.parent_id, item.dir_id) {
564 Ok(prefix) => format!("{}{}", prefix, item.name),
565 Err(_) => item.name.clone(),
566 }
567 })
568 .collect();
569
570 let mut full_paths: HashMap<u64, String> = HashMap::new();
573 let root_ids: Vec<u64> = items.iter().map(|i| i.root_id).collect();
574 for root_id in root_ids {
575 build_full_path(
576 root_id,
577 top_id,
578 &id_to_idx,
579 &segments,
580 items,
581 &mut full_paths,
582 );
583 }
584
585 for item in items.iter_mut() {
586 if let Some(path) = full_paths.remove(&item.root_id) {
587 item.name = path;
588 }
589 }
590
591 Ok(())
592}
593
594fn build_full_path(
604 root_id: u64,
605 top_id: u64,
606 id_to_idx: &HashMap<u64, usize>,
607 segments: &[String],
608 items: &[SubvolumeListItem],
609 cache: &mut HashMap<u64, String>,
610) -> String {
611 let mut chain: Vec<u64> = Vec::new();
615 let mut visited: HashMap<u64, usize> = HashMap::new();
616 let mut cur = root_id;
617 loop {
618 if cache.contains_key(&cur) {
619 break;
620 }
621 if visited.contains_key(&cur) {
622 let cycle_start = visited[&cur];
625 chain.truncate(cycle_start);
626 break;
627 }
628 let Some(&idx) = id_to_idx.get(&cur) else {
629 break;
630 };
631 visited.insert(cur, chain.len());
632 chain.push(cur);
633 let parent = items[idx].parent_id;
634 if parent == 0
635 || parent == FS_TREE_OBJECTID
636 || parent == top_id
637 || !id_to_idx.contains_key(&parent)
638 {
639 break;
640 }
641 cur = parent;
642 }
643
644 for &id in chain.iter().rev() {
647 let Some(&idx) = id_to_idx.get(&id) else {
648 cache.insert(id, String::new());
649 continue;
650 };
651 let segment = &segments[idx];
652 let parent_id = items[idx].parent_id;
653
654 let full_path = if parent_id == 0
655 || parent_id == FS_TREE_OBJECTID
656 || parent_id == top_id
657 || !id_to_idx.contains_key(&parent_id)
658 {
659 segment.clone()
660 } else if let Some(parent_path) = cache.get(&parent_id) {
661 if parent_path.is_empty() {
662 segment.clone()
663 } else {
664 format!("{}/{}", parent_path, segment)
665 }
666 } else {
667 segment.clone()
668 };
669
670 cache.insert(id, full_path);
671 }
672
673 cache.get(&root_id).cloned().unwrap_or_default()
674}
675
676fn parse_root_item(root_id: u64, data: &[u8]) -> Option<SubvolumeListItem> {
678 use std::mem::offset_of;
679
680 let legacy_boundary = offset_of!(btrfs_root_item, generation_v2);
683 if data.len() < legacy_boundary {
684 return None;
685 }
686
687 let generation = read_le_u64(data, offset_of!(btrfs_root_item, generation));
688 let flags_raw = read_le_u64(data, offset_of!(btrfs_root_item, flags));
689 let flags = SubvolumeFlags::from_bits_truncate(flags_raw);
690
691 let otime_nsec =
693 offset_of!(btrfs_root_item, otime) + offset_of!(btrfs_timespec, nsec);
694 let (uuid, parent_uuid, received_uuid, otransid, otime) = if data.len()
695 >= otime_nsec + field_size!(btrfs_timespec, nsec)
696 {
697 let off_uuid = offset_of!(btrfs_root_item, uuid);
698 let off_parent = offset_of!(btrfs_root_item, parent_uuid);
699 let off_received = offset_of!(btrfs_root_item, received_uuid);
700 let uuid_size = field_size!(btrfs_root_item, uuid);
701 let uuid = Uuid::from_bytes(
702 data[off_uuid..off_uuid + uuid_size].try_into().unwrap(),
703 );
704 let parent_uuid = Uuid::from_bytes(
705 data[off_parent..off_parent + uuid_size].try_into().unwrap(),
706 );
707 let received_uuid = Uuid::from_bytes(
708 data[off_received..off_received + uuid_size]
709 .try_into()
710 .unwrap(),
711 );
712 let otransid = read_le_u64(data, offset_of!(btrfs_root_item, otransid));
713 let otime_sec = offset_of!(btrfs_root_item, otime);
714 let otime = timespec_to_system_time(
715 read_le_u64(data, otime_sec),
716 read_le_u32(data, otime_nsec),
717 );
718 (uuid, parent_uuid, received_uuid, otransid, otime)
719 } else {
720 (Uuid::nil(), Uuid::nil(), Uuid::nil(), 0, UNIX_EPOCH)
721 };
722
723 Some(SubvolumeListItem {
724 root_id,
725 parent_id: 0,
726 dir_id: 0,
727 generation,
728 flags,
729 uuid,
730 parent_uuid,
731 received_uuid,
732 otransid,
733 otime,
734 name: String::new(),
735 })
736}
737
738fn parse_root_ref(data: &[u8]) -> Option<(u64, String)> {
741 use crate::raw::btrfs_root_ref;
742 use std::mem::{offset_of, size_of};
743
744 let header_size = size_of::<btrfs_root_ref>();
745 if data.len() < header_size {
746 return None;
747 }
748 let dir_id = read_le_u64(data, offset_of!(btrfs_root_ref, dirid));
749 let name_off = offset_of!(btrfs_root_ref, name_len);
750 let name_len =
751 u16::from_le_bytes([data[name_off], data[name_off + 1]]) as usize;
752 if data.len() < header_size + name_len {
753 return None;
754 }
755 let name =
756 String::from_utf8_lossy(&data[header_size..header_size + name_len])
757 .into_owned();
758 Some((dir_id, name))
759}
760
761fn timespec_to_system_time(sec: u64, nsec: u32) -> SystemTime {
764 if sec == 0 {
765 return UNIX_EPOCH;
766 }
767 UNIX_EPOCH + Duration::new(sec, nsec)
768}
769
770fn ioctl_timespec_to_system_time(sec: u64, nsec: u32) -> SystemTime {
773 if sec == 0 {
774 return UNIX_EPOCH;
775 }
776 UNIX_EPOCH + Duration::new(sec, nsec)
777}
778
779#[derive(Debug, Clone, Copy, PartialEq, Eq)]
781pub struct SubvolRootRef {
782 pub treeid: u64,
784 pub dirid: u64,
786}
787
788pub fn subvol_rootrefs(fd: BorrowedFd) -> nix::Result<Vec<SubvolRootRef>> {
798 use crate::raw::{
799 btrfs_ioc_get_subvol_rootref, btrfs_ioctl_get_subvol_rootref_args,
800 };
801
802 let mut results = Vec::new();
803 let mut min_treeid: u64 = 0;
804
805 loop {
806 let mut args: btrfs_ioctl_get_subvol_rootref_args =
807 unsafe { std::mem::zeroed() };
808 args.min_treeid = min_treeid;
809
810 let ret =
811 unsafe { btrfs_ioc_get_subvol_rootref(fd.as_raw_fd(), &mut args) };
812
813 let overflow = match ret {
817 Ok(_) => false,
818 Err(nix::errno::Errno::EOVERFLOW) => true,
819 Err(e) => return Err(e),
820 };
821
822 let count = args.num_items as usize;
823 for i in 0..count {
824 let r = &args.rootref[i];
825 results.push(SubvolRootRef {
826 treeid: r.treeid,
827 dirid: r.dirid,
828 });
829 }
830
831 if !overflow || count == 0 {
832 break;
833 }
834
835 min_treeid = args.rootref[count - 1].treeid + 1;
837 }
838
839 Ok(results)
840}
841
842pub fn subvol_sync_wait_one(fd: BorrowedFd, subvolid: u64) -> nix::Result<()> {
850 let args = btrfs_ioctl_subvol_wait {
851 subvolid,
852 mode: BTRFS_SUBVOL_SYNC_WAIT_FOR_ONE,
853 count: 0,
854 };
855 match unsafe { btrfs_ioc_subvol_sync_wait(fd.as_raw_fd(), &args) } {
856 Ok(_) | Err(nix::errno::Errno::ENOENT) => Ok(()),
857 Err(e) => Err(e),
858 }
859}
860
861pub fn subvol_sync_wait_all(fd: BorrowedFd) -> nix::Result<()> {
867 let args = btrfs_ioctl_subvol_wait {
868 subvolid: 0,
869 mode: BTRFS_SUBVOL_SYNC_WAIT_FOR_QUEUED,
870 count: 0,
871 };
872 unsafe { btrfs_ioc_subvol_sync_wait(fd.as_raw_fd(), &args) }?;
873 Ok(())
874}
875
876#[cfg(test)]
877mod tests {
878 use super::*;
879 use std::{
880 collections::HashMap,
881 time::{Duration, UNIX_EPOCH},
882 };
883 use uuid::Uuid;
884
885 fn test_item(root_id: u64, parent_id: u64) -> SubvolumeListItem {
886 SubvolumeListItem {
887 root_id,
888 parent_id,
889 dir_id: 0,
890 generation: 0,
891 flags: SubvolumeFlags::empty(),
892 uuid: Uuid::nil(),
893 parent_uuid: Uuid::nil(),
894 received_uuid: Uuid::nil(),
895 otransid: 0,
896 otime: UNIX_EPOCH,
897 name: String::new(),
898 }
899 }
900
901 #[test]
902 fn timespec_zero_returns_epoch() {
903 assert_eq!(timespec_to_system_time(0, 0), UNIX_EPOCH);
904 }
905
906 #[test]
907 fn timespec_zero_sec_with_nonzero_nsec_returns_epoch() {
908 assert_eq!(timespec_to_system_time(0, 500_000_000), UNIX_EPOCH);
910 }
911
912 #[test]
913 fn timespec_nonzero_returns_correct_time() {
914 let t = timespec_to_system_time(1000, 500);
915 assert_eq!(t, UNIX_EPOCH + Duration::new(1000, 500));
916 }
917
918 #[test]
919 fn subvolume_flags_display_readonly() {
920 let flags = SubvolumeFlags::RDONLY;
921 assert_eq!(format!("{}", flags), "readonly");
922 }
923
924 #[test]
925 fn subvolume_flags_display_empty() {
926 let flags = SubvolumeFlags::empty();
927 assert_eq!(format!("{}", flags), "-");
928 }
929
930 #[test]
931 fn parse_root_ref_valid() {
932 let name = b"mysubvol";
934 let mut buf = Vec::new();
935 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);
939
940 let result = parse_root_ref(&buf);
941 assert!(result.is_some());
942 let (dir_id, parsed_name) = result.unwrap();
943 assert_eq!(dir_id, 42);
944 assert_eq!(parsed_name, "mysubvol");
945 }
946
947 #[test]
948 fn parse_root_ref_too_short_header() {
949 let buf = [0u8; 10];
951 assert!(parse_root_ref(&buf).is_none());
952 }
953
954 #[test]
955 fn parse_root_ref_too_short_name() {
956 let mut buf = vec![0u8; 18];
958 buf[16] = 10;
960 buf[17] = 0;
961 assert!(parse_root_ref(&buf).is_none());
962 }
963
964 #[test]
965 fn parse_root_ref_empty_name() {
966 let mut buf = Vec::new();
967 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);
972 assert!(result.is_some());
973 let (dir_id, parsed_name) = result.unwrap();
974 assert_eq!(dir_id, 100);
975 assert_eq!(parsed_name, "");
976 }
977
978 #[test]
979 fn build_full_path_single_subvol_parent_fs_tree() {
980 let items = vec![test_item(256, FS_TREE_OBJECTID)];
982 let segments = vec!["mysub".to_string()];
983 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
984 let mut cache = HashMap::new();
985
986 let path = build_full_path(
987 256,
988 FS_TREE_OBJECTID,
989 &id_to_idx,
990 &segments,
991 &items,
992 &mut cache,
993 );
994 assert_eq!(path, "mysub");
995 }
996
997 #[test]
998 fn build_full_path_nested_chain() {
999 let items = vec![
1001 test_item(256, FS_TREE_OBJECTID),
1002 test_item(257, 256),
1003 test_item(258, 257),
1004 ];
1005 let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1006 let id_to_idx: HashMap<u64, usize> =
1007 [(256, 0), (257, 1), (258, 2)].into();
1008 let mut cache = HashMap::new();
1009
1010 let path = build_full_path(
1011 258,
1012 FS_TREE_OBJECTID,
1013 &id_to_idx,
1014 &segments,
1015 &items,
1016 &mut cache,
1017 );
1018 assert_eq!(path, "A/B/C");
1019 }
1020
1021 #[test]
1022 fn build_full_path_stops_at_top_id() {
1023 let items = vec![
1027 test_item(256, FS_TREE_OBJECTID),
1028 test_item(257, 256),
1029 test_item(258, 257),
1030 ];
1031 let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1032 let id_to_idx: HashMap<u64, usize> =
1033 [(256, 0), (257, 1), (258, 2)].into();
1034 let mut cache = HashMap::new();
1035
1036 let path = build_full_path(
1037 258, 257, &id_to_idx, &segments, &items, &mut cache,
1038 );
1039 assert_eq!(path, "C");
1040
1041 let path_b = build_full_path(
1047 257, 257, &id_to_idx, &segments, &items, &mut cache,
1048 );
1049 assert_eq!(path_b, "A/B");
1053 }
1054
1055 #[test]
1056 fn build_full_path_cycle_detection() {
1057 let items = vec![test_item(256, 257), test_item(257, 256)];
1059 let segments = vec!["A".to_string(), "B".to_string()];
1060 let id_to_idx: HashMap<u64, usize> = [(256, 0), (257, 1)].into();
1061 let mut cache = HashMap::new();
1062
1063 let _path = build_full_path(
1065 256,
1066 FS_TREE_OBJECTID,
1067 &id_to_idx,
1068 &segments,
1069 &items,
1070 &mut cache,
1071 );
1072 }
1075
1076 #[test]
1077 fn build_full_path_cached_ancestor() {
1078 let items = vec![
1081 test_item(256, FS_TREE_OBJECTID),
1082 test_item(257, 256),
1083 test_item(258, 257),
1084 ];
1085 let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1086 let id_to_idx: HashMap<u64, usize> =
1087 [(256, 0), (257, 1), (258, 2)].into();
1088 let mut cache = HashMap::new();
1089 cache.insert(257, "A/B".to_string());
1090
1091 let path = build_full_path(
1092 258,
1093 FS_TREE_OBJECTID,
1094 &id_to_idx,
1095 &segments,
1096 &items,
1097 &mut cache,
1098 );
1099 assert_eq!(path, "A/B/C");
1100 }
1101
1102 #[test]
1103 fn build_full_path_unknown_parent() {
1104 let items = vec![test_item(256, 999)];
1106 let segments = vec!["orphan".to_string()];
1107 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
1108 let mut cache = HashMap::new();
1109
1110 let path = build_full_path(
1111 256,
1112 FS_TREE_OBJECTID,
1113 &id_to_idx,
1114 &segments,
1115 &items,
1116 &mut cache,
1117 );
1118 assert_eq!(path, "orphan");
1119 }
1120
1121 #[test]
1122 fn build_full_path_parent_id_zero() {
1123 let items = vec![test_item(256, 0)];
1125 let segments = vec!["noparent".to_string()];
1126 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
1127 let mut cache = HashMap::new();
1128
1129 let path = build_full_path(
1130 256,
1131 FS_TREE_OBJECTID,
1132 &id_to_idx,
1133 &segments,
1134 &items,
1135 &mut cache,
1136 );
1137 assert_eq!(path, "noparent");
1138 }
1139
1140 #[test]
1141 fn build_full_path_already_cached_target() {
1142 let items = vec![test_item(256, FS_TREE_OBJECTID)];
1144 let segments = vec!["A".to_string()];
1145 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
1146 let mut cache = HashMap::new();
1147 cache.insert(256, "cached/path".to_string());
1148
1149 let path = build_full_path(
1150 256,
1151 FS_TREE_OBJECTID,
1152 &id_to_idx,
1153 &segments,
1154 &items,
1155 &mut cache,
1156 );
1157 assert_eq!(path, "cached/path");
1158 }
1159
1160 #[test]
1161 fn build_full_path_root_id_not_in_items() {
1162 let items = vec![test_item(256, FS_TREE_OBJECTID)];
1164 let segments = vec!["A".to_string()];
1165 let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
1166 let mut cache = HashMap::new();
1167
1168 let path = build_full_path(
1169 999,
1170 FS_TREE_OBJECTID,
1171 &id_to_idx,
1172 &segments,
1173 &items,
1174 &mut cache,
1175 );
1176 assert_eq!(path, "");
1177 }
1178}