/*
 * mountpoint_test.go - Tests for reading information about all mountpoints.
 *
 * 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.
 */

// Note: these tests assume the existence of some well-known directories and
// devices: /mnt, /home, /tmp, and /dev/loop0.  This is because the mountpoint
// loading code only retains mountpoints on valid directories, and only retains
// device names for valid device nodes.

package filesystem

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestLoadMountInfo(t *testing.T) {
	if err := UpdateMountInfo(); err != nil {
		t.Error(err)
	}
}

// Lock the mount maps so that concurrent tests don't interfere with each other.
func beginLoadMountInfoTest() {
	mountMutex.Lock()
}

func endLoadMountInfoTest() {
	// Invalidate the fake mount information in case a test runs later which
	// needs the real mount information.
	mountsInitialized = false
	mountMutex.Unlock()
}

func loadMountInfoFromString(str string) {
	readMountInfo(strings.NewReader(str))
}

func mountForDevice(deviceNumberStr string) *Mount {
	deviceNumber, _ := newDeviceNumberFromString(deviceNumberStr)
	return mountsByDevice[deviceNumber]
}

// Test basic loading of a single mountpoint.
func TestLoadMountInfoBasic(t *testing.T) {
	var mountinfo = `
15 0 259:3 / / rw,relatime shared:1 - ext4 /dev/root rw,data=ordered
`
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	if len(mountsByDevice) != 1 {
		t.Error("Loaded wrong number of mounts")
	}
	mnt := mountForDevice("259:3")
	if mnt == nil {
		t.Fatal("Failed to load mount")
	}
	if mnt.Path != "/" {
		t.Error("Wrong path")
	}
	if mnt.FilesystemType != "ext4" {
		t.Error("Wrong filesystem type")
	}
	if mnt.DeviceNumber.String() != "259:3" {
		t.Error("Wrong device number")
	}
	if mnt.Subtree != "/" {
		t.Error("Wrong subtree")
	}
	if mnt.ReadOnly {
		t.Error("Wrong readonly flag")
	}
	if len(mountsByPath) != 1 {
		t.Error("mountsByPath doesn't contain exactly one entry")
	}
	if mountsByPath[mnt.Path] != mnt {
		t.Error("mountsByPath doesn't contain the correct entry")
	}
}

// Test that Mount.Device is set to the mountpoint's source device if
// applicable, otherwise it is set to the empty string.
func TestLoadSourceDevice(t *testing.T) {
	var mountinfo = `
15 0 7:0 / / rw shared:1 - foo /dev/loop0 rw,data=ordered
31 15 0:27 / /tmp rw,nosuid,nodev shared:17 - tmpfs tmpfs rw
`
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	mnt := mountForDevice("7:0")
	if mnt.Device != "/dev/loop0" {
		t.Error("mnt.Device wasn't set to source device")
	}
	mnt = mountForDevice("0:27")
	if mnt.Device != "" {
		t.Error("mnt.Device wasn't set to empty string for an invalid device")
	}
}

// Test that non-directory mounts are ignored.
func TestNondirectoryMountsIgnored(t *testing.T) {
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	file, err := ioutil.TempFile("", "fscrypt_regfile")
	if err != nil {
		t.Fatal(err)
	}
	file.Close()
	defer os.Remove(file.Name())

	mountinfo := fmt.Sprintf("15 0 259:3 /foo %s rw,relatime shared:1 - ext4 /dev/root rw", file.Name())
	loadMountInfoFromString(mountinfo)
	if len(mountsByDevice) != 0 {
		t.Error("Non-directory mount wasn't ignored")
	}
}

// Test that when multiple mounts are on one directory, the last is the one
// which is kept.
func TestNonLatestMountsIgnored(t *testing.T) {
	mountinfo := `
15 0 259:3 / / rw shared:1 - ext4 /dev/root rw
15 0 259:3 / / rw shared:1 - f2fs /dev/root rw
15 0 259:3 / / rw shared:1 - ubifs /dev/root rw
`
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	mnt := mountForDevice("259:3")
	if mnt.FilesystemType != "ubifs" {
		t.Error("Last mount didn't supersede previous ones")
	}
}

