// Copyright 2018 Google LLC
//
// 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 fdpipe

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path"
	"syscall"
	"testing"
	"time"

	"github.com/google/uuid"
	"gvisor.googlesource.com/gvisor/pkg/fd"
	"gvisor.googlesource.com/gvisor/pkg/sentry/context"
	"gvisor.googlesource.com/gvisor/pkg/sentry/context/contexttest"
	"gvisor.googlesource.com/gvisor/pkg/sentry/fs"
	"gvisor.googlesource.com/gvisor/pkg/sentry/usermem"
	"gvisor.googlesource.com/gvisor/pkg/syserror"
)

type hostOpener struct {
	name string
}

func (h *hostOpener) NonBlockingOpen(_ context.Context, p fs.PermMask) (*fd.FD, error) {
	var flags int
	switch {
	case p.Read && p.Write:
		flags = syscall.O_RDWR
	case p.Write:
		flags = syscall.O_WRONLY
	case p.Read:
		flags = syscall.O_RDONLY
	default:
		return nil, syscall.EINVAL
	}
	f, err := syscall.Open(h.name, flags|syscall.O_NONBLOCK, 0666)
	if err != nil {
		return nil, err
	}
	return fd.New(f), nil
}

func pipename() string {
	return fmt.Sprintf(path.Join(os.TempDir(), "test-named-pipe-%s"), uuid.New())
}

func mkpipe(name string) error {
	return syscall.Mknod(name, syscall.S_IFIFO|0666, 0)
}

func TestTryOpen(t *testing.T) {
	for _, test := range []struct {
		// desc is the test's description.
		desc string

		// makePipe is true if the test case should create the pipe.
		makePipe bool

		// flags are the fs.FileFlags used to open the pipe.
		flags fs.FileFlags

		// expectFile is true if a fs.File is expected.
		expectFile bool

		// err is the expected error
		err error
	}{
		{
			desc:       "FileFlags lacking Read and Write are invalid",
			makePipe:   false,
			flags:      fs.FileFlags{}, /* bogus */
			expectFile: false,
			err:        syscall.EINVAL,
		},
		{
			desc:       "NonBlocking Read only error returns immediately",
			makePipe:   false, /* causes the error */
			flags:      fs.FileFlags{Read: true, NonBlocking: true},
			expectFile: false,
			err:        syscall.ENOENT,
		},
		{
			desc:       "NonBlocking Read only success returns immediately",
			makePipe:   true,
			flags:      fs.FileFlags{Read: true, NonBlocking: true},
			expectFile: true,
			err:        nil,
		},
		{
			desc:       "NonBlocking Write only error returns immediately",
			makePipe:   false, /* causes the error */
			flags:      fs.FileFlags{Write: true, NonBlocking: true},
			expectFile: false,
			err:        syscall.ENOENT,
		},
		{
			desc:       "NonBlocking Write only no reader error returns immediately",
			makePipe:   true,
			flags:      fs.FileFlags{Write: true, NonBlocking: true},
			expectFile: false,
			err:        syscall.ENXIO,
		},
		{
			desc:       "ReadWrite error returns immediately",
			makePipe:   false, /* causes the error */
			flags:      fs.FileFlags{Read: true, Write: true},
			expectFile: false,
			err:        syscall.ENOENT,
		},
		{
			desc:       "ReadWrite returns immediately",
			makePipe:   true,
			flags:      fs.FileFlags{Read: true, Write: true},
			expectFile: true,
			err:        nil,
		},
		{
			desc:       "Blocking Write only returns open error",
			makePipe:   false, /* causes the error */
			flags:      fs.FileFlags{Write: true},
			expectFile: false,
			err:        syscall.ENOENT, /* from bogus perms */
		},
		{
			desc:       "Blocking Read only returns open error",
			makePipe:   false, /* causes the error */
			flags:      fs.FileFlags{Read: true},
			expectFile: false,
			err:        syscall.ENOENT,
		},
		{
			desc:       "Blocking Write only returns with syserror.ErrWouldBlock",
			makePipe:   true,
			flags:      fs.FileFlags{Write: true},
			expectFile: false,
			err:        syserror.ErrWouldBlock,
		},
		{
			desc:       "Blocking Read only returns with syserror.ErrWouldBlock",
			makePipe:   true,
			flags:      fs.FileFlags{Read: true},
			expectFile: false,
			err:        syserror.ErrWouldBlock,
		},
	} {
		name := pipename()
		if test.makePipe {
			// Create the pipe.  We do this per-test case to keep tests independent.
			if err := mkpipe(name); err != nil {
				t.Errorf("%s: failed to make host pipe: %v", test.desc, err)
				continue
			}
			defer syscall.Unlink(name)
		}

		// Use a host opener to keep things simple.
		opener := &hostOpener{name: name}

		pipeOpenState := &pipeOpenState{}
		ctx := contexttest.Context(t)
		pipeOps, err := pipeOpenState.TryOpen(ctx, opener, test.flags)
		if unwrapError(err) != test.err {
			t.Errorf("%s: got error %v, want %v", test.desc, err, test.err)
			if pipeOps != nil {
				// Cleanup the state of the pipe, and remove the fd from the
				// fdnotifier.  Sadly this needed to maintain the correctness
				// of other tests because the fdnotifier is global.
				pipeOps.Release()
			}
			continue
		}
		if (pipeOps != nil) != test.expectFile {
			t.Errorf("%s: got non-nil file %v, want %v", test.desc, pipeOps != nil, test.expectFile)
		}
		if pipeOps != nil {
			// Same as above.
			pipeOps.Release()
		}
	}
}

