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

import (
	"bytes"
	"unicode"
	"unicode/utf8"

	"gvisor.dev/gvisor/pkg/abi/linux"
	"gvisor.dev/gvisor/pkg/context"
	"gvisor.dev/gvisor/pkg/errors/linuxerr"
	"gvisor.dev/gvisor/pkg/sentry/arch"
	"gvisor.dev/gvisor/pkg/sentry/kernel"
	"gvisor.dev/gvisor/pkg/sync"
	"gvisor.dev/gvisor/pkg/usermem"
	"gvisor.dev/gvisor/pkg/waiter"
)

const (
	// canonMaxBytes is the number of bytes that fit into a single line of
	// terminal input in canonical mode. This corresponds to N_TTY_BUF_SIZE
	// in include/linux/tty.h.
	canonMaxBytes = 4096

	// nonCanonMaxBytes is the maximum number of bytes that can be read at
	// a time in noncanonical mode.
	nonCanonMaxBytes = canonMaxBytes - 1

	spacesPerTab = 8
)

// lineDiscipline dictates how input and output are handled between the
// pseudoterminal (pty) master and replica. It can be configured to alter I/O,
// modify control characters (e.g. Ctrl-C for SIGINT), etc. The following man
// pages are good resources for how to affect the line discipline:
//
//   - termios(3)
//   - tty_ioctl(4)
//
// This file corresponds most closely to drivers/tty/n_tty.c.
//
// lineDiscipline has a simple structure but supports a multitude of options
// (see the above man pages). It consists of two queues of bytes: one from the
// terminal master to replica (the input queue) and one from replica to master
// (the output queue). When bytes are written to one end of the pty, the line
// discipline reads the bytes, modifies them or takes special action if
// required, and enqueues them to be read by the other end of the pty:
//
//	   input from terminal    +-------------+   input to process (e.g. bash)
//	+------------------------>| input queue |---------------------------+
//	|   (inputQueueWrite)     +-------------+     (inputQueueRead)      |
//	|                                                                   |
//	|                                                                   v
//
// masterFD                                                           replicaFD
//
//	^                                                                   |
//	|                                                                   |
//	|   output to terminal   +--------------+    output from process    |
//	+------------------------| output queue |<--------------------------+
//	    (outputQueueRead)    +--------------+    (outputQueueWrite)
//
// There is special handling for the ECHO option, where bytes written to the
// input queue are also output back to the terminal by being written to
// l.outQueue by the input queue transformer.
//
// Lock order:
//
//	termiosMu
//	  inQueue.mu
//	    outQueue.mu
//
// +stateify savable
type lineDiscipline struct {
	// sizeMu protects size.
	sizeMu sync.Mutex `state:"nosave"`

	// size is the terminal size (width and height).
	size linux.Winsize

	// inQueue is the input queue of the terminal.
	inQueue queue

	// outQueue is the output queue of the terminal.
	outQueue queue

	// termiosMu protects termios.
	termiosMu sync.RWMutex `state:"nosave"`

	// termios is the terminal configuration used by the lineDiscipline.
	termios linux.KernelTermios

	// column is the location in a row of the cursor. This is important for
	// handling certain special characters like backspace.
	column int

	// numReplicas is the number of replica file descriptors.
	numReplicas int

	// masterWaiter is used to wait on the master end of the TTY.
	masterWaiter waiter.Queue

	// replicaWaiter is used to wait on the replica end of the TTY.
	replicaWaiter waiter.Queue

	// terminal is the terminal linked to this lineDiscipline.
	terminal *Terminal

	// packet indicates the master is in packet mode.
	packet bool

	// packetStatus contains pending TIOCPKT_* status bits for the next master
	// read while packet mode is enabled.
	//
	// Currently only TIOCPKT_FLUSHREAD and TIOCPKT_FLUSHWRITE are emitted
	// through packetStatus.
	packetStatus uint8
}

