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