// Copyright 2021 The gVisor Authors.
//
// 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 control

import (
	"fmt"
	"io"
	"os"
	"path"
	"strings"

	"golang.org/x/sys/unix"
	"gvisor.dev/gvisor/pkg/abi/linux"
	"gvisor.dev/gvisor/pkg/cleanup"
	"gvisor.dev/gvisor/pkg/context"
	"gvisor.dev/gvisor/pkg/fspath"
	"gvisor.dev/gvisor/pkg/sentry/kernel"
	"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
	"gvisor.dev/gvisor/pkg/sentry/vfs"
	"gvisor.dev/gvisor/pkg/urpc"
	"gvisor.dev/gvisor/pkg/usermem"
)

// Fs includes fs-related functions.
type Fs struct {
	Kernel *kernel.Kernel
}

// TarRootfsUpperLayerOpts contains options for the TarRootfsUpperLayer RPC.
type TarRootfsUpperLayerOpts struct {
	// ContainerID identifies which container's rootfs upper layer should be
	// serialized.
	ContainerID string
	// FilePayload contains the destination for output.
	urpc.FilePayload
}

// Returns a referenced mount namespace for the given container ID,
// or the root container if no ID is provided. Caller must DecRef the
// returned mntns when done.
func (f *Fs) mountNamespaceForContainer(containerID string) (*vfs.MountNamespace, error) {
	leader, err := findContainerInitProcess(f.Kernel, containerID)
	if err != nil {
		return nil, err
	}
	mntns := leader.MountNamespace()
	if mntns == nil || !mntns.TryIncRef() {
		return nil, fmt.Errorf("mount namespace for container %s has been destroyed", containerID)
	}
	return mntns, nil
}

// TarRootfsUpperLayer is a RPC stub which serializes the rootfs upper layer to
// a tar file. When the rootfs is not an overlayfs, it returns an error.
func (f *Fs) TarRootfsUpperLayer(o *TarRootfsUpperLayerOpts, _ *struct{}) error {
	if len(o.FilePayload.Files) != 1 {
		return ErrInvalidFiles
	}
	outFD := o.FilePayload.Files[0]
	defer outFD.Close()

	ctx := f.Kernel.SupervisorContext()
	mntns, err := f.mountNamespaceForContainer(o.ContainerID)
	if err != nil {
		return err
	}
	defer mntns.DecRef(ctx)

	root := mntns.Root(ctx)
	defer root.DecRef(ctx)
	ts, ok := root.Mount().Filesystem().Impl().(vfs.TarSerializer)
	if !ok {
		return fmt.Errorf("rootfs is not an overlayfs")
	}
	if err := ts.TarUpperLayer(ctx, outFD); err != nil {
		return fmt.Errorf("failed to serialize rootfs upper layer to tar: %v", err)
	}
	return nil
}

// CatOpts contains options for the Cat RPC call.
type CatOpts struct {
	// Files are the filesystem paths for the files to cat.
	Files []string `json:"files"`

	// FilePayload contains the destination for output.
	urpc.FilePayload
}

// Cat is a RPC stub which prints out and returns the content of the files.
func (f *Fs) Cat(o *CatOpts, _ *struct{}) error {
	// Create an output stream.
	if len(o.FilePayload.Files) != 1 {
		return ErrInvalidFiles
	}

	output := o.FilePayload.Files[0]
	for _, file := range o.Files {
		if err := cat(f.Kernel, file, output); err != nil {
			return fmt.Errorf("cannot read from file %s: %v", file, err)
		}
	}

	return nil
}

// fdReader provides an io.Reader interface for a vfs.FileDescription.
type fdReader struct {
	ctx    context.Context
	fd     *vfs.FileDescription
	offset int64
}

// Read implements io.Reader.Read.
func (f *fdReader) Read(p []byte) (int, error) {
	if f.offset == 0 {
		n, err := f.fd.Read(f.ctx, usermem.BytesIOSequence(p), vfs.ReadOptions{})
		return int(n), err
	}

	// PRead reads from the explicit byte offset f.offset without modifying the
	// file description's internal offset.
	n, err := f.fd.PRead(f.ctx, usermem.BytesIOSequence(p), f.offset, vfs.ReadOptions{})
	f.offset += n
	return int(n), err
}

