linuxutils_system/
mountpoint.rs1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use rustix::fs::{FileType, lstat, major, minor, stat};
7use std::{
8 io::{self, BufRead},
9 path::{Path, PathBuf},
10 process::ExitCode,
11};
12
13const EXIT_NOTMOUNT: u8 = 32;
14
15#[derive(Parser)]
16#[command(
17 name = "mountpoint",
18 version,
19 about = "See if a directory or file is a mountpoint"
20)]
21pub struct Args {
22 #[arg(short = 'd', long = "fs-devno", conflicts_with = "devno")]
24 fs_devno: bool,
25
26 #[arg(short = 'q', long = "quiet")]
28 quiet: bool,
29
30 #[arg(long = "nofollow")]
32 nofollow: bool,
33
34 #[arg(short = 'x', long = "devno", conflicts_with_all = ["fs_devno", "quiet", "nofollow"])]
36 devno: bool,
37
38 pub path: PathBuf,
40}
41
42pub fn run(args: Args) -> ExitCode {
43 if args.devno {
44 return show_devno(&args);
45 }
46
47 let st = match do_stat(&args.path, args.nofollow) {
48 Ok(s) => s,
49 Err(e) => {
50 if !args.quiet {
51 eprintln!("mountpoint: {}: {e}", args.path.display());
52 }
53 return ExitCode::from(1);
54 }
55 };
56
57 if args.fs_devno {
58 if let Some((maj, min)) = mountinfo_devno(&args.path) {
59 println!("{maj}:{min}");
60 } else {
61 println!("{}:{}", major(st.st_dev), minor(st.st_dev));
62 }
63 return ExitCode::SUCCESS;
64 }
65
66 let is_mount = is_mountpoint(&args.path);
67
68 if !args.quiet {
69 if is_mount {
70 println!("{} is a mountpoint", args.path.display());
71 } else {
72 println!("{} is not a mountpoint", args.path.display());
73 }
74 }
75
76 if is_mount {
77 ExitCode::SUCCESS
78 } else {
79 ExitCode::from(EXIT_NOTMOUNT)
80 }
81}
82
83fn do_stat(
84 path: &PathBuf,
85 nofollow: bool,
86) -> Result<rustix::fs::Stat, rustix::io::Errno> {
87 if nofollow { lstat(path) } else { stat(path) }
88}
89
90fn show_devno(args: &Args) -> ExitCode {
91 let st = match do_stat(&args.path, false) {
92 Ok(s) => s,
93 Err(e) => {
94 if !args.quiet {
95 eprintln!("mountpoint: {}: {e}", args.path.display());
96 }
97 return ExitCode::from(1);
98 }
99 };
100
101 if !FileType::from_raw_mode(st.st_mode).is_block_device() {
103 if !args.quiet {
104 eprintln!(
105 "mountpoint: {}: not a block device",
106 args.path.display()
107 );
108 }
109 return ExitCode::from(EXIT_NOTMOUNT);
110 }
111
112 println!("{}:{}", major(st.st_rdev), minor(st.st_rdev));
113 ExitCode::SUCCESS
114}
115
116fn is_mountpoint(path: &PathBuf) -> bool {
118 if find_in_mountinfo(path).is_some() {
119 return true;
120 }
121 match stat(path) {
123 Ok(st) => is_mountpoint_by_stat(&st, path),
124 Err(_) => false,
125 }
126}
127
128fn find_in_mountinfo(path: &PathBuf) -> Option<(u32, u32)> {
130 let canonical = std::fs::canonicalize(path).ok()?;
131 let canonical = canonical.to_string_lossy();
132
133 let file = std::fs::File::open("/proc/self/mountinfo").ok()?;
134 let reader = io::BufReader::new(file);
135
136 for line in reader.lines() {
137 let Ok(line) = line else { continue };
138 let mut fields = line.split_whitespace();
140 let Some(_id) = fields.next() else { continue };
141 let Some(_parent) = fields.next() else {
142 continue;
143 };
144 let Some(devno) = fields.next() else { continue };
145 let Some(_root) = fields.next() else { continue };
146 let Some(mount_point) = fields.next() else {
147 continue;
148 };
149
150 let decoded = unescape_mountinfo(mount_point);
151 if *canonical == decoded {
152 let (maj_s, min_s) = devno.split_once(':')?;
153 let maj = maj_s.parse().ok()?;
154 let min = min_s.parse().ok()?;
155 return Some((maj, min));
156 }
157 }
158
159 None
160}
161
162fn mountinfo_devno(path: &PathBuf) -> Option<(u32, u32)> {
164 find_in_mountinfo(path)
165}
166
167fn is_mountpoint_by_stat(st: &rustix::fs::Stat, path: &Path) -> bool {
169 let parent = path.join("..");
170 match stat(&parent) {
171 Ok(parent_st) => st.st_dev != parent_st.st_dev,
172 Err(_) => false,
173 }
174}
175
176fn unescape_mountinfo(s: &str) -> String {
178 let mut result = String::with_capacity(s.len());
179 let mut chars = s.chars();
180 while let Some(c) = chars.next() {
181 if c == '\\' {
182 let oct: String = chars.by_ref().take(3).collect();
183 if let Ok(byte) = u8::from_str_radix(&oct, 8) {
184 result.push(byte as char);
185 } else {
186 result.push('\\');
187 result.push_str(&oct);
188 }
189 } else {
190 result.push(c);
191 }
192 }
193 result
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn unescape_simple() {
202 assert_eq!(unescape_mountinfo("/mnt/my\\040drive"), "/mnt/my drive");
203 }
204
205 #[test]
206 fn unescape_no_escapes() {
207 assert_eq!(unescape_mountinfo("/mnt/data"), "/mnt/data");
208 }
209
210 #[test]
211 fn root_is_mountpoint() {
212 assert!(is_mountpoint(&PathBuf::from("/")));
213 }
214}