diff --git a/src/mount/state.rs b/src/mount/state.rs index c95ffea..caa62c5 100644 --- a/src/mount/state.rs +++ b/src/mount/state.rs @@ -30,7 +30,12 @@ fn parse_mountinfo_source(mountinfo: &str, mount_point: &Path) -> Option let fields: Vec<&str> = line.split_whitespace().collect(); // Feld 4 (Index 4) ist der Mountpoint; danach folgen optionale Felder bis zum // Trenner "-", danach fs_type (Index+1) und source (Index+2). - if fields.len() < 5 || fields[4] != target { + // + // Der Kernel escaped Leerzeichen/Tab/Newline/Backslash in Pfadfeldern oktal + // (siehe `man 5 proc`, Abschnitt zu mountinfo) - ohne `unescape_octal_field` würde ein + // Mountpoint mit z. B. einem Leerzeichen im Pfad hier nie als "gemountet" erkannt, weil + // `\040` niemals gleich einem echten Leerzeichen ist. + if fields.len() < 5 || unescape_octal_field(fields[4]) != target { continue; } let Some(dash_pos) = fields.iter().position(|&f| f == "-") else { @@ -44,6 +49,36 @@ fn parse_mountinfo_source(mountinfo: &str, mount_point: &Path) -> Option result } +/// Kehrt die oktale Escape-Kodierung um, die der Kernel in `/proc/self/mountinfo` für +/// Leerzeichen (`\040`), Tab (`\011`), Newline (`\012`) und Backslash (`\134`) in Pfadfeldern +/// verwendet (siehe `man 5 proc`). +fn unescape_octal_field(field: &str) -> std::borrow::Cow<'_, str> { + if !field.contains('\\') { + return std::borrow::Cow::Borrowed(field); + } + + // Arbeitet auf rohen Bytes statt `char`s: ein Byte einer mehrbytigen UTF-8-Sequenz per + // `as char` in einen eigenständigen `char` umzuwandeln würde nicht-ASCII-Zeichen im Pfad + // (z. B. Umlaute) in mehrere falsche Codepoints zerlegen. + let bytes = field.as_bytes(); + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'\\' + && i + 3 < bytes.len() + && let Ok(octal) = std::str::from_utf8(&bytes[i + 1..i + 4]) + && let Ok(value) = u8::from_str_radix(octal, 8) + { + out.push(value); + i += 4; + } else { + out.push(bytes[i]); + i += 1; + } + } + std::borrow::Cow::Owned(String::from_utf8_lossy(&out).into_owned()) +} + #[cfg(test)] mod tests { use super::*; @@ -66,6 +101,13 @@ mod tests { assert_eq!(source, None); } + #[test] + fn matches_a_mount_point_containing_a_space_escaped_by_the_kernel() { + let mountinfo = "43 36 0:26 / /mnt/my\\040drive rw,relatime shared:2 - cifs //server/share rw"; + let source = parse_mountinfo_source(mountinfo, &PathBuf::from("/mnt/my drive")); + assert_eq!(source.as_deref(), Some("//server/share")); + } + #[test] fn last_matching_entry_wins_for_stacked_mounts() { let mountinfo = "36 35 98:0 / /mnt/x rw - nfs server:/export rw\n\