// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2017 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package osutil_test

import (
	"math"
	"os"
	"syscall"

	. "gopkg.in/check.v1"

	"github.com/snapcore/snapd/osutil"
)

type entrySuite struct{}

var _ = Suite(&entrySuite{})

func (s *entrySuite) TestString(c *C) {
	ent0 := osutil.MountEntry{}
	c.Assert(ent0.String(), Equals, "none none none defaults 0 0")
	ent1 := osutil.MountEntry{
		Name:    "/var/snap/foo/common",
		Dir:     "/var/snap/bar/common",
		Options: []string{"bind"},
	}
	c.Assert(ent1.String(), Equals,
		"/var/snap/foo/common /var/snap/bar/common none bind 0 0")
	ent2 := osutil.MountEntry{
		Name:    "/dev/sda5",
		Dir:     "/media/foo",
		Type:    "ext4",
		Options: []string{"rw,noatime"},
	}
	c.Assert(ent2.String(), Equals, "/dev/sda5 /media/foo ext4 rw,noatime 0 0")
	ent3 := osutil.MountEntry{
		Name:    "/dev/sda5",
		Dir:     "/media/My Files",
		Type:    "ext4",
		Options: []string{"rw,noatime"},
	}
	c.Assert(ent3.String(), Equals, `/dev/sda5 /media/My\040Files ext4 rw,noatime 0 0`)
}

func (s *entrySuite) TestEqual(c *C) {
	var a, b *osutil.MountEntry
	a = &osutil.MountEntry{}
	b = &osutil.MountEntry{}
	c.Assert(a.Equal(b), Equals, true)
	a = &osutil.MountEntry{Dir: "foo"}
	b = &osutil.MountEntry{Dir: "foo"}
	c.Assert(a.Equal(b), Equals, true)
	a = &osutil.MountEntry{Options: []string{"ro"}}
	b = &osutil.MountEntry{Options: []string{"ro"}}
	c.Assert(a.Equal(b), Equals, true)
	a = &osutil.MountEntry{Dir: "foo"}
	b = &osutil.MountEntry{Dir: "bar"}
	c.Assert(a.Equal(b), Equals, false)
	a = &osutil.MountEntry{}
	b = &osutil.MountEntry{Options: []string{"ro"}}
	c.Assert(a.Equal(b), Equals, false)
	a = &osutil.MountEntry{Options: []string{"ro"}}
	b = &osutil.MountEntry{Options: []string{"rw"}}
	c.Assert(a.Equal(b), Equals, false)
}

// Test that typical fstab entry is parsed correctly.
func (s *entrySuite) TestParseMountEntry1(c *C) {
	e, err := osutil.ParseMountEntry("UUID=394f32c0-1f94-4005-9717-f9ab4a4b570b /               ext4    errors=remount-ro 0       1")
	c.Assert(err, IsNil)
	c.Assert(e.Name, Equals, "UUID=394f32c0-1f94-4005-9717-f9ab4a4b570b")
	c.Assert(e.Dir, Equals, "/")
	c.Assert(e.Type, Equals, "ext4")
	c.Assert(e.Options, DeepEquals, []string{"errors=remount-ro"})
	c.Assert(e.DumpFrequency, Equals, 0)
	c.Assert(e.CheckPassNumber, Equals, 1)

	e, err = osutil.ParseMountEntry("none /tmp tmpfs")
	c.Assert(err, IsNil)
	c.Assert(e.Name, Equals, "none")
	c.Assert(e.Dir, Equals, "/tmp")
	c.Assert(e.Type, Equals, "tmpfs")
	c.Assert(e.Options, IsNil)
	c.Assert(e.DumpFrequency, Equals, 0)
	c.Assert(e.CheckPassNumber, Equals, 0)
}

