// 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 devpts

import (
	"strconv"

	"gvisor.dev/gvisor/pkg/abi/linux"
	"gvisor.dev/gvisor/pkg/context"
	"gvisor.dev/gvisor/pkg/errors/linuxerr"
	"gvisor.dev/gvisor/pkg/fspath"
	"gvisor.dev/gvisor/pkg/marshal/primitive"
	"gvisor.dev/gvisor/pkg/sentry/arch"
	"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
	"gvisor.dev/gvisor/pkg/sentry/kernel"
	"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
	"gvisor.dev/gvisor/pkg/sentry/unimpl"
	"gvisor.dev/gvisor/pkg/sentry/vfs"
	"gvisor.dev/gvisor/pkg/usermem"
	"gvisor.dev/gvisor/pkg/waiter"
)

// masterInode is the inode for the master end of the Terminal.
//
// +stateify savable
type masterInode struct {
	implStatFS
	kernfs.InodeAttrs
	kernfs.InodeNoopRefCount
	kernfs.InodeNotAnonymous
	kernfs.InodeNotDirectory
	kernfs.InodeNotSymlink
	kernfs.InodeFSOwned
	kernfs.InodeWatches

	locks vfs.FileLocks

	// root is the devpts root inode.
	root *rootInode
}

var _ kernfs.Inode = (*masterInode)(nil)

// Open implements kernfs.Inode.Open.
func (mi *masterInode) Open(ctx context.Context, rp *vfs.ResolvingPath, d *kernfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) {
	t, err := mi.root.allocateTerminal(ctx, rp.Credentials())
	if err != nil {
		return nil, err
	}

	fd := &masterFileDescription{
		inode: mi,
		t:     t,
	}
	fd.LockFD.Init(&mi.locks)
	if err := fd.vfsfd.Init(fd, opts.Flags, rp.Credentials(), rp.Mount(), d.VFSDentry(), &vfs.FileDescriptionOptions{
		SpecialFile: true,
	}); err != nil {
		return nil, err
	}
	return &fd.vfsfd, nil
}

// Stat implements kernfs.Inode.Stat.
func (mi *masterInode) Stat(ctx context.Context, vfsfs *vfs.Filesystem, opts vfs.StatOptions) (linux.Statx, error) {
	statx, err := mi.InodeAttrs.Stat(ctx, vfsfs, opts)
	if err != nil {
		return linux.Statx{}, err
	}
	statx.Blksize = 1024
	statx.RdevMajor = linux.TTYAUX_MAJOR
	statx.RdevMinor = linux.PTMX_MINOR
	return statx, nil
}

// SetStat implements kernfs.Inode.SetStat
func (mi *masterInode) SetStat(ctx context.Context, vfsfs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions) error {
	if opts.Stat.Mask&linux.STATX_SIZE != 0 {
		return linuxerr.EINVAL
	}
	return mi.InodeAttrs.SetStat(ctx, vfsfs, creds, opts)
}

// +stateify savable
type masterFileDescription struct {
	vfsfd vfs.FileDescription
	vfs.FileDescriptionDefaultImpl
	vfs.LockFD

	inode *masterInode
	t     *Terminal
}

var _ vfs.FileDescriptionImpl = (*masterFileDescription)(nil)

// Release implements vfs.FileDescriptionImpl.Release.
func (mfd *masterFileDescription) Release(ctx context.Context) {
	mfd.inode.root.masterClose(ctx, mfd.t)
}

// EventRegister implements waiter.Waitable.EventRegister.
func (mfd *masterFileDescription) EventRegister(e *waiter.Entry) error {
	mfd.t.ld.masterWaiter.EventRegister(e)
	return nil
}

// EventUnregister implements waiter.Waitable.EventUnregister.
func (mfd *masterFileDescription) EventUnregister(e *waiter.Entry) {
	mfd.t.ld.masterWaiter.EventUnregister(e)
}

// Readiness implements waiter.Waitable.Readiness.
func (mfd *masterFileDescription) Readiness(mask waiter.EventMask) waiter.EventMask {
	return mfd.t.ld.masterReadiness()
}

// Epollable implements FileDescriptionImpl.Epollable.
func (mfd *masterFileDescription) Epollable() bool {
	return true
}