// Test that escape sequences in the mountinfo file are unescaped correctly.
func TestLoadMountWithSpecialCharacters(t *testing.T) {
	tempDir, err := ioutil.TempDir("", "fscrypt")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)
	tempDir, err = filepath.Abs(tempDir)
	if err != nil {
		t.Fatal(err)
	}
	mountpoint := filepath.Join(tempDir, "/My Directory\t\n\\")
	if err := os.Mkdir(mountpoint, 0700); err != nil {
		t.Fatal(err)
	}
	mountinfo := fmt.Sprintf("15 0 259:3 / %s/My\\040Directory\\011\\012\\134 rw shared:1 - ext4 /dev/root rw", tempDir)

	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	mnt := mountForDevice("259:3")
	if mnt.Path != mountpoint {
		t.Fatal("Wrong mountpoint")
	}
}

// Tests the EscapeString() and unescapeString() functions.
func TestStringEscaping(t *testing.T) {
	charsNeedEscaping := " \t\n\\"
	charsDontNeedEscaping := "ABCDEF\u2603\xff\xff\v"

	orig := charsNeedEscaping + charsDontNeedEscaping
	escaped := `\040\011\012\134` + charsDontNeedEscaping
	if EscapeString(orig) != escaped {
		t.Fatal("EscapeString gave wrong result")
	}
	if unescapeString(escaped) != orig {
		t.Fatal("unescapeString gave wrong result")
	}
}

// Test parsing some invalid mountinfo lines.
func TestLoadBadMountInfo(t *testing.T) {
	mountinfos := []string{"a",
		"a a a a a a a a a a a a a a a",
		"a a a a a a a a a a a a - a a",
		"15 0 BAD:3 / / rw,relatime shared:1 - ext4 /dev/root rw,data=ordered"}
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	for _, mountinfo := range mountinfos {
		loadMountInfoFromString(mountinfo)
		if len(mountsByDevice) != 0 {
			t.Error("Loaded mount from invalid mountinfo line")
		}
	}
}

// Test that the ReadOnly flag is set if the mount is readonly, even if the
// filesystem is read-write.
func TestLoadReadOnlyMount(t *testing.T) {
	mountinfo := `
222 15 259:3 / /mnt ro,relatime shared:1 - ext4 /dev/root rw,data=ordered
`
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	mnt := mountForDevice("259:3")
	if !mnt.ReadOnly {
		t.Error("Wrong readonly flag")
	}
}

// Test that a read-write mount is preferred over a read-only mount.
func TestReadWriteMountIsPreferredOverReadOnlyMount(t *testing.T) {
	mountinfo := `
222 15 259:3 / /home ro shared:1 - ext4 /dev/root rw
222 15 259:3 / /mnt rw shared:1 - ext4 /dev/root rw
222 15 259:3 / /tmp ro shared:1 - ext4 /dev/root rw
`
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	mnt := mountForDevice("259:3")
	if mnt.Path != "/mnt" {
		t.Error("Wrong mount was chosen")
	}
}

// Test that a mount of the full filesystem is preferred over mounts of non-root
// subtrees, given independent mountpoints.
func TestRootSubtreeIsPreferred(t *testing.T) {
	mountinfo := `
222 15 259:3 /subtree1 /home rw shared:1 - ext4 /dev/root rw
222 15 259:3 / /mnt rw shared:1 - ext4 /dev/root rw
222 15 259:3 /subtree2 /tmp rw shared:1 - ext4 /dev/root rw
`
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	mnt := mountForDevice("259:3")
	if mnt.Subtree != "/" {
		t.Error("Wrong mount was chosen")
	}
}

// Test that a mount that is not of the full filesystem but still contains all
// other mounted subtrees is preferred, given independent mountpoints.
func TestHighestSubtreeIsPreferred(t *testing.T) {
	mountinfo := `
222 15 259:3 /foo/bar /mnt rw shared:1 - ext4 /dev/root rw
222 15 259:3 /foo /tmp rw shared:1 - ext4 /dev/root rw
222 15 259:3 /foo/baz /home rw shared:1 - ext4 /dev/root rw
`
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	deviceNumber, _ := newDeviceNumberFromString("259:3")
	mnt := mountsByDevice[deviceNumber]
	if mnt.Subtree != "/foo" {
		t.Error("Wrong mount was chosen")
	}
}

// Test that mountpoint "/" is preferred, given independent subtrees.
func TestRootMountpointIsPreferred(t *testing.T) {
	mountinfo := `
222 15 259:3 /var/cache/pacman/pkg /mnt rw shared:1 - ext4 /dev/root rw
222 15 259:3 /var/lib/lxc/base/rootfs / rw shared:1 - ext4 /dev/root rw
222 15 259:3 /srv/repo/x86_64 /home rw shared:1 - ext4 /dev/root rw
`
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	deviceNumber, _ := newDeviceNumberFromString("259:3")
	mnt := mountsByDevice[deviceNumber]
	if mnt.Subtree != "/var/lib/lxc/base/rootfs" {
		t.Error("Wrong mount was chosen")
	}
}

