// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package fuzz

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
	"unsafe"
)

type sharedMemSys struct {
	mapObj syscall.Handle
}

func sharedMemMapFile(f *os.File, size int, removeOnClose bool) (mem *sharedMem, err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("mapping temporary file %s: %w", f.Name(), err)
		}
	}()

	// Create a file mapping object. The object itself is not shared.
	mapObj, err := syscall.CreateFileMapping(
		syscall.Handle(f.Fd()), // fhandle
		nil,                    // sa
		syscall.PAGE_READWRITE, // prot
		0,                      // maxSizeHigh
		0,                      // maxSizeLow
		nil,                    // name
	)
	if err != nil {
		return nil, err
	}

	// Create a view from the file mapping object.
	access := uint32(syscall.FILE_MAP_READ | syscall.FILE_MAP_WRITE)
	addr, err := syscall.MapViewOfFile(
		mapObj,        // handle
		access,        // access
		0,             // offsetHigh
		0,             // offsetLow
		uintptr(size), // length
	)
	if err != nil {
		syscall.CloseHandle(mapObj)
		return nil, err
	}

	region := unsafe.Slice((*byte)(unsafe.Pointer(addr)), size)
	return &sharedMem{
		f:             f,
		region:        region,
		removeOnClose: removeOnClose,
		sys:           sharedMemSys{mapObj: mapObj},
	}, nil
}

// Close unmaps the shared memory and closes the temporary file. If this
// sharedMem was created with sharedMemTempFile, Close also removes the file.
func (m *sharedMem) Close() error {
	// Attempt all operations, even if we get an error for an earlier operation.
	// os.File.Close may fail due to I/O errors, but we still want to delete
	// the temporary file.
	var errs []error
	errs = append(errs,
		syscall.UnmapViewOfFile(uintptr(unsafe.Pointer(&m.region[0]))),
		syscall.CloseHandle(m.sys.mapObj),
		m.f.Close())
	if m.removeOnClose {
		errs = append(errs, os.Remove(m.f.Name()))
	}
	for _, err := range errs {
		if err != nil {
			return err
		}
	}
	return nil
}

// setWorkerComm configures communication channels on the cmd that will
// run a worker process.
func setWorkerComm(cmd *exec.Cmd, comm workerComm) {
	mem := <-comm.memMu
	memFD := mem.f.Fd()
	comm.memMu <- mem
	syscall.SetHandleInformation(syscall.Handle(comm.fuzzIn.Fd()), syscall.HANDLE_FLAG_INHERIT, 1)
	syscall.SetHandleInformation(syscall.Handle(comm.fuzzOut.Fd()), syscall.HANDLE_FLAG_INHERIT, 1)
	syscall.SetHandleInformation(syscall.Handle(memFD), syscall.HANDLE_FLAG_INHERIT, 1)
	cmd.Env = append(cmd.Env, fmt.Sprintf("GO_TEST_FUZZ_WORKER_HANDLES=%x,%x,%x", comm.fuzzIn.Fd(), comm.fuzzOut.Fd(), memFD))
	cmd.SysProcAttr = &syscall.SysProcAttr{AdditionalInheritedHandles: []syscall.Handle{syscall.Handle(comm.fuzzIn.Fd()), syscall.Handle(comm.fuzzOut.Fd()), syscall.Handle(memFD)}}
}

// getWorkerComm returns communication channels in the worker process.
func getWorkerComm() (comm workerComm, err error) {
	v := os.Getenv("GO_TEST_FUZZ_WORKER_HANDLES")
	if v == "" {
		return workerComm{}, fmt.Errorf("GO_TEST_FUZZ_WORKER_HANDLES not set")
	}
	var fuzzInFD, fuzzOutFD, memFileFD uintptr
	if _, err := fmt.Sscanf(v, "%x,%x,%x", &fuzzInFD, &fuzzOutFD, &memFileFD); err != nil {
		return workerComm{}, fmt.Errorf("parsing GO_TEST_FUZZ_WORKER_HANDLES=%s: %v", v, err)
	}

	fuzzIn := os.NewFile(fuzzInFD, "fuzz_in")
	fuzzOut := os.NewFile(fuzzOutFD, "fuzz_out")
	memFile := os.NewFile(memFileFD, "fuzz_mem")
	fi, err := memFile.Stat()
	if err != nil {
		return workerComm{}, fmt.Errorf("worker checking temp file size: %w", err)
	}
	size := int(fi.Size())
	if int64(size) != fi.Size() {
		return workerComm{}, fmt.Errorf("fuzz temp file exceeds maximum size")
	}
	removeOnClose := false
	mem, err := sharedMemMapFile(memFile, size, removeOnClose)
	if err != nil {
		return workerComm{}, err
	}
	memMu := make(chan *sharedMem, 1)
	memMu <- mem

	return workerComm{fuzzIn: fuzzIn, fuzzOut: fuzzOut, memMu: memMu}, nil
}

func isInterruptError(err error) bool {
	// On Windows, we can't tell whether the process was interrupted by the error
	// returned by Wait. It looks like an ExitError with status 1.
	return false
}

// terminationSignal returns -1 and false because Windows doesn't have signals.
func terminationSignal(err error) (os.Signal, bool) {
	return syscall.Signal(-1), false
}

// isCrashSignal is not implemented because Windows doesn't have signals.
func isCrashSignal(signal os.Signal) bool {
	panic("not implemented: no signals on windows")
}
