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

import (
	"gvisor.dev/gvisor/pkg/abi/linux"
	"gvisor.dev/gvisor/pkg/context"
	"gvisor.dev/gvisor/pkg/marshal/primitive"
	"gvisor.dev/gvisor/pkg/sentry/arch"
	"gvisor.dev/gvisor/pkg/sentry/fs"
	"gvisor.dev/gvisor/pkg/sentry/kernel"
	"gvisor.dev/gvisor/pkg/sentry/unimpl"
	"gvisor.dev/gvisor/pkg/sync"
	"gvisor.dev/gvisor/pkg/syserror"
	"gvisor.dev/gvisor/pkg/usermem"
)

// LINT.IfChange

// TTYFileOperations implements fs.FileOperations for a host file descriptor
// that wraps a TTY FD.
//
// +stateify savable
type TTYFileOperations struct {
	fileOperations

	// mu protects the fields below.
	mu sync.Mutex `state:"nosave"`

	// session is the session attached to this TTYFileOperations.
	session *kernel.Session

	// fgProcessGroup is the foreground process group that is currently
	// connected to this TTY.
	fgProcessGroup *kernel.ProcessGroup

	// termios contains the terminal attributes for this TTY.
	termios linux.KernelTermios
}

// newTTYFile returns a new fs.File that wraps a TTY FD.
func newTTYFile(ctx context.Context, dirent *fs.Dirent, flags fs.FileFlags, iops *inodeOperations) *fs.File {
	return fs.NewFile(ctx, dirent, flags, &TTYFileOperations{
		fileOperations: fileOperations{iops: iops},
		termios:        linux.DefaultReplicaTermios,
	})
}

// InitForegroundProcessGroup sets the foreground process group and session for
// the TTY. This should only be called once, after the foreground process group
// has been created, but before it has started running.
func (t *TTYFileOperations) InitForegroundProcessGroup(pg *kernel.ProcessGroup) {
	t.mu.Lock()
	defer t.mu.Unlock()
	if t.fgProcessGroup != nil {
		panic("foreground process group is already set")
	}
	t.fgProcessGroup = pg
	t.session = pg.Session()
}

// ForegroundProcessGroup returns the foreground process for the TTY.
func (t *TTYFileOperations) ForegroundProcessGroup() *kernel.ProcessGroup {
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.fgProcessGroup
}

// Read implements fs.FileOperations.Read.
//
// Reading from a TTY is only allowed for foreground process groups. Background
// process groups will either get EIO or a SIGTTIN.
//
// See drivers/tty/n_tty.c:n_tty_read()=>job_control().
func (t *TTYFileOperations) Read(ctx context.Context, file *fs.File, dst usermem.IOSequence, offset int64) (int64, error) {
	t.mu.Lock()
	defer t.mu.Unlock()

	// Are we allowed to do the read?
	// drivers/tty/n_tty.c:n_tty_read()=>job_control()=>tty_check_change().
	if err := t.checkChange(ctx, linux.SIGTTIN); err != nil {
		return 0, err
	}

	// Do the read.
	return t.fileOperations.Read(ctx, file, dst, offset)
}

// Write implements fs.FileOperations.Write.
func (t *TTYFileOperations) Write(ctx context.Context, file *fs.File, src usermem.IOSequence, offset int64) (int64, error) {
	t.mu.Lock()
	defer t.mu.Unlock()

	// Check whether TOSTOP is enabled. This corresponds to the check in
	// drivers/tty/n_tty.c:n_tty_write().
	if t.termios.LEnabled(linux.TOSTOP) {
		if err := t.checkChange(ctx, linux.SIGTTOU); err != nil {
			return 0, err
		}
	}
	return t.fileOperations.Write(ctx, file, src, offset)
}

// Release implements fs.FileOperations.Release.
func (t *TTYFileOperations) Release(ctx context.Context) {
	t.mu.Lock()
	t.fgProcessGroup = nil
	t.mu.Unlock()

	t.fileOperations.Release(ctx)
}

