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