Skip to main content

btrfs_uapi/
inode.rs

1//! # Inode and path resolution: mapping between inodes, logical addresses, and paths
2//!
3//! Covers looking up the subvolume root ID that contains a given file, resolving
4//! an inode number to its filesystem paths, mapping a logical byte address back
5//! to the inodes that reference it, and resolving a subvolume ID to its path
6//! within the filesystem.
7
8use crate::{
9    raw::{
10        BTRFS_FIRST_FREE_OBJECTID, btrfs_ioc_ino_lookup,
11        btrfs_ioc_ino_lookup_user, btrfs_ioc_ino_paths,
12        btrfs_ioc_logical_ino_v2, btrfs_ioctl_ino_lookup_user_args,
13        btrfs_root_ref,
14    },
15    tree_search::{SearchFilter, tree_search},
16};
17use std::os::fd::{AsRawFd, BorrowedFd};
18
19/// Look up the tree ID (root ID) of the subvolume containing the given file or directory.
20///
21/// For a file or directory, returns the containing tree root ID.
22/// For a subvolume, returns its own tree ID.
23///
24/// # Arguments
25///
26/// * `fd` - File descriptor to a file or directory on the btrfs filesystem
27///
28/// # Returns
29///
30/// The tree ID (root ID) of the containing subvolume
31///
32/// # Errors
33///
34/// Returns an error if the ioctl fails (e.g., file descriptor is not on a btrfs filesystem)
35pub fn lookup_path_rootid(fd: BorrowedFd<'_>) -> nix::Result<u64> {
36    let mut args = crate::raw::btrfs_ioctl_ino_lookup_args {
37        treeid: 0,
38        objectid: u64::from(BTRFS_FIRST_FREE_OBJECTID),
39        ..unsafe { std::mem::zeroed() }
40    };
41
42    unsafe {
43        btrfs_ioc_ino_lookup(fd.as_raw_fd(), &raw mut args)?;
44    }
45
46    Ok(args.treeid)
47}
48
49/// Get file system paths for the given inode.
50///
51/// Returns a vector of path strings relative to the filesystem root that correspond
52/// to the given inode number.
53///
54/// # Arguments
55///
56/// * `fd` - File descriptor to a file or directory on the btrfs filesystem
57/// * `inum` - Inode number to look up
58///
59/// # Returns
60///
61/// A vector of path strings for the given inode
62///
63/// # Errors
64///
65/// Returns an error if the ioctl fails
66pub fn ino_paths(fd: BorrowedFd<'_>, inum: u64) -> nix::Result<Vec<String>> {
67    const PATH_MAX: usize = 4096;
68
69    // First, allocate a buffer for the response
70    // The buffer needs to be large enough to hold btrfs_data_container plus the path data
71    let mut buf = vec![0u8; PATH_MAX];
72
73    // Set up the ioctl arguments
74    let mut args = crate::raw::btrfs_ioctl_ino_path_args {
75        inum,
76        size: PATH_MAX as u64,
77        reserved: [0; 4],
78        fspath: buf.as_mut_ptr() as u64,
79    };
80
81    unsafe {
82        btrfs_ioc_ino_paths(fd.as_raw_fd(), &raw mut args)?;
83    }
84
85    // Parse the results from the data container
86    // The buffer is laid out as: btrfs_data_container header, followed by val[] array
87    // SAFETY: buf is u64-aligned from the Vec<u8> allocation, and the ioctl
88    // populated it as a btrfs_data_container. The cast is safe because the
89    // buffer was allocated with at least PATH_MAX bytes.
90    #[allow(clippy::cast_ptr_alignment)]
91    let container =
92        unsafe { &*buf.as_ptr().cast::<crate::raw::btrfs_data_container>() };
93
94    let mut paths = Vec::new();
95
96    // Each element in val[] is an offset into the data buffer where a path string starts
97    for i in 0..container.elem_cnt as usize {
98        // Get the offset from val[i]
99        #[allow(clippy::cast_possible_truncation)] // offsets fit in usize
100        let val_offset = unsafe {
101            let val_ptr = container.val.as_ptr();
102            *val_ptr.add(i) as usize
103        };
104
105        // The path string starts at the base of val array plus the offset
106        let val_base = container.val.as_ptr() as usize;
107        let path_ptr = (val_base + val_offset) as *const i8;
108
109        // Convert C string to Rust String
110        let c_str = unsafe { std::ffi::CStr::from_ptr(path_ptr) };
111        if let Ok(path_str) = c_str.to_str() {
112            paths.push(path_str.to_string());
113        }
114    }
115
116    Ok(paths)
117}
118
119/// Result from logical-ino resolution: (inode, offset, root)
120#[derive(Debug, Clone)]
121pub struct LogicalInoResult {
122    /// Inode number that contains data at the queried logical address.
123    pub inode: u64,
124    /// Byte offset within the file where the logical address maps.
125    pub offset: u64,
126    /// Subvolume tree ID (root) that owns this inode.
127    pub root: u64,
128}
129
130/// Get inode, offset, and root information for a logical address.
131///
132/// Resolves a logical address on the filesystem to one or more (inode, offset, root) tuples.
133/// The offset is the position within the file, and the root is the subvolume ID.
134///
135/// # Arguments
136///
137/// * `fd` - File descriptor to a file or directory on the btrfs filesystem
138/// * `logical` - Logical address to resolve
139/// * `ignore_offset` - If true, ignores offsets when matching references
140/// * `bufsize` - Size of buffer to allocate (default 64KB, max 16MB)
141///
142/// # Returns
143///
144/// A vector of (inode, offset, root) tuples
145///
146/// # Errors
147///
148/// Returns an error if the ioctl fails
149pub fn logical_ino(
150    fd: BorrowedFd<'_>,
151    logical: u64,
152    ignore_offset: bool,
153    bufsize: Option<u64>,
154) -> nix::Result<Vec<LogicalInoResult>> {
155    const MAX_BUFSIZE: u64 = 16 * 1024 * 1024; // 16MB
156    const DEFAULT_BUFSIZE: u64 = 64 * 1024; // 64KB
157
158    let size = std::cmp::min(bufsize.unwrap_or(DEFAULT_BUFSIZE), MAX_BUFSIZE);
159    #[allow(clippy::cast_possible_truncation)] // buffer size fits in usize
160    let mut buf = vec![0u8; size as usize];
161
162    // Set up flags for v2 ioctl
163    let mut flags = 0u64;
164    if ignore_offset {
165        flags |= u64::from(crate::raw::BTRFS_LOGICAL_INO_ARGS_IGNORE_OFFSET);
166    }
167
168    // Set up the ioctl arguments
169    let mut args = crate::raw::btrfs_ioctl_logical_ino_args {
170        logical,
171        size,
172        reserved: [0; 3],
173        flags,
174        inodes: buf.as_mut_ptr() as u64,
175    };
176
177    unsafe {
178        btrfs_ioc_logical_ino_v2(fd.as_raw_fd(), &raw mut args)?;
179    }
180
181    // Parse the results from the data container.
182    // SAFETY: buf is correctly sized and was populated by the ioctl.
183    #[allow(clippy::cast_ptr_alignment)]
184    let container =
185        unsafe { &*buf.as_ptr().cast::<crate::raw::btrfs_data_container>() };
186
187    let mut results = Vec::new();
188
189    // Each result is 3 consecutive u64 values: inum, offset, root
190    for i in (0..container.elem_cnt as usize).step_by(3) {
191        if i + 2 < container.elem_cnt as usize {
192            let val_offset_inum = unsafe {
193                let val_ptr = container.val.as_ptr();
194                *val_ptr.add(i)
195            };
196
197            let val_offset_offset = unsafe {
198                let val_ptr = container.val.as_ptr();
199                *val_ptr.add(i + 1)
200            };
201
202            let val_offset_root = unsafe {
203                let val_ptr = container.val.as_ptr();
204                *val_ptr.add(i + 2)
205            };
206
207            results.push(LogicalInoResult {
208                inode: val_offset_inum,
209                offset: val_offset_offset,
210                root: val_offset_root,
211            });
212        }
213    }
214
215    Ok(results)
216}
217
218/// Resolve a subvolume ID to its full path on the filesystem.
219///
220/// Recursively resolves the path to a subvolume by walking the root tree and using
221/// `INO_LOOKUP` to get directory names. The path is built from the subvolume's name
222/// and the names of all parent directories up to the mount point.
223///
224/// # Arguments
225///
226/// * `fd` - File descriptor to a file or directory on the btrfs filesystem
227/// * `subvol_id` - The subvolume ID to resolve
228///
229/// # Returns
230///
231/// The full path to the subvolume relative to the filesystem root, or an empty string
232/// for the filesystem root itself (`FS_TREE_OBJECTID`).
233///
234/// # Errors
235///
236/// Returns an error if:
237/// * The ioctl fails (fd is not on a btrfs filesystem)
238/// * The subvolume ID does not exist
239/// * The path buffer overflows
240///
241/// # Example
242///
243/// ```ignore
244/// let path = subvolid_resolve(fd, 5)?;
245/// println!("Subvolume 5 is at: {}", path);
246/// ```
247pub fn subvolid_resolve(
248    fd: BorrowedFd<'_>,
249    subvol_id: u64,
250) -> nix::Result<String> {
251    let mut path = String::new();
252    subvolid_resolve_sub(fd, &mut path, subvol_id)?;
253    Ok(path)
254}
255
256fn subvolid_resolve_sub(
257    fd: BorrowedFd<'_>,
258    path: &mut String,
259    subvol_id: u64,
260) -> nix::Result<()> {
261    use crate::raw::BTRFS_FS_TREE_OBJECTID;
262
263    // If this is the filesystem root, we're done (empty path means root)
264    if subvol_id == u64::from(BTRFS_FS_TREE_OBJECTID) {
265        return Ok(());
266    }
267
268    // Search the root tree for ROOT_BACKREF_KEY entries for this subvolume.
269    // ROOT_BACKREF_KEY (item type 156) has:
270    // - objectid: the subvolume ID
271    // - offset: the parent subvolume ID
272    // - data: btrfs_root_ref struct containing the subvolume name
273    let mut found = false;
274
275    tree_search(
276        fd,
277        SearchFilter::for_objectid_range(
278            u64::from(crate::raw::BTRFS_ROOT_TREE_OBJECTID),
279            crate::raw::BTRFS_ROOT_BACKREF_KEY,
280            subvol_id,
281            subvol_id,
282        ),
283        |hdr, data| {
284            use std::mem::{offset_of, size_of};
285
286            found = true;
287
288            // The parent subvolume ID is stored in the offset field
289            let parent_subvol_id = hdr.offset;
290
291            // Recursively resolve the parent path first
292            subvolid_resolve_sub(fd, path, parent_subvol_id)?;
293
294            // data is a packed btrfs_root_ref followed by name bytes.
295
296            let header_size = size_of::<btrfs_root_ref>();
297            if data.len() < header_size {
298                return Err(nix::errno::Errno::EOVERFLOW);
299            }
300
301            let dirid = u64::from_le_bytes(
302                data[offset_of!(btrfs_root_ref, dirid)..][..8]
303                    .try_into()
304                    .unwrap(),
305            );
306
307            let name_off = offset_of!(btrfs_root_ref, name_len);
308            let name_len =
309                u16::from_le_bytes([data[name_off], data[name_off + 1]])
310                    as usize;
311
312            if data.len() < header_size + name_len {
313                return Err(nix::errno::Errno::EOVERFLOW);
314            }
315
316            let name_bytes = &data[header_size..header_size + name_len];
317
318            // If dirid is not the first free objectid, we need to resolve the directory path too
319            if dirid != u64::from(BTRFS_FIRST_FREE_OBJECTID) {
320                // Look up the directory in the parent subvolume
321                let mut ino_lookup_args =
322                    crate::raw::btrfs_ioctl_ino_lookup_args {
323                        treeid: parent_subvol_id,
324                        objectid: dirid,
325                        ..unsafe { std::mem::zeroed() }
326                    };
327
328                unsafe {
329                    btrfs_ioc_ino_lookup(
330                        fd.as_raw_fd(),
331                        &raw mut ino_lookup_args,
332                    )?;
333                }
334
335                // Get the directory name (it's a null-terminated C string)
336                let dir_name = unsafe {
337                    std::ffi::CStr::from_ptr(ino_lookup_args.name.as_ptr())
338                }
339                .to_str()
340                .map_err(|_| nix::errno::Errno::EINVAL)?;
341
342                if !dir_name.is_empty() {
343                    if !path.is_empty() {
344                        path.push('/');
345                    }
346                    path.push_str(dir_name);
347                }
348            }
349
350            // Append the subvolume name
351            if !path.is_empty() {
352                path.push('/');
353            }
354
355            // Convert name bytes to string
356            let name_str = std::str::from_utf8(name_bytes)
357                .map_err(|_| nix::errno::Errno::EINVAL)?;
358            path.push_str(name_str);
359
360            Ok(())
361        },
362    )?;
363
364    if !found {
365        return Err(nix::errno::Errno::ENOENT);
366    }
367
368    Ok(())
369}
370
371/// Result of an unprivileged inode lookup (`BTRFS_IOC_INO_LOOKUP_USER`).
372///
373/// Contains the subvolume name and the path from the fd's subvolume root
374/// to the directory containing the subvolume.
375#[derive(Debug, Clone)]
376pub struct InoLookupUserResult {
377    /// Name of the subvolume.
378    pub name: String,
379    /// Path from the fd's subvolume root to the directory containing the
380    /// subvolume entry (`dirid`). Empty if the subvolume sits directly
381    /// under the subvolume root.
382    pub path: String,
383}
384
385/// Unprivileged inode lookup: resolve a subvolume's name and parent path.
386///
387/// Given a subvolume ID (`treeid`) and the inode of the directory that
388/// contains it (`dirid`), returns the subvolume name and the path from
389/// the fd's subvolume root to that directory.
390///
391/// Unlike [`subvolid_resolve`], this does not require `CAP_SYS_ADMIN`.
392/// However, it only resolves one level — for nested subvolumes the caller
393/// must walk up the tree.
394///
395/// # Errors
396///
397/// Returns `Err` if the ioctl fails.
398pub fn ino_lookup_user(
399    fd: BorrowedFd<'_>,
400    treeid: u64,
401    dirid: u64,
402) -> nix::Result<InoLookupUserResult> {
403    let mut args = btrfs_ioctl_ino_lookup_user_args {
404        dirid,
405        treeid,
406        ..unsafe { std::mem::zeroed() }
407    };
408
409    unsafe {
410        btrfs_ioc_ino_lookup_user(fd.as_raw_fd(), &raw mut args)?;
411    }
412
413    let name = unsafe { std::ffi::CStr::from_ptr(args.name.as_ptr()) }
414        .to_str()
415        .map_err(|_| nix::errno::Errno::EINVAL)?
416        .to_owned();
417
418    let path = unsafe { std::ffi::CStr::from_ptr(args.path.as_ptr()) }
419        .to_str()
420        .map_err(|_| nix::errno::Errno::EINVAL)?
421        .to_owned();
422
423    Ok(InoLookupUserResult { name, path })
424}