///|
fn number(
text : String,
line : Int32,
positive~ : Bool = false,
) -> Int32 raise MountError {
if text.is_empty() {
raise Invalid(line~, reason="empty integer")
}
let mut n = 0
for c in text {
if c < '0' || c > '9' {
raise Invalid(line~, reason="non-decimal integer")
}
let digit = c.to_int() - 48
if n > (2147483647 - digit) / 10 {
raise Invalid(line~, reason="integer overflow")
}
n = n * 10 + digit
}
if positive && n == 0 {
raise Invalid(line~, reason="ID must be positive")
}
n
}
///|
fn unescape(
text : String,
line : Int32,
source_field~ : Bool = false,
) -> String raise MountError {
let chars = text.to_array()
let out = StringBuilder()
for i = 0; i < chars.length(); {
let c = chars[i]
if c == '\\' {
if i + 3 >= chars.length() {
raise Invalid(line~, reason="truncated escape")
}
let a = chars[i + 1]
let b = chars[i + 2]
let c = chars[i + 3]
let decoded : Char = match (a, b, c) {
('0', '4', '0') => ' '
('0', '1', '1') => '\t'
('0', '1', '2') => '\n'
('1', '3', '4') => '\\'
('0', '4', '3') if source_field => '#'
_ => raise Invalid(line~, reason="unsupported escape")
}
out.write_char(decoded)
continue i + 4
} else {
if c == '\u0000' || c == '\t' {
raise Invalid(line~, reason="unescaped control character")
}
out.write_char(c)
continue i + 1
}
}
out.to_string()
}
///|
/// Check canonical absolute namespace_id paths without following symlinks.
fn valid_path(path : String) -> Bool {
if !path.has_prefix("/") || path.contains_char('\u0000') {
return false
}
if path == "/" {
return true
}
if path.has_suffix("/") {
return false
}
for part in path.strip_prefix("/").unwrap().split("/") {
if part == "" || part == "." || part == ".." {
return false
}
}
true
}
///|
fn parse_line(text : String, line : Int32) -> Mount raise MountError {
// Linux only quotes space, tab, LF and backslash in pathname fields.
// CR and other legal filename bytes must survive the text parser unchanged.
if text.contains_char('\u0000') || text.contains_char('\t') {
raise Invalid(line~, reason="raw NUL or tab")
}
let fields = text.split(" ").map(fn(s) { s.to_owned() }).collect()
if fields.any(fn(s) { s == "" }) || fields.length() < 10 {
raise Invalid(line~, reason="invalid field layout")
}
// Only the delimiter position after the six fixed fields is significant.
let mut sep = -1
for i in 6.. shared = Some(group)
"master" => master = Some(group)
_ => propagate_from = Some(group)
}
}
}
}
if (unbindable && (shared != None || master != None)) ||
(propagate_from != None && master == None) {
raise Invalid(line~, reason="inconsistent propagation tags")
}
{
id,
parent_id,
major,
minor,
root,
point,
options: fields[5],
optional_fields: optional.join(" "),
shared,
master,
propagate_from,
unbindable,
fs_type: unescape(fields[sep + 1], line, source_field=true),
source: unescape(fields[sep + 2], line, source_field=true),
super_options: fields[sep + 3],
}
}
///|
/// Parse a complete mountinfo text (maximum 8 Mi UTF-16 units / 16384 mounts).
/// Missing parents are legal for process roots that hide outer mounts.
pub fn parse(
text : String,
namespace_id~ : String,
) -> Snapshot raise MountError {
if namespace_id.is_empty() || namespace_id.length() > 256 {
raise Limit(reason="namespace_id token must contain 1..256 code units")
}
if text.length() > 8 * 1024 * 1024 {
raise Limit(reason="mountinfo exceeds text budget")
}
let entries : Array[Mount] = []
let index : Map[Int32, Mount] = Map([])
let child_index : Map[Int32, Array[Mount]] = Map([])
let lines = text.split("\n").collect()
for i, raw in lines {
if i == lines.length() - 1 && raw == "" {
continue
}
if entries.length() >= 16384 {
raise Limit(reason="too many mounts")
}
if raw.length() > 65536 {
raise Limit(reason="mountinfo line exceeds budget")
}
let mount = parse_line(raw.to_owned(), i + 1)
if index.contains(mount.id) {
raise Invalid(line=i + 1, reason="duplicate mount ID")
}
entries.push(mount)
index[mount.id] = mount
if mount.id != mount.parent_id {
match child_index.get(mount.parent_id) {
Some(children) => children.push(mount)
None => child_index[mount.parent_id] = [mount]
}
} else if mount.point != "/" {
raise Topology(reason="self-parent is only allowed at namespace_id root")
}
}
if entries.is_empty() {
raise Topology(reason="empty mount table")
}
// Each node is visited at most once in this graph validation.
let done : Map[Int32, Bool] = Map([])
for entry in entries {
let path : Array[Int32] = []
let active : Map[Int32, Bool] = Map([])
let mut current = Some(entry)
while current is Some(m) {
if done.contains(m.id) {
break
}
if active.contains(m.id) {
raise Topology(reason="parent cycle")
}
active[m.id] = true
path.push(m.id)
if m.id == m.parent_id {
break
}
current = index.get(m.parent_id)
if current is Some(parent) && !path_prefix(parent.point, m.point) {
raise Topology(reason="child mountpoint outside parent mountpoint")
}
}
for id in path {
done[id] = true
}
}
{ token: namespace_id, entries, index, child_index, source_text: text, }
}