func newLineDiscipline(termios linux.KernelTermios, terminal *Terminal) *lineDiscipline {
	ld := lineDiscipline{
		termios:  termios,
		terminal: terminal,
	}
	ld.inQueue.transformer = &inputQueueTransformer{}
	ld.outQueue.transformer = &outputQueueTransformer{}
	return &ld
}

type ptyEndpoint int

const (
	masterEndpoint ptyEndpoint = iota
	replicaEndpoint
)

// getTermios gets the linux.Termios for the tty.
func (l *lineDiscipline) getTermios(task *kernel.Task, args arch.SyscallArguments) (uintptr, error) {
	l.termiosMu.RLock()
	defer l.termiosMu.RUnlock()
	// We must copy a Termios struct, not KernelTermios.
	t := l.termios.ToTermios()
	_, err := t.CopyOut(task, args[2].Pointer())
	return 0, err
}

// getTermios2 gets the linux.KernelTermios for the tty.
func (l *lineDiscipline) getTermios2(task *kernel.Task, args arch.SyscallArguments) (uintptr, error) {
	l.termiosMu.RLock()
	defer l.termiosMu.RUnlock()
	_, err := l.termios.CopyOut(task, args[2].Pointer())
	return 0, err
}

// setTermios2 sets a linux.KernelTermios for the tty.
func (l *lineDiscipline) setTermios2(task *kernel.Task, args arch.SyscallArguments) (uintptr, error) {
	var t linux.KernelTermios
	if _, err := t.CopyIn(task, args[2].Pointer()); err != nil {
		return 0, err
	}

	l.termiosMu.Lock()
	oldCanonEnabled := l.termios.LEnabled(linux.ICANON)
	l.termios = t

	// If canonical mode is turned off, move bytes from inQueue's wait
	// buffer to its read buffer. Anything already in the read buffer is
	// now readable.
	if oldCanonEnabled && !l.termios.LEnabled(linux.ICANON) {
		l.inQueue.mu.Lock()
		l.inQueue.pushWaitBufLocked(l)
		l.inQueue.readable = len(l.inQueue.readBuf) > 0
		l.inQueue.mu.Unlock()
		l.termiosMu.Unlock()
		l.replicaWaiter.Notify(waiter.ReadableEvents)
	} else {
		l.termiosMu.Unlock()
	}

	return 0, nil
}

// setTermios sets a linux.Termios for the tty.
func (l *lineDiscipline) setTermios(task *kernel.Task, args arch.SyscallArguments) (uintptr, error) {
	// We must copy a Termios struct, not KernelTermios.
	var t linux.Termios
	if _, err := t.CopyIn(task, args[2].Pointer()); err != nil {
		return 0, err
	}

	l.termiosMu.Lock()
	oldCanonEnabled := l.termios.LEnabled(linux.ICANON)
	l.termios.FromTermios(t)

	// If canonical mode is turned off, move bytes from inQueue's wait
	// buffer to its read buffer. Anything already in the read buffer is
	// now readable.
	if oldCanonEnabled && !l.termios.LEnabled(linux.ICANON) {
		l.inQueue.mu.Lock()
		l.inQueue.pushWaitBufLocked(l)
		l.inQueue.readable = len(l.inQueue.readBuf) > 0
		l.inQueue.mu.Unlock()
		l.termiosMu.Unlock()
		l.replicaWaiter.Notify(waiter.ReadableEvents)
	} else {
		l.termiosMu.Unlock()
	}

	return 0, nil
}

func (l *lineDiscipline) windowSize(t *kernel.Task, args arch.SyscallArguments) error {
	l.sizeMu.Lock()
	defer l.sizeMu.Unlock()
	_, err := l.size.CopyOut(t, args[2].Pointer())
	return err
}

func (l *lineDiscipline) setWindowSize(t *kernel.Task, args arch.SyscallArguments) error {
	l.sizeMu.Lock()
	defer l.sizeMu.Unlock()
	oldSize := l.size
	_, err := l.size.CopyIn(t, args[2].Pointer())
	if err != nil {
		return err
	}
	if oldSize != l.size {
		l.terminal.replicaKTTY.SignalForegroundProcessGroup(kernel.SignalInfoPriv(linux.SIGWINCH))
	}
	return nil
}

