// Copyright 2018 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 gofer implements a remote 9p filesystem.
package gofer

import (
	"errors"
	"fmt"
	"strconv"

	"gvisor.dev/gvisor/pkg/context"
	"gvisor.dev/gvisor/pkg/p9"
	"gvisor.dev/gvisor/pkg/sentry/fs"
)

// The following are options defined by the Linux 9p client that we support,
// see Documentation/filesystems/9p.txt.
const (
	// The transport method.
	transportKey = "trans"

	// The file tree to access when the file server
	// is exporting several file systems. Stands for "attach name".
	anameKey = "aname"

	// The caching policy.
	cacheKey = "cache"

	// The file descriptor for reading with trans=fd.
	readFDKey = "rfdno"

	// The file descriptor for writing with trans=fd.
	writeFDKey = "wfdno"

	// The number of bytes to use for a 9p packet payload.
	msizeKey = "msize"

	// The 9p protocol version.
	versionKey = "version"

	// If set to true allows the creation of unix domain sockets inside the
	// sandbox using files backed by the gofer. If set to false, unix sockets
	// cannot be bound to gofer files without an overlay on top.
	privateUnixSocketKey = "privateunixsocket"

	// If present, sets CachingInodeOperationsOptions.LimitHostFDTranslation to
	// true.
	limitHostFDTranslationKey = "limit_host_fd_translation"

	// overlayfsStaleRead if present closes cached readonly file after the first
	// write. This is done to workaround a limitation of Linux overlayfs.
	overlayfsStaleRead = "overlayfs_stale_read"
)

// defaultAname is the default attach name.
const defaultAname = "/"

// defaultMSize is the message size used for chunking large read and write requests.
// This has been tested to give good enough performance up to 64M.
const defaultMSize = 1024 * 1024 // 1M

// defaultVersion is the default 9p protocol version. Will negotiate downwards with
// file server if needed.
var defaultVersion = p9.HighestVersionString()

// Number of names of non-children to cache, preventing unneeded walks.  64 is
// plenty for nodejs, which seems to stat about 4 children on every require().
const nonChildrenCacheSize = 64

var (
	// ErrNoTransport is returned when there is no 'trans' option.
	ErrNoTransport = errors.New("missing required option: 'trans='")

	// ErrFileNoReadFD is returned when there is no 'rfdno' option.
	ErrFileNoReadFD = errors.New("missing required option: 'rfdno='")

	// ErrFileNoWriteFD is returned when there is no 'wfdno' option.
	ErrFileNoWriteFD = errors.New("missing required option: 'wfdno='")
)

// filesystem is a 9p client.
//
// +stateify savable
type filesystem struct{}

var _ fs.Filesystem = (*filesystem)(nil)

func init() {
	fs.RegisterFilesystem(&filesystem{})
}

// FilesystemName is the name under which the filesystem is registered.
// The name matches fs/9p/vfs_super.c:v9fs_fs_type.name.
const FilesystemName = "9p"

// Name is the name of the filesystem.
func (*filesystem) Name() string {
	return FilesystemName
}

// AllowUserMount prohibits users from using mount(2) with this file system.
func (*filesystem) AllowUserMount() bool {
	return false
}

// AllowUserList allows this filesystem to be listed in /proc/filesystems.
func (*filesystem) AllowUserList() bool {
	return true
}

// Flags returns that there is nothing special about this file system.
//
// The 9p Linux client returns FS_RENAME_DOES_D_MOVE, see fs/9p/vfs_super.c.
func (*filesystem) Flags() fs.FilesystemFlags {
	return 0
}

// Mount returns an attached 9p client that can be positioned in the vfs.
func (f *filesystem) Mount(ctx context.Context, device string, flags fs.MountSourceFlags, data string, _ interface{}) (*fs.Inode, error) {
	// Parse and validate the mount options.
	o, err := options(data)
	if err != nil {
		return nil, err
	}

	// Construct the 9p root to mount. We intentionally diverge from Linux in that
	// the first Tversion and Tattach requests are done lazily.
	return Root(ctx, device, f, flags, o)
}