// Ioctl implements fs.FileOperations.Ioctl.
func (t *TTYFileOperations) Ioctl(ctx context.Context, _ *fs.File, io usermem.IO, args arch.SyscallArguments) (uintptr, error) {
	task := kernel.TaskFromContext(ctx)
	if task == nil {
		return 0, syserror.ENOTTY
	}

	// Ignore arg[0].  This is the real FD:
	fd := t.fileOperations.iops.fileState.FD()
	ioctl := args[1].Uint64()
	switch ioctl {
	case linux.TCGETS:
		termios, err := ioctlGetTermios(fd)
		if err != nil {
			return 0, err
		}
		_, err = termios.CopyOut(task, args[2].Pointer())
		return 0, err

	case linux.TCSETS, linux.TCSETSW, linux.TCSETSF:
		t.mu.Lock()
		defer t.mu.Unlock()

		if err := t.checkChange(ctx, linux.SIGTTOU); err != nil {
			return 0, err
		}

		var termios linux.Termios
		if _, err := termios.CopyIn(task, args[2].Pointer()); err != nil {
			return 0, err
		}
		err := ioctlSetTermios(fd, ioctl, &termios)
		if err == nil {
			t.termios.FromTermios(termios)
		}
		return 0, err

	case linux.TIOCGPGRP:
		// Args: pid_t *argp
		// When successful, equivalent to *argp = tcgetpgrp(fd).
		// Get the process group ID of the foreground process group on
		// this terminal.

		pidns := kernel.PIDNamespaceFromContext(ctx)
		if pidns == nil {
			return 0, syserror.ENOTTY
		}

		t.mu.Lock()
		defer t.mu.Unlock()

		// Map the ProcessGroup into a ProcessGroupID in the task's PID
		// namespace.
		pgID := primitive.Int32(pidns.IDOfProcessGroup(t.fgProcessGroup))
		_, err := pgID.CopyOut(task, args[2].Pointer())
		return 0, err

	case linux.TIOCSPGRP:
		// Args: const pid_t *argp
		// Equivalent to tcsetpgrp(fd, *argp).
		// Set the foreground process group ID of this terminal.

		t.mu.Lock()
		defer t.mu.Unlock()

		// Check that we are allowed to set the process group.
		if err := t.checkChange(ctx, linux.SIGTTOU); err != nil {
			// drivers/tty/tty_io.c:tiocspgrp() converts -EIO from
			// tty_check_change() to -ENOTTY.
			if err == syserror.EIO {
				return 0, syserror.ENOTTY
			}
			return 0, err
		}

		// Check that calling task's process group is in the TTY
		// session.
		if task.ThreadGroup().Session() != t.session {
			return 0, syserror.ENOTTY
		}

		var pgIDP primitive.Int32
		if _, err := pgIDP.CopyIn(task, args[2].Pointer()); err != nil {
			return 0, err
		}
		pgID := kernel.ProcessGroupID(pgIDP)

		// pgID must be non-negative.
		if pgID < 0 {
			return 0, syserror.EINVAL
		}

		// Process group with pgID must exist in this PID namespace.
		pidns := task.PIDNamespace()
		pg := pidns.ProcessGroupWithID(pgID)
		if pg == nil {
			return 0, syserror.ESRCH
		}

		// Check that new process group is in the TTY session.
		if pg.Session() != t.session {
			return 0, syserror.EPERM
		}

		t.fgProcessGroup = pg
		return 0, nil

	case linux.TIOCGWINSZ:
		// Args: struct winsize *argp
		// Get window size.
		winsize, err := ioctlGetWinsize(fd)
		if err != nil {
			return 0, err
		}
		_, err = winsize.CopyOut(task, args[2].Pointer())
		return 0, err

	case linux.TIOCSWINSZ:
		// Args: const struct winsize *argp
		// Set window size.

		// Unlike setting the termios, any process group (even
		// background ones) can set the winsize.

		var winsize linux.Winsize
		if _, err := winsize.CopyIn(task, args[2].Pointer()); err != nil {
			return 0, err
		}
		err := ioctlSetWinsize(fd, &winsize)
		return 0, err

	// Unimplemented commands.
	case linux.TIOCSETD,
		linux.TIOCSBRK,
		linux.TIOCCBRK,
		linux.TCSBRK,
		linux.TCSBRKP,
		linux.TIOCSTI,
		linux.TIOCCONS,
		linux.FIONBIO,
		linux.TIOCEXCL,
		linux.TIOCNXCL,
		linux.TIOCGEXCL,
		linux.TIOCNOTTY,
		linux.TIOCSCTTY,
		linux.TIOCGSID,
		linux.TIOCGETD,
		linux.TIOCVHANGUP,
		linux.TIOCGDEV,
		linux.TIOCMGET,
		linux.TIOCMSET,
		linux.TIOCMBIC,
		linux.TIOCMBIS,
		linux.TIOCGICOUNT,
		linux.TCFLSH,
		linux.TIOCSSERIAL,
		linux.TIOCGPTPEER:

		unimpl.EmitUnimplementedEvent(ctx)
		fallthrough
	default:
		return 0, syserror.ENOTTY
	}
}

// checkChange checks that the process group is allowed to read, write, or
// change the state of the TTY.
//
// This corresponds to Linux drivers/tty/tty_io.c:tty_check_change(). The logic
// is a bit convoluted, but documented inline.
//
// Preconditions: t.mu must be held.
func (t *TTYFileOperations) checkChange(ctx context.Context, sig linux.Signal) error {
	task := kernel.TaskFromContext(ctx)
	if task == nil {
		// No task? Linux does not have an analog for this case, but
		// tty_check_change only blocks specific cases and is
		// surprisingly permissive. Allowing the change seems
		// appropriate.
		return nil
	}

	tg := task.ThreadGroup()
	pg := tg.ProcessGroup()

	// If the session for the task is different than the session for the
	// controlling TTY, then the change is allowed. Seems like a bad idea,
	// but that's exactly what linux does.
	if tg.Session() != t.fgProcessGroup.Session() {
		return nil
	}

	// If we are the foreground process group, then the change is allowed.
	if pg == t.fgProcessGroup {
		return nil
	}

	// We are not the foreground process group.

	// Is the provided signal blocked or ignored?
	if (task.SignalMask()&linux.SignalSetOf(sig) != 0) || tg.SignalHandlers().IsIgnored(sig) {
		// If the signal is SIGTTIN, then we are attempting to read
		// from the TTY. Don't send the signal and return EIO.
		if sig == linux.SIGTTIN {
			return syserror.EIO
		}

		// Otherwise, we are writing or changing terminal state. This is allowed.
		return nil
	}

	// If the process group is an orphan, return EIO.
	if pg.IsOrphan() {
		return syserror.EIO
	}

	// Otherwise, send the signal to the process group and return ERESTARTSYS.
	//
	// Note that Linux also unconditionally sets TIF_SIGPENDING on current,
	// but this isn't necessary in gVisor because the rationale given in
	// 040b6362d58f "tty: fix leakage of -ERESTARTSYS to userland" doesn't
	// apply: the sentry will handle -ERESTARTSYS in
	// kernel.runApp.execute() even if the kernel.Task isn't interrupted.
	//
	// Linux ignores the result of kill_pgrp().
	_ = pg.SendSignal(kernel.SignalInfoPriv(sig))
	return syserror.ERESTARTSYS
}

// LINT.ThenChange(../../fsimpl/host/tty.go)