use crate::{
raw::{
BTRFS_DIR_ITEM_KEY, BTRFS_FIRST_FREE_OBJECTID, BTRFS_FS_TREE_OBJECTID,
BTRFS_LAST_FREE_OBJECTID, BTRFS_ROOT_BACKREF_KEY, BTRFS_ROOT_ITEM_KEY,
BTRFS_ROOT_TREE_DIR_OBJECTID, BTRFS_ROOT_TREE_OBJECTID,
BTRFS_SUBVOL_QGROUP_INHERIT, BTRFS_SUBVOL_RDONLY,
BTRFS_SUBVOL_SPEC_BY_ID, BTRFS_SUBVOL_SYNC_WAIT_FOR_ONE,
BTRFS_SUBVOL_SYNC_WAIT_FOR_QUEUED, btrfs_ioc_default_subvol,
btrfs_ioc_get_subvol_info, btrfs_ioc_ino_lookup,
btrfs_ioc_snap_create_v2, btrfs_ioc_snap_destroy_v2,
btrfs_ioc_subvol_create_v2, btrfs_ioc_subvol_getflags,
btrfs_ioc_subvol_setflags, btrfs_ioc_subvol_sync_wait,
btrfs_ioctl_get_subvol_info_args, btrfs_ioctl_ino_lookup_args,
btrfs_ioctl_subvol_wait, btrfs_ioctl_vol_args_v2, btrfs_qgroup_inherit,
},
tree_search::{SearchFilter, tree_search},
};
use bitflags::bitflags;
use nix::libc::c_char;
use std::{
collections::HashMap,
ffi::CStr,
mem,
os::{fd::AsRawFd, unix::io::BorrowedFd},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use uuid::Uuid;
pub const FS_TREE_OBJECTID: u64 = BTRFS_FS_TREE_OBJECTID as u64;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubvolumeFlags: u64 {
const RDONLY = BTRFS_SUBVOL_RDONLY as u64;
}
}
impl std::fmt::Display for SubvolumeFlags {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.contains(SubvolumeFlags::RDONLY) {
write!(f, "readonly")
} else {
write!(f, "-")
}
}
}
#[derive(Debug, Clone)]
pub struct SubvolumeInfo {
pub id: u64,
pub name: String,
pub parent_id: u64,
pub dir_id: u64,
pub generation: u64,
pub flags: SubvolumeFlags,
pub uuid: Uuid,
pub parent_uuid: Uuid,
pub received_uuid: Uuid,
pub ctransid: u64,
pub otransid: u64,
pub stransid: u64,
pub rtransid: u64,
pub ctime: SystemTime,
pub otime: SystemTime,
pub stime: SystemTime,
pub rtime: SystemTime,
}
#[derive(Debug, Clone)]
pub struct SubvolumeListItem {
pub root_id: u64,
pub parent_id: u64,
pub dir_id: u64,
pub generation: u64,
pub flags: SubvolumeFlags,
pub uuid: Uuid,
pub parent_uuid: Uuid,
pub received_uuid: Uuid,
pub otransid: u64,
pub otime: SystemTime,
pub name: String,
}
#[allow(clippy::cast_possible_wrap)] fn set_v2_name(
args: &mut btrfs_ioctl_vol_args_v2,
name: &CStr,
) -> nix::Result<()> {
let bytes = name.to_bytes(); let name_buf: &mut [c_char] = unsafe { &mut args.__bindgen_anon_2.name };
if bytes.len() >= name_buf.len() {
return Err(nix::errno::Errno::ENAMETOOLONG);
}
for (i, &b) in bytes.iter().enumerate() {
name_buf[i] = b as c_char;
}
Ok(())
}
fn build_qgroup_inherit(qgroups: &[u64]) -> Vec<u64> {
let base_size = mem::size_of::<btrfs_qgroup_inherit>();
let total_size = base_size + std::mem::size_of_val(qgroups);
let num_u64 = total_size.div_ceil(8);
let mut buf = vec![0u64; num_u64];
let inherit =
unsafe { &mut *buf.as_mut_ptr().cast::<btrfs_qgroup_inherit>() };
inherit.num_qgroups = qgroups.len() as u64;
if !qgroups.is_empty() {
let array = unsafe { inherit.qgroups.as_mut_slice(qgroups.len()) };
array.copy_from_slice(qgroups);
}
buf
}
fn set_qgroup_inherit(
args: &mut btrfs_ioctl_vol_args_v2,
buf: &[u64],
num_qgroups: usize,
) {
args.flags |= u64::from(BTRFS_SUBVOL_QGROUP_INHERIT);
let base_size = mem::size_of::<btrfs_qgroup_inherit>();
let total_size = base_size + num_qgroups * mem::size_of::<u64>();
args.__bindgen_anon_1.__bindgen_anon_1.size = total_size as u64;
args.__bindgen_anon_1.__bindgen_anon_1.qgroup_inherit =
buf.as_ptr() as *mut btrfs_qgroup_inherit;
}
pub fn subvolume_create(
parent_fd: BorrowedFd,
name: &CStr,
qgroups: &[u64],
) -> nix::Result<()> {
let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
set_v2_name(&mut args, name)?;
let inherit_buf;
if !qgroups.is_empty() {
inherit_buf = build_qgroup_inherit(qgroups);
set_qgroup_inherit(&mut args, &inherit_buf, qgroups.len());
}
unsafe {
btrfs_ioc_subvol_create_v2(parent_fd.as_raw_fd(), &raw const args)
}?;
Ok(())
}
pub fn subvolume_delete(parent_fd: BorrowedFd, name: &CStr) -> nix::Result<()> {
let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
set_v2_name(&mut args, name)?;
unsafe {
btrfs_ioc_snap_destroy_v2(parent_fd.as_raw_fd(), &raw const args)
}?;
Ok(())
}
pub fn subvolume_delete_by_id(
fd: BorrowedFd,
subvolid: u64,
) -> nix::Result<()> {
let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
args.flags = u64::from(BTRFS_SUBVOL_SPEC_BY_ID);
args.__bindgen_anon_2.subvolid = subvolid;
unsafe { btrfs_ioc_snap_destroy_v2(fd.as_raw_fd(), &raw const args) }?;
Ok(())
}
pub fn snapshot_create(
parent_fd: BorrowedFd,
source_fd: BorrowedFd,
name: &CStr,
readonly: bool,
qgroups: &[u64],
) -> nix::Result<()> {
let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
args.fd = i64::from(source_fd.as_raw_fd());
if readonly {
args.flags = u64::from(BTRFS_SUBVOL_RDONLY);
}
set_v2_name(&mut args, name)?;
let inherit_buf;
if !qgroups.is_empty() {
inherit_buf = build_qgroup_inherit(qgroups);
set_qgroup_inherit(&mut args, &inherit_buf, qgroups.len());
}
unsafe {
btrfs_ioc_snap_create_v2(parent_fd.as_raw_fd(), &raw const args)
}?;
Ok(())
}
pub fn subvolume_info(fd: BorrowedFd) -> nix::Result<SubvolumeInfo> {
subvolume_info_by_id(fd, 0)
}
pub fn subvolume_info_by_id(
fd: BorrowedFd,
rootid: u64,
) -> nix::Result<SubvolumeInfo> {
let mut raw: btrfs_ioctl_get_subvol_info_args = unsafe { mem::zeroed() };
raw.treeid = rootid;
unsafe { btrfs_ioc_get_subvol_info(fd.as_raw_fd(), &raw mut raw) }?;
let name = unsafe { CStr::from_ptr(raw.name.as_ptr()) }
.to_string_lossy()
.into_owned();
Ok(SubvolumeInfo {
id: raw.treeid,
name,
parent_id: raw.parent_id,
dir_id: raw.dirid,
generation: raw.generation,
flags: SubvolumeFlags::from_bits_truncate(raw.flags),
uuid: Uuid::from_bytes(raw.uuid),
parent_uuid: Uuid::from_bytes(raw.parent_uuid),
received_uuid: Uuid::from_bytes(raw.received_uuid),
ctransid: raw.ctransid,
otransid: raw.otransid,
stransid: raw.stransid,
rtransid: raw.rtransid,
ctime: timespec_to_system_time(raw.ctime.sec, raw.ctime.nsec),
otime: timespec_to_system_time(raw.otime.sec, raw.otime.nsec),
stime: timespec_to_system_time(raw.stime.sec, raw.stime.nsec),
rtime: timespec_to_system_time(raw.rtime.sec, raw.rtime.nsec),
})
}
pub fn subvolume_flags_get(fd: BorrowedFd) -> nix::Result<SubvolumeFlags> {
let mut flags: u64 = 0;
unsafe { btrfs_ioc_subvol_getflags(fd.as_raw_fd(), &raw mut flags) }?;
Ok(SubvolumeFlags::from_bits_truncate(flags))
}
pub fn subvolume_flags_set(
fd: BorrowedFd,
flags: SubvolumeFlags,
) -> nix::Result<()> {
let raw: u64 = flags.bits();
unsafe { btrfs_ioc_subvol_setflags(fd.as_raw_fd(), &raw const raw) }?;
Ok(())
}
pub fn subvolume_default_get(fd: BorrowedFd) -> nix::Result<u64> {
let mut default_id: Option<u64> = None;
tree_search(
fd,
SearchFilter::for_objectid_range(
u64::from(BTRFS_ROOT_TREE_OBJECTID),
BTRFS_DIR_ITEM_KEY,
u64::from(BTRFS_ROOT_TREE_DIR_OBJECTID),
u64::from(BTRFS_ROOT_TREE_DIR_OBJECTID),
),
|_hdr, data| {
use crate::raw::btrfs_dir_item;
use std::mem::{offset_of, size_of};
let header_size = size_of::<btrfs_dir_item>();
if data.len() < header_size {
return Ok(());
}
let name_off = offset_of!(btrfs_dir_item, name_len);
let name_len =
u16::from_le_bytes([data[name_off], data[name_off + 1]])
as usize;
if data.len() < header_size + name_len {
return Ok(());
}
let item_name = &data[header_size..header_size + name_len];
if item_name == b"default" {
let loc_off = offset_of!(btrfs_dir_item, location);
let target_id = u64::from_le_bytes(
data[loc_off..loc_off + 8].try_into().unwrap(),
);
default_id = Some(target_id);
}
Ok(())
},
)?;
Ok(default_id.unwrap_or(u64::from(BTRFS_FS_TREE_OBJECTID)))
}
pub fn subvolume_default_set(fd: BorrowedFd, subvolid: u64) -> nix::Result<()> {
unsafe { btrfs_ioc_default_subvol(fd.as_raw_fd(), &raw const subvolid) }?;
Ok(())
}
#[allow(clippy::cast_sign_loss)] pub fn subvolume_list(fd: BorrowedFd) -> nix::Result<Vec<SubvolumeListItem>> {
let mut items: Vec<SubvolumeListItem> = Vec::new();
tree_search(
fd,
SearchFilter::for_objectid_range(
u64::from(BTRFS_ROOT_TREE_OBJECTID),
BTRFS_ROOT_ITEM_KEY,
u64::from(BTRFS_FIRST_FREE_OBJECTID),
BTRFS_LAST_FREE_OBJECTID as u64,
),
|hdr, data| {
if hdr.item_type != BTRFS_ROOT_ITEM_KEY {
return Ok(());
}
if let Some(item) = parse_root_item(hdr.objectid, data) {
items.push(item);
}
Ok(())
},
)?;
tree_search(
fd,
SearchFilter::for_objectid_range(
u64::from(BTRFS_ROOT_TREE_OBJECTID),
BTRFS_ROOT_BACKREF_KEY,
u64::from(BTRFS_FIRST_FREE_OBJECTID),
BTRFS_LAST_FREE_OBJECTID as u64,
),
|hdr, data| {
if hdr.item_type != BTRFS_ROOT_BACKREF_KEY {
return Ok(());
}
let root_id = hdr.objectid;
let parent_id = hdr.offset;
if let Some(item) = items.iter_mut().find(|i| i.root_id == root_id)
{
if item.parent_id == 0 {
item.parent_id = parent_id;
if let Some((dir_id, name)) = parse_root_ref(data) {
item.dir_id = dir_id;
item.name = name;
}
}
}
Ok(())
},
)?;
let top_id =
crate::inode::lookup_path_rootid(fd).unwrap_or(FS_TREE_OBJECTID);
resolve_full_paths(fd, &mut items, top_id);
Ok(items)
}
fn ino_lookup_dir_path(
fd: BorrowedFd,
parent_tree: u64,
dir_id: u64,
) -> nix::Result<String> {
let mut args = btrfs_ioctl_ino_lookup_args {
treeid: parent_tree,
objectid: dir_id,
..unsafe { mem::zeroed() }
};
unsafe { btrfs_ioc_ino_lookup(fd.as_raw_fd(), &raw mut args) }?;
let name_ptr: *const c_char = args.name.as_ptr();
let cstr = unsafe { CStr::from_ptr(name_ptr) };
Ok(cstr.to_string_lossy().into_owned())
}
fn resolve_full_paths(
fd: BorrowedFd,
items: &mut [SubvolumeListItem],
top_id: u64,
) {
let id_to_idx: HashMap<u64, usize> = items
.iter()
.enumerate()
.map(|(i, item)| (item.root_id, i))
.collect();
let segments: Vec<String> = items
.iter()
.map(|item| {
if item.parent_id == 0 || item.name.is_empty() {
return item.name.clone();
}
match ino_lookup_dir_path(fd, item.parent_id, item.dir_id) {
Ok(prefix) => format!("{}{}", prefix, item.name),
Err(_) => item.name.clone(),
}
})
.collect();
let mut full_paths: HashMap<u64, String> = HashMap::new();
let root_ids: Vec<u64> = items.iter().map(|i| i.root_id).collect();
for root_id in root_ids {
build_full_path(
root_id,
top_id,
&id_to_idx,
&segments,
items,
&mut full_paths,
);
}
for item in items.iter_mut() {
if let Some(path) = full_paths.remove(&item.root_id) {
item.name = path;
}
}
}
fn build_full_path(
root_id: u64,
top_id: u64,
id_to_idx: &HashMap<u64, usize>,
segments: &[String],
items: &[SubvolumeListItem],
cache: &mut HashMap<u64, String>,
) -> String {
let mut chain: Vec<u64> = Vec::new();
let mut visited: HashMap<u64, usize> = HashMap::new();
let mut cur = root_id;
loop {
if cache.contains_key(&cur) {
break;
}
if visited.contains_key(&cur) {
let cycle_start = visited[&cur];
chain.truncate(cycle_start);
break;
}
let Some(&idx) = id_to_idx.get(&cur) else {
break;
};
visited.insert(cur, chain.len());
chain.push(cur);
let parent = items[idx].parent_id;
if parent == 0
|| parent == FS_TREE_OBJECTID
|| parent == top_id
|| !id_to_idx.contains_key(&parent)
{
break;
}
cur = parent;
}
for &id in chain.iter().rev() {
let Some(&idx) = id_to_idx.get(&id) else {
cache.insert(id, String::new());
continue;
};
let segment = &segments[idx];
let parent_id = items[idx].parent_id;
let full_path = if parent_id == 0
|| parent_id == FS_TREE_OBJECTID
|| parent_id == top_id
|| !id_to_idx.contains_key(&parent_id)
{
segment.clone()
} else if let Some(parent_path) = cache.get(&parent_id) {
if parent_path.is_empty() {
segment.clone()
} else {
format!("{parent_path}/{segment}")
}
} else {
segment.clone()
};
cache.insert(id, full_path);
}
cache.get(&root_id).cloned().unwrap_or_default()
}
fn parse_root_item(root_id: u64, data: &[u8]) -> Option<SubvolumeListItem> {
let ri = btrfs_disk::items::RootItem::parse(data)?;
let flags = SubvolumeFlags::from_bits_truncate(ri.flags.bits());
let otime = timespec_to_system_time(ri.otime.sec, ri.otime.nsec);
Some(SubvolumeListItem {
root_id,
parent_id: 0,
dir_id: 0,
generation: ri.generation,
flags,
uuid: ri.uuid,
parent_uuid: ri.parent_uuid,
received_uuid: ri.received_uuid,
otransid: ri.otransid,
otime,
name: String::new(),
})
}
fn parse_root_ref(data: &[u8]) -> Option<(u64, String)> {
let rr = btrfs_disk::items::RootRef::parse(data)?;
let name = String::from_utf8_lossy(&rr.name).into_owned();
Some((rr.dirid, name))
}
fn timespec_to_system_time(sec: u64, nsec: u32) -> SystemTime {
if sec == 0 {
return UNIX_EPOCH;
}
UNIX_EPOCH + Duration::new(sec, nsec)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubvolRootRef {
pub treeid: u64,
pub dirid: u64,
}
pub fn subvol_rootrefs(fd: BorrowedFd) -> nix::Result<Vec<SubvolRootRef>> {
use crate::raw::{
btrfs_ioc_get_subvol_rootref, btrfs_ioctl_get_subvol_rootref_args,
};
let mut results = Vec::new();
let mut min_treeid: u64 = 0;
loop {
let mut args: btrfs_ioctl_get_subvol_rootref_args =
unsafe { std::mem::zeroed() };
args.min_treeid = min_treeid;
let ret = unsafe {
btrfs_ioc_get_subvol_rootref(fd.as_raw_fd(), &raw mut args)
};
let overflow = match ret {
Ok(_) => false,
Err(nix::errno::Errno::EOVERFLOW) => true,
Err(e) => return Err(e),
};
let count = args.num_items as usize;
for i in 0..count {
let r = &args.rootref[i];
results.push(SubvolRootRef {
treeid: r.treeid,
dirid: r.dirid,
});
}
if !overflow || count == 0 {
break;
}
min_treeid = args.rootref[count - 1].treeid + 1;
}
Ok(results)
}
pub fn subvol_sync_wait_one(fd: BorrowedFd, subvolid: u64) -> nix::Result<()> {
let args = btrfs_ioctl_subvol_wait {
subvolid,
mode: BTRFS_SUBVOL_SYNC_WAIT_FOR_ONE,
count: 0,
};
match unsafe { btrfs_ioc_subvol_sync_wait(fd.as_raw_fd(), &raw const args) }
{
Ok(_) | Err(nix::errno::Errno::ENOENT) => Ok(()),
Err(e) => Err(e),
}
}
pub fn subvol_sync_wait_all(fd: BorrowedFd) -> nix::Result<()> {
let args = btrfs_ioctl_subvol_wait {
subvolid: 0,
mode: BTRFS_SUBVOL_SYNC_WAIT_FOR_QUEUED,
count: 0,
};
unsafe { btrfs_ioc_subvol_sync_wait(fd.as_raw_fd(), &raw const args) }?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
collections::HashMap,
time::{Duration, UNIX_EPOCH},
};
use uuid::Uuid;
fn test_item(root_id: u64, parent_id: u64) -> SubvolumeListItem {
SubvolumeListItem {
root_id,
parent_id,
dir_id: 0,
generation: 0,
flags: SubvolumeFlags::empty(),
uuid: Uuid::nil(),
parent_uuid: Uuid::nil(),
received_uuid: Uuid::nil(),
otransid: 0,
otime: UNIX_EPOCH,
name: String::new(),
}
}
#[test]
fn timespec_zero_returns_epoch() {
assert_eq!(timespec_to_system_time(0, 0), UNIX_EPOCH);
}
#[test]
fn timespec_zero_sec_with_nonzero_nsec_returns_epoch() {
assert_eq!(timespec_to_system_time(0, 500_000_000), UNIX_EPOCH);
}
#[test]
fn timespec_nonzero_returns_correct_time() {
let t = timespec_to_system_time(1000, 500);
assert_eq!(t, UNIX_EPOCH + Duration::new(1000, 500));
}
#[test]
fn subvolume_flags_display_readonly() {
let flags = SubvolumeFlags::RDONLY;
assert_eq!(format!("{}", flags), "readonly");
}
#[test]
fn subvolume_flags_display_empty() {
let flags = SubvolumeFlags::empty();
assert_eq!(format!("{}", flags), "-");
}
#[test]
fn parse_root_ref_valid() {
let name = b"mysubvol";
let mut buf = Vec::new();
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);
let result = parse_root_ref(&buf);
assert!(result.is_some());
let (dir_id, parsed_name) = result.unwrap();
assert_eq!(dir_id, 42);
assert_eq!(parsed_name, "mysubvol");
}
#[test]
fn parse_root_ref_too_short_header() {
let buf = [0u8; 10];
assert!(parse_root_ref(&buf).is_none());
}
#[test]
fn parse_root_ref_truncated_name() {
let mut buf = vec![0u8; 18];
buf[16] = 10; buf[17] = 0;
let result = parse_root_ref(&buf);
assert!(result.is_some());
let (_, name) = result.unwrap();
assert!(name.is_empty());
}
#[test]
fn parse_root_ref_empty_name() {
let mut buf = Vec::new();
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);
assert!(result.is_some());
let (dir_id, parsed_name) = result.unwrap();
assert_eq!(dir_id, 100);
assert_eq!(parsed_name, "");
}
#[test]
fn build_full_path_single_subvol_parent_fs_tree() {
let items = vec![test_item(256, FS_TREE_OBJECTID)];
let segments = vec!["mysub".to_string()];
let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
let mut cache = HashMap::new();
let path = build_full_path(
256,
FS_TREE_OBJECTID,
&id_to_idx,
&segments,
&items,
&mut cache,
);
assert_eq!(path, "mysub");
}
#[test]
fn build_full_path_nested_chain() {
let items = vec![
test_item(256, FS_TREE_OBJECTID),
test_item(257, 256),
test_item(258, 257),
];
let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
let id_to_idx: HashMap<u64, usize> =
[(256, 0), (257, 1), (258, 2)].into();
let mut cache = HashMap::new();
let path = build_full_path(
258,
FS_TREE_OBJECTID,
&id_to_idx,
&segments,
&items,
&mut cache,
);
assert_eq!(path, "A/B/C");
}
#[test]
fn build_full_path_stops_at_top_id() {
let items = vec![
test_item(256, FS_TREE_OBJECTID),
test_item(257, 256),
test_item(258, 257),
];
let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
let id_to_idx: HashMap<u64, usize> =
[(256, 0), (257, 1), (258, 2)].into();
let mut cache = HashMap::new();
let path = build_full_path(
258, 257, &id_to_idx, &segments, &items, &mut cache,
);
assert_eq!(path, "C");
let path_b = build_full_path(
257, 257, &id_to_idx, &segments, &items, &mut cache,
);
assert_eq!(path_b, "A/B");
}
#[test]
fn build_full_path_cycle_detection() {
let items = vec![test_item(256, 257), test_item(257, 256)];
let segments = vec!["A".to_string(), "B".to_string()];
let id_to_idx: HashMap<u64, usize> = [(256, 0), (257, 1)].into();
let mut cache = HashMap::new();
let _path = build_full_path(
256,
FS_TREE_OBJECTID,
&id_to_idx,
&segments,
&items,
&mut cache,
);
}
#[test]
fn build_full_path_cached_ancestor() {
let items = vec![
test_item(256, FS_TREE_OBJECTID),
test_item(257, 256),
test_item(258, 257),
];
let segments = vec!["A".to_string(), "B".to_string(), "C".to_string()];
let id_to_idx: HashMap<u64, usize> =
[(256, 0), (257, 1), (258, 2)].into();
let mut cache = HashMap::new();
cache.insert(257, "A/B".to_string());
let path = build_full_path(
258,
FS_TREE_OBJECTID,
&id_to_idx,
&segments,
&items,
&mut cache,
);
assert_eq!(path, "A/B/C");
}
#[test]
fn build_full_path_unknown_parent() {
let items = vec![test_item(256, 999)];
let segments = vec!["orphan".to_string()];
let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
let mut cache = HashMap::new();
let path = build_full_path(
256,
FS_TREE_OBJECTID,
&id_to_idx,
&segments,
&items,
&mut cache,
);
assert_eq!(path, "orphan");
}
#[test]
fn build_full_path_parent_id_zero() {
let items = vec![test_item(256, 0)];
let segments = vec!["noparent".to_string()];
let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
let mut cache = HashMap::new();
let path = build_full_path(
256,
FS_TREE_OBJECTID,
&id_to_idx,
&segments,
&items,
&mut cache,
);
assert_eq!(path, "noparent");
}
#[test]
fn build_full_path_already_cached_target() {
let items = vec![test_item(256, FS_TREE_OBJECTID)];
let segments = vec!["A".to_string()];
let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
let mut cache = HashMap::new();
cache.insert(256, "cached/path".to_string());
let path = build_full_path(
256,
FS_TREE_OBJECTID,
&id_to_idx,
&segments,
&items,
&mut cache,
);
assert_eq!(path, "cached/path");
}
#[test]
fn build_full_path_root_id_not_in_items() {
let items = vec![test_item(256, FS_TREE_OBJECTID)];
let segments = vec!["A".to_string()];
let id_to_idx: HashMap<u64, usize> = [(256, 0)].into();
let mut cache = HashMap::new();
let path = build_full_path(
999,
FS_TREE_OBJECTID,
&id_to_idx,
&segments,
&items,
&mut cache,
);
assert_eq!(path, "");
}
}