1use std::collections::BTreeSet;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct MountInfoRow {
16 pub raw_mount_id: u64,
17 pub raw_parent_id: u64,
18 pub raw_device: u64,
19 pub root: Vec<u8>,
20 pub raw_peer_groups: Vec<u64>,
21}
22
23pub const MOUNT_PEER_PREFIXES: [&[u8]; 3] = [b"shared:", b"master:", b"propagate_from:"];
26
27pub fn mount_ids_are_ordered_subset(visible: &[u64], captured: &[u64]) -> bool {
30 let mut seen = BTreeSet::new();
31 if !visible.iter().all(|raw| seen.insert(*raw)) {
32 return false;
33 }
34 let mut captured = captured.iter();
35 visible
36 .iter()
37 .all(|raw| captured.by_ref().any(|candidate| candidate == raw))
38}
39
40fn decimal(field: &[u8]) -> Option<u64> {
41 if field.is_empty() || field.iter().any(|byte| !byte.is_ascii_digit()) {
42 return None;
43 }
44 std::str::from_utf8(field).ok()?.parse().ok()
45}
46
47fn mount_peer_group(field: &[u8]) -> Option<Option<u64>> {
48 for prefix in MOUNT_PEER_PREFIXES {
49 if let Some(raw) = field.strip_prefix(prefix) {
50 return Some(Some(decimal(raw)?));
51 }
52 }
53 Some(None)
54}
55
56fn parse_mountinfo_row(line: &[u8]) -> Option<MountInfoRow> {
57 let fields = line.split(|byte| *byte == b' ').collect::<Vec<_>>();
58 if fields.iter().any(|field| field.is_empty()) {
59 return None;
60 }
61 let separator = fields.iter().position(|field| *field == b"-")?;
62 if separator < 6 || separator + 4 != fields.len() {
63 return None;
64 }
65 let mut device = fields[2].split(|byte| *byte == b':');
66 let major = u32::try_from(decimal(device.next()?)?).ok()?;
67 let minor = u32::try_from(decimal(device.next()?)?).ok()?;
68 if device.next().is_some() {
69 return None;
70 }
71 let mut raw_peer_groups = Vec::new();
72 for field in &fields[6..separator] {
73 if let Some(raw) = mount_peer_group(field)? {
74 raw_peer_groups.push(raw);
75 } else if field
76 .iter()
77 .position(|byte| *byte == b':')
78 .is_some_and(|separator| decimal(&field[separator + 1..]).is_some())
79 {
80 return None;
85 }
86 }
87 Some(MountInfoRow {
88 raw_mount_id: decimal(fields[0])?,
89 raw_parent_id: decimal(fields[1])?,
90 raw_device: libc::makedev(major, minor),
91 root: fields[3].to_vec(),
92 raw_peer_groups,
93 })
94}
95
96pub fn parse_mountinfo(contents: &[u8]) -> Option<Vec<MountInfoRow>> {
101 if contents.is_empty() {
102 return Some(Vec::new());
103 }
104 let body = contents.strip_suffix(b"\n").unwrap_or(contents);
105 if body.is_empty() {
106 return None;
107 }
108 let rows = body
109 .split(|byte| *byte == b'\n')
110 .map(parse_mountinfo_row)
111 .collect::<Option<Vec<_>>>()?;
112 let mut seen = BTreeSet::new();
113 rows.iter()
114 .all(|row| seen.insert(row.raw_mount_id))
115 .then_some(rows)
116}
117
118pub fn parse_fdinfo_mount_id(contents: &[u8]) -> Option<u64> {
125 let mut mount_id = None;
126 for line in contents.split(|byte| *byte == b'\n') {
127 let Some(value) = line.strip_prefix(b"mnt_id:") else {
128 continue;
129 };
130 let value = value
131 .strip_prefix(b"\t")
132 .or_else(|| value.strip_prefix(b" "))?;
133 if value.is_empty() || value.iter().any(|byte| !byte.is_ascii_digit()) || mount_id.is_some()
134 {
135 return None;
136 }
137 let text = std::str::from_utf8(value).ok()?;
138 mount_id = Some(text.parse().ok()?);
139 }
140 mount_id
141}
142
143#[cfg(test)]
144mod tests {
145 use super::mount_ids_are_ordered_subset;
146 use super::parse_fdinfo_mount_id;
147 use super::parse_mountinfo;
148
149 #[test]
150 fn fdinfo_mount_id_is_strict_and_unique() {
151 assert_eq!(parse_fdinfo_mount_id(b"pos:\t0\nmnt_id:\t37\n"), Some(37));
152 assert_eq!(parse_fdinfo_mount_id(b"mnt_id: 0\n"), Some(0));
153 for malformed in [
154 b"pos:\t0\n".as_slice(),
155 b"mnt_id:\tbad\nmnt_id:\t37\n".as_slice(),
156 b"mnt_id:\t37\nmnt_id:\t38\n".as_slice(),
157 b"mnt_id:\t37 trailing\n".as_slice(),
158 b"mnt_id:\t18446744073709551616\n".as_slice(),
159 b"mnt_id:37\n".as_slice(),
160 ] {
161 assert_eq!(parse_fdinfo_mount_id(malformed), None, "{malformed:?}");
162 }
163 }
164
165 #[test]
166 fn mountinfo_parser_accepts_empty_and_refuses_malformed_or_duplicate_rows() {
167 assert_eq!(parse_mountinfo(b""), Some(Vec::new()));
168 assert_eq!(parse_mountinfo(b"\n"), None);
169 let row = b"37 1 8:1 / / rw shared:9 - ext4 /dev/root rw\n";
170 let parsed = parse_mountinfo(row).expect("valid mountinfo row");
171 assert_eq!(parsed.len(), 1);
172 assert_eq!(parsed[0].raw_mount_id, 37);
173 assert_eq!(parsed[0].raw_parent_id, 1);
174 assert_eq!(parsed[0].root, b"/");
175 assert_eq!(parsed[0].raw_peer_groups, [9]);
176 assert!(parse_mountinfo(b"37 1 bad / / rw - ext4 /dev/root rw\n").is_none());
177 assert!(
178 parse_mountinfo(b"37 1 8:1 / / rw unbindable nosymfollow - ext4 /dev/root rw\n")
179 .is_some(),
180 "known bare optional flags must remain accepted"
181 );
182 assert!(
183 parse_mountinfo(b"37 1 8:1 / / rw future_peer:19 - ext4 /dev/root rw\n").is_none(),
184 "unknown numeric optional fields must fail closed"
185 );
186 assert!(
187 parse_mountinfo(b"37 1 8:1 / / rw future_flag:value - ext4 /dev/root rw\n").is_some(),
188 "unknown nonnumeric flags carry no raw numeric identity"
189 );
190 let duplicate = [row.as_slice(), row.as_slice()].concat();
191 assert!(parse_mountinfo(&duplicate).is_none());
192 }
193
194 #[test]
195 fn mount_id_subset_requires_unique_members_in_captured_order() {
196 assert!(mount_ids_are_ordered_subset(&[], &[10, 20, 30]));
197 assert!(mount_ids_are_ordered_subset(&[10, 30], &[10, 20, 30]));
198 assert!(!mount_ids_are_ordered_subset(&[30, 10], &[10, 20, 30]));
199 assert!(!mount_ids_are_ordered_subset(&[10, 99], &[10, 20, 30]));
200 assert!(!mount_ids_are_ordered_subset(&[10, 10], &[10, 20, 30]));
201 }
202}