func cat(k *kernel.Kernel, path string, output *os.File) error {
	ctx := k.SupervisorContext()
	creds := auth.NewRootCredentials(k.RootUserNamespace())
	mns := k.GlobalInit().Leader().MountNamespace()
	root := mns.Root(ctx)
	defer root.DecRef(ctx)

	fd, err := k.VFS().OpenAt(ctx, creds, &vfs.PathOperation{
		Root:  root,
		Start: root,
		Path:  fspath.Parse(path),
	}, &vfs.OpenOptions{
		Flags: linux.O_RDONLY,
	})
	if err != nil {
		return fmt.Errorf("failed to open file %s: %v", path, err)
	}
	defer fd.DecRef(ctx)

	_, err = io.Copy(output, &fdReader{ctx: ctx, fd: fd})
	return err
}

// ReadOpts contains options for the Read RPC call.
type ReadOpts struct {
	// ContainerID identifies which container's filesystem to read from.
	ContainerID string `json:"container_id"`

	// Path is the filesystem path for the file to read.
	Path string `json:"path"`

	// Offset is the byte offset in the file to read from.
	Offset int64 `json:"offset"`

	// Size is the maximum number of bytes to read (0 means unlimited).
	Size int64 `json:"size"`

	// FilePayload contains the destination for output.
	urpc.FilePayload
}

// Read is a RPC stub which prints out and returns the content of the file up to the specified size.
func (f *Fs) Read(o *ReadOpts, _ *struct{}) error {
	if len(o.FilePayload.Files) != 1 {
		return ErrInvalidFiles
	}

	output := o.FilePayload.Files[0]
	ctx := f.Kernel.SupervisorContext()
	mntns, err := f.mountNamespaceForContainer(o.ContainerID)
	if err != nil {
		return err
	}
	defer mntns.DecRef(ctx)

	creds := auth.NewRootCredentials(f.Kernel.RootUserNamespace())
	root := mntns.Root(ctx)
	defer root.DecRef(ctx)

	fd, err := f.Kernel.VFS().OpenAt(ctx, creds, &vfs.PathOperation{
		Root:  root,
		Start: root,
		Path:  fspath.Parse(o.Path),
	}, &vfs.OpenOptions{
		Flags: linux.O_RDONLY,
	})
	if err != nil {
		return fmt.Errorf("failed to open file %s: %v", o.Path, err)
	}
	defer fd.DecRef(ctx)

	reader := &fdReader{ctx: ctx, fd: fd, offset: o.Offset}
	if o.Size > 0 {
		_, err = io.Copy(output, io.LimitReader(reader, o.Size))
	} else {
		_, err = io.Copy(output, reader)
	}
	return err
}

// MountOpts contains options for the Mount RPC call.
type MountOpts struct {
	// ContainerID identifies which container's mount namespace to mount into.
	ContainerID string `json:"container_id"`

	// Source is the mount source (e.g. device name or host path).
	Source string `json:"source"`

	// Target is the absolute path inside the container where the filesystem
	// will be mounted.
	Target string `json:"target"`

	// FSType is the filesystem type name (e.g., "gofer", "tmpfs").
	FSType string `json:"fs_type"`

	// Flags are the Linux mount flags (e.g. MS_RDONLY, MS_NOEXEC).
	Flags uint64 `json:"flags"`

	// Data contains filesystem-specific mount options (e.g. "cache=remote,aname=/").
	Data string `json:"data"`

	// FilePayload contains file descriptors sent over SCM_RIGHTS (e.g. socket FD to Gofer).
	urpc.FilePayload
}