func (l *lineDiscipline) masterReadiness() waiter.EventMask {
	// The master termios is immutable so termiosMu is not needed.
	res := l.inQueue.writeReadiness(&linux.MasterTermios) | l.outQueue.readReadiness(&linux.MasterTermios)
	l.termiosMu.RLock()
	if l.packet && l.packetStatus != 0 {
		res |= waiter.EventPri | waiter.ReadableEvents
	}
	if l.numReplicas == 0 {
		res |= waiter.EventHUp
	}
	l.termiosMu.RUnlock()
	return res
}

func (l *lineDiscipline) replicaReadiness() waiter.EventMask {
	l.termiosMu.RLock()
	defer l.termiosMu.RUnlock()
	return l.outQueue.writeReadiness(&l.termios) | l.inQueue.readReadiness(&l.termios)
}

func (l *lineDiscipline) inputQueueReadSize(t *kernel.Task, io usermem.IO, args arch.SyscallArguments) error {
	return l.inQueue.readableSize(t, io, args)
}

func (l *lineDiscipline) inputQueueRead(ctx context.Context, dst usermem.IOSequence) (int64, error) {
	l.termiosMu.RLock()
	// Replica never reads in packet mode.
	n, pushed, notifyEcho, err := l.inQueue.read(ctx, dst, l, false /* packet */)
	isCanon := l.termios.LEnabled(linux.ICANON)
	l.termiosMu.RUnlock()
	if err != nil {
		return 0, err
	}
	if n > 0 {
		if notifyEcho {
			l.masterWaiter.Notify(waiter.ReadableEvents | waiter.WritableEvents)
		} else {
			l.masterWaiter.Notify(waiter.WritableEvents)
		}
		if pushed {
			l.replicaWaiter.Notify(waiter.ReadableEvents)
		}
		return n, nil
	}
	if notifyEcho {
		l.masterWaiter.Notify(waiter.ReadableEvents)
	}
	if !pushed && isCanon {
		return 0, nil // EOF
	}

	return 0, linuxerr.ErrWouldBlock
}

func (l *lineDiscipline) inputQueueWrite(ctx context.Context, src usermem.IOSequence) (int64, error) {
	l.termiosMu.RLock()
	n, notifyEcho, err := l.inQueue.write(ctx, src, l)
	l.termiosMu.RUnlock()
	if err != nil {
		return 0, err
	}
	if notifyEcho {
		l.masterWaiter.Notify(waiter.ReadableEvents)
	}
	if n > 0 {
		l.replicaWaiter.Notify(waiter.ReadableEvents)
		return n, nil
	}
	return 0, linuxerr.ErrWouldBlock
}

func (l *lineDiscipline) outputQueueReadSize(t *kernel.Task, io usermem.IO, args arch.SyscallArguments) error {
	return l.outQueue.readableSize(t, io, args)
}

func (l *lineDiscipline) outputQueueRead(ctx context.Context, dst usermem.IOSequence) (int64, error) {
	if dst.NumBytes() == 0 {
		return 0, nil
	}
	if status, ok := l.consumePacketStatus(); ok {
		n, err := dst.CopyOut(ctx, []byte{status})
		return int64(n), err
	}
	l.termiosMu.RLock()
	// Ignore notifyEcho, as it cannot happen when reading from the output queue.
	n, pushed, _, err := l.outQueue.read(ctx, dst, l, l.packet)
	l.termiosMu.RUnlock()
	if err != nil {
		return 0, err
	}
	if n > 0 {
		l.replicaWaiter.Notify(waiter.WritableEvents)
		if pushed {
			l.masterWaiter.Notify(waiter.ReadableEvents)
		}
		return n, nil
	}
	return 0, linuxerr.ErrWouldBlock
}