// Test that a mountpoint that is not "/" but still contains all other
// mountpoints is preferred, given independent subtrees.
func TestHighestMountpointIsPreferred(t *testing.T) {
	tempDir, err := ioutil.TempDir("", "fscrypt")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)
	tempDir, err = filepath.Abs(tempDir)
	if err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(tempDir+"/a/b", 0700); err != nil {
		t.Fatal(err)
	}
	if err := os.Mkdir(tempDir+"/a/c", 0700); err != nil {
		t.Fatal(err)
	}
	mountinfo := fmt.Sprintf(`
222 15 259:3 /0 %s rw shared:1 - ext4 /dev/root rw
222 15 259:3 /1 %s rw shared:1 - ext4 /dev/root rw
222 15 259:3 /2 %s rw shared:1 - ext4 /dev/root rw
`, tempDir+"/a/b", tempDir+"/a", tempDir+"/a/c")

	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	deviceNumber, _ := newDeviceNumberFromString("259:3")
	mnt := mountsByDevice[deviceNumber]
	if mnt.Subtree != "/1" {
		t.Error("Wrong mount was chosen")
	}
}

// Test that if some subtrees are contained in other subtrees, *and* some
// mountpoints are contained in other mountpoints, the chosen Mount is the root
// of a tree of mountpoints whose mounted subtrees contain all mounted subtrees.
func TestLoadContainedSubtreesAndMountpoints(t *testing.T) {
	tempDir, err := ioutil.TempDir("", "fscrypt")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)
	tempDir, err = filepath.Abs(tempDir)
	if err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(tempDir+"/a/b", 0700); err != nil {
		t.Fatal(err)
	}
	if err := os.Mkdir(tempDir+"/a/c", 0700); err != nil {
		t.Fatal(err)
	}
	if err := os.Mkdir(tempDir+"/d", 0700); err != nil {
		t.Fatal(err)
	}
	if err := os.Mkdir(tempDir+"/e", 0700); err != nil {
		t.Fatal(err)
	}
	// The first three mounts form a tree of mountpoints.  The rest have
	// independent mountpoints but have mounted subtrees contained in the
	// mounted subtrees of the first mountpoint tree.
	mountinfo := fmt.Sprintf(`
222 15 259:3 /0 %s rw shared:1 - ext4 /dev/root rw
222 15 259:3 /1 %s rw shared:1 - ext4 /dev/root rw
222 15 259:3 /2 %s rw shared:1 - ext4 /dev/root rw
222 15 259:3 /1/3 %s rw shared:1 - ext4 /dev/root rw
222 15 259:3 /2/4 %s rw shared:1 - ext4 /dev/root rw
`, tempDir+"/a/b", tempDir+"/a", tempDir+"/a/c",
		tempDir+"/d", tempDir+"/e")

	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	deviceNumber, _ := newDeviceNumberFromString("259:3")
	mnt := mountsByDevice[deviceNumber]
	if mnt.Subtree != "/1" {
		t.Error("Wrong mount was chosen")
	}
}

// Test loading mounts with independent subtrees *and* independent mountpoints.
// This case is ambiguous, so an explicit nil entry should be stored.
func TestLoadAmbiguousMounts(t *testing.T) {
	mountinfo := `
222 15 259:3 /foo /mnt rw shared:1 - ext4 /dev/root rw
222 15 259:3 /bar /tmp rw shared:1 - ext4 /dev/root rw
`
	beginLoadMountInfoTest()
	defer endLoadMountInfoTest()
	loadMountInfoFromString(mountinfo)
	deviceNumber, _ := newDeviceNumberFromString("259:3")
	mnt, ok := mountsByDevice[deviceNumber]
	if !ok {
		t.Error("Entry should exist")
	}
	if mnt != nil {
		t.Error("Entry should be nil")
	}
}

