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

/*
 * Copyright (C) 2016 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 (
	"os"
	"path/filepath"
	"strings"
	"syscall"

	. "gopkg.in/check.v1"

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

type nfsSuite struct{}

var _ = Suite(&nfsSuite{})

func (s *nfsSuite) TestIsHomeUsingRemoteFS(c *C) {
	cases := []struct {
		mountinfo, fstab string
		isRemoteFS       bool
		errorPattern     string
	}{{
		// Errors from parsing mountinfo and fstab are propagated.
		mountinfo:    "bad syntax",
		errorPattern: "cannot parse mountinfo:.*, .*",
	}, {
		fstab:        "bad syntax",
		errorPattern: "cannot parse .*/fstab.*, .*",
	}, {
		// NFSv3 {tcp,udp} and NFSv4 currently mounted at /home/zyga/nfs are recognized.
		mountinfo:  "1074 28 0:59 / /home/zyga/nfs rw,relatime shared:342 - nfs localhost:/srv/nfs rw,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=127.0.0.1,mountvers=3,mountport=54125,mountproto=tcp,local_lock=none,addr=127.0.0.1",
		isRemoteFS: true,
	}, {
		mountinfo:  "1074 28 0:59 / /home/zyga/nfs rw,relatime shared:342 - nfs localhost:/srv/nfs rw,vers=3,rsize=32768,wsize=32768,namlen=255,hard,proto=udp,timeo=11,retrans=3,sec=sys,mountaddr=127.0.0.1,mountvers=3,mountport=47875,mountproto=udp,local_lock=none,addr=127.0.0.1",
		isRemoteFS: true,
	}, {
		mountinfo:  "680 27 0:59 / /home/zyga/nfs rw,relatime shared:478 - nfs4 localhost:/srv/nfs rw,vers=4.2,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=127.0.0.1,local_lock=none,addr=127.0.0.1",
		isRemoteFS: true,
	}, {
		// NFSv3 {tcp,udp} and NFSv4 currently mounted at /home/zyga/nfs are ignored (not in $HOME).
		mountinfo: "1074 28 0:59 / /mnt/nfs rw,relatime shared:342 - nfs localhost:/srv/nfs rw,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=127.0.0.1,mountvers=3,mountport=54125,mountproto=tcp,local_lock=none,addr=127.0.0.1",
	}, {
		mountinfo: "1074 28 0:59 / /mnt/nfs rw,relatime shared:342 - nfs localhost:/srv/nfs rw,vers=3,rsize=32768,wsize=32768,namlen=255,hard,proto=udp,timeo=11,retrans=3,sec=sys,mountaddr=127.0.0.1,mountvers=3,mountport=47875,mountproto=udp,local_lock=none,addr=127.0.0.1",
	}, {
		mountinfo: "680 27 0:59 / /mnt/nfs rw,relatime shared:478 - nfs4 localhost:/srv/nfs rw,vers=4.2,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=127.0.0.1,local_lock=none,addr=127.0.0.1",
	}, {
		// NFS that may be mounted at /home and /home/zyga/nfs is recognized.
		// Two spellings are possible, "nfs" and "nfs4" (they are equivalent
		// nowadays).
		fstab:      "localhost:/srv/nfs /home nfs defaults 0 0",
		isRemoteFS: true,
	}, {
		fstab:      "localhost:/srv/nfs /home nfs4 defaults 0 0",
		isRemoteFS: true,
	}, {
		fstab:      "localhost:/srv/nfs /home/zyga/nfs nfs defaults 0 0",
		isRemoteFS: true,
	}, {
		fstab:      "localhost:/srv/nfs /home/zyga/nfs nfs4 defaults 0 0",
		isRemoteFS: true,
	}, {
		// NFS that may be mounted at /mnt/nfs is ignored (not in $HOME).
		fstab: "localhost:/srv/nfs /mnt/nfs nfs defaults 0 0",
	}, {
		// autofs that is mounted at /home.
		mountinfo:  "137 29 0:50 / /home rw,relatime shared:87 - autofs /etc/auto.master.d/home rw,fd=7,pgrp=22588,timeout=300,minproto=5,maxproto=5,indirect,pipe_ino=173399",
		isRemoteFS: true,
	}, {
		// cifs that is mounted at /home
		// This is not real data, it is made-up.
		mountinfo:  "0 0 0:0 / /home rw,relatime shared:0 - cifs //sub.example.org/path$/all-users irrelevant-options",
		isRemoteFS: true,
	}, {
		// cifs that is mounted at /home/$USERNAME
		// This is not real data, it is made-up.
		mountinfo:  "0 0 0:0 / /home/some-user rw,relatime shared:0 - cifs //sub.example.org/path$/some-user irrelevant-options",
		isRemoteFS: true,
	}}
	for _, tc := range cases {
		restore := osutil.MockMountInfo(tc.mountinfo)
		defer restore()
		restore = osutil.MockEtcFstab(tc.fstab)
		defer restore()

		isRemoteFS, err := osutil.IsHomeUsingRemoteFS()
		if tc.errorPattern != "" {
			c.Assert(err, ErrorMatches, tc.errorPattern, Commentf("test case %#v", tc))
		} else {
			c.Assert(err, IsNil)
		}
		c.Assert(isRemoteFS, Equals, tc.isRemoteFS)
	}
}

func (s *nfsSuite) TestSnapDirsUnderNFSMounts(c *C) {
	restore := osutil.MockSyscallStatfs(func(path string, statfs *syscall.Statfs_t) error {
		dir, _ := filepath.Split(path)
		_, fs := filepath.Split(strings.TrimSuffix(dir, "/"))

		// statfs.Type is arch dependent:
		// https://go.googlesource.com/go/+/refs/heads/dev.boringcrypto.go1.16/src/syscall/ztypes_linux_<arch>.go
		switch fs {
		case "autofs":
			statfs.Type = 0x0187
		case "nfs":
			statfs.Type = 0x6969
		case "cifs":
			// CIFS_MAGIC_NUMBER 0xFF534D42 does not fit int32 (armhf, s390x). For this test focused on NFS,
			// rather use SMB_SUPER_MAGIC as a stable non-NFS network filesystem value that fits statfs.Type
			// on both 32-bit and 64-bit arches.
			statfs.Type = 0x517B
		default:
			c.Fatalf("unknown filesystem %q", fs)
		}

		return nil
	})
	defer restore()

	dirPath := c.MkDir()
	restore = osutil.MockAllDataHomeGlobs(func() []string {
		return []string{filepath.Join(dirPath, "*", "snap")}
	})
	defer restore()

	types := []string{"autofs", "nfs", "cifs"}

	for _, typ := range types {
		cmt := Commentf("testcase %q", typ)

		fsDir := filepath.Join(dirPath, typ)
		err := os.MkdirAll(filepath.Join(fsDir, "snap"), 0755)
		c.Assert(err, IsNil, cmt)

		res, err := osutil.SnapDirsUnderNFSMounts()
		c.Assert(err, IsNil, cmt)
		c.Assert(res, Equals, typ == "nfs")

		err = os.RemoveAll(fsDir)
		c.Assert(err, IsNil)
	}
}