func TestPipeOpenUnblocksEventually(t *testing.T) {
	for _, test := range []struct {
		// desc is the test's description.
		desc string

		// partnerIsReader is true if the goroutine opening the same pipe as the test case
		// should open the pipe read only.  Otherwise write only.  This also means that the
		// test case will open the pipe in the opposite way.
		partnerIsReader bool

		// partnerIsBlocking is true if the goroutine opening the same pipe as the test case
		// should do so without the O_NONBLOCK flag, otherwise opens the pipe with O_NONBLOCK
		// until ENXIO is not returned.
		partnerIsBlocking bool
	}{
		{
			desc:              "Blocking Read with blocking writer partner opens eventually",
			partnerIsReader:   false,
			partnerIsBlocking: true,
		},
		{
			desc:              "Blocking Write with blocking reader partner opens eventually",
			partnerIsReader:   true,
			partnerIsBlocking: true,
		},
		{
			desc:              "Blocking Read with non-blocking writer partner opens eventually",
			partnerIsReader:   false,
			partnerIsBlocking: false,
		},
		{
			desc:              "Blocking Write with non-blocking reader partner opens eventually",
			partnerIsReader:   true,
			partnerIsBlocking: false,
		},
	} {
		// Create the pipe.  We do this per-test case to keep tests independent.
		name := pipename()
		if err := mkpipe(name); err != nil {
			t.Errorf("%s: failed to make host pipe: %v", test.desc, err)
			continue
		}
		defer syscall.Unlink(name)

		// Spawn the partner.
		type fderr struct {
			fd  int
			err error
		}
		errch := make(chan fderr, 1)
		go func() {
			var flags int
			if test.partnerIsReader {
				flags = syscall.O_RDONLY
			} else {
				flags = syscall.O_WRONLY
			}
			if test.partnerIsBlocking {
				fd, err := syscall.Open(name, flags, 0666)
				errch <- fderr{fd: fd, err: err}
			} else {
				var fd int
				err := error(syscall.ENXIO)
				for err == syscall.ENXIO {
					fd, err = syscall.Open(name, flags|syscall.O_NONBLOCK, 0666)
					time.Sleep(1 * time.Second)
				}
				errch <- fderr{fd: fd, err: err}
			}
		}()

		// Setup file flags for either a read only or write only open.
		flags := fs.FileFlags{
			Read:  !test.partnerIsReader,
			Write: test.partnerIsReader,
		}

		// Open the pipe in a blocking way, which should succeed eventually.
		opener := &hostOpener{name: name}
		ctx := contexttest.Context(t)
		pipeOps, err := Open(ctx, opener, flags)
		if pipeOps != nil {
			// Same as TestTryOpen.
			pipeOps.Release()
		}

		// Check that the partner opened the file successfully.
		e := <-errch
		if e.err != nil {
			t.Errorf("%s: partner got error %v, wanted nil", test.desc, e.err)
			continue
		}
		// If so, then close the partner fd to avoid leaking an fd.
		syscall.Close(e.fd)

		// Check that our blocking open was successful.
		if err != nil {
			t.Errorf("%s: blocking open got error %v, wanted nil", test.desc, err)
			continue
		}
		if pipeOps == nil {
			t.Errorf("%s: blocking open got nil file, wanted non-nil", test.desc)
			continue
		}
	}
}

