// Package mounts parses the kernel mount table (/proc/mounts). Both the system // dashboard (disk usage) and the storage module (mount management) need it, so // it lives here rather than being duplicated. It also exposes the octal // unescaping that /proc/mounts and /etc/fstab use for spaces/tabs in paths. package mounts import ( "os" "strings" ) // procMounts is a var so tests can point it at a fixture. var procMounts = "/proc/mounts" // Mount is one mount-table line: the backing device, where it's mounted, the // filesystem type, and the comma-separated mount options. type Mount struct { Device string `json:"device" example:"/dev/sda1"` Mountpoint string `json:"mountpoint" example:"/mnt/data"` FSType string `json:"fstype" example:"ext4"` Options string `json:"options" example:"rw,relatime"` } // Proc reads and parses /proc/mounts. func Proc() ([]Mount, error) { data, err := os.ReadFile(procMounts) if err != nil { return nil, err } return parseProc(string(data)), nil } func parseProc(data string) []Mount { entries := []Mount{} for line := range strings.SplitSeq(data, "\n") { f := strings.Fields(line) if len(f) < 4 { // device mountpoint fstype options [dump pass] continue } entries = append(entries, Mount{ Device: Unescape(f[0]), Mountpoint: Unescape(f[1]), FSType: f[2], Options: f[3], }) } return entries } // Unescape decodes the octal escapes (\040 space, \011 tab, \012 newline, // \134 backslash) that mount tables and fstab use for whitespace in fields. func Unescape(s string) string { if !strings.ContainsRune(s, '\\') { return s } var b strings.Builder for i := 0; i < len(s); i++ { if s[i] == '\\' && i+3 < len(s) && isOctal(s[i+1]) && isOctal(s[i+2]) && isOctal(s[i+3]) { b.WriteByte((s[i+1]-'0')*64 + (s[i+2]-'0')*8 + (s[i+3] - '0')) i += 3 continue } b.WriteByte(s[i]) } return b.String() } func isOctal(c byte) bool { return c >= '0' && c <= '7' }