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().cast::<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 |= u64::from(BTRFS_SUBVOL_QGROUP_INHERIT);
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 {
222 btrfs_ioc_subvol_create_v2(parent_fd.as_raw_fd(), &raw const args)
223 }?;
224 Ok(())
225}
226
227pub fn subvolume_delete(parent_fd: BorrowedFd, name: &CStr) -> nix::Result<()> {
240 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
241 set_v2_name(&mut args, name)?;
242 unsafe {
243 btrfs_ioc_snap_destroy_v2(parent_fd.as_raw_fd(), &raw const args)
244 }?;
245 Ok(())
246}
247
248pub fn subvolume_delete_by_id(
257 fd: BorrowedFd,
258 subvolid: u64,
259) -> nix::Result<()> {
260 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
261 args.flags = u64::from(BTRFS_SUBVOL_SPEC_BY_ID);
262 args.__bindgen_anon_2.subvolid = subvolid;
263 unsafe { btrfs_ioc_snap_destroy_v2(fd.as_raw_fd(), &raw const args) }?;
264 Ok(())
265}
266
267pub fn snapshot_create(
279 parent_fd: BorrowedFd,
280 source_fd: BorrowedFd,
281 name: &CStr,
282 readonly: bool,
283 qgroups: &[u64],
284) -> nix::Result<()> {
285 let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
286 args.fd = i64::from(source_fd.as_raw_fd());
288 if readonly {
289 args.flags = u64::from(BTRFS_SUBVOL_RDONLY);
290 }
291 set_v2_name(&mut args, name)?;
292
293 let inherit_buf;
294 if !qgroups.is_empty() {
295 inherit_buf = build_qgroup_inherit(qgroups);
296 set_qgroup_inherit(&mut args, &inherit_buf, qgroups.len());
297 }
298
299 unsafe {
300 btrfs_ioc_snap_create_v2(parent_fd.as_raw_fd(), &raw const args)
301 }?;
302 Ok(())
303}
304
305pub fn subvolume_info(fd: BorrowedFd) -> nix::Result<SubvolumeInfo> {
310 subvolume_info_by_id(fd, 0)
311}
312
313pub fn subvolume_info_by_id(
322 fd: BorrowedFd,
323 rootid: u64,
324) -> nix::Result<SubvolumeInfo> {
325 let mut raw: btrfs_ioctl_get_subvol_info_args = unsafe { mem::zeroed() };
326 raw.treeid = rootid;
327 unsafe { btrfs_ioc_get_subvol_info(fd.as_raw_fd(), &raw mut raw) }?;
328
329 let name = unsafe { CStr::from_ptr(raw.name.as_ptr()) }
330 .to_string_lossy()
331 .into_owned();
332
333 Ok(SubvolumeInfo {
334 id: raw.treeid,
335 name,
336 parent_id: raw.parent_id,
337 dir_id: raw.dirid,
338 generation: raw.generation,
339 flags: SubvolumeFlags::from_bits_truncate(raw.flags),
340 uuid: Uuid::from_bytes(raw.uuid),
341 parent_uuid: Uuid::from_bytes(raw.parent_uuid),
342 received_uuid: Uuid::from_bytes(raw.received_uuid),
343 ctransid: raw.ctransid,
344 otransid: raw.otransid,
345 stransid: raw.stransid,
346 rtransid: raw.rtransid,
347 ctime: ioctl_timespec_to_system_time(raw.ctime.sec, raw.ctime.nsec),
348 otime: ioctl_timespec_to_system_time(raw.otime.sec, raw.otime.nsec),
349 stime: ioctl_timespec_to_system_time(raw.stime.sec, raw.stime.nsec),
350 rtime: ioctl_timespec_to_system_time(raw.rtime.sec, raw.rtime.nsec),
351 })
352}
353
354pub fn subvolume_flags_get(fd: BorrowedFd) -> nix::Result<SubvolumeFlags> {
356 let mut flags: u64 = 0;
357 unsafe { btrfs_ioc_subvol_getflags(fd.as_raw_fd(), &raw mut flags) }?;
358 Ok(SubvolumeFlags::from_bits_truncate(flags))
359}
360
361pub fn subvolume_flags_set(
366 fd: BorrowedFd,
367 flags: SubvolumeFlags,
368) -> nix::Result<()> {
369 let raw: u64 = flags.bits();
370 unsafe { btrfs_ioc_subvol_setflags(fd.as_raw_fd(), &raw const raw) }?;
371 Ok(())
372}
373
374pub fn subvolume_default_get(fd: BorrowedFd) -> nix::Result<u64> {
383 let mut default_id: Option<u64> = None;
384
385 tree_search(
386 fd,
387 SearchKey::for_objectid_range(
388 u64::from(BTRFS_ROOT_TREE_OBJECTID),
389 BTRFS_DIR_ITEM_KEY,
390 u64::from(BTRFS_ROOT_TREE_DIR_OBJECTID),
391 u64::from(BTRFS_ROOT_TREE_DIR_OBJECTID),
392 ),
393 |_hdr, data| {
394 use crate::raw::btrfs_dir_item;
395 use std::mem::{offset_of, size_of};
396
397 let header_size = size_of::<btrfs_dir_item>();
398 if data.len() < header_size {
399 return Ok(());
400 }
401 let name_off = offset_of!(btrfs_dir_item, name_len);
402 let name_len =
403 u16::from_le_bytes([data[name_off], data[name_off + 1]])
404 as usize;
405 if data.len() < header_size + name_len {
406 return Ok(());
407 }
408 let item_name = &data[header_size..header_size + name_len];
409 if item_name == b"default" {
410 let loc_off = offset_of!(btrfs_dir_item, location);
411 let target_id = u64::from_le_bytes(
412 data[loc_off..loc_off + 8].try_into().unwrap(),
413 );
414 default_id = Some(target_id);
415 }
416 Ok(())
417 },
418 )?;
419
420 Ok(default_id.unwrap_or(u64::from(BTRFS_FS_TREE_OBJECTID)))
421}
422
423pub fn subvolume_default_set(fd: BorrowedFd, subvolid: u64) -> nix::Result<()> {
428 unsafe { btrfs_ioc_default_subvol(fd.as_raw_fd(), &raw const subvolid) }?;
429 Ok(())
430}
431
432pub fn subvolume_list(fd: BorrowedFd) -> nix::Result<Vec<SubvolumeListItem>> {
447 let mut items: Vec<SubvolumeListItem> = Vec::new();
448
449 tree_search(
450 fd,
451 SearchKey::for_objectid_range(
452 u64::from(BTRFS_ROOT_TREE_OBJECTID),
453 BTRFS_ROOT_ITEM_KEY,
454 u64::from(BTRFS_FIRST_FREE_OBJECTID),
455 BTRFS_LAST_FREE_OBJECTID as u64,
456 ),
457 |hdr, data| {
458 if let Some(item) = parse_root_item(hdr.objectid, data) {
459 items.push(item);
460 }
461 Ok(())
462 },
463 )?;
464
465 tree_search(
466 fd,
467 SearchKey::for_objectid_range(
468 u64::from(BTRFS_ROOT_TREE_OBJECTID),
469 BTRFS_ROOT_BACKREF_KEY,
470 u64::from(BTRFS_FIRST_FREE_OBJECTID),
471 BTRFS_LAST_FREE_OBJECTID as u64,
472 ),
473 |hdr, data| {
474 let root_id = hdr.objectid;
476 let parent_id = hdr.offset;
477
478 if let Some(item) = items.iter_mut().find(|i| i.root_id == root_id)
479 {
480 if item.parent_id == 0 {
488 item.parent_id = parent_id;
489 if let Some((dir_id, name)) = parse_root_ref(data) {
490 item.dir_id = dir_id;
491 item.name = name;
492 }
493 }
494 }
495 Ok(())
496 },
497 )?;
498
499 let top_id =
502 crate::inode::lookup_path_rootid(fd).unwrap_or(FS_TREE_OBJECTID);
503
504 resolve_full_paths(fd, &mut items, top_id);
505
506 Ok(items)
507}
508
509fn ino_lookup_dir_path(
517 fd: BorrowedFd,
518 parent_tree: u64,
519 dir_id: u64,
520) -> nix::Result<String> {
521 let mut args = btrfs_ioctl_ino_lookup_args {
522 treeid: parent_tree,
523 objectid: dir_id,
524 ..unsafe { mem::zeroed() }
525 };
526 unsafe { btrfs_ioc_ino_lookup(fd.as_raw_fd(), &raw mut args) }?;
529
530 let name_ptr: *const c_char = args.name.as_ptr();
532 let cstr = unsafe { CStr::from_ptr(name_ptr) };
534 Ok(cstr.to_string_lossy().into_owned())
535}
536
537fn resolve_full_paths(
548 fd: BorrowedFd,
549 items: &mut [SubvolumeListItem],
550 top_id: u64,
551) {
552 let id_to_idx: HashMap<u64, usize> = items
554 .iter()
555 .enumerate()
556 .map(|(i, item)| (item.root_id, i))
557 .collect();
558
559 let segments: Vec<String> = items
564 .iter()
565 .map(|item| {
566 if item.parent_id == 0 || item.name.is_empty() {
567 return item.name.clone();
568 }
569 match ino_lookup_dir_path(fd, item.parent_id, item.dir_id) {
570 Ok(prefix) => format!("{}{}", prefix, item.name),
571 Err(_) => item.name.clone(),
572 }
573 })
574 .collect();
575
576 let mut full_paths: HashMap<u64, String> = HashMap::new();
579 let root_ids: Vec<u64> = items.iter().map(|i| i.root_id).collect();
580 for root_id in root_ids {
581 build_full_path(
582 root_id,
583 top_id,
584 &id_to_idx,
585 &segments,
586 items,
587 &mut full_paths,
588 );
589 }
590
591 for item in items.iter_mut() {
592 if let Some(path) = full_paths.remove(&item.root_id) {
593 item.name = path;
594 }
595 }
596}
597
598fn build_full_path(
608 root_id: u64,
609 top_id: u64,
610 id_to_idx: &HashMap<u64, usize>,
611 segments: &[String],
612 items: &[SubvolumeListItem],
613 cache: &mut HashMap<u64, String>,
614) -> String {
615 let mut chain: Vec<u64> = Vec::new();
619 let mut visited: HashMap<u64, usize> = HashMap::new();
620 let mut cur = root_id;
621 loop {
622 if cache.contains_key(&cur) {
623 break;
624 }
625 if visited.contains_key(&cur) {
626 let cycle_start = visited[&cur];
629 chain.truncate(cycle_start);
630 break;
631 }
632 let Some(&idx) = id_to_idx.get(&cur) else {
633 break;
634 };
635 visited.insert(cur, chain.len());
636 chain.push(cur);
637 let parent = items[idx].parent_id;
638 if parent == 0
639 || parent == FS_TREE_OBJECTID
640 || parent == top_id
641 || !id_to_idx.contains_key(&parent)
642 {
643 break;
644 }
645 cur = parent;
646 }
647
648 for &id in chain.iter().rev() {
651 let Some(&idx) = id_to_idx.get(&id) else {
652 cache.insert(id, String::new());
653 continue;
654 };
655 let segment = &segments[idx];
656 let parent_id = items[idx].parent_id;
657
658 let full_path = if parent_id == 0
659 || parent_id == FS_TREE_OBJECTID
660 || parent_id == top_id
661 || !id_to_idx.contains_key(&parent_id)
662 {
663 segment.clone()
664 } else if let Some(parent_path) = cache.get(&parent_id) {
665 if parent_path.is_empty() {
666 segment.clone()
667 } else {
668 format!("{parent_path}/{segment}")
669 }
670 } else {
671 segment.clone()
672 };
673
674 cache.insert(id, full_path);
675 }
676
677 cache.get(&root_id).cloned().unwrap_or_default()
678}
679
680fn parse_root_item(root_id: u64, data: &[u8]) -> Option<SubvolumeListItem> {
682 use std::mem::offset_of;
683
684 let legacy_boundary = offset_of!(btrfs_root_item, generation_v2);
687 if data.len() < legacy_boundary {
688 return None;
689 }
690
691 let generation = read_le_u64(data, offset_of!(btrfs_root_item, generation));
692 let flags_raw = read_le_u64(data, offset_of!(btrfs_root_item, flags));
693 let flags = SubvolumeFlags::from_bits_truncate(flags_raw);
694
695 let otime_nsec =
697 offset_of!(btrfs_root_item, otime) + offset_of!(btrfs_timespec, nsec);
698 let (uuid, parent_uuid, received_uuid, otransid, otime) = if data.len()
699 >= otime_nsec + field_size!(btrfs_timespec, nsec)
700 {
701 let off_uuid = offset_of!(btrfs_root_item, uuid);
702 let off_parent = offset_of!(btrfs_root_item, parent_uuid);
703 let off_received = offset_of!(btrfs_root_item, received_uuid);
704 let uuid_size = field_size!(btrfs_root_item, uuid);
705 let uuid = Uuid::from_bytes(
706 data[off_uuid..off_uuid + uuid_size].try_into().unwrap(),
707 );
708 let parent_uuid = Uuid::from_bytes(
709 data[off_parent..off_parent + uuid_size].try_into().unwrap(),
710 );
711 let received_uuid = Uuid::from_bytes(
712 data[off_received..off_received + uuid_size]
713 .try_into()
714 .unwrap(),
715 );
716 let otransid = read_le_u64(data, offset_of!(btrfs_root_item, otransid));
717 let otime_sec = offset_of!(btrfs_root_item, otime);
718 let otime = timespec_to_system_time(
719 read_le_u64(data, otime_sec),
720 read_le_u32(data, otime_nsec),
721 );
722 (uuid, parent_uuid, received_uuid, otransid, otime)
723 } else {
724 (Uuid::nil(), Uuid::nil(), Uuid::nil(), 0, UNIX_EPOCH)
725 };
726
727 Some(SubvolumeListItem {
728 root_id,
729 parent_id: 0,
730 dir_id: 0,
731 generation,
732 flags,
733 uuid,
734 parent_uuid,
735 received_uuid,
736 otransid,
737 otime,
738 name: String::new(),
739 })
740}
741
742fn parse_root_ref(data: &[u8]) -> Option<(u64, String)> {
745 use crate::raw::btrfs_root_ref;
746 use std::mem::{offset_of, size_of};
747
748 let header_size = size_of::<btrfs_root_ref>();
749 if data.len() < header_size {
750 return None;
751 }
752 let dir_id = read_le_u64(data, offset_of!(btrfs_root_ref, dirid));
753 let name_off = offset_of!(btrfs_root_ref, name_len);
754 let name_len =
755 u16::from_le_bytes([data[name_off], data[name_off + 1]]) as usize;
756 if data.len() < header_size + name_len {
757 return None;
758 }
759 let name =
760 String::from_utf8_lossy(&data[header_size..header_size + name_len])
761 .into_owned();
762 Some((dir_id, name))
763}
764
765fn timespec_to_system_time(sec: u64, nsec: u32) -> SystemTime {
768 if sec == 0 {
769 return UNIX_EPOCH;
770 }
771 UNIX_EPOCH + Duration::new(sec, nsec)
772}
773
774fn ioctl_timespec_to_system_time(sec: u64, nsec: u32) -> SystemTime {
777 if sec == 0 {
778 return UNIX_EPOCH;
779 }
780 UNIX_EPOCH + Duration::new(sec, nsec)
781}
782
783#[derive(Debug, Clone, Copy, PartialEq, Eq)]
785pub struct SubvolRootRef {
786 pub treeid: u64,
788 pub dirid: u64,
790}
791
792pub fn subvol_rootrefs(fd: BorrowedFd) -> nix::Result<Vec<SubvolRootRef>> {
802 use crate::raw::{
803 btrfs_ioc_get_subvol_rootref, btrfs_ioctl_get_subvol_rootref_args,
804 };
805
806 let mut results = Vec::new();
807 let mut min_treeid: u64 = 0;
808
809 loop {
810 let mut args: btrfs_ioctl_get_subvol_rootref_args =
811 unsafe { std::mem::zeroed() };
812 args.min_treeid = min_treeid;
813
814 let ret = unsafe {
815 btrfs_ioc_get_subvol_rootref(fd.as_raw_fd(), &raw mut args)
816 };
817
818 let overflow = match ret {
822 Ok(_) => false,
823 Err(nix::errno::Errno::EOVERFLOW) => true,
824 Err(e) => return Err(e),
825 };
826
827 let count = args.num_items as usize;
828 for i in 0..count {
829 let r = &args.rootref[i];
830 results.push(SubvolRootRef {
831 treeid: r.treeid,
832 dirid: r.dirid,
833 });
834 }
835
836 if !overflow || count == 0 {
837 break;
838 }
839
840 min_treeid = args.rootref[count - 1].treeid + 1;
842 }
843
844 Ok(results)
845}
846
847pub fn subvol_sync_wait_one(fd: BorrowedFd, subvolid: u64) -> nix::Result<()> {
855 let args = btrfs_ioctl_subvol_wait {
856 subvolid,
857 mode: BTRFS_SUBVOL_SYNC_WAIT_FOR_ONE,
858 count: 0,
859 };
860 match unsafe { btrfs_ioc_subvol_sync_wait(fd.as_raw_fd(), &raw const args) }
861 {
862 Ok(_) | Err(nix::errno::Errno::ENOENT) => Ok(()),
863 Err(e) => Err(e),
864 }
865}
866
867pub fn subvol_sync_wait_all(fd: BorrowedFd) -> nix::Result<()> {
873 let args = btrfs_ioctl_subvol_wait {
874 subvolid: 0,
875 mode: BTRFS_SUBVOL_SYNC_WAIT_FOR_QUEUED,
876 count: 0,
877 };
878 unsafe { btrfs_ioc_subvol_sync_wait(fd.as_raw_fd(), &raw const args) }?;
879 Ok(())
880}
881
882#[cfg(test)]
883mod tests {
884 use super::*;
885 use std::{
886 collections::HashMap,
887 time::{Duration, UNIX_EPOCH},
888 };
889 use uuid::Uuid;
890
891 fn test_item(root_id: u64, parent_id: u64) -> SubvolumeListItem {
892 SubvolumeListItem {
893 root_id,
894 parent_id,
895 dir_id: 0,
896 generation: 0,
897 flags: SubvolumeFlags::empty(),
898 uuid: Uuid::nil(),
899 parent_uuid: Uuid::nil(),
900 received_uuid: Uuid::nil(),
901 otransid: 0,
902 otime: UNIX_EPOCH,
903 name: String::new(),
904 }
905 }
906
907 #[test]
908 fn timespec_zero_returns_epoch() {
909 assert_eq!(timespec_to_system_time(0, 0), UNIX_EPOCH);
910 }
911
912 #[test]
913 fn timespec_zero_sec_with_nonzero_nsec_returns_epoch() {
914 assert_eq!(timespec_to_system_time(0, 500_000_000), UNIX_EPOCH);
916 }
917
918 #[test]
919 fn timespec_nonzero_returns_correct_time() {
920 let t = timespec_to_system_time(1000, 500);
921 assert_eq!(t, UNIX_EPOCH + Duration::new(1000, 500));
922 }
923
924 #[test]
925 fn subvolume_flags_display_readonly() {
926 let flags = SubvolumeFlags::RDONLY;
927 assert_eq!(format!("{}", flags), "readonly");
928 }
929
930 #[test]
931 fn subvolume_flags_display_empty() {
932 let flags = SubvolumeFlags::empty();
933 assert_eq!(format!("{}", flags), "-");
934 }
935
936 #[test]
937 fn parse_root_ref_valid() {
938 let name = b"mysubvol";
940 let mut buf = Vec::new();
941 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);
945
946 let result = parse_root_ref(&buf);
947 assert!(result.is_some());
948 let (dir_id, parsed_name) = result.unwrap();
949 assert_eq!(dir_id, 42);
950 assert_eq!(parsed_name, "mysubvol");
951 }
952
953 #[test]
954 fn parse_root_ref_too_short_header() {
955 let buf = [0u8; 10];
957 assert!(parse_root_ref(&buf).is_none());
958 }
959
960 #[test]
961 fn parse_root_ref_too_short_name() {
962 let mut buf = vec![0u8; 18];
964 buf[16] = 10;
966 buf[17] = 0;
967 assert!(parse_root_ref(&buf).is_none());
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}