// opts are parsed 9p mount options.
type opts struct {
	fd                     int
	aname                  string
	policy                 cachePolicy
	msize                  uint32
	version                string
	privateunixsocket      bool
	limitHostFDTranslation bool
	overlayfsStaleRead     bool
}

// options parses mount(2) data into structured options.
func options(data string) (opts, error) {
	var o opts

	// Parse generic comma-separated key=value options, this file system expects them.
	options := fs.GenericMountSourceOptions(data)

	// Check for the required 'trans=fd' option.
	trans, ok := options[transportKey]
	if !ok {
		return o, ErrNoTransport
	}
	if trans != "fd" {
		return o, fmt.Errorf("unsupported transport: 'trans=%s'", trans)
	}
	delete(options, transportKey)

	// Check for the required 'rfdno=' option.
	srfd, ok := options[readFDKey]
	if !ok {
		return o, ErrFileNoReadFD
	}
	delete(options, readFDKey)

	// Check for the required 'wfdno=' option.
	swfd, ok := options[writeFDKey]
	if !ok {
		return o, ErrFileNoWriteFD
	}
	delete(options, writeFDKey)

	// Parse the read fd.
	rfd, err := strconv.Atoi(srfd)
	if err != nil {
		return o, fmt.Errorf("invalid fd for 'rfdno=%s': %v", srfd, err)
	}

	// Parse the write fd.
	wfd, err := strconv.Atoi(swfd)
	if err != nil {
		return o, fmt.Errorf("invalid fd for 'wfdno=%s': %v", swfd, err)
	}

	// Require that the read and write fd are the same.
	if rfd != wfd {
		return o, fmt.Errorf("fd in 'rfdno=%d' and 'wfdno=%d' must match", rfd, wfd)
	}
	o.fd = rfd

	// Parse the attach name.
	o.aname = defaultAname
	if an, ok := options[anameKey]; ok {
		o.aname = an
		delete(options, anameKey)
	}

	// Parse the cache policy. Reject unsupported policies.
	o.policy = cacheAll
	if policy, ok := options[cacheKey]; ok {
		cp, err := parseCachePolicy(policy)
		if err != nil {
			return o, err
		}
		o.policy = cp
		delete(options, cacheKey)
	}

	// Parse the message size. Reject malformed options.
	o.msize = uint32(defaultMSize)
	if m, ok := options[msizeKey]; ok {
		i, err := strconv.ParseUint(m, 10, 32)
		if err != nil {
			return o, fmt.Errorf("invalid message size for 'msize=%s': %v", m, err)
		}
		o.msize = uint32(i)
		delete(options, msizeKey)
	}

	// Parse the protocol version.
	o.version = defaultVersion
	if v, ok := options[versionKey]; ok {
		o.version = v
		delete(options, versionKey)
	}

	// Parse the unix socket policy. Reject non-booleans.
	if v, ok := options[privateUnixSocketKey]; ok {
		b, err := strconv.ParseBool(v)
		if err != nil {
			return o, fmt.Errorf("invalid boolean value for '%s=%s': %v", privateUnixSocketKey, v, err)
		}
		o.privateunixsocket = b
		delete(options, privateUnixSocketKey)
	}

	if _, ok := options[limitHostFDTranslationKey]; ok {
		o.limitHostFDTranslation = true
		delete(options, limitHostFDTranslationKey)
	}

	if _, ok := options[overlayfsStaleRead]; ok {
		o.overlayfsStaleRead = true
		delete(options, overlayfsStaleRead)
	}

	// Fail to attach if the caller wanted us to do something that we
	// don't support.
	if len(options) > 0 {
		return o, fmt.Errorf("unsupported mount options: %v", options)
	}

	return o, nil
}