// Read implements vfs.FileDescriptionImpl.Read.
func (mfd *masterFileDescription) Read(ctx context.Context, dst usermem.IOSequence, _ vfs.ReadOptions) (int64, error) {
	return mfd.t.ld.outputQueueRead(ctx, dst)
}

// Write implements vfs.FileDescriptionImpl.Write.
func (mfd *masterFileDescription) Write(ctx context.Context, src usermem.IOSequence, _ vfs.WriteOptions) (int64, error) {
	return mfd.t.ld.inputQueueWrite(ctx, src)
}

// Ioctl implements vfs.FileDescriptionImpl.Ioctl.
func (mfd *masterFileDescription) Ioctl(ctx context.Context, io usermem.IO, sysno uintptr, args arch.SyscallArguments) (uintptr, error) {
	t := kernel.TaskFromContext(ctx)
	if t == nil {
		// ioctl(2) may only be called from a task goroutine.
		return 0, linuxerr.ENOTTY
	}

	switch cmd := args[1].Uint(); cmd {
	case linux.FIONREAD: // linux.FIONREAD == linux.TIOCINQ
		// Get the number of bytes in the output queue read buffer.
		return 0, mfd.t.ld.outputQueueReadSize(t, io, args)
	case linux.TCGETS:
		// N.B. TCGETS on the master actually returns the configuration
		// of the replica end.
		return mfd.t.ld.getTermios(t, args)
	case linux.TCGETS2:
		return mfd.t.ld.getTermios2(t, args)
	case linux.TCSETS,
		linux.TCSETSW,
		linux.TCSETSF:
		// N.B. TCSETS on the master actually affects the configuration
		// of the replica end.
		//
		// Note that TCSETSW and TCSETSF should drain the output queue
		// first (and flush input for F), but we don't implement that
		// yet.
		return mfd.t.ld.setTermios(t, args)
	case linux.TCSETS2,
		linux.TCSETSW2,
		linux.TCSETSF2:
		// N.B. TCSETS2 on the master actually affects the configuration
		// of the replica end.
		//
		// Note that TCSETSW2 and TCSETSF2 should drain the output queue
		// first (and flush input for F), but we don't implement that
		// yet.
		return mfd.t.ld.setTermios2(t, args)
	case linux.TCSBRK:
		// TCSBRK with arg != 0 is tcdrain, which waits for output to
		// drain. For a pty with no real hardware, this is a no-op.
		// TCSBRK with arg == 0 sends a break, also a no-op for ptys.
		return 0, nil
	case linux.TCFLSH:
		return 0, mfd.t.ld.tcFlush(masterEndpoint, args[2].Uint())
	case linux.TIOCGPTN:
		nP := primitive.Uint32(mfd.t.n)
		_, err := nP.CopyOut(t, args[2].Pointer())
		return 0, err
	case linux.TIOCSPTLCK:
		// For now just pretend we implement pty locking.
		return 0, nil
	case linux.TIOCGWINSZ:
		return 0, mfd.t.ld.windowSize(t, args)
	case linux.TIOCSWINSZ:
		return 0, mfd.t.ld.setWindowSize(t, args)
	case linux.TIOCSCTTY:
		// Make the given terminal the controlling terminal of the
		// calling process.
		steal := args[2].Int() == 1
		return 0, t.ThreadGroup().SetControllingTTY(ctx, mfd.t.masterKTTY, steal, mfd.vfsfd.IsReadable())
	case linux.TIOCNOTTY:
		// Release this process's controlling terminal.
		return 0, t.ThreadGroup().ReleaseControllingTTY(mfd.t.masterKTTY)
	case linux.TIOCGPGRP:
		// Get the foreground process group id.
		pgid, err := t.ThreadGroup().ForegroundProcessGroupID(mfd.t.masterKTTY)
		if err != nil {
			return 0, err
		}
		ret := primitive.Int32(pgid)
		_, err = ret.CopyOut(t, args[2].Pointer())
		return 0, err
	case linux.TIOCSPGRP:
		// Set the foreground process group id.
		var pgid primitive.Int32
		if _, err := pgid.CopyIn(t, args[2].Pointer()); err != nil {
			return 0, err
		}
		return 0, t.ThreadGroup().SetForegroundProcessGroupID(ctx, mfd.t.masterKTTY, kernel.ProcessGroupID(pgid))
	case linux.TIOCPKT:
		// Enable or disable packet mode.
		var mode primitive.Int32
		if _, err := mode.CopyIn(t, args[2].Pointer()); err != nil {
			return 0, err
		}
		mfd.t.ld.setPacketMode(int(mode))
		return 0, nil
	case linux.TIOCGPKT:
		// Get packet mode.
		mode := mfd.t.ld.getPacketMode()
		ret := primitive.Int32(mode)
		_, err := ret.CopyOut(t, args[2].Pointer())
		return 0, err
	case linux.TIOCGPTPEER:
		flags := args[2].Uint()
		n := mfd.t.n
		masterMnt := mfd.vfsfd.Mount()

		// devpts files should always be in kernfs mounts on gVisor, but better safe than sorry.
		if _, ok := masterMnt.Root().Impl().(*kernfs.Dentry); !ok {
			return 0, linuxerr.EINVAL
		}

		// Lookup the replica's dentry.
		// Since devpts can't be written to, userspace shouldn't be able to replace the file underneath us.
		rootVD := vfs.MakeVirtualDentry(masterMnt, masterMnt.Root())
		pop := vfs.PathOperation{
			Root:  rootVD,
			Start: rootVD,
			Path:  fspath.Parse(strconv.FormatUint(uint64(n), 10)),
		}
		// We use a combination of GetDentryAt() and OpenTTY(), rather than OpenAt(), to avoid
		// DAC permission checks, which TIOCGPTPEER exists in part to skip.
		replicaD, err := t.Kernel().VFS().GetDentryAt(t, t.Credentials(), &pop, &vfs.GetDentryOptions{})
		if err != nil {
			return 0, err
		}
		defer replicaD.DecRef(t)
		replica, err := mfd.t.OpenTTY(t, masterMnt, replicaD.Dentry(), vfs.OpenOptions{
			Flags: flags & ^uint32(linux.O_CLOEXEC),
		})
		if err != nil {
			return 0, err
		}
		defer replica.DecRef(t)
		fd, err := t.NewFDFrom(0, replica, kernel.FDFlags{
			CloseOnExec: flags&linux.O_CLOEXEC != 0,
		})
		if err != nil {
			return 0, err
		}
		return uintptr(fd), nil

	default:
		maybeEmitUnimplementedEvent(ctx, sysno, cmd)
		return 0, linuxerr.ENOTTY
	}
}