// Test that hash inside a field value is supported.
func (s *entrySuite) TestHashInFieldValue(c *C) {
	e, err := osutil.ParseMountEntry("mhddfs#/mnt/dir1,/mnt/dir2 /mnt/dir fuse defaults,allow_other 0 0")
	c.Assert(err, IsNil)
	c.Assert(e.Name, Equals, "mhddfs#/mnt/dir1,/mnt/dir2")
	c.Assert(e.Dir, Equals, "/mnt/dir")
	c.Assert(e.Type, Equals, "fuse")
	c.Assert(e.Options, DeepEquals, []string{"defaults", "allow_other"})
	c.Assert(e.DumpFrequency, Equals, 0)
	c.Assert(e.CheckPassNumber, Equals, 0)
}

// Test that options are parsed correctly
func (s *entrySuite) TestParseMountEntry2(c *C) {
	e, err := osutil.ParseMountEntry("name dir type options,comma,separated 0 0")
	c.Assert(err, IsNil)
	c.Assert(e.Name, Equals, "name")
	c.Assert(e.Dir, Equals, "dir")
	c.Assert(e.Type, Equals, "type")
	c.Assert(e.Options, DeepEquals, []string{"options", "comma", "separated"})
	c.Assert(e.DumpFrequency, Equals, 0)
	c.Assert(e.CheckPassNumber, Equals, 0)
}

// Test that whitespace escape codes are honored
func (s *entrySuite) TestParseMountEntry3(c *C) {
	e, err := osutil.ParseMountEntry(`na\040me d\011ir ty\012pe optio\134ns 0 0`)
	c.Assert(err, IsNil)
	c.Assert(e.Name, Equals, "na me")
	c.Assert(e.Dir, Equals, "d\tir")
	c.Assert(e.Type, Equals, "ty\npe")
	c.Assert(e.Options, DeepEquals, []string{`optio\ns`})
	c.Assert(e.DumpFrequency, Equals, 0)
	c.Assert(e.CheckPassNumber, Equals, 0)
}

// Test that number of fields is checked
func (s *entrySuite) TestParseMountEntry4(c *C) {
	for _, s := range []string{
		"", "1", "1 2" /* skip 3, 4, 5 and 6 fields (valid case) */, "1 2 3 4 5 6 7",
	} {
		_, err := osutil.ParseMountEntry(s)
		c.Assert(err, ErrorMatches, "expected between 3 and 6 fields, found [01237]")
	}
}

// Test that integers are parsed and error checked
func (s *entrySuite) TestParseMountEntry5(c *C) {
	_, err := osutil.ParseMountEntry("name dir type options foo 0")
	c.Assert(err, ErrorMatches, "cannot parse dump frequency: .*")
	_, err = osutil.ParseMountEntry("name dir type options 0 foo")
	c.Assert(err, ErrorMatches, "cannot parse check pass number: .*")
}

// Test that last two integer fields default to zero if not present.
func (s *entrySuite) TestParseMountEntry6(c *C) {
	e, err := osutil.ParseMountEntry("name dir type options")
	c.Assert(err, IsNil)
	c.Assert(e.DumpFrequency, Equals, 0)
	c.Assert(e.CheckPassNumber, Equals, 0)

	e, err = osutil.ParseMountEntry("name dir type options 5")
	c.Assert(err, IsNil)
	c.Assert(e.DumpFrequency, Equals, 5)
	c.Assert(e.CheckPassNumber, Equals, 0)

	e, err = osutil.ParseMountEntry("name dir type options 5 7")
	c.Assert(err, IsNil)
	c.Assert(e.DumpFrequency, Equals, 5)
	c.Assert(e.CheckPassNumber, Equals, 7)
}

// Test (string) options -> (int) flag conversion code.
func (s *entrySuite) TestMountOptsToFlags(c *C) {
	flags, err := osutil.MountOptsToFlags(nil)
	c.Assert(err, IsNil)
	c.Assert(flags, Equals, 0)
	flags, err = osutil.MountOptsToFlags([]string{"ro", "nodev", "nosuid"})
	c.Assert(err, IsNil)
	c.Assert(flags, Equals, syscall.MS_RDONLY|syscall.MS_NODEV|syscall.MS_NOSUID)
	_, err = osutil.MountOptsToFlags([]string{"bogus"})
	c.Assert(err, ErrorMatches, `unsupported mount option: "bogus"`)
	// The x-snapd-prefix is reserved for non-kernel parameters that do not
	// translate to kernel level mount flags. This is similar to systemd or
	// udisks that use fstab options to convey additional data.
	flags, err = osutil.MountOptsToFlags([]string{"x-snapd.foo"})
	c.Assert(err, IsNil)
	c.Assert(flags, Equals, 0)
}

