// 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' }