func (l *lineDiscipline) outputQueueWrite(ctx context.Context, src usermem.IOSequence) (int64, error) {
	l.termiosMu.RLock()
	// Ignore notifyEcho, as it cannot happen when writing to the output queue.
	n, _, err := l.outQueue.write(ctx, src, l)
	l.termiosMu.RUnlock()
	if err != nil {
		return 0, err
	}
	l.masterWaiter.Notify(waiter.ReadableEvents)
	return n, nil
}

// replicaOpen is called when a replica file descriptor is opened.
func (l *lineDiscipline) replicaOpen() {
	l.termiosMu.Lock()
	defer l.termiosMu.Unlock()
	l.numReplicas++
}

// replicaClose is called when a replica file descriptor is closed.
func (l *lineDiscipline) replicaClose() {
	l.termiosMu.Lock()
	l.numReplicas--
	notify := l.numReplicas == 0
	l.termiosMu.Unlock()
	if notify {
		l.masterWaiter.Notify(waiter.EventHUp)
	}
}

// transformer is a helper interface to make it easier to stateify queue.
type transformer interface {
	// transform functions require queue's mutex to be held.
	// The boolean indicates whether there was any echoed bytes.
	transform(*lineDiscipline, *queue, []byte) (int, bool)
}

// outputQueueTransformer implements transformer. It performs line discipline
// transformations on the output queue.
//
// +stateify savable
type outputQueueTransformer struct{}

// transform does output processing for one end of the pty. See
// drivers/tty/n_tty.c:do_output_char for an analogous kernel function.
//
// Preconditions:
//   - l.termiosMu must be held for reading.
//   - q.mu must be held.
func (*outputQueueTransformer) transform(l *lineDiscipline, q *queue, buf []byte) (int, bool) {
	// transformOutput is effectively always in noncanonical mode, as the
	// master termios never has ICANON set.
	sizeBudget := nonCanonMaxBytes - len(q.readBuf)
	if sizeBudget <= 0 {
		return 0, false
	}

	if !l.termios.OEnabled(linux.OPOST) {
		copySize := min(len(buf), sizeBudget)
		q.readBuf = append(q.readBuf, buf[:copySize]...)
		if len(q.readBuf) > 0 {
			q.readable = true
		}
		return copySize, false
	}

	var ret int

Outer:
	for ; len(buf) > 0 && sizeBudget > 0; sizeBudget = nonCanonMaxBytes - len(q.readBuf) {
		size := l.peek(buf)
		if size > sizeBudget {
			break Outer
		}
		cBytes := append([]byte{}, buf[:size]...)
		buf = buf[size:]
		// We're guaranteed that cBytes has at least one element.
	cByteSwitch:
		switch cBytes[0] {
		case '\n':
			if l.termios.OEnabled(linux.ONLRET) {
				l.column = 0
			}
			if l.termios.OEnabled(linux.ONLCR) {
				if sizeBudget < 2 {
					break Outer
				}
				ret += size
				q.readBuf = append(q.readBuf, '\r', '\n')
				continue Outer
			}
		case '\r':
			if l.termios.OEnabled(linux.ONOCR) && l.column == 0 {
				// Treat the carriage return as processed, since it's a no-op.
				ret += size
				continue Outer
			}
			if l.termios.OEnabled(linux.OCRNL) {
				cBytes[0] = '\n'
				if l.termios.OEnabled(linux.ONLRET) {
					l.column = 0
				}
				break cByteSwitch
			}
			l.column = 0
		case '\t':
			spaces := spacesPerTab - l.column%spacesPerTab
			if l.termios.OutputFlags&linux.TABDLY == linux.XTABS {
				if sizeBudget < spacesPerTab {
					break Outer
				}
				ret += size
				l.column += spaces
				q.readBuf = append(q.readBuf, bytes.Repeat([]byte{' '}, spacesPerTab)...)
				continue Outer
			}
			l.column += spaces
		case '\b':
			if l.column > 0 {
				l.column--
			}
		default:
			l.column++
		}
		ret += size
		q.readBuf = append(q.readBuf, cBytes...)
	}
	if len(q.readBuf) > 0 {
		q.readable = true
	}
	return ret, false
}