// Test (string) options -> (int, unparsed) flag conversion code.
func (s *entrySuite) TestMountOptsToCommonFlags(c *C) {
	flags, unparsed := osutil.MountOptsToCommonFlags(nil)
	c.Assert(flags, Equals, 0)
	c.Assert(unparsed, HasLen, 0)
	flags, unparsed = osutil.MountOptsToCommonFlags([]string{"ro", "nodev", "nosuid"})
	c.Assert(flags, Equals, syscall.MS_RDONLY|syscall.MS_NODEV|syscall.MS_NOSUID)
	c.Assert(unparsed, HasLen, 0)
	flags, unparsed = osutil.MountOptsToCommonFlags([]string{"bogus"})
	c.Assert(flags, Equals, 0)
	c.Assert(unparsed, DeepEquals, []string{"bogus"})
	// The x-snapd-prefix is reserved for non-kernel parameters that do not
	// translate to kernel level mount flags. This is similar to systemd or
	// udisks that use fstab options to convey additional data. Those are not
	// returned as "unparsed" as we don't want to pass them to the kernel.
	flags, unparsed = osutil.MountOptsToCommonFlags([]string{"x-snapd.foo"})
	c.Assert(flags, Equals, 0)
	c.Assert(unparsed, HasLen, 0)
	// The "rw" flag is recognized but doesn't translate to an actual value
	// since read-write is the implicit default and there are no kernel level
	// flags to express it.
	flags, unparsed = osutil.MountOptsToCommonFlags([]string{"rw"})
	c.Assert(flags, Equals, 0)
	c.Assert(unparsed, DeepEquals, []string(nil))
}

func (s *entrySuite) TestOptStr(c *C) {
	e := &osutil.MountEntry{Options: []string{"key=value"}}
	val, ok := e.OptStr("key")
	c.Assert(ok, Equals, true)
	c.Assert(val, Equals, "value")

	val, ok = e.OptStr("missing")
	c.Assert(ok, Equals, false)
	c.Assert(val, Equals, "")
}

func (s *entrySuite) TestOptBool(c *C) {
	e := &osutil.MountEntry{Options: []string{"key"}}
	val := e.OptBool("key")
	c.Assert(val, Equals, true)

	val = e.OptBool("missing")
	c.Assert(val, Equals, false)
}

func (s *entrySuite) TestOptionHelpers(c *C) {
	c.Assert(osutil.XSnapdUser(1000), Equals, "x-snapd.user=1000")
	c.Assert(osutil.XSnapdGroup(1000), Equals, "x-snapd.group=1000")
	c.Assert(osutil.XSnapdMode(0755), Equals, "x-snapd.mode=0755")
	c.Assert(osutil.XSnapdSymlink("oldname"), Equals, "x-snapd.symlink=oldname")
}

func (s *entrySuite) TestXSnapdMode(c *C) {
	// Mode has a default value.
	e := &osutil.MountEntry{}
	mode, err := e.XSnapdMode()
	c.Assert(err, IsNil)
	c.Assert(mode, Equals, os.FileMode(0755))

	// Mode is parsed from the x-snapd.mode= option.
	e = &osutil.MountEntry{Options: []string{"x-snapd.mode=0700"}}
	mode, err = e.XSnapdMode()
	c.Assert(err, IsNil)
	c.Assert(mode, Equals, os.FileMode(0700))

	// Empty value is invalid.
	e = &osutil.MountEntry{Options: []string{"x-snapd.mode="}}
	_, err = e.XSnapdMode()
	c.Assert(err, ErrorMatches, `cannot parse octal file mode from ""`)

	// As well as other bogus values.
	e = &osutil.MountEntry{Options: []string{"x-snapd.mode=pasta"}}
	_, err = e.XSnapdMode()
	c.Assert(err, ErrorMatches, `cannot parse octal file mode from "pasta"`)

	// And even valid values with trailing garbage.
	e = &osutil.MountEntry{Options: []string{"x-snapd.mode=0700pasta"}}
	mode, err = e.XSnapdMode()
	c.Assert(err, ErrorMatches, `cannot parse octal file mode from "0700pasta"`)
	c.Assert(mode, Equals, os.FileMode(0))
}

