Skip to main content

btrfs_uapi/
subvolume.rs

1//! # Subvolume and snapshot management: creating, deleting, and querying subvolumes
2//!
3//! Subvolumes are independently snapshotable subtrees within a btrfs filesystem.
4//! Snapshots are subvolumes created as copy-on-write clones of an existing
5//! subvolume.  This module covers the full lifecycle: creating and deleting
6//! subvolumes and snapshots, reading subvolume metadata and flags, listing all
7//! subvolumes in a filesystem, and getting or setting the default subvolume
8//! that is mounted when no subvolume is explicitly requested.
9
10use 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
34/// The top-level subvolume (FS tree); objectid 5, always present.
35///
36/// Returned by [`subvolume_default_get`] when no explicit default has been set.
37pub const FS_TREE_OBJECTID: u64 = BTRFS_FS_TREE_OBJECTID as u64;
38
39bitflags! {
40    /// Flags on a btrfs subvolume (the `flags` field of the root item /
41    /// `BTRFS_IOC_SUBVOL_{GET,SET}FLAGS`).
42    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
43    pub struct SubvolumeFlags: u64 {
44        /// Subvolume is read-only.
45        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/// Subvolume metadata returned by `BTRFS_IOC_GET_SUBVOL_INFO`.
60#[derive(Debug, Clone)]
61pub struct SubvolumeInfo {
62    /// Root ID (subvolume ID) of this subvolume.
63    pub id: u64,
64    /// Name of this subvolume within its parent directory.
65    pub name: String,
66    /// Root ID of the parent subvolume.
67    pub parent_id: u64,
68    /// Inode number of the directory within the parent that holds this subvolume.
69    pub dir_id: u64,
70    /// Current generation of the subvolume.
71    pub generation: u64,
72    /// Subvolume flags (e.g. read-only).
73    pub flags: SubvolumeFlags,
74    /// UUID of this subvolume.
75    pub uuid: Uuid,
76    /// UUID of the parent subvolume (non-nil for snapshots).
77    pub parent_uuid: Uuid,
78    /// UUID of the received subvolume (non-nil for received snapshots).
79    pub received_uuid: Uuid,
80    /// Transaction ID when the subvolume was last changed.
81    pub ctransid: u64,
82    /// Transaction ID when the subvolume was created.
83    pub otransid: u64,
84    /// Transaction ID when a send was last performed.
85    pub stransid: u64,
86    /// Transaction ID when this subvolume was last received.
87    pub rtransid: u64,
88    /// Time of last change.
89    pub ctime: SystemTime,
90    /// Creation time.
91    pub otime: SystemTime,
92    /// Time of last send.
93    pub stime: SystemTime,
94    /// Time of last receive.
95    pub rtime: SystemTime,
96}
97
98/// A single subvolume entry returned by [`subvolume_list`].
99#[derive(Debug, Clone)]
100pub struct SubvolumeListItem {
101    /// Root ID (subvolume ID).
102    pub root_id: u64,
103    /// Root ID of the parent subvolume (`0` if unknown / not found).
104    pub parent_id: u64,
105    /// Inode of the directory in the parent that contains this subvolume.
106    pub dir_id: u64,
107    /// Current generation.
108    pub generation: u64,
109    /// Subvolume flags.
110    pub flags: SubvolumeFlags,
111    /// UUID of this subvolume.
112    pub uuid: Uuid,
113    /// UUID of the parent subvolume.
114    pub parent_uuid: Uuid,
115    /// UUID of the received subvolume.
116    pub received_uuid: Uuid,
117    /// Transaction ID when created.
118    pub otransid: u64,
119    /// Creation time.
120    pub otime: SystemTime,
121    /// Subvolume name (the leaf name within its parent).
122    ///
123    /// Full path resolution relative to the filesystem root requires
124    /// `BTRFS_IOC_INO_LOOKUP` and is not yet implemented; this field contains
125    /// only the leaf name as stored in the root backref item.
126    pub name: String,
127}
128
129/// Write `name` into the `name` union member of a zeroed
130/// `btrfs_ioctl_vol_args_v2`, returning `ENAMETOOLONG` if it does not fit.
131fn set_v2_name(args: &mut btrfs_ioctl_vol_args_v2, name: &CStr) -> nix::Result<()> {
132    let bytes = name.to_bytes(); // excludes nul terminator
133    // SAFETY: name is the active union member; the struct is already zeroed so
134    // the implicit nul terminator is already present.
135    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
145/// Create a new subvolume named `name` inside the directory referred to by
146/// `parent_fd`.
147///
148/// `name` must be a plain leaf name (no slashes).  The caller is responsible
149/// for opening the correct parent directory.  Requires `CAP_SYS_ADMIN`.
150pub 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
157/// Delete the subvolume or snapshot named `name` from the directory referred
158/// to by `parent_fd`.
159///
160/// `name` must be a plain leaf name (no slashes).  Requires `CAP_SYS_ADMIN`.
161pub 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
168/// Create a snapshot of the subvolume referred to by `source_fd`, placing it
169/// as `name` inside the directory referred to by `parent_fd`.
170///
171/// If `readonly` is `true` the new snapshot is created read-only.
172/// Requires `CAP_SYS_ADMIN`.
173pub 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    // The `fd` field carries the source subvolume file descriptor.
181    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
190/// Query detailed information about the subvolume that `fd` belongs to.
191///
192/// `fd` can be any file or directory within the target subvolume.
193/// Does not require elevated privileges.
194pub 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
223/// Read the flags of the subvolume that `fd` belongs to.
224pub 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
230/// Set the flags of the subvolume that `fd` belongs to.
231///
232/// Requires `CAP_SYS_ADMIN` to make a subvolume read-only; any user may
233/// clear the read-only flag from a subvolume they own.
234pub 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
240/// Query the ID of the default subvolume of the filesystem referred to by
241/// `fd`.
242///
243/// Searches the root tree for the `BTRFS_DIR_ITEM_KEY` entry at objectid
244/// `BTRFS_ROOT_TREE_DIR_OBJECTID` that stores the default subvolume ID.
245/// Returns [`FS_TREE_OBJECTID`] if no default has been set.
246///
247/// Requires `CAP_SYS_ADMIN`.
248pub 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
285/// Set the default subvolume of the filesystem referred to by `fd` to
286/// `subvolid`.
287///
288/// Pass [`FS_TREE_OBJECTID`] to restore the default.  Requires `CAP_SYS_ADMIN`.
289pub 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
294/// List all user subvolumes and snapshots in the filesystem referred to by
295/// `fd` by walking the root tree.
296///
297/// Two tree-search passes are made:
298/// 1. `ROOT_ITEM_KEY` — reads each subvolume's metadata (generation, flags,
299///    UUIDs, creation time).
300/// 2. `BTRFS_ROOT_BACKREF_KEY` — reads each subvolume's parent ID and leaf name.
301///
302/// Subvolumes for which no backref is found are still included; their
303/// `parent_id`, `dir_id`, and `name` will be zeroed / empty.
304///
305/// Requires `CAP_SYS_ADMIN`.
306pub 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            // ROOT_BACKREF_KEY: objectid = subvol root_id, offset = parent root_id
335            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                // Only take the first ROOT_BACKREF for each subvolume.  A
340                // subvolume can have multiple ROOT_BACKREF entries when it is
341                // referenced from more than one parent (e.g. the subvolume
342                // also appears as a snapshot inside another subvolume).
343                // Items are returned in offset-ascending order, so the first
344                // entry has the smallest parent_id — which is the canonical
345                // location btrfs-progs uses for "top level" and path.
346                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    // Determine which subvolume the fd is open on.  Paths are expressed
359    // relative to this subvolume, matching btrfs-progs behaviour.
360    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
367/// Call `BTRFS_IOC_INO_LOOKUP` for `dir_id` within `parent_tree` and return
368/// the path from that tree's root to the directory.
369///
370/// The kernel returns the path with a trailing `/` when the directory is not
371/// the tree root, and an empty string when `dir_id` is the tree root itself.
372/// This prefix can be concatenated directly with the subvolume's leaf name to
373/// form the full segment within the parent.
374fn 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    // SAFETY: args is a valid, zeroed btrfs_ioctl_ino_lookup_args; the ioctl
381    // fills in args.name with a null-terminated path string.
382    unsafe { btrfs_ioc_ino_lookup(fd.as_raw_fd(), &mut args) }?;
383
384    // args.name is [c_char; 4080]; find the null terminator and decode.
385    let name_ptr: *const c_char = args.name.as_ptr();
386    // SAFETY: the ioctl guarantees null termination within the 4080-byte buffer.
387    let cstr = unsafe { CStr::from_ptr(name_ptr) };
388    Ok(cstr.to_string_lossy().into_owned())
389}
390
391/// Resolve the `name` field of every item in `items` from a bare leaf name to
392/// the full path relative to the filesystem root.
393///
394/// For each subvolume we already have `parent_id`, `dir_id`, and the leaf name
395/// from the ROOT_BACKREF pass.  A single `BTRFS_IOC_INO_LOOKUP` call per item
396/// gives the path from the parent tree's root down to the directory that
397/// contains the subvolume (the "dir prefix").  Concatenating that prefix with
398/// the leaf name yields the subvolume's segment within its parent.  Walking up
399/// the parent chain (using the in-memory items map) and joining segments with
400/// `/` gives the final full path.
401fn resolve_full_paths(
402    fd: BorrowedFd,
403    items: &mut Vec<SubvolumeListItem>,
404    top_id: u64,
405) -> nix::Result<()> {
406    // Map root_id → index for O(1) parent lookups.
407    let id_to_idx: HashMap<u64, usize> = items
408        .iter()
409        .enumerate()
410        .map(|(i, item)| (item.root_id, i))
411        .collect();
412
413    // Compute the "segment" for each item: the path of this subvolume within
414    // its immediate parent (dir prefix from INO_LOOKUP + leaf name).
415    // If INO_LOOKUP is not available (e.g. missing CAP_SYS_ADMIN), fall back
416    // to the bare leaf name so the list still works.
417    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    // Walk each item's parent chain, joining segments, and cache results so
431    // every ancestor is resolved at most once.
432    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
454/// Compute the full path for `root_id`, memoizing into `cache`.
455///
456/// Walks the ancestor chain iteratively to avoid stack overflow on deep
457/// subvolume trees.  Collects segments from the target up to the FS tree
458/// root, then joins them in reverse order.
459///
460/// Cycle detection is included: ROOT_BACKREF entries can form mutual parent
461/// relationships (e.g. a snapshot stored inside the subvolume it was taken
462/// from), which would otherwise loop forever.
463fn 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    // Collect the chain of root_ids from `root_id` up to the top subvolume
472    // (the one the fd is open on) or the FS tree root, stopping early if we
473    // hit an already-cached ancestor or a cycle.
474    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            // Cycle detected: truncate the chain back to where the cycle
483            // starts so we don't join nonsensical repeated segments.
484            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    // Resolve each node in the chain from root toward the target, building
505    // on any already-cached prefix we stopped at.
506    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
536/// `btrfs_root_item` field offsets (packed, LE).
537fn parse_root_item(root_id: u64, data: &[u8]) -> Option<SubvolumeListItem> {
538    use std::mem::offset_of;
539
540    // Items shorter than generation_v2 are "legacy" and do not carry
541    // UUID / otime / otransid fields.
542    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    // Extended fields exist only in non-legacy items.
552    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
590/// Parse a `btrfs_root_ref` payload (packed, LE). The name immediately
591/// follows the fixed-size header.
592fn 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
620/// Convert an on-disk `btrfs_timespec` (LE sec + LE nsec, packed) to
621/// [`SystemTime`].  Returns [`UNIX_EPOCH`] if sec == 0.
622fn 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
629/// Convert a `btrfs_ioctl_timespec` (host byte order, with padding) to
630/// [`SystemTime`].  Returns [`UNIX_EPOCH`] if sec == 0.
631fn 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        // sec==0 triggers the early return regardless of nsec
695        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        // btrfs_root_ref: dirid (8 LE) + sequence (8 LE) + name_len (2 LE) + name bytes
719        let name = b"mysubvol";
720        let mut buf = Vec::new();
721        buf.extend_from_slice(&42u64.to_le_bytes()); // dirid
722        buf.extend_from_slice(&1u64.to_le_bytes()); // sequence
723        buf.extend_from_slice(&(name.len() as u16).to_le_bytes()); // name_len
724        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        // Less than 18 bytes (sizeof btrfs_root_ref)
736        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        // Header claims 10-byte name but buffer only has the header
743        let mut buf = vec![0u8; 18];
744        // Set name_len = 10 at offset 16
745        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()); // dirid
754        buf.extend_from_slice(&0u64.to_le_bytes()); // sequence
755        buf.extend_from_slice(&0u16.to_le_bytes()); // name_len = 0
756
757        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        // Subvolume 256, parent is FS_TREE (5)
767        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        // A (256) -> B (257) -> C (258), all parented under FS_TREE
786        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        // A (256) -> B (257) -> C (258), top_id = 257 (B)
809        // Paths are relative to top_id, so C's parent (257) == top_id means
810        // C's path is just its own segment, not "A/B/C".
811        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        // B's path is also just "B" (its parent 256/A is below top_id in the
824        // tree, but B's own parent is not top_id — A's parent is FS_TREE).
825        // With top_id=257, building B: parent=256, 256 is in id_to_idx but
826        // 256's parent is FS_TREE (5) which triggers the stop, so chain = [257, 256],
827        // and A gets its segment, B gets "A/B".
828        let path_b = build_full_path(257, 257, &id_to_idx, &segments, &items, &mut cache);
829        // 257 itself: its parent is 256, 256 != top_id (257), so we walk up.
830        // 256's parent is FS_TREE (5), which triggers stop. chain = [257, 256].
831        // 256 resolves to "A" (parent is FS_TREE), 257 resolves to "A/B".
832        assert_eq!(path_b, "A/B");
833    }
834
835    #[test]
836    fn build_full_path_cycle_detection() {
837        // A (256) parent is B (257), B (257) parent is A (256) — mutual cycle
838        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        // Must not hang. The result is truncated due to cycle detection.
844        let _path = build_full_path(
845            256,
846            FS_TREE_OBJECTID,
847            &id_to_idx,
848            &segments,
849            &items,
850            &mut cache,
851        );
852        // Just verify it terminates and returns something (exact value depends
853        // on cycle truncation heuristic).
854    }
855
856    #[test]
857    fn build_full_path_cached_ancestor() {
858        // A (256) -> B (257) -> C (258)
859        // Pre-cache B's path; building C should use it.
860        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        // Subvolume 256, parent 999 not in id_to_idx
884        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        // Subvolume 256, parent_id == 0 (no backref found)
903        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        // If the target itself is already cached, return the cached value.
922        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        // root_id not present in id_to_idx at all
942        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}