// inputQueueTransformer implements transformer. It performs line discipline
// transformations on the input queue.
//
// +stateify savable
type inputQueueTransformer struct{}

// transform does input processing for one end of the pty. Characters read are
// transformed according to flags set in the termios struct. See
// drivers/tty/n_tty.c:n_tty_receive_char_special for an analogous kernel
// function.
// It returns an extra boolean indicating whether any characters need to be
// echoed, in which case we need to notify readers.
//
// Preconditions:
//   - l.termiosMu must be held for reading.
//   - q.mu must be held.
func (*inputQueueTransformer) transform(l *lineDiscipline, q *queue, buf []byte) (int, bool) {
	// If there's a line waiting to be read in canonical mode, don't write
	// anything else to the read buffer.
	if l.termios.LEnabled(linux.ICANON) && q.readable {
		return 0, false
	}

	maxBytes := nonCanonMaxBytes
	if l.termios.LEnabled(linux.ICANON) {
		maxBytes = canonMaxBytes
	}

	var ret int
	var notifyEcho bool
	for len(buf) > 0 && len(q.readBuf) < canonMaxBytes {
		size := l.peek(buf)
		cBytes := append([]byte{}, buf[:size]...)
		// We're guaranteed that cBytes has at least one element.
		switch cBytes[0] {
		case '\r':
			if l.termios.IEnabled(linux.IGNCR) {
				buf = buf[size:]
				ret += size
				continue
			}
			if l.termios.IEnabled(linux.ICRNL) {
				cBytes[0] = '\n'
			}
		case '\n':
			if l.termios.IEnabled(linux.INLCR) {
				cBytes[0] = '\r'
			}
		case l.termios.ControlCharacters[linux.VINTR]: // ctrl-c
			if l.termios.LEnabled(linux.ISIG) {
				l.terminal.replicaKTTY.SignalForegroundProcessGroup(kernel.SignalInfoPriv(linux.SIGINT))
				// In Linux, signal characters are consumed and not passed to
				// the reading process. See n_tty.c:n_tty_receive_signal_char().
				buf = buf[size:]
				ret += size
				continue
			}
		case l.termios.ControlCharacters[linux.VSUSP]: // ctrl-z
			if l.termios.LEnabled(linux.ISIG) {
				l.terminal.replicaKTTY.SignalForegroundProcessGroup(kernel.SignalInfoPriv(linux.SIGTSTP))
				buf = buf[size:]
				ret += size
				continue
			}
		case l.termios.ControlCharacters[linux.VQUIT]: // ctrl-\
			if l.termios.LEnabled(linux.ISIG) {
				l.terminal.replicaKTTY.SignalForegroundProcessGroup(kernel.SignalInfoPriv(linux.SIGQUIT))
				buf = buf[size:]
				ret += size
				continue
			}

		// In canonical mode, some characters need to be handled specially; for example, backspace.
		// This roughly aligns with n_tty.c:n_tty_receive_char_canon and n_tty.c:eraser
		// cBytes[0] == ControlCharacters[linux.VKILL] is also handled by n_tty.c:eraser, but this isn't implemented
		case l.termios.ControlCharacters[linux.VWERASE]:
			if !l.termios.LEnabled(linux.IEXTEN) {
				break
			}
			fallthrough
		case l.termios.ControlCharacters[linux.VERASE]:
			if !l.termios.LEnabled(linux.ICANON) {
				break
			}

			c := cBytes[0]
			killType := linux.VERASE
			if c == l.termios.ControlCharacters[linux.VWERASE] {
				killType = linux.VWERASE
			}
			seenAlphanumeric := false
			for len(q.readBuf) > 0 {
				// Erase a character. If IUTF8 is enabled, erase an entire multibyte unicode character.
				var toErase byte
				cnt := 0
				isContinuationByte := true
				for ; cnt < len(q.readBuf) && isContinuationByte; cnt++ {
					toErase = q.readBuf[len(q.readBuf)-cnt-1]
					isContinuationByte = l.termios.IEnabled(linux.IUTF8) && (toErase&0xc0) == 0x80
				}
				if isContinuationByte {
					// Do not partially erase a multibyte unicode character.
					break
				}

				// VWERASE will continue erasing characters until we encounter the first non-alphanumeric character
				// that follows some alphanumeric character. We consider "_" to be alphanumeric.
				if killType == linux.VWERASE {
					if unicode.IsLetter(rune(toErase)) || unicode.IsDigit(rune(toErase)) || toErase == '_' {
						seenAlphanumeric = true
					} else if seenAlphanumeric {
						break
					}
				}

				q.readBuf = q.readBuf[:len(q.readBuf)-cnt]
				if l.termios.LEnabled(linux.ECHO) {
					if l.termios.LEnabled(linux.ECHOPRT) {
						// Not implemented
					} else if killType == linux.VERASE && !l.termios.LEnabled(linux.ECHOE) {
						// Not implemented
					} else if toErase == '\t' {
						// Not implemented
					} else {
						const unicodeDelete byte = 0x7f
						isCtrl := toErase < 0x20 || toErase == unicodeDelete
						echoctl := l.termios.LEnabled(linux.ECHOCTL)

						charsToDelete := 1
						if isCtrl {
							// echoctl controls how we echo control characters, which also determines how we delete them.
							if echoctl {
								// echoctl echoes control characters as ^X, so we need to erase two characters.
								charsToDelete = 2
							} else {
								// if echoctl is disabled, we don't echo control characters so we don't have to erase anything.
								charsToDelete = 0
							}
						}
						for i := 0; i < charsToDelete; i++ {
							// Linux's kernel does character deletion with this sequence
							// of bytes, presumably because some older terminals don't erase
							// characters with \b, so we need to "erase" the old character
							// by writing a space over it.
							l.outQueue.writeBytes([]byte{'\b', ' ', '\b'}, l)
						}
					}
				}

				// VERASE only erases a single character
				if killType == linux.VERASE {
					break
				}
			}

			buf = buf[1:]
			ret += 1
			notifyEcho = true
			continue
		}

		// In canonical mode, we discard non-terminating characters
		// after the first 4095.
		if l.shouldDiscard(q, cBytes) {
			buf = buf[size:]
			ret += size
			continue
		}

		// Stop if the buffer would be overfilled.
		if len(q.readBuf)+size > maxBytes {
			break
		}
		buf = buf[size:]
		ret += size

		// If we get EOF, make the buffer available for reading.
		if l.termios.LEnabled(linux.ICANON) && l.termios.IsEOF(cBytes[0]) {
			q.readable = true
			break
		}

		q.readBuf = append(q.readBuf, cBytes...)

		// Anything written to the readBuf will have to be echoed.
		if l.termios.LEnabled(linux.ECHO) {
			l.outQueue.writeBytes(cBytes, l)
			notifyEcho = true
		}

		// If we finish a line, make it available for reading.
		if l.termios.LEnabled(linux.ICANON) && l.termios.IsTerminating(cBytes) {
			q.readable = true
			break
		}
	}

	// In noncanonical mode, everything is readable.
	if !l.termios.LEnabled(linux.ICANON) && len(q.readBuf) > 0 {
		q.readable = true
	}

	return ret, notifyEcho
}

