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