// Copyright 2020 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 fsbridge

import (
	"io"
	"strings"

	"gvisor.dev/gvisor/pkg/abi/linux"
	"gvisor.dev/gvisor/pkg/context"
	"gvisor.dev/gvisor/pkg/sentry/fs"
	"gvisor.dev/gvisor/pkg/sentry/memmap"
	"gvisor.dev/gvisor/pkg/sentry/vfs"
	"gvisor.dev/gvisor/pkg/syserror"
	"gvisor.dev/gvisor/pkg/usermem"
)

// fsFile implements File interface over fs.File.
//
// +stateify savable
type fsFile struct {
	file *fs.File
}

var _ File = (*fsFile)(nil)

// NewFSFile creates a new File over fs.File.
func NewFSFile(file *fs.File) File {
	return &fsFile{file: file}
}

// PathnameWithDeleted implements File.
func (f *fsFile) PathnameWithDeleted(ctx context.Context) string {
	root := fs.RootFromContext(ctx)
	if root == nil {
		// This doesn't correspond to anything in Linux because the vfs is
		// global there.
		return ""
	}
	defer root.DecRef()

	name, _ := f.file.Dirent.FullName(root)
	return name
}

// ReadFull implements File.
func (f *fsFile) ReadFull(ctx context.Context, dst usermem.IOSequence, offset int64) (int64, error) {
	var total int64
	for dst.NumBytes() > 0 {
		n, err := f.file.Preadv(ctx, dst, offset+total)
		total += n
		if err == io.EOF && total != 0 {
			return total, io.ErrUnexpectedEOF
		} else if err != nil {
			return total, err
		}
		dst = dst.DropFirst64(n)
	}
	return total, nil
}

// ConfigureMMap implements File.
func (f *fsFile) ConfigureMMap(ctx context.Context, opts *memmap.MMapOpts) error {
	return f.file.ConfigureMMap(ctx, opts)
}

// Type implements File.
func (f *fsFile) Type(context.Context) (linux.FileMode, error) {
	return linux.FileMode(f.file.Dirent.Inode.StableAttr.Type.LinuxType()), nil
}

// IncRef implements File.
func (f *fsFile) IncRef() {
	f.file.IncRef()
}

// DecRef implements File.
func (f *fsFile) DecRef() {
	f.file.DecRef()
}

// fsLookup implements Lookup interface using fs.File.
//
// +stateify savable
type fsLookup struct {
	mntns *fs.MountNamespace

	root       *fs.Dirent
	workingDir *fs.Dirent
}

var _ Lookup = (*fsLookup)(nil)

// NewFSLookup creates a new Lookup using VFS1.
func NewFSLookup(mntns *fs.MountNamespace, root, workingDir *fs.Dirent) Lookup {
	return &fsLookup{
		mntns:      mntns,
		root:       root,
		workingDir: workingDir,
	}
}

// OpenPath implements Lookup.
func (l *fsLookup) OpenPath(ctx context.Context, path string, opts vfs.OpenOptions, remainingTraversals *uint, resolveFinal bool) (File, error) {
	var d *fs.Dirent
	var err error
	if resolveFinal {
		d, err = l.mntns.FindInode(ctx, l.root, l.workingDir, path, remainingTraversals)
	} else {
		d, err = l.mntns.FindLink(ctx, l.root, l.workingDir, path, remainingTraversals)
	}
	if err != nil {
		return nil, err
	}
	defer d.DecRef()

	if !resolveFinal && fs.IsSymlink(d.Inode.StableAttr) {
		return nil, syserror.ELOOP
	}

	fsPerm := openOptionsToPermMask(&opts)
	if err := d.Inode.CheckPermission(ctx, fsPerm); err != nil {
		return nil, err
	}

	// If they claim it's a directory, then make sure.
	if strings.HasSuffix(path, "/") {
		if d.Inode.StableAttr.Type != fs.Directory {
			return nil, syserror.ENOTDIR
		}
	}

	if opts.FileExec && d.Inode.StableAttr.Type != fs.RegularFile {
		ctx.Infof("%q is not a regular file: %v", path, d.Inode.StableAttr.Type)
		return nil, syserror.EACCES
	}

	f, err := d.Inode.GetFile(ctx, d, flagsToFileFlags(opts.Flags))
	if err != nil {
		return nil, err
	}

	return &fsFile{file: f}, nil
}

func openOptionsToPermMask(opts *vfs.OpenOptions) fs.PermMask {
	mode := opts.Flags & linux.O_ACCMODE
	return fs.PermMask{
		Read:    mode == linux.O_RDONLY || mode == linux.O_RDWR,
		Write:   mode == linux.O_WRONLY || mode == linux.O_RDWR,
		Execute: opts.FileExec,
	}
}

func flagsToFileFlags(flags uint32) fs.FileFlags {
	return fs.FileFlags{
		Direct:      flags&linux.O_DIRECT != 0,
		DSync:       flags&(linux.O_DSYNC|linux.O_SYNC) != 0,
		Sync:        flags&linux.O_SYNC != 0,
		NonBlocking: flags&linux.O_NONBLOCK != 0,
		Read:        (flags & linux.O_ACCMODE) != linux.O_WRONLY,
		Write:       (flags & linux.O_ACCMODE) != linux.O_RDONLY,
		Append:      flags&linux.O_APPEND != 0,
		Directory:   flags&linux.O_DIRECTORY != 0,
		Async:       flags&linux.O_ASYNC != 0,
		LargeFile:   flags&linux.O_LARGEFILE != 0,
		Truncate:    flags&linux.O_TRUNC != 0,
	}
}