// shouldDiscard returns whether c should be discarded. In canonical mode, if
// too many bytes are enqueued, we keep reading input and discarding it until
// we find a terminating character. Signal/echo processing still occurs.
//
// Precondition:
//   - l.termiosMu must be held for reading.
//   - q.mu must be held.
func (l *lineDiscipline) shouldDiscard(q *queue, cBytes []byte) bool {
	return l.termios.LEnabled(linux.ICANON) && len(q.readBuf)+len(cBytes) >= canonMaxBytes && !l.termios.IsTerminating(cBytes)
}

// peek returns the size in bytes of the next character to process. As long as
// b isn't empty, peek returns a value of at least 1.
func (l *lineDiscipline) peek(b []byte) int {
	size := 1
	// If UTF-8 support is enabled, runes might be multiple bytes.
	if l.termios.IEnabled(linux.IUTF8) {
		_, size = utf8.DecodeRune(b)
	}
	return size
}

func (l *lineDiscipline) setPacketMode(mode int) {
	l.termiosMu.Lock()
	defer l.termiosMu.Unlock()
	if mode == 0 {
		l.packet = false
		return
	}
	if !l.packet {
		l.packetStatus = 0
	}
	l.packet = true
}

func (l *lineDiscipline) getPacketMode() int {
	l.termiosMu.RLock()
	defer l.termiosMu.RUnlock()
	if l.packet {
		return 1
	}
	return 0
}

func (l *lineDiscipline) consumePacketStatus() (byte, bool) {
	l.termiosMu.Lock()
	defer l.termiosMu.Unlock()
	if !l.packet || l.packetStatus == 0 {
		return 0, false
	}
	status := l.packetStatus
	l.packetStatus = 0
	return status, true
}

func (l *lineDiscipline) packetStatusForFlush(endpoint ptyEndpoint, arg uint32) uint8 {
	if endpoint != replicaEndpoint {
		return 0
	}
	switch arg {
	case linux.TCIFLUSH:
		return linux.TIOCPKT_FLUSHREAD
	case linux.TCOFLUSH:
		return linux.TIOCPKT_FLUSHWRITE
	case linux.TCIOFLUSH:
		return linux.TIOCPKT_FLUSHREAD | linux.TIOCPKT_FLUSHWRITE
	default:
		return 0
	}
}

func (l *lineDiscipline) notifyPacketStatus(status uint8) {
	if status == 0 {
		return
	}
	l.termiosMu.Lock()
	if !l.packet {
		l.termiosMu.Unlock()
		return
	}
	l.packetStatus |= status
	l.termiosMu.Unlock()
	l.masterWaiter.Notify(waiter.EventPri | waiter.ReadableEvents)
}

func (l *lineDiscipline) inputQueueForEndpoint(endpoint ptyEndpoint) *queue {
	if endpoint == masterEndpoint {
		return &l.outQueue
	}
	return &l.inQueue
}

func (l *lineDiscipline) outputQueueForEndpoint(endpoint ptyEndpoint) *queue {
	if endpoint == masterEndpoint {
		return &l.inQueue
	}
	return &l.outQueue
}

func (l *lineDiscipline) notifyWritableForFlushedQueue(q *queue) {
	switch q {
	case &l.inQueue:
		l.masterWaiter.Notify(waiter.WritableEvents)
	case &l.outQueue:
		l.replicaWaiter.Notify(waiter.WritableEvents)
	}
}

// tcFlush handles the TCFLSH ioctl from the perspective of the calling PTY
// endpoint. Linux interprets TCIFLUSH/TCOFLUSH relative to the fd that issued
// the ioctl, not relative to the internal queue names.
func (l *lineDiscipline) tcFlush(endpoint ptyEndpoint, arg uint32) error {
	inputQueue := l.inputQueueForEndpoint(endpoint)
	outputQueue := l.outputQueueForEndpoint(endpoint)
	flushAndNotify := func(q *queue) {
		q.flush()
		l.notifyWritableForFlushedQueue(q)
	}

	switch arg {
	case linux.TCIFLUSH:
		flushAndNotify(inputQueue)
	case linux.TCOFLUSH:
		flushAndNotify(outputQueue)
	case linux.TCIOFLUSH:
		flushAndNotify(inputQueue)
		flushAndNotify(outputQueue)
	default:
		return linuxerr.EINVAL
	}
	l.notifyPacketStatus(l.packetStatusForFlush(endpoint, arg))
	return nil
}