// Mount is a RPC stub which mounts a filesystem into a container's mount namespace.
func (f *Fs) Mount(o *MountOpts, _ *struct{}) error {
	target := path.Clean(o.Target)
	if target == "" || !path.IsAbs(target) {
		return fmt.Errorf("target must be an absolute path: %q", o.Target)
	}

	supportedFlags := uint64(linux.MS_RDONLY | linux.MS_NOEXEC | linux.MS_NODEV | linux.MS_NOSUID | linux.MS_NOATIME)
	if (o.Flags & ^supportedFlags) != 0 {
		return unix.EINVAL
	}

	// Close all payload files upon return.
	defer func() {
		for _, file := range o.FilePayload.Files {
			file.Close()
		}
	}()

	var cu cleanup.Cleanup
	defer cu.Clean()

	ctx := f.Kernel.SupervisorContext()
	mntns, err := f.mountNamespaceForContainer(o.ContainerID)
	if err != nil {
		return err
	}
	defer mntns.DecRef(ctx)

	creds := auth.NewRootCredentials(f.Kernel.RootUserNamespace())
	root := mntns.Root(ctx)
	defer root.DecRef(ctx)

	fsType := o.FSType
	if fsType == "gofer" {
		fsType = "9p"
	}

	data := o.Data
	goferFD := -1
	// For 9p/gofer mounts, we receive the host connection FD in FilePayload.
	// We must duplicate this FD because the incoming FD will be closed when the
	// RPC returns (via FilePayload cleanup), but the VFS mount needs to keep
	// the connection open.
	if len(o.FilePayload.Files) > 0 && fsType == "9p" {
		fd := int(o.FilePayload.Files[0].Fd())
		dupFD, err := unix.Dup(fd)
		if err != nil {
			return fmt.Errorf("failed to dup gofer FD: %v", err)
		}
		goferFD = dupFD
		cu.Add(func() { _ = unix.Close(goferFD) })
		// Append the duped FD to mount options if not already specified.
		if !strings.Contains(data, "trans=fd") {
			if len(data) > 0 {
				data += ","
			}
			data += fmt.Sprintf("trans=fd,rfdno=%d,wfdno=%d", goferFD, goferFD)
		}
	}

	// Ensure the parent directory exists. We do not automatically create
	// parent directories to match standard Linux mount behavior where the
	// target mount point's parent must exist.
	parent := path.Dir(target)
	parentPop := &vfs.PathOperation{
		Root:               root,
		Start:              root,
		Path:               fspath.Parse(parent),
		FollowFinalSymlink: true,
	}
	if stat, err := f.Kernel.VFS().StatAt(ctx, creds, parentPop, &vfs.StatOptions{Mask: linux.STATX_TYPE}); err != nil {
		return fmt.Errorf("parent directory %s does not exist: %w", parent, err)
	} else if stat.Mask&linux.STATX_TYPE == 0 || stat.Mode&linux.FileTypeMask != linux.ModeDirectory {
		return fmt.Errorf("parent %s is not a directory", parent)
	}

	// Ensure target directory exists (or create it if parent is writeable).
	if err := f.Kernel.VFS().MkdirAllAt(ctx, target, root, creds, &vfs.MkdirOptions{Mode: 0755, ForSyntheticMountpoint: true}, true /* mustBeDir */); err != nil {
		return fmt.Errorf("failed to ensure target directory %s exists: %v", target, err)
	}

	opts := &vfs.MountOptions{
		ReadOnly: (o.Flags & linux.MS_RDONLY) != 0,
		Flags: vfs.MountFlags{
			NoExec:  (o.Flags & linux.MS_NOEXEC) != 0,
			NoDev:   (o.Flags & linux.MS_NODEV) != 0,
			NoSUID:  (o.Flags & linux.MS_NOSUID) != 0,
			NoATime: (o.Flags & linux.MS_NOATIME) != 0,
		},
		GetFilesystemOptions: vfs.GetFilesystemOptions{
			Data:          data,
			InternalMount: true,
		},
	}

	pop := &vfs.PathOperation{
		Root:               root,
		Start:              root,
		Path:               fspath.Parse(target),
		FollowFinalSymlink: true,
	}

	if _, err := f.Kernel.VFS().MountAt(ctx, creds, o.Source, pop, fsType, opts); err != nil {
		return fmt.Errorf("failed to mount %s at %s: %v", o.FSType, target, err)
	}

	cu.Release()
	return nil
}

// UmountOpts contains options for the Umount RPC call.
type UmountOpts struct {
	// ContainerID identifies which container's mount namespace to unmount from.
	ContainerID string `json:"container_id"`

	// Target is the absolute path inside the container to unmount.
	Target string `json:"target"`

	// Flags are Linux umount2 flags (e.g. MNT_DETACH).
	Flags uint32 `json:"flags"`
}

// Umount is a RPC stub which unmounts a filesystem from a container's mount namespace.
func (f *Fs) Umount(o *UmountOpts, _ *struct{}) error {
	target := path.Clean(o.Target)
	if target == "" || !path.IsAbs(target) {
		return fmt.Errorf("target must be an absolute path: %q", o.Target)
	}

	ctx := f.Kernel.SupervisorContext()
	mntns, err := f.mountNamespaceForContainer(o.ContainerID)
	if err != nil {
		return err
	}
	defer mntns.DecRef(ctx)

	creds := auth.NewRootCredentials(f.Kernel.RootUserNamespace())
	root := mntns.Root(ctx)
	defer root.DecRef(ctx)

	pop := &vfs.PathOperation{
		Root:               root,
		Start:              root,
		Path:               fspath.Parse(target),
		FollowFinalSymlink: (o.Flags & linux.UMOUNT_NOFOLLOW) == 0,
	}

	opts := &vfs.UmountOptions{
		Flags: o.Flags,
	}

	if err := f.Kernel.VFS().UmountAt(ctx, creds, pop, opts); err != nil {
		return fmt.Errorf("failed to unmount %s: %v", target, err)
	}

	return nil
}
