/*
 * mountpoint.go - Contains all the functionality for finding mountpoints and
 * using UUIDs to refer to them. Specifically, we can find the mountpoint of a
 * path, get info about a mountpoint, and find mountpoints with a specific UUID.
 *
 * Copyright 2017 Google Inc.
 * Author: Joe Richey (joerichey@google.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package filesystem

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"

	"github.com/pkg/errors"
)

var (
	// These maps hold data about the state of the system's filesystems.
	//
	// They only contain one Mount per filesystem, even if there are
	// additional bind mounts, since we want to store fscrypt metadata in
	// only one place per filesystem.  When it is ambiguous which Mount
	// should be used for a filesystem, mountsByDevice will contain an
	// explicit nil entry, and mountsByPath won't contain an entry.
	mountsByDevice map[DeviceNumber]*Mount
	mountsByPath   map[string]*Mount
	// Used to make the mount functions thread safe
	mountMutex sync.Mutex
	// True if the maps have been successfully initialized.
	mountsInitialized bool
	// Supported tokens for filesystem links
	uuidToken = "UUID"
	pathToken = "PATH"
	// Location to perform UUID lookup
	uuidDirectory = "/dev/disk/by-uuid"
)

// Unescape octal-encoded escape sequences in a string from the mountinfo file.
// The kernel encodes the ' ', '\t', '\n', and '\\' bytes this way.  This
// function exactly inverts what the kernel does, including by preserving
// invalid UTF-8.
func unescapeString(str string) string {
	var sb strings.Builder
	for i := 0; i < len(str); i++ {
		b := str[i]
		if b == '\\' && i+3 < len(str) {
			if parsed, err := strconv.ParseInt(str[i+1:i+4], 8, 8); err == nil {
				b = uint8(parsed)
				i += 3
			}
		}
		sb.WriteByte(b)
	}
	return sb.String()
}

// EscapeString is the reverse of unescapeString.  Use this to avoid injecting
// spaces or newlines into output that uses these characters as separators.
func EscapeString(str string) string {
	var sb strings.Builder
	for _, b := range []byte(str) {
		switch b {
		case ' ', '\t', '\n', '\\':
			sb.WriteString(fmt.Sprintf("\\%03o", b))
		default:
			sb.WriteByte(b)
		}
	}
	return sb.String()
}

// We get the device name via the device number rather than use the mount source
// field directly.  This is necessary to handle a rootfs that was mounted via
// the kernel command line, since mountinfo always shows /dev/root for that.
// This assumes that the device nodes are in the standard location.
func getDeviceName(num DeviceNumber) string {
	linkPath := fmt.Sprintf("/sys/dev/block/%v", num)
	if target, err := os.Readlink(linkPath); err == nil {
		return fmt.Sprintf("/dev/%s", filepath.Base(target))
	}
	return ""
}

// Parse one line of /proc/self/mountinfo.
//
// The line contains the following space-separated fields:
//	[0] mount ID
//	[1] parent ID
//	[2] major:minor
//	[3] root
//	[4] mount point
//	[5] mount options
//	[6...n-1] optional field(s)
//	[n] separator
//	[n+1] filesystem type
//	[n+2] mount source
//	[n+3] super options
//
// For more details, see https://www.kernel.org/doc/Documentation/filesystems/proc.txt
func parseMountInfoLine(line string) *Mount {
	fields := strings.Split(line, " ")
	if len(fields) < 10 {
		return nil
	}

	// Count the optional fields.  In case new fields are appended later,
	// don't simply assume that n == len(fields) - 4.
	n := 6
	for fields[n] != "-" {
		n++
		if n >= len(fields) {
			return nil
		}
	}
	if n+3 >= len(fields) {
		return nil
	}

	var mnt *Mount = &Mount{}
	var err error
	mnt.DeviceNumber, err = newDeviceNumberFromString(fields[2])
	if err != nil {
		return nil
	}
	mnt.Subtree = unescapeString(fields[3])
	mnt.Path = unescapeString(fields[4])
	for _, opt := range strings.Split(fields[5], ",") {
		if opt == "ro" {
			mnt.ReadOnly = true
		}
	}
	mnt.FilesystemType = unescapeString(fields[n+1])
	mnt.Device = getDeviceName(mnt.DeviceNumber)
	return mnt
}

type mountpointTreeNode struct {
	mount    *Mount
	parent   *mountpointTreeNode
	children []*mountpointTreeNode
}

func addUncontainedSubtreesRecursive(dst map[string]bool,
	node *mountpointTreeNode, allUncontainedSubtrees map[string]bool) {
	if allUncontainedSubtrees[node.mount.Subtree] {
		dst[node.mount.Subtree] = true
	}
	for _, child := range node.children {
		addUncontainedSubtreesRecursive(dst, child, allUncontainedSubtrees)
	}
}

// findMainMount finds the "main" Mount of a filesystem.  The "main" Mount is
// where the filesystem's fscrypt metadata is stored.
//
// Normally, there is just one Mount and it's of the entire filesystem
// (mnt.Subtree == "/").  But in general, the filesystem might be mounted in
// multiple places, including "bind mounts" where mnt.Subtree != "/".  Also, the
// filesystem might have a combination of read-write and read-only mounts.
//
// To handle most cases, we could just choose a mount with mnt.Subtree == "/",
// preferably a read-write mount.  However, that doesn't work in containers
// where the "/" subtree might not be mounted.  Here's a real-world example:
//
//              mnt.Subtree               mnt.Path
//              -----------               --------
//              /var/lib/lxc/base/rootfs  /
//              /var/cache/pacman/pkg     /var/cache/pacman/pkg
//              /srv/repo/x86_64          /srv/http/x86_64
//
// In this case, all mnt.Subtree are independent.  To handle this case, we must
// choose the Mount whose mnt.Path contains the others, i.e. the first one.
// Note: the fscrypt metadata won't be usable from outside the container since
// it won't be at the real root of the filesystem, but that may be acceptable.
//
// However, we can't look *only* at mnt.Path, since in some cases mnt.Subtree is
// needed to correctly handle bind mounts.  For example, in the following case,
// the first Mount should be chosen:
//
//              mnt.Subtree               mnt.Path
//              -----------               --------
//              /foo                      /foo
//              /foo/dir                  /dir
//
// To solve this, we divide the mounts into non-overlapping trees of mnt.Path.
// Then, we choose one of these trees which contains (exactly or via path
// prefix) *all* mnt.Subtree.  We then return the root of this tree.  In both
// the above examples, this algorithm returns the first Mount.
func findMainMount(filesystemMounts []*Mount) *Mount {
	// Index this filesystem's mounts by path.  Note: paths are unique here,
	// since non-last mounts were already excluded earlier.
	//
	// Also build the set of all mounted subtrees.
	filesystemMountsByPath := make(map[string]*mountpointTreeNode)
	allSubtrees := make(map[string]bool)
	for _, mnt := range filesystemMounts {
		filesystemMountsByPath[mnt.Path] = &mountpointTreeNode{mount: mnt}
		allSubtrees[mnt.Subtree] = true
	}

	// Divide the mounts into non-overlapping trees of mountpoints.
	for path, mntNode := range filesystemMountsByPath {
		for path != "/" && mntNode.parent == nil {
			path = filepath.Dir(path)
			if parent := filesystemMountsByPath[path]; parent != nil {
				mntNode.parent = parent
				parent.children = append(parent.children, mntNode)
			}
		}
	}

	// Build the set of mounted subtrees that aren't contained in any other
	// mounted subtree.
	allUncontainedSubtrees := make(map[string]bool)
	for subtree := range allSubtrees {
		contained := false
		for t := subtree; t != "/" && !contained; {
			t = filepath.Dir(t)
			contained = allSubtrees[t]
		}
		if !contained {
			allUncontainedSubtrees[subtree] = true
		}
	}

	// Select the root of a mountpoint tree whose mounted subtrees contain
	// *all* mounted subtrees.  Equivalently, select a mountpoint tree in
	// which every uncontained subtree is mounted.
	var mainMount *Mount
	for _, mntNode := range filesystemMountsByPath {
		mnt := mntNode.mount
		if mntNode.parent != nil {
			continue
		}
		uncontainedSubtrees := make(map[string]bool)
		addUncontainedSubtreesRecursive(uncontainedSubtrees, mntNode, allUncontainedSubtrees)
		if len(uncontainedSubtrees) != len(allUncontainedSubtrees) {
			continue
		}
		// If there's more than one eligible mount, they should have the
		// same Subtree.  Otherwise it's ambiguous which one to use.
		if mainMount != nil && mainMount.Subtree != mnt.Subtree {
			log.Printf("Unsupported case: %q (%v) has multiple non-overlapping mounts. This filesystem will be ignored!",
				mnt.Device, mnt.DeviceNumber)
			return nil
		}
		// Prefer a read-write mount to a read-only one.
		if mainMount == nil || mainMount.ReadOnly {
			mainMount = mnt
		}
	}
	return mainMount
}

// This is separate from loadMountInfo() only for unit testing.
func readMountInfo(r io.Reader) error {
	mountsByDevice = make(map[DeviceNumber]*Mount)
	mountsByPath = make(map[string]*Mount)
	allMountsByDevice := make(map[DeviceNumber][]*Mount)
	allMountsByPath := make(map[string]*Mount)

	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := scanner.Text()
		mnt := parseMountInfoLine(line)
		if mnt == nil {
			log.Printf("ignoring invalid mountinfo line %q", line)
			continue
		}

		// We can only use mountpoints that are directories for fscrypt.
		if !isDir(mnt.Path) {
			log.Printf("ignoring mountpoint %q because it is not a directory", mnt.Path)
			continue
		}

		// Note this overrides the info if we have seen the mountpoint
		// earlier in the file. This is correct behavior because the
		// mountpoints are listed in mount order.
		allMountsByPath[mnt.Path] = mnt
	}
	// For each filesystem, choose a "main" Mount and discard any additional
	// bind mounts.  fscrypt only cares about the main Mount, since it's
	// where the fscrypt metadata is stored.  Store all the main Mounts in
	// mountsByDevice and mountsByPath so that they can be found later.
	for _, mnt := range allMountsByPath {
		allMountsByDevice[mnt.DeviceNumber] =
			append(allMountsByDevice[mnt.DeviceNumber], mnt)
	}
	for deviceNumber, filesystemMounts := range allMountsByDevice {
		mnt := findMainMount(filesystemMounts)
		mountsByDevice[deviceNumber] = mnt // may store an explicit nil entry
		if mnt != nil {
			mountsByPath[mnt.Path] = mnt
		}
	}
	return nil
}

// loadMountInfo populates the Mount mappings by parsing /proc/self/mountinfo.
// It returns an error if the Mount mappings cannot be populated.
func loadMountInfo() error {
	if !mountsInitialized {
		file, err := os.Open("/proc/self/mountinfo")
		if err != nil {
			return err
		}
		defer file.Close()
		if err := readMountInfo(file); err != nil {
			return err
		}
		mountsInitialized = true
	}
	return nil
}

func filesystemLacksMainMountError(deviceNumber DeviceNumber) error {
	return errors.Errorf("Device %q (%v) lacks a \"main\" mountpoint in the current mount namespace, so it's ambiguous where to store the fscrypt metadata.",
		getDeviceName(deviceNumber), deviceNumber)
}

// AllFilesystems lists all mounted filesystems ordered by path to their "main"
// Mount.  Use CheckSetup() to see if they are set up for use with fscrypt.
func AllFilesystems() ([]*Mount, error) {
	mountMutex.Lock()
	defer mountMutex.Unlock()
	if err := loadMountInfo(); err != nil {
		return nil, err
	}

	mounts := make([]*Mount, 0, len(mountsByPath))
	for _, mount := range mountsByPath {
		mounts = append(mounts, mount)
	}

	sort.Sort(PathSorter(mounts))
	return mounts, nil
}

// UpdateMountInfo updates the filesystem mountpoint maps with the current state
// of the filesystem mountpoints. Returns error if the initialization fails.
func UpdateMountInfo() error {
	mountMutex.Lock()
	defer mountMutex.Unlock()
	mountsInitialized = false
	return loadMountInfo()
}

// FindMount returns the main Mount object for the filesystem which contains the
// file at the specified path. An error is returned if the path is invalid or if
// we cannot load the required mount data. If a mount has been updated since the
// last call to one of the mount functions, run UpdateMountInfo to see changes.
func FindMount(path string) (*Mount, error) {
	mountMutex.Lock()
	defer mountMutex.Unlock()
	if err := loadMountInfo(); err != nil {
		return nil, err
	}
	// First try to find the mount by the number of the containing device.
	deviceNumber, err := getNumberOfContainingDevice(path)
	if err != nil {
		return nil, err
	}
	mnt, ok := mountsByDevice[deviceNumber]
	if ok {
		if mnt == nil {
			return nil, filesystemLacksMainMountError(deviceNumber)
		}
		return mnt, nil
	}
	// The mount couldn't be found by the number of the containing device.
	// Fall back to walking up the directory hierarchy and checking for a
	// mount at each directory path.  This is necessary for btrfs, where
	// files report a different st_dev from the /proc/self/mountinfo entry.
	curPath, err := canonicalizePath(path)
	if err != nil {
		return nil, err
	}
	for {
		mnt := mountsByPath[curPath]
		if mnt != nil {
			return mnt, nil
		}
		// Move to the parent directory unless we have reached the root.
		parent := filepath.Dir(curPath)
		if parent == curPath {
			return nil, errors.Errorf("couldn't find mountpoint containing %q", path)
		}
		curPath = parent
	}
}

// GetMount is like FindMount, except GetMount also returns an error if the path
// doesn't name the same file as the filesystem's "main" Mount.  For example, if
// a filesystem is fully mounted at "/mnt" and if "/mnt/a" exists, then
// FindMount("/mnt/a") will succeed whereas GetMount("/mnt/a") will fail.  This
// is true even if "/mnt/a" is a bind mount of part of the same filesystem.
func GetMount(mountpoint string) (*Mount, error) {
	mnt, err := FindMount(mountpoint)
	if err != nil {
		return nil, &ErrNotAMountpoint{mountpoint}
	}
	// Check whether 'mountpoint' names the same directory as 'mnt.Path'.
	// Use os.SameFile() (i.e., compare inode numbers) rather than compare
	// canonical paths, since filesystems may be mounted in multiple places.
	fi1, err := os.Stat(mountpoint)
	if err != nil {
		return nil, err
	}
	fi2, err := os.Stat(mnt.Path)
	if err != nil {
		return nil, err
	}
	if !os.SameFile(fi1, fi2) {
		return nil, &ErrNotAMountpoint{mountpoint}
	}
	return mnt, nil
}

func uuidToDeviceNumber(uuid string) (DeviceNumber, error) {
	uuidSymlinkPath := filepath.Join(uuidDirectory, uuid)
	return getDeviceNumber(uuidSymlinkPath)
}

func deviceNumberToMount(deviceNumber DeviceNumber) (*Mount, bool) {
	mountMutex.Lock()
	defer mountMutex.Unlock()
	if err := loadMountInfo(); err != nil {
		log.Print(err)
		return nil, false
	}
	mnt, ok := mountsByDevice[deviceNumber]
	return mnt, ok
}

// getMountFromLink returns the main Mount, if any, for the filesystem which the
// given link points to.  The link should contain a series of token-value pairs
// (<token>=<value>), one per line.  The supported tokens are "UUID" and "PATH".
// If the UUID is present and it works, then it is used; otherwise, PATH is used
// if it is present.  (The fallback from UUID to PATH will keep the link working
// if the UUID of the target filesystem changes but its mountpoint doesn't.)
//
// If a mount has been updated since the last call to one of the mount
// functions, make sure to run UpdateMountInfo first.
func getMountFromLink(link string) (*Mount, error) {
	// Parse the link.
	uuid := ""
	path := ""
	lines := strings.Split(link, "\n")
	for _, line := range lines {
		line := strings.TrimSpace(line)
		if line == "" {
			continue
		}
		pair := strings.Split(line, "=")
		if len(pair) != 2 {
			log.Printf("ignoring invalid line in filesystem link file: %q", line)
			continue
		}
		token := pair[0]
		value := pair[1]
		switch token {
		case uuidToken:
			uuid = value
		case pathToken:
			path = value
		default:
			log.Printf("ignoring unknown link token %q", token)
		}
	}
	// At least one of UUID and PATH must be present.
	if uuid == "" && path == "" {
		return nil, &ErrFollowLink{link, errors.Errorf("invalid filesystem link file")}
	}

	// Try following the UUID.
	errMsg := ""
	if uuid != "" {
		deviceNumber, err := uuidToDeviceNumber(uuid)
		if err == nil {
			mnt, ok := deviceNumberToMount(deviceNumber)
			if mnt != nil {
				log.Printf("resolved filesystem link using UUID %q", uuid)
				return mnt, nil
			}
			if ok {
				return nil, &ErrFollowLink{link, filesystemLacksMainMountError(deviceNumber)}
			}
			log.Printf("cannot find filesystem with UUID %q", uuid)
		} else {
			log.Printf("cannot find filesystem with UUID %q: %v", uuid, err)
		}
		errMsg += fmt.Sprintf("cannot find filesystem with UUID %q", uuid)
		if path != "" {
			log.Printf("falling back to using mountpoint path instead of UUID")
		}
	}
	// UUID didn't work.  As a fallback, try the mountpoint path.
	if path != "" {
		mnt, err := GetMount(path)
		if mnt != nil {
			log.Printf("resolved filesystem link using mountpoint path %q", path)
			return mnt, nil
		}
		log.Print(err)
		if errMsg == "" {
			errMsg = fmt.Sprintf("cannot find filesystem with main mountpoint %q", path)
		} else {
			errMsg += fmt.Sprintf(" or main mountpoint %q", path)
		}
	}
	// No method worked; return an error.
	return nil, &ErrFollowLink{link, errors.New(errMsg)}
}

func (mnt *Mount) getFilesystemUUID() (string, error) {
	dirEntries, err := os.ReadDir(uuidDirectory)
	if err != nil {
		return "", err
	}
	for _, dirEntry := range dirEntries {
		fileInfo, err := dirEntry.Info()
		if err != nil {
			continue
		}
		if fileInfo.Mode()&os.ModeSymlink == 0 {
			continue // Only interested in UUID symlinks
		}
		uuid := fileInfo.Name()
		deviceNumber, err := uuidToDeviceNumber(uuid)
		if err != nil {
			log.Print(err)
			continue
		}
		if mnt.DeviceNumber == deviceNumber {
			return uuid, nil
		}
	}
	return "", errors.Errorf("cannot determine UUID of device %q (%v)",
		mnt.Device, mnt.DeviceNumber)
}

// makeLink creates the contents of a link file which will point to the given
// filesystem.  This will normally be a string of the form
// "UUID=<uuid>\nPATH=<path>\n".  If the UUID cannot be determined, the UUID
// portion will be omitted.
func makeLink(mnt *Mount) (string, error) {
	uuid, err := mnt.getFilesystemUUID()
	if err != nil {
		// The UUID could not be determined.  This happens for btrfs
		// filesystems, as the device number found via
		// /dev/disk/by-uuid/* for btrfs filesystems differs from the
		// actual device number of the mounted filesystem.  Just rely
		// entirely on the fallback to mountpoint path.
		log.Print(err)
		return fmt.Sprintf("%s=%s\n", pathToken, mnt.Path), nil
	}
	return fmt.Sprintf("%s=%s\n%s=%s\n", uuidToken, uuid, pathToken, mnt.Path), nil
}