// Test making a filesystem link and following it, and test that leading and
// trailing whitespace in the link is ignored.
func TestGetMountFromLink(t *testing.T) {
	mnt, err := getTestMount(t)
	if err != nil {
		t.Skip(err)
	}
	link, err := makeLink(mnt)
	if err != nil {
		t.Fatal(err)
	}
	linkedMnt, err := getMountFromLink(link)
	if err != nil {
		t.Fatal(err)
	}
	if linkedMnt != mnt {
		t.Fatal("Link doesn't point to the same Mount")
	}
	if linkedMnt, err = getMountFromLink(link + "\n"); err != nil {
		t.Fatal(err)
	}
	if linkedMnt != mnt {
		t.Fatal("Link doesn't point to the same Mount")
	}
	if linkedMnt, err = getMountFromLink("  " + link + "  \r\n"); err != nil {
		t.Fatal(err)
	}
	if linkedMnt != mnt {
		t.Fatal("Link doesn't point to the same Mount")
	}
}

// Test that makeLink() is including the expected information in links.
func TestMakeLink(t *testing.T) {
	mnt, err := getTestMount(t)
	if err != nil {
		t.Skip(err)
	}
	link, err := makeLink(mnt)
	if err != nil {
		t.Fatal(err)
	}

	// Normally, both UUID and PATH should be included.
	if !strings.Contains(link, "UUID=") {
		t.Fatal("Link doesn't contain UUID")
	}
	if !strings.Contains(link, "PATH=") {
		t.Fatal("Link doesn't contain PATH")
	}

	// Without a valid device number, only PATH should be included.
	mntCopy := *mnt
	mntCopy.DeviceNumber = 0
	link, err = makeLink(&mntCopy)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(link, "UUID=") {
		t.Fatal("Link shouldn't contain UUID")
	}
	if !strings.Contains(link, "PATH=") {
		t.Fatal("Link doesn't contain PATH")
	}
}

// Test that old filesystem links that contain a UUID only still work.
func TestGetMountFromLegacyLink(t *testing.T) {
	mnt, err := getTestMount(t)
	if err != nil {
		t.Skip(err)
	}
	uuid, err := mnt.getFilesystemUUID()
	if uuid == "" || err != nil {
		t.Fatal("Can't get UUID of test filesystem")
	}

	link := fmt.Sprintf("UUID=%s", uuid)
	linkedMnt, err := getMountFromLink(link)
	if err != nil {
		t.Fatal(err)
	}
	if linkedMnt != mnt {
		t.Fatal("Link doesn't point to the same Mount")
	}
}

// Test that if the UUID in a filesystem link doesn't work, then the PATH is
// used instead, and vice versa.
func TestGetMountFromLinkFallback(t *testing.T) {
	mnt, err := getTestMount(t)
	if err != nil {
		t.Skip(err)
	}
	badUUID := "00000000-0000-0000-0000-000000000000"
	badPath := "/NONEXISTENT_MOUNT"
	goodUUID, err := mnt.getFilesystemUUID()
	if goodUUID == "" || err != nil {
		t.Fatal("Can't get UUID of test filesystem")
	}

	// only PATH valid (should succeed)
	link := fmt.Sprintf("UUID=%s\nPATH=%s\n", badUUID, mnt.Path)
	linkedMnt, err := getMountFromLink(link)
	if err != nil {
		t.Fatal(err)
	}
	if linkedMnt != mnt {
		t.Fatal("Link doesn't point to the same Mount")
	}

	// only PATH given at all (should succeed)
	link = fmt.Sprintf("PATH=%s\n", mnt.Path)
	linkedMnt, err = getMountFromLink(link)
	if err != nil {
		t.Fatal(err)
	}
	if linkedMnt != mnt {
		t.Fatal("Link doesn't point to the same Mount")
	}

	// only UUID valid (should succeed)
	link = fmt.Sprintf("UUID=%s\nPATH=%s\n", goodUUID, badPath)
	if linkedMnt, err = getMountFromLink(link); err != nil {
		t.Fatal(err)
	}
	if linkedMnt != mnt {
		t.Fatal("Link doesn't point to the same Mount")
	}

	// neither valid (should fail)
	link = fmt.Sprintf("UUID=%s\nPATH=%s\n", badUUID, badPath)
	linkedMnt, err = getMountFromLink(link)
	if linkedMnt != nil || err == nil {
		t.Fatal("Following a bad link succeeded")
	}
}

// Benchmarks how long it takes to update the mountpoint data
func BenchmarkLoadFirst(b *testing.B) {
	for n := 0; n < b.N; n++ {
		err := UpdateMountInfo()
		if err != nil {
			b.Fatal(err)
		}
	}
}