func (s *entrySuite) TestXSnapdUID(c *C) {
	// User has a default value.
	e := &osutil.MountEntry{}
	uid, err := e.XSnapdUID()
	c.Assert(err, IsNil)
	c.Assert(uid, Equals, uint64(0))

	// User is parsed from the x-snapd.uid= option.
	e = &osutil.MountEntry{Options: []string{"x-snapd.uid=root"}}
	uid, err = e.XSnapdUID()
	c.Assert(err, ErrorMatches, `cannot parse user name "root"`)
	c.Assert(uid, Equals, uint64(math.MaxUint64))

	// Numeric names are used as-is.
	e = &osutil.MountEntry{Options: []string{"x-snapd.uid=123"}}
	uid, err = e.XSnapdUID()
	c.Assert(err, IsNil)
	c.Assert(uid, Equals, uint64(123))

	// And even valid values with trailing garbage.
	e = &osutil.MountEntry{Options: []string{"x-snapd.uid=0bogus"}}
	uid, err = e.XSnapdUID()
	c.Assert(err, ErrorMatches, `cannot parse user name "0bogus"`)
	c.Assert(uid, Equals, uint64(math.MaxUint64))
}

func (s *entrySuite) TestXSnapdGID(c *C) {
	// Group has a default value.
	e := &osutil.MountEntry{}
	gid, err := e.XSnapdGID()
	c.Assert(err, IsNil)
	c.Assert(gid, Equals, uint64(0))

	e = &osutil.MountEntry{Options: []string{"x-snapd.gid=root"}}
	gid, err = e.XSnapdGID()
	c.Assert(err, ErrorMatches, `cannot parse group name "root"`)
	c.Assert(gid, Equals, uint64(math.MaxUint64))

	// Numeric names are used as-is.
	e = &osutil.MountEntry{Options: []string{"x-snapd.gid=456"}}
	gid, err = e.XSnapdGID()
	c.Assert(err, IsNil)
	c.Assert(gid, Equals, uint64(456))

	// And even valid values with trailing garbage.
	e = &osutil.MountEntry{Options: []string{"x-snapd.gid=0bogus"}}
	gid, err = e.XSnapdGID()
	c.Assert(err, ErrorMatches, `cannot parse group name "0bogus"`)
	c.Assert(gid, Equals, uint64(math.MaxUint64))
}

func (s *entrySuite) TestXSnapdEntryID(c *C) {
	// Entry ID is optional and defaults to the mount point.
	e := &osutil.MountEntry{Dir: "/foo"}
	c.Assert(e.XSnapdEntryID(), Equals, "/foo")

	// Entry ID is parsed from the x-snapd.id= option.
	e = &osutil.MountEntry{Dir: "/foo", Options: []string{"x-snapd.id=foo"}}
	c.Assert(e.XSnapdEntryID(), Equals, "foo")
}

func (s *entrySuite) TestXSnapdNeededBy(c *C) {
	// The needed-by attribute is optional.
	e := &osutil.MountEntry{}
	c.Assert(e.XSnapdNeededBy(), Equals, "")

	// The needed-by attribute parsed from the x-snapd.needed-by= option.
	e = &osutil.MountEntry{Options: []string{"x-snap.id=foo", "x-snapd.needed-by=bar"}}
	c.Assert(e.XSnapdNeededBy(), Equals, "bar")

	// There's a helper function that returns this option string.
	c.Assert(osutil.XSnapdNeededBy("foo"), Equals, "x-snapd.needed-by=foo")
}