func TestCopiedReadAheadBuffer(t *testing.T) {
	// Create the pipe.
	name := pipename()
	if err := mkpipe(name); err != nil {
		t.Fatalf("failed to make host pipe: %v", err)
	}
	defer syscall.Unlink(name)

	// We're taking advantage of the fact that pipes opened read only always return
	// success, but internally they are not deemed "opened" until we're sure that
	// another writer comes along.  This means we can open the same pipe write only
	// with no problems + write to it, given that opener.Open already tried to open
	// the pipe RDONLY and succeeded, which we know happened if TryOpen returns
	// syserror.ErrwouldBlock.
	//
	// This simulates the open(RDONLY) <-> open(WRONLY)+write race we care about, but
	// does not cause our test to be racy (which would be terrible).
	opener := &hostOpener{name: name}
	pipeOpenState := &pipeOpenState{}
	ctx := contexttest.Context(t)
	pipeOps, err := pipeOpenState.TryOpen(ctx, opener, fs.FileFlags{Read: true})
	if pipeOps != nil {
		pipeOps.Release()
		t.Fatalf("open(%s, %o) got file, want nil", name, syscall.O_RDONLY)
	}
	if err != syserror.ErrWouldBlock {
		t.Fatalf("open(%s, %o) got error %v, want %v", name, syscall.O_RDONLY, err, syserror.ErrWouldBlock)
	}

	// Then open the same pipe write only and write some bytes to it.  The next
	// time we try to open the pipe read only again via the pipeOpenState, we should
	// succeed and buffer some of the bytes written.
	fd, err := syscall.Open(name, syscall.O_WRONLY, 0666)
	if err != nil {
		t.Fatalf("open(%s, %o) got error %v, want nil", name, syscall.O_WRONLY, err)
	}
	defer syscall.Close(fd)

	data := []byte("hello")
	if n, err := syscall.Write(fd, data); n != len(data) || err != nil {
		t.Fatalf("write(%v) got (%d, %v), want (%d, nil)", data, n, err, len(data))
	}

	// Try the read again, knowing that it should succeed this time.
	pipeOps, err = pipeOpenState.TryOpen(ctx, opener, fs.FileFlags{Read: true})
	if pipeOps == nil {
		t.Fatalf("open(%s, %o) got nil file, want not nil", name, syscall.O_RDONLY)
	}
	defer pipeOps.Release()

	if err != nil {
		t.Fatalf("open(%s, %o) got error %v, want nil", name, syscall.O_RDONLY, err)
	}

	inode := fs.NewMockInode(ctx, fs.NewMockMountSource(nil), fs.StableAttr{
		Type: fs.Pipe,
	})
	file := fs.NewFile(ctx, fs.NewDirent(inode, "pipe"), fs.FileFlags{Read: true}, pipeOps)

	// Check that the file we opened points to a pipe with a non-empty read ahead buffer.
	bufsize := len(pipeOps.readAheadBuffer)
	if bufsize != 1 {
		t.Fatalf("read ahead buffer got %d bytes, want %d", bufsize, 1)
	}

	// Now for the final test, try to read everything in, expecting to get back all of
	// the bytes that were written at once.  Note that in the wild there is no atomic
	// read size so expecting to get all bytes from a single writer when there are
	// multiple readers is a bad expectation.
	buf := make([]byte, len(data))
	ioseq := usermem.BytesIOSequence(buf)
	n, err := pipeOps.Read(ctx, file, ioseq, 0)
	if err != nil {
		t.Fatalf("read request got error %v, want nil", err)
	}
	if n != int64(len(data)) {
		t.Fatalf("read request got %d bytes, want %d", n, len(data))
	}
	if !bytes.Equal(buf, data) {
		t.Errorf("read request got bytes [%v], want [%v]", buf, data)
	}
}