// SetStat implements vfs.FileDescriptionImpl.SetStat.
func (mfd *masterFileDescription) SetStat(ctx context.Context, opts vfs.SetStatOptions) error {
	creds := auth.CredentialsFromContext(ctx)
	fs := mfd.vfsfd.VirtualDentry().Mount().Filesystem()
	return mfd.inode.SetStat(ctx, fs, creds, opts)
}

// Stat implements vfs.FileDescriptionImpl.Stat.
func (mfd *masterFileDescription) Stat(ctx context.Context, opts vfs.StatOptions) (linux.Statx, error) {
	fs := mfd.vfsfd.VirtualDentry().Mount().Filesystem()
	return mfd.inode.Stat(ctx, fs, opts)
}

// maybeEmitUnimplementedEvent emits unimplemented event if cmd is valid.
func maybeEmitUnimplementedEvent(ctx context.Context, sysno uintptr, cmd uint32) {
	switch cmd {
	case linux.TIOCSETD,
		linux.TIOCSBRK,
		linux.TIOCCBRK,
		linux.TCSBRKP,
		linux.TIOCSTI,
		linux.TIOCCONS,
		linux.FIONBIO,
		linux.TIOCEXCL,
		linux.TIOCNXCL,
		linux.TIOCGEXCL,
		linux.TIOCGSID,
		linux.TIOCGETD,
		linux.TIOCVHANGUP,
		linux.TIOCGDEV,
		linux.TIOCMGET,
		linux.TIOCMSET,
		linux.TIOCMBIC,
		linux.TIOCMBIS,
		linux.TIOCGICOUNT,
		linux.TIOCSSERIAL:

		unimpl.EmitUnimplementedEvent(ctx, sysno)
	}
}