func (s *entrySuite) TestXSnapdSynthetic(c *C) {
	// Entries are not synthetic unless tagged as such.
	e := &osutil.MountEntry{}
	c.Assert(e.XSnapdSynthetic(), Equals, false)

	// Tagging is done with x-snapd.synthetic option.
	e = &osutil.MountEntry{Options: []string{"x-snapd.synthetic"}}
	c.Assert(e.XSnapdSynthetic(), Equals, true)

	// There's a helper function that returns this option string.
	c.Assert(osutil.XSnapdSynthetic(), Equals, "x-snapd.synthetic")
}

func (s *entrySuite) TestXSnapdOrigin(c *C) {
	// Entries have no origin by default.
	e := &osutil.MountEntry{}
	c.Assert(e.XSnapdOrigin(), Equals, "")

	// Origin can be indicated with the x-snapd.origin= option.
	e = &osutil.MountEntry{Options: []string{osutil.XSnapdOriginLayout()}}
	c.Assert(e.XSnapdOrigin(), Equals, "layout")

	// There's a helper function that returns this option string.
	c.Assert(osutil.XSnapdOriginLayout(), Equals, "x-snapd.origin=layout")

	// Origin can also indicate a parallel snap instance setup
	e = &osutil.MountEntry{Options: []string{osutil.XSnapdOriginOvername()}}
	c.Assert(e.XSnapdOrigin(), Equals, "overname")
	c.Assert(osutil.XSnapdOriginOvername(), Equals, "x-snapd.origin=overname")
}

func (s *entrySuite) TestXSnapdDetach(c *C) {
	// Entries are not detached by default.
	e := &osutil.MountEntry{}
	c.Assert(e.XSnapdDetach(), Equals, false)

	// Detach can be requested with the x-snapd.detach option.
	e = &osutil.MountEntry{Options: []string{osutil.XSnapdDetach()}}
	c.Assert(e.XSnapdDetach(), Equals, true)

	// There's a helper function that returns this option string.
	c.Assert(osutil.XSnapdDetach(), Equals, "x-snapd.detach")
}

func (s *entrySuite) TestXSnapdKind(c *C) {
	// Entries have a kind (directory, file or symlink). Directory is spelled
	// as an empty string though, for backwards compatibility.
	e := &osutil.MountEntry{}
	c.Assert(e.XSnapdKind(), Equals, "")

	// A bind mount entry can refer to a file using the x-snapd.kind=file option string.
	e = &osutil.MountEntry{Options: []string{osutil.XSnapdKindFile()}}
	c.Assert(e.XSnapdKind(), Equals, "file")

	// There's a helper function that returns this option string.
	c.Assert(osutil.XSnapdKindFile(), Equals, "x-snapd.kind=file")

	// A mount entry can create a symlink by using the x-snapd.kind=symlink option string.
	e = &osutil.MountEntry{Options: []string{osutil.XSnapdKindSymlink()}}
	c.Assert(e.XSnapdKind(), Equals, "symlink")

	// There's a helper function that returns this option string.
	c.Assert(osutil.XSnapdKindSymlink(), Equals, "x-snapd.kind=symlink")
}

func (s *entrySuite) TestXSnapdSymlink(c *C) {
	// Entries without the x-snapd.symlink key return an empty string
	e := &osutil.MountEntry{}
	c.Assert(e.XSnapdSymlink(), Equals, "")

	// A mount entry can list a symlink target
	e = &osutil.MountEntry{Options: []string{osutil.XSnapdSymlink("target")}}
	c.Assert(e.XSnapdSymlink(), Equals, "target")
}

func (s *entrySuite) TestXSnapdIgnoreMissing(c *C) {
	// By default entries will not have the ignore missing flag set
	e := &osutil.MountEntry{}
	c.Assert(e.XSnapdIgnoreMissing(), Equals, false)

	// A mount entry can specify that it should be ignored if the
	// mount source or target are missing with the
	// x-snapd.ignore-missing option.
	e = &osutil.MountEntry{Options: []string{osutil.XSnapdIgnoreMissing()}}
	c.Assert(e.XSnapdIgnoreMissing(), Equals, true)

	// There's a helper function that returns this option string.
	c.Assert(osutil.XSnapdIgnoreMissing(), Equals, "x-snapd.ignore-missing")
}