func TestPipeHangup(t *testing.T) {
	for _, test := range []struct {
		// desc is the test's description.
		desc string

		// flags control how we open our end of the pipe and must be read
		// only or write only.  They also dicate how a coordinating partner
		// fd is opened, which is their inverse (read only -> write only, etc).
		flags fs.FileFlags

		// hangupSelf if true causes the test case to close our end of the pipe
		// and causes hangup errors to be asserted on our coordinating partner's
		// fd.  If hangupSelf is false, then our partner's fd is closed and the
		// hangup errors are expected on our end of the pipe.
		hangupSelf bool
	}{
		{
			desc:  "Read only gets hangup error",
			flags: fs.FileFlags{Read: true},
		},
		{
			desc:  "Write only gets hangup error",
			flags: fs.FileFlags{Write: true},
		},
		{
			desc:       "Read only generates hangup error",
			flags:      fs.FileFlags{Read: true},
			hangupSelf: true,
		},
		{
			desc:       "Write only generates hangup error",
			flags:      fs.FileFlags{Write: true},
			hangupSelf: true,
		},
	} {
		if test.flags.Read == test.flags.Write {
			t.Errorf("%s: test requires a single reader or writer", test.desc)
			continue
		}

		// Create the pipe.  We do this per-test case to keep tests independent.
		name := pipename()
		if err := mkpipe(name); err != nil {
			t.Errorf("%s: failed to make host pipe: %v", test.desc, err)
			continue
		}
		defer syscall.Unlink(name)

		// Fire off a partner routine which tries to open the same pipe blocking,
		// which will synchronize with us.  The channel allows us to get back the
		// fd once we expect this partner routine to succeed, so we can manifest
		// hangup events more directly.
		fdchan := make(chan int, 1)
		go func() {
			// Be explicit about the flags to protect the test from
			// misconfiguration.
			var flags int
			if test.flags.Read {
				flags = syscall.O_WRONLY
			} else {
				flags = syscall.O_RDONLY
			}
			fd, err := syscall.Open(name, flags, 0666)
			if err != nil {
				t.Logf("Open(%q, %o, 0666) partner failed: %v", name, flags, err)
			}
			fdchan <- fd
		}()

		// Open our end in a blocking way to ensure that we coordinate.
		opener := &hostOpener{name: name}
		ctx := contexttest.Context(t)
		pipeOps, err := Open(ctx, opener, test.flags)
		if err != nil {
			t.Errorf("%s: Open got error %v, want nil", test.desc, err)
			continue
		}
		// Don't defer file.DecRef here because that causes the hangup we're
		// trying to test for.

		// Expect the partner routine to have coordinated with us and get back
		// its open fd.
		f := <-fdchan
		if f < 0 {
			t.Errorf("%s: partner routine got fd %d, want > 0", test.desc, f)
			pipeOps.Release()
			continue
		}

		if test.hangupSelf {
			// Hangup self and assert that our partner got the expected hangup
			// error.
			pipeOps.Release()

			if test.flags.Read {
				// Partner is writer.
				assertWriterHungup(t, test.desc, fd.NewReadWriter(f))
			} else {
				// Partner is reader.
				assertReaderHungup(t, test.desc, fd.NewReadWriter(f))
			}
		} else {
			// Hangup our partner and expect us to get the hangup error.
			syscall.Close(f)
			defer pipeOps.Release()

			if test.flags.Read {
				assertReaderHungup(t, test.desc, pipeOps.(*pipeOperations).file)
			} else {
				assertWriterHungup(t, test.desc, pipeOps.(*pipeOperations).file)
			}
		}
	}
}

func assertReaderHungup(t *testing.T, desc string, reader io.Reader) bool {
	// Drain the pipe completely, it might have crap in it, but expect EOF eventually.
	var err error
	for err == nil {
		_, err = reader.Read(make([]byte, 10))
	}
	if err != io.EOF {
		t.Errorf("%s: read from self after hangup got error %v, want %v", desc, err, io.EOF)
		return false
	}
	return true
}

func assertWriterHungup(t *testing.T, desc string, writer io.Writer) bool {
	if _, err := writer.Write([]byte("hello")); unwrapError(err) != syscall.EPIPE {
		t.Errorf("%s: write to self after hangup got error %v, want %v", desc, err, syscall.EPIPE)
		return false
	}
	return true
}