Skip to main content

fxcp_core/
mount_info.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2// Copyright (C) 2025 Joel Wirāmu Pauling <aenertia@aenertia.net>
3//
4// fxcp-core/src/mount_info.rs  --  Unified mount enumeration via statmount(2) / mountinfo
5
6//! Unified mount enumeration API.
7//!
8//! Uses `statmount(2)` / `listmount(2)` on Linux >= 6.8 for direct kernel mount
9//! queries, falling back to `/proc/self/mountinfo` text parsing on older kernels.
10//! On kernels where `statmount` is available but `SB_SOURCE` is not (e.g. RHEL
11//! 10.2 kernel 6.12), the statmount results are enriched with source and options
12//! from `/proc/self/mountinfo`.
13
14#![allow(clippy::expect_used)]
15use std::collections::HashMap;
16use std::sync::OnceLock;
17use tracing::{debug, warn};
18
19// ---------------------------------------------------------------------------
20// MountEntry  --  unified result struct
21// ---------------------------------------------------------------------------
22
23/// A single mount point with fields matching `/proc/self/mountinfo` columns.
24#[derive(Debug, Clone)]
25pub struct MountEntry {
26    /// Unique mount ID (field 1 in mountinfo, `mnt_id_old` in statmount).
27    pub mount_id: u64,
28    /// Parent mount ID (field 2 in mountinfo, `mnt_parent_id_old` in statmount).
29    pub parent_id: u64,
30    /// Device major number (field 3 `major:minor` in mountinfo).
31    pub dev_major: u32,
32    /// Device minor number (field 3 `major:minor` in mountinfo).
33    pub dev_minor: u32,
34    /// Mount root within the filesystem (field 4 in mountinfo).
35    pub root: String,
36    /// Mount point path (field 5 in mountinfo).
37    pub mount_point: String,
38    /// Per-mount options (field 6 in mountinfo, e.g. `rw,relatime`).
39    pub mount_options: String,
40    /// Filesystem type (after ` - ` separator in mountinfo, e.g. `xfs`, `nfs4`).
41    pub fs_type: String,
42    /// Mount source device or server (after ` - ` separator, e.g. `/dev/sda1`).
43    pub source: String,
44    /// Superblock options (after ` - ` separator, e.g. `rw,seclabel,attr2`).
45    pub super_options: String,
46}
47
48// ---------------------------------------------------------------------------
49// Syscall numbers
50// ---------------------------------------------------------------------------
51
52/// `statmount(2)` syscall number  --  457 on x86_64, aarch64, s390x, and powerpc64.
53#[cfg(any(
54    target_arch = "x86_64",
55    target_arch = "aarch64",
56    target_arch = "s390x",
57    target_arch = "powerpc64"
58))]
59const SYS_STATMOUNT: libc::c_long = 457;
60
61/// `listmount(2)` syscall number  --  458 on x86_64, aarch64, s390x, and powerpc64.
62#[cfg(any(
63    target_arch = "x86_64",
64    target_arch = "aarch64",
65    target_arch = "s390x",
66    target_arch = "powerpc64"
67))]
68const SYS_LISTMOUNT: libc::c_long = 458;
69
70// ---------------------------------------------------------------------------
71// statmount request mask flags (from <linux/mount.h>)
72// ---------------------------------------------------------------------------
73
74/// Want sb_dev_major, sb_dev_minor, sb_magic, sb_flags.
75const STATMOUNT_SB_BASIC: u64 = 0x0000_0001;
76/// Want mnt_id, mnt_parent_id, mnt_attr, mnt_propagation, mnt_peer_group, mnt_master.
77const STATMOUNT_MNT_BASIC: u64 = 0x0000_0002;
78/// Want mnt_root string.
79const STATMOUNT_MNT_ROOT: u64 = 0x0000_0008;
80/// Want mnt_point string.
81const STATMOUNT_MNT_POINT: u64 = 0x0000_0010;
82/// Want fs_type string.
83const STATMOUNT_FS_TYPE: u64 = 0x0000_0020;
84/// Want mnt_opts string (filesystem-specific options via `show_options()`).
85const STATMOUNT_MNT_OPTS: u64 = 0x0000_0080;
86/// Want sb_source string (mount source device). Kernel >= 6.13.
87const STATMOUNT_SB_SOURCE: u64 = 0x0000_0200;
88
89/// `LSMT_ROOT`  --  list all mounts in the current namespace.
90const LSMT_ROOT: u64 = 0xffff_ffff_ffff_ffff;
91
92/// `MNT_ID_REQ_SIZE_VER0`  --  24-byte v0 request (compatible with all >= 6.8).
93const MNT_ID_REQ_SIZE_VER0: u32 = 24;
94
95// ---------------------------------------------------------------------------
96// statmount struct byte offsets (str[] is always at offset 512)
97// ---------------------------------------------------------------------------
98
99const OFF_SIZE: usize = 0; // u32  --  total size including strings
100const OFF_MNT_OPTS_STR: usize = 4; // u32  --  string offset into str[]
101const OFF_MASK: usize = 8; // u64  --  which fields were filled
102const OFF_SB_DEV_MAJOR: usize = 16; // u32
103const OFF_SB_DEV_MINOR: usize = 20; // u32
104const OFF_FS_TYPE_STR: usize = 36; // u32  --  string offset into str[]
105const _OFF_MNT_ID: usize = 40; // u64  --  new-style unique mount ID (reserved for future use)
106const _OFF_MNT_PARENT_ID: usize = 48; // u64 (reserved for future use)
107const OFF_MNT_ID_OLD: usize = 56; // u32  --  old-style (mountinfo-compatible)
108const OFF_MNT_PARENT_ID_OLD: usize = 60; // u32
109const OFF_MNT_ROOT_STR: usize = 104; // u32  --  string offset into str[]
110const OFF_MNT_POINT_STR: usize = 108; // u32  --  string offset into str[]
111// sb_source: at offset 124 on kernels that define it (>= 6.13).
112// On older kernels this falls inside __spare2 (zeroed).
113const OFF_SB_SOURCE_STR: usize = 124; // u32  --  string offset into str[]
114/// `str[]` flexible array starts at this fixed offset in every kernel version.
115const OFF_STR: usize = 512;
116
117// ---------------------------------------------------------------------------
118// mnt_id_req  --  request struct for statmount(2) / listmount(2)
119// ---------------------------------------------------------------------------
120
121/// Kernel `struct mnt_id_req` (v0, 24 bytes).
122///
123/// For `statmount`: `param` = requested mask bits.
124/// For `listmount`: `param` = last mount ID seen (pagination cursor).
125#[repr(C)]
126struct MntIdReq {
127    size: u32,
128    spare: u32,
129    mnt_id: u64,
130    param: u64,
131}
132
133// ---------------------------------------------------------------------------
134// Availability probe (cached)
135// ---------------------------------------------------------------------------
136
137static STATMOUNT_AVAILABLE: OnceLock<bool> = OnceLock::new();
138
139#[cfg(any(
140    target_arch = "x86_64",
141    target_arch = "aarch64",
142    target_arch = "s390x",
143    target_arch = "powerpc64"
144))]
145fn probe_statmount() -> bool {
146    *STATMOUNT_AVAILABLE.get_or_init(|| {
147        // SAFETY: Null pointer probe  --  the kernel returns EFAULT (syscall exists)
148        // or ENOSYS (not implemented). No memory is written.
149        let ret = unsafe {
150            libc::syscall(
151                SYS_STATMOUNT,
152                std::ptr::null::<u8>(),
153                std::ptr::null::<u8>(),
154                0usize,
155                0u32,
156            )
157        };
158        let available = if ret == -1 {
159            let errno = unsafe { *libc::__errno_location() };
160            errno != libc::ENOSYS
161        } else {
162            true // unexpected success with null args, but syscall exists
163        };
164        if available {
165            debug!("statmount(2)/listmount(2) available  --  using kernel mount API");
166        } else {
167            debug!("statmount(2) not available (ENOSYS)  --  using /proc/self/mountinfo");
168        }
169        available
170    })
171}
172
173#[cfg(not(any(
174    target_arch = "x86_64",
175    target_arch = "aarch64",
176    target_arch = "s390x",
177    target_arch = "powerpc64"
178)))]
179fn probe_statmount() -> bool {
180    false
181}
182
183// ---------------------------------------------------------------------------
184// Buffer helpers
185// ---------------------------------------------------------------------------
186
187/// Read a native-endian `u32` from `buf` at `off`.
188#[inline]
189fn read_u32(buf: &[u8], off: usize) -> u32 {
190    let bytes: [u8; 4] = buf[off..off + 4].try_into().expect("slice too short for u32");
191    u32::from_ne_bytes(bytes)
192}
193
194/// Read a native-endian `u64` from `buf` at `off`.
195#[inline]
196fn read_u64(buf: &[u8], off: usize) -> u64 {
197    let bytes: [u8; 8] = buf[off..off + 8].try_into().expect("slice too short for u64");
198    u64::from_ne_bytes(bytes)
199}
200
201/// Read a NUL-terminated string from the `str[]` section of a statmount buffer.
202fn read_statmount_str(buf: &[u8], str_off: u32) -> String {
203    let start = OFF_STR + str_off as usize;
204    if start >= buf.len() {
205        return String::new();
206    }
207    let remaining = &buf[start..];
208    let end = remaining
209        .iter()
210        .position(|&b| b == 0)
211        .unwrap_or(remaining.len());
212    String::from_utf8_lossy(&remaining[..end]).into_owned()
213}
214
215// ---------------------------------------------------------------------------
216// statmount(2) / listmount(2) path
217// ---------------------------------------------------------------------------
218
219#[cfg(any(
220    target_arch = "x86_64",
221    target_arch = "aarch64",
222    target_arch = "s390x",
223    target_arch = "powerpc64"
224))]
225fn listmount_ids() -> Option<Vec<u64>> {
226    let mut all_ids = Vec::new();
227    let mut last_id: u64 = 0;
228
229    loop {
230        let req = MntIdReq {
231            size: MNT_ID_REQ_SIZE_VER0,
232            spare: 0,
233            mnt_id: LSMT_ROOT,
234            param: last_id,
235        };
236        let mut ids = [0u64; 1024];
237
238        // SAFETY: `req` and `ids` are valid stack allocations with correct sizes.
239        // The kernel writes mount IDs into `ids` and returns the count.
240        let ret = unsafe {
241            libc::syscall(
242                SYS_LISTMOUNT,
243                &req as *const MntIdReq,
244                ids.as_mut_ptr(),
245                ids.len(),
246                0u32,
247            )
248        };
249
250        if ret < 0 {
251            let errno = unsafe { *libc::__errno_location() };
252            warn!("listmount(2) failed: errno={errno}");
253            return None;
254        }
255
256        let count = ret as usize;
257        if count == 0 {
258            break;
259        }
260
261        all_ids.extend_from_slice(&ids[..count]);
262
263        if count < ids.len() {
264            break; // no more mounts
265        }
266
267        // Paginate: set cursor to the last returned ID.
268        last_id = ids[count - 1];
269    }
270
271    Some(all_ids)
272}
273
274#[cfg(any(
275    target_arch = "x86_64",
276    target_arch = "aarch64",
277    target_arch = "s390x",
278    target_arch = "powerpc64"
279))]
280fn statmount_entry(mnt_id: u64) -> Option<MountEntry> {
281    let mask = STATMOUNT_SB_BASIC
282        | STATMOUNT_MNT_BASIC
283        | STATMOUNT_MNT_ROOT
284        | STATMOUNT_MNT_POINT
285        | STATMOUNT_FS_TYPE
286        | STATMOUNT_MNT_OPTS
287        | STATMOUNT_SB_SOURCE; // safe to request even if unsupported
288
289    let req = MntIdReq {
290        size: MNT_ID_REQ_SIZE_VER0,
291        spare: 0,
292        mnt_id,
293        param: mask,
294    };
295
296    // 4 KB is sufficient for most mounts (512 byte header + strings).
297    let mut buf = vec![0u8; 4096];
298
299    // SAFETY: `req` is a valid stack allocation. `buf` is a properly-sized
300    // mutable buffer. The kernel fills `buf` with the statmount struct + strings.
301    let ret = unsafe {
302        libc::syscall(
303            SYS_STATMOUNT,
304            &req as *const MntIdReq,
305            buf.as_mut_ptr(),
306            buf.len(),
307            0u32,
308        )
309    };
310
311    if ret < 0 {
312        let errno = unsafe { *libc::__errno_location() };
313        if errno == libc::EOVERFLOW {
314            // Buffer too small  --  the kernel wrote the needed size into buf[0..4].
315            let needed = read_u32(&buf, OFF_SIZE) as usize;
316            if needed > 0 && needed <= 1 << 20 {
317                buf.resize(needed, 0);
318                let ret2 = unsafe {
319                    libc::syscall(
320                        SYS_STATMOUNT,
321                        &req as *const MntIdReq,
322                        buf.as_mut_ptr(),
323                        buf.len(),
324                        0u32,
325                    )
326                };
327                if ret2 < 0 {
328                    return None;
329                }
330            } else {
331                return None;
332            }
333        } else {
334            // Mount may have disappeared between listmount and statmount.
335            return None;
336        }
337    }
338
339    if buf.len() < OFF_STR {
340        return None;
341    }
342
343    let filled = read_u64(&buf, OFF_MASK);
344
345    // Device IDs.
346    let dev_major = if filled & STATMOUNT_SB_BASIC != 0 {
347        read_u32(&buf, OFF_SB_DEV_MAJOR)
348    } else {
349        0
350    };
351    let dev_minor = if filled & STATMOUNT_SB_BASIC != 0 {
352        read_u32(&buf, OFF_SB_DEV_MINOR)
353    } else {
354        0
355    };
356
357    // Use old-style mount IDs for compatibility with /proc/self/mountinfo field 0.
358    let mount_id = if filled & STATMOUNT_MNT_BASIC != 0 {
359        read_u32(&buf, OFF_MNT_ID_OLD) as u64
360    } else {
361        0
362    };
363    let parent_id = if filled & STATMOUNT_MNT_BASIC != 0 {
364        read_u32(&buf, OFF_MNT_PARENT_ID_OLD) as u64
365    } else {
366        0
367    };
368
369    // Strings.
370    let root = if filled & STATMOUNT_MNT_ROOT != 0 {
371        read_statmount_str(&buf, read_u32(&buf, OFF_MNT_ROOT_STR))
372    } else {
373        String::new()
374    };
375    let mount_point = if filled & STATMOUNT_MNT_POINT != 0 {
376        read_statmount_str(&buf, read_u32(&buf, OFF_MNT_POINT_STR))
377    } else {
378        String::new()
379    };
380    let fs_type = if filled & STATMOUNT_FS_TYPE != 0 {
381        read_statmount_str(&buf, read_u32(&buf, OFF_FS_TYPE_STR))
382    } else {
383        String::new()
384    };
385
386    // mnt_opts contains filesystem-specific options via the fs's show_options().
387    // For NFS this includes vers=4.2,addr=...; for XFS it includes inode64, etc.
388    // This maps to mountinfo's "super_options" (after the ` - ` separator).
389    let mnt_opts = if filled & STATMOUNT_MNT_OPTS != 0 {
390        read_statmount_str(&buf, read_u32(&buf, OFF_MNT_OPTS_STR))
391    } else {
392        String::new()
393    };
394
395    // sb_source: only filled on kernels that define STATMOUNT_SB_SOURCE (>= 6.13).
396    // On older kernels the mask bit is silently ignored; the offset falls inside
397    // __spare2 (zeroed), so we'd read an empty string  --  but we check the mask
398    // anyway for clarity.
399    let source = if filled & STATMOUNT_SB_SOURCE != 0 {
400        read_statmount_str(&buf, read_u32(&buf, OFF_SB_SOURCE_STR))
401    } else {
402        String::new()
403    };
404
405    Some(MountEntry {
406        mount_id,
407        parent_id,
408        dev_major,
409        dev_minor,
410        root,
411        mount_point,
412        mount_options: String::new(), // enriched from proc when needed
413        fs_type,
414        source,
415        super_options: mnt_opts, // statmount mnt_opts ~= mountinfo super_options
416    })
417}
418
419#[cfg(any(
420    target_arch = "x86_64",
421    target_arch = "aarch64",
422    target_arch = "s390x",
423    target_arch = "powerpc64"
424))]
425fn enrich_from_proc(entries: &mut [MountEntry]) {
426    let proc_entries = list_mounts_proc();
427    let proc_map: HashMap<u64, &MountEntry> = proc_entries
428        .iter()
429        .map(|e| (e.mount_id, e))
430        .collect();
431
432    for entry in entries.iter_mut() {
433        if let Some(pe) = proc_map.get(&entry.mount_id) {
434            if entry.source.is_empty() {
435                entry.source.clone_from(&pe.source);
436            }
437            if entry.mount_options.is_empty() {
438                entry.mount_options.clone_from(&pe.mount_options);
439            }
440            if entry.super_options.is_empty() {
441                entry.super_options.clone_from(&pe.super_options);
442            }
443        }
444    }
445}
446
447#[cfg(any(
448    target_arch = "x86_64",
449    target_arch = "aarch64",
450    target_arch = "s390x",
451    target_arch = "powerpc64"
452))]
453fn list_mounts_statmount() -> Option<Vec<MountEntry>> {
454    let ids = listmount_ids()?;
455    let mut entries = Vec::with_capacity(ids.len());
456
457    for id in ids {
458        if let Some(entry) = statmount_entry(id) {
459            entries.push(entry);
460        }
461    }
462
463    // Enrich with /proc/self/mountinfo for fields statmount could not fill.
464    let needs_enrichment = entries
465        .iter()
466        .any(|e| e.source.is_empty() || e.mount_options.is_empty());
467    if needs_enrichment {
468        enrich_from_proc(&mut entries);
469    }
470
471    Some(entries)
472}
473
474// ---------------------------------------------------------------------------
475// /proc/self/mountinfo fallback
476// ---------------------------------------------------------------------------
477
478/// Parse `/proc/self/mountinfo` into [`MountEntry`] values.
479pub fn list_mounts_proc() -> Vec<MountEntry> {
480    parse_mountinfo_text(
481        &std::fs::read_to_string("/proc/self/mountinfo").unwrap_or_default(),
482    )
483}
484
485/// Parse mountinfo-formatted text into [`MountEntry`] values.
486///
487/// Format per line:
488/// ```text
489/// mnt_id parent_id major:minor root mount_point options optional_fields - fs_type source super_options
490/// ```
491fn parse_mountinfo_text(text: &str) -> Vec<MountEntry> {
492    let mut entries = Vec::new();
493
494    for line in text.lines() {
495        let fields: Vec<&str> = line.split_whitespace().collect();
496        if fields.len() < 10 {
497            continue;
498        }
499
500        let mount_id: u64 = match fields[0].parse() {
501            Ok(v) => v,
502            Err(_) => continue,
503        };
504        let parent_id: u64 = match fields[1].parse() {
505            Ok(v) => v,
506            Err(_) => continue,
507        };
508
509        // Parse major:minor.
510        let (dev_major, dev_minor) = match fields[2].split_once(':') {
511            Some((maj, min)) => match (maj.parse::<u32>(), min.parse::<u32>()) {
512                (Ok(ma), Ok(mi)) => (ma, mi),
513                _ => continue,
514            },
515            None => continue,
516        };
517
518        let root = fields[3].to_string();
519        let mount_point = fields[4].to_string();
520        let mount_options = fields[5].to_string();
521
522        // Find the " - " separator (may be preceded by optional tagged fields).
523        let sep_pos = match fields.iter().position(|&f| f == "-") {
524            Some(p) => p,
525            None => continue,
526        };
527        if sep_pos + 3 > fields.len() {
528            continue;
529        }
530
531        let fs_type = fields[sep_pos + 1].to_string();
532        let source = fields[sep_pos + 2].to_string();
533        let super_options = if sep_pos + 3 < fields.len() {
534            fields[sep_pos + 3].to_string()
535        } else {
536            String::new()
537        };
538
539        entries.push(MountEntry {
540            mount_id,
541            parent_id,
542            dev_major,
543            dev_minor,
544            root,
545            mount_point,
546            mount_options,
547            fs_type,
548            source,
549            super_options,
550        });
551    }
552
553    entries
554}
555
556// ---------------------------------------------------------------------------
557// Public API
558// ---------------------------------------------------------------------------
559
560/// Enumerate all mounts visible to the current process.
561///
562/// Uses `statmount(2)` / `listmount(2)` on Linux >= 6.8 for direct kernel
563/// queries, falling back to `/proc/self/mountinfo` parsing on older kernels.
564pub fn list_mounts() -> Vec<MountEntry> {
565    #[cfg(any(
566        target_arch = "x86_64",
567        target_arch = "aarch64",
568        target_arch = "s390x",
569        target_arch = "powerpc64"
570    ))]
571    if probe_statmount() {
572        if let Some(entries) = list_mounts_statmount() {
573            return entries;
574        }
575        warn!("statmount enumeration failed  --  falling back to /proc/self/mountinfo");
576    }
577    list_mounts_proc()
578}
579
580/// Find the mount entry whose `mount_point` is the longest prefix of `path`.
581///
582/// This is the common "find covering mount" operation used by all four existing
583/// mountinfo parsers.
584pub fn find_mount_for_path(path: &std::path::Path) -> Option<MountEntry> {
585    let canonical = path.canonicalize().ok()?;
586    let canonical_str = canonical.to_string_lossy();
587    let entries = list_mounts();
588
589    entries
590        .into_iter()
591        .filter(|e| canonical_str.starts_with(&e.mount_point) || e.mount_point == "/")
592        .max_by_key(|e| {
593            if e.mount_point == "/" {
594                0
595            } else {
596                e.mount_point.len()
597            }
598        })
599}
600
601// ---------------------------------------------------------------------------
602// Tests
603// ---------------------------------------------------------------------------
604
605#[cfg(test)]
606mod tests {
607    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
608    use super::*;
609
610    #[test]
611    fn test_probe_statmount_does_not_panic() {
612        let available = probe_statmount();
613        println!("statmount(2) available: {available}");
614    }
615
616    #[test]
617    fn test_list_mounts_returns_entries() {
618        let entries = list_mounts();
619        assert!(
620            !entries.is_empty(),
621            "list_mounts() should return at least one mount"
622        );
623
624        // Root filesystem must always be present.
625        let has_root = entries.iter().any(|e| e.mount_point == "/");
626        assert!(has_root, "root mount (/) must be present");
627
628        // All entries should have non-empty mount_point and fs_type.
629        for e in &entries {
630            assert!(!e.mount_point.is_empty(), "mount_point empty: {e:?}");
631            assert!(!e.fs_type.is_empty(), "fs_type empty: {e:?}");
632        }
633
634        println!("list_mounts() returned {} entries", entries.len());
635    }
636
637    #[test]
638    fn test_list_mounts_proc_returns_entries() {
639        let entries = list_mounts_proc();
640        assert!(
641            !entries.is_empty(),
642            "proc fallback should return entries"
643        );
644        assert!(entries.iter().any(|e| e.mount_point == "/"));
645    }
646
647    #[test]
648    fn test_statmount_vs_proc_field_counts() {
649        // Compare entry counts between statmount and proc paths.
650        let proc_entries = list_mounts_proc();
651        let proc_count = proc_entries.len();
652
653        if probe_statmount() {
654            if let Some(sm_entries) = list_mounts_statmount() {
655                let sm_count = sm_entries.len();
656                println!(
657                    "statmount: {sm_count} entries, /proc/self/mountinfo: {proc_count} entries"
658                );
659                // Allow minor differences (kernel internal mounts may appear/disappear).
660                let diff = (sm_count as i64 - proc_count as i64).unsigned_abs();
661                assert!(
662                    diff <= 5,
663                    "statmount ({sm_count}) vs proc ({proc_count}) differ by more than 5"
664                );
665            } else {
666                println!("statmount probe succeeded but enumeration failed  --  skipping comparison");
667            }
668        } else {
669            println!("statmount not available  --  skipping comparison test");
670        }
671    }
672
673    #[test]
674    fn test_parse_mountinfo_text() {
675        let text = "\
67622 1 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=continue
67729 22 0:25 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw
67830 22 0:5 / /dev rw,nosuid shared:2 - devtmpfs devtmpfs rw,size=4096k
679100 22 0:50 / /mnt/nfs rw,relatime shared:50 - nfs4 server:/export rw,vers=4.2,addr=10.0.0.1
680";
681        let entries = parse_mountinfo_text(text);
682        assert_eq!(entries.len(), 4);
683
684        assert_eq!(entries[0].mount_id, 22);
685        assert_eq!(entries[0].parent_id, 1);
686        assert_eq!(entries[0].dev_major, 8);
687        assert_eq!(entries[0].dev_minor, 1);
688        assert_eq!(entries[0].root, "/");
689        assert_eq!(entries[0].mount_point, "/");
690        assert_eq!(entries[0].mount_options, "rw,relatime");
691        assert_eq!(entries[0].fs_type, "ext4");
692        assert_eq!(entries[0].source, "/dev/sda1");
693        assert_eq!(entries[0].super_options, "rw,errors=continue");
694
695        // NFS entry.
696        assert_eq!(entries[3].fs_type, "nfs4");
697        assert_eq!(entries[3].source, "server:/export");
698        assert!(entries[3].super_options.contains("vers=4.2"));
699    }
700
701    #[test]
702    fn test_find_mount_for_path() {
703        let entry = find_mount_for_path(std::path::Path::new("/"));
704        assert!(entry.is_some(), "should find mount for /");
705        assert_eq!(entry.unwrap().mount_point, "/");
706    }
707}