// Copyright 2016 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 carchive_test

import (
	"bufio"
	"bytes"
	"debug/elf"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"
	"testing"
	"time"
	"unicode"
)

// Program to run.
var bin []string

// C compiler with args (from $(go env CC) $(go env GOGCCFLAGS)).
var cc []string

// An environment with GOPATH=$(pwd).
var gopathEnv []string

// ".exe" on Windows.
var exeSuffix string

var GOOS, GOARCH string
var libgodir string

func init() {
	GOOS = goEnv("GOOS")
	GOARCH = goEnv("GOARCH")
	bin = cmdToRun("./testp")

	ccOut := goEnv("CC")
	cc = []string{string(ccOut)}

	out := goEnv("GOGCCFLAGS")
	quote := '\000'
	start := 0
	lastSpace := true
	backslash := false
	s := string(out)
	for i, c := range s {
		if quote == '\000' && unicode.IsSpace(c) {
			if !lastSpace {
				cc = append(cc, s[start:i])
				lastSpace = true
			}
		} else {
			if lastSpace {
				start = i
				lastSpace = false
			}
			if quote == '\000' && !backslash && (c == '"' || c == '\'') {
				quote = c
				backslash = false
			} else if !backslash && quote == c {
				quote = '\000'
			} else if (quote == '\000' || quote == '"') && !backslash && c == '\\' {
				backslash = true
			} else {
				backslash = false
			}
		}
	}
	if !lastSpace {
		cc = append(cc, s[start:])
	}

	if GOOS == "darwin" {
		// For Darwin/ARM.
		// TODO(crawshaw): can we do better?
		cc = append(cc, []string{"-framework", "CoreFoundation", "-framework", "Foundation"}...)
	}
	libgodir = GOOS + "_" + GOARCH
	switch GOOS {
	case "darwin":
		if GOARCH == "arm" || GOARCH == "arm64" {
			libgodir += "_shared"
		}
	case "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris":
		libgodir += "_shared"
	}
	cc = append(cc, "-I", filepath.Join("pkg", libgodir))

	// Build an environment with GOPATH=$(pwd)
	env := os.Environ()
	var n []string
	for _, e := range env {
		if !strings.HasPrefix(e, "GOPATH=") {
			n = append(n, e)
		}
	}
	dir, err := os.Getwd()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(2)
	}
	n = append(n, "GOPATH="+dir)
	gopathEnv = n

	if GOOS == "windows" {
		exeSuffix = ".exe"
	}
}

func goEnv(key string) string {
	out, err := exec.Command("go", "env", key).Output()
	if err != nil {
		fmt.Fprintf(os.Stderr, "go env %s failed:\n%s\n", key, err)
		if ee, ok := err.(*exec.ExitError); ok {
			fmt.Fprintf(os.Stderr, "%s", ee.Stderr)
		}
		os.Exit(2)
	}
	return strings.TrimSpace(string(out))
}

func cmdToRun(name string) []string {
	execScript := "go_" + goEnv("GOOS") + "_" + goEnv("GOARCH") + "_exec"
	executor, err := exec.LookPath(execScript)
	if err != nil {
		return []string{name}
	}
	return []string{executor, name}
}

func testInstall(t *testing.T, exe, libgoa, libgoh string, buildcmd ...string) {
	t.Helper()
	cmd := exec.Command(buildcmd[0], buildcmd[1:]...)
	cmd.Env = gopathEnv
	t.Log(buildcmd)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
	defer func() {
		os.Remove(libgoa)
		os.Remove(libgoh)
	}()

	ccArgs := append(cc, "-o", exe, "main.c")
	if GOOS == "windows" {
		ccArgs = append(ccArgs, "main_windows.c", libgoa, "-lntdll", "-lws2_32", "-lwinmm")
	} else {
		ccArgs = append(ccArgs, "main_unix.c", libgoa)
	}
	t.Log(ccArgs)
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
	defer os.Remove(exe)

	binArgs := append(cmdToRun(exe), "arg1", "arg2")
	if out, err := exec.Command(binArgs[0], binArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
}

func TestInstall(t *testing.T) {
	defer os.RemoveAll("pkg")

	testInstall(t, "./testp1"+exeSuffix,
		filepath.Join("pkg", libgodir, "libgo.a"),
		filepath.Join("pkg", libgodir, "libgo.h"),
		"go", "install", "-i", "-buildmode=c-archive", "libgo")

	// Test building libgo other than installing it.
	// Header files are now present.
	testInstall(t, "./testp2"+exeSuffix, "libgo.a", "libgo.h",
		"go", "build", "-buildmode=c-archive", filepath.Join("src", "libgo", "libgo.go"))

	testInstall(t, "./testp3"+exeSuffix, "libgo.a", "libgo.h",
		"go", "build", "-buildmode=c-archive", "-o", "libgo.a", "libgo")
}

func TestEarlySignalHandler(t *testing.T) {
	switch GOOS {
	case "darwin":
		switch GOARCH {
		case "arm", "arm64":
			t.Skipf("skipping on %s/%s; see https://golang.org/issue/13701", GOOS, GOARCH)
		}
	case "windows":
		t.Skip("skipping signal test on Windows")
	}

	defer func() {
		os.Remove("libgo2.a")
		os.Remove("libgo2.h")
		os.Remove("testp")
		os.RemoveAll("pkg")
	}()

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo2.a", "libgo2")
	cmd.Env = gopathEnv
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	ccArgs := append(cc, "-o", "testp"+exeSuffix, "main2.c", "libgo2.a")
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if out, err := exec.Command(bin[0], bin[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
}

func TestSignalForwarding(t *testing.T) {
	checkSignalForwardingTest(t)

	defer func() {
		os.Remove("libgo2.a")
		os.Remove("libgo2.h")
		os.Remove("testp")
		os.RemoveAll("pkg")
	}()

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo2.a", "libgo2")
	cmd.Env = gopathEnv
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	ccArgs := append(cc, "-o", "testp"+exeSuffix, "main5.c", "libgo2.a")
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	cmd = exec.Command(bin[0], append(bin[1:], "1")...)

	out, err := cmd.CombinedOutput()
	t.Logf("%s", out)
	expectSignal(t, err, syscall.SIGSEGV)

	// Test SIGPIPE forwarding
	cmd = exec.Command(bin[0], append(bin[1:], "3")...)

	out, err = cmd.CombinedOutput()
	t.Logf("%s", out)
	expectSignal(t, err, syscall.SIGPIPE)
}

func TestSignalForwardingExternal(t *testing.T) {
	checkSignalForwardingTest(t)

	defer func() {
		os.Remove("libgo2.a")
		os.Remove("libgo2.h")
		os.Remove("testp")
		os.RemoveAll("pkg")
	}()

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo2.a", "libgo2")
	cmd.Env = gopathEnv
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	ccArgs := append(cc, "-o", "testp"+exeSuffix, "main5.c", "libgo2.a")
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	// We want to send the process a signal and see if it dies.
	// Normally the signal goes to the C thread, the Go signal
	// handler picks it up, sees that it is running in a C thread,
	// and the program dies. Unfortunately, occasionally the
	// signal is delivered to a Go thread, which winds up
	// discarding it because it was sent by another program and
	// there is no Go handler for it. To avoid this, run the
	// program several times in the hopes that it will eventually
	// fail.
	const tries = 20
	for i := 0; i < tries; i++ {
		cmd = exec.Command(bin[0], append(bin[1:], "2")...)

		stderr, err := cmd.StderrPipe()
		if err != nil {
			t.Fatal(err)
		}
		defer stderr.Close()

		r := bufio.NewReader(stderr)

		err = cmd.Start()

		if err != nil {
			t.Fatal(err)
		}

		// Wait for trigger to ensure that the process is started.
		ok, err := r.ReadString('\n')

		// Verify trigger.
		if err != nil || ok != "OK\n" {
			t.Fatalf("Did not receive OK signal")
		}

		// Give the program a chance to enter the sleep function.
		time.Sleep(time.Millisecond)

		cmd.Process.Signal(syscall.SIGSEGV)

		err = cmd.Wait()

		if err == nil {
			continue
		}

		if expectSignal(t, err, syscall.SIGSEGV) {
			return
		}
	}

	t.Errorf("program succeeded unexpectedly %d times", tries)
}

// checkSignalForwardingTest calls t.Skip if the SignalForwarding test
// doesn't work on this platform.
func checkSignalForwardingTest(t *testing.T) {
	switch GOOS {
	case "darwin":
		switch GOARCH {
		case "arm", "arm64":
			t.Skipf("skipping on %s/%s; see https://golang.org/issue/13701", GOOS, GOARCH)
		}
	case "windows":
		t.Skip("skipping signal test on Windows")
	}
}

// expectSignal checks that err, the exit status of a test program,
// shows a failure due to a specific signal. Returns whether we found
// the expected signal.
func expectSignal(t *testing.T, err error, sig syscall.Signal) bool {
	if err == nil {
		t.Error("test program succeeded unexpectedly")
	} else if ee, ok := err.(*exec.ExitError); !ok {
		t.Errorf("error (%v) has type %T; expected exec.ExitError", err, err)
	} else if ws, ok := ee.Sys().(syscall.WaitStatus); !ok {
		t.Errorf("error.Sys (%v) has type %T; expected syscall.WaitStatus", ee.Sys(), ee.Sys())
	} else if !ws.Signaled() || ws.Signal() != sig {
		t.Errorf("got %v; expected signal %v", ee, sig)
	} else {
		return true
	}
	return false
}

func TestOsSignal(t *testing.T) {
	switch GOOS {
	case "windows":
		t.Skip("skipping signal test on Windows")
	}

	defer func() {
		os.Remove("libgo3.a")
		os.Remove("libgo3.h")
		os.Remove("testp")
		os.RemoveAll("pkg")
	}()

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo3.a", "libgo3")
	cmd.Env = gopathEnv
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	ccArgs := append(cc, "-o", "testp"+exeSuffix, "main3.c", "libgo3.a")
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if out, err := exec.Command(bin[0], bin[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
}

func TestSigaltstack(t *testing.T) {
	switch GOOS {
	case "windows":
		t.Skip("skipping signal test on Windows")
	}

	defer func() {
		os.Remove("libgo4.a")
		os.Remove("libgo4.h")
		os.Remove("testp")
		os.RemoveAll("pkg")
	}()

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo4.a", "libgo4")
	cmd.Env = gopathEnv
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	ccArgs := append(cc, "-o", "testp"+exeSuffix, "main4.c", "libgo4.a")
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if out, err := exec.Command(bin[0], bin[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
}

const testar = `#!/usr/bin/env bash
while expr $1 : '[-]' >/dev/null; do
  shift
done
echo "testar" > $1
echo "testar" > PWD/testar.ran
`

func TestExtar(t *testing.T) {
	switch GOOS {
	case "windows":
		t.Skip("skipping signal test on Windows")
	}

	defer func() {
		os.Remove("libgo4.a")
		os.Remove("libgo4.h")
		os.Remove("testar")
		os.Remove("testar.ran")
		os.RemoveAll("pkg")
	}()

	os.Remove("testar")
	dir, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	s := strings.Replace(testar, "PWD", dir, 1)
	if err := ioutil.WriteFile("testar", []byte(s), 0777); err != nil {
		t.Fatal(err)
	}

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-ldflags=-extar="+filepath.Join(dir, "testar"), "-o", "libgo4.a", "libgo4")
	cmd.Env = gopathEnv
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if _, err := os.Stat("testar.ran"); err != nil {
		if os.IsNotExist(err) {
			t.Error("testar does not exist after go build")
		} else {
			t.Errorf("error checking testar: %v", err)
		}
	}
}

func TestPIE(t *testing.T) {
	switch GOOS {
	case "windows", "darwin", "plan9":
		t.Skipf("skipping PIE test on %s", GOOS)
	}

	defer func() {
		os.Remove("testp" + exeSuffix)
		os.RemoveAll("pkg")
	}()

	cmd := exec.Command("go", "install", "-i", "-buildmode=c-archive", "libgo")
	cmd.Env = gopathEnv
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	ccArgs := append(cc, "-fPIE", "-pie", "-o", "testp"+exeSuffix, "main.c", "main_unix.c", filepath.Join("pkg", libgodir, "libgo.a"))
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	binArgs := append(bin, "arg1", "arg2")
	if out, err := exec.Command(binArgs[0], binArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	f, err := elf.Open("testp" + exeSuffix)
	if err != nil {
		t.Fatal("elf.Open failed: ", err)
	}
	defer f.Close()
	if hasDynTag(t, f, elf.DT_TEXTREL) {
		t.Errorf("%s has DT_TEXTREL flag", "testp"+exeSuffix)
	}
}

func hasDynTag(t *testing.T, f *elf.File, tag elf.DynTag) bool {
	ds := f.SectionByType(elf.SHT_DYNAMIC)
	if ds == nil {
		t.Error("no SHT_DYNAMIC section")
		return false
	}
	d, err := ds.Data()
	if err != nil {
		t.Errorf("can't read SHT_DYNAMIC contents: %v", err)
		return false
	}
	for len(d) > 0 {
		var t elf.DynTag
		switch f.Class {
		case elf.ELFCLASS32:
			t = elf.DynTag(f.ByteOrder.Uint32(d[:4]))
			d = d[8:]
		case elf.ELFCLASS64:
			t = elf.DynTag(f.ByteOrder.Uint64(d[:8]))
			d = d[16:]
		}
		if t == tag {
			return true
		}
	}
	return false
}

func TestSIGPROF(t *testing.T) {
	switch GOOS {
	case "windows", "plan9":
		t.Skipf("skipping SIGPROF test on %s", GOOS)
	case "darwin":
		t.Skipf("skipping SIGPROF test on %s; see https://golang.org/issue/19320", GOOS)
	}

	t.Parallel()

	defer func() {
		os.Remove("testp6" + exeSuffix)
		os.Remove("libgo6.a")
		os.Remove("libgo6.h")
	}()

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo6.a", "libgo6")
	cmd.Env = gopathEnv
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	ccArgs := append(cc, "-o", "testp6"+exeSuffix, "main6.c", "libgo6.a")
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	argv := cmdToRun("./testp6")
	cmd = exec.Command(argv[0], argv[1:]...)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
}

// TestCompileWithoutShared tests that if we compile code without the
// -shared option, we can put it into an archive. When we use the go
// tool with -buildmode=c-archive, it passes -shared to the compiler,
// so we override that. The go tool doesn't work this way, but Bazel
// will likely do it in the future. And it ought to work. This test
// was added because at one time it did not work on PPC GNU/Linux.
func TestCompileWithoutShared(t *testing.T) {
	// For simplicity, reuse the signal forwarding test.
	checkSignalForwardingTest(t)

	defer func() {
		os.Remove("libgo2.a")
		os.Remove("libgo2.h")
	}()

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-gcflags=-shared=false", "-o", "libgo2.a", "libgo2")
	cmd.Env = gopathEnv
	t.Log(cmd.Args)
	out, err := cmd.CombinedOutput()
	t.Logf("%s", out)
	if err != nil {
		t.Fatal(err)
	}

	exe := "./testnoshared" + exeSuffix

	// In some cases, -no-pie is needed here, but not accepted everywhere. First try
	// if -no-pie is accepted. See #22126.
	ccArgs := append(cc, "-o", exe, "-no-pie", "main5.c", "libgo2.a")
	t.Log(ccArgs)
	out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()

	// If -no-pie unrecognized, try -nopie if this is possibly clang
	if err != nil && bytes.Contains(out, []byte("unknown")) && !strings.Contains(cc[0], "gcc") {
		ccArgs = append(cc, "-o", exe, "-nopie", "main5.c", "libgo2.a")
		t.Log(ccArgs)
		out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()
	}

	// Don't use either -no-pie or -nopie
	if err != nil && bytes.Contains(out, []byte("unrecognized")) {
		ccArgs := append(cc, "-o", exe, "main5.c", "libgo2.a")
		t.Log(ccArgs)
		out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()
	}
	t.Logf("%s", out)
	if err != nil {
		t.Fatal(err)
	}
	defer os.Remove(exe)

	binArgs := append(cmdToRun(exe), "3")
	t.Log(binArgs)
	out, err = exec.Command(binArgs[0], binArgs[1:]...).CombinedOutput()
	t.Logf("%s", out)
	expectSignal(t, err, syscall.SIGPIPE)
}

// Test that installing a second time recreates the header files.
func TestCachedInstall(t *testing.T) {
	defer os.RemoveAll("pkg")

	h1 := filepath.Join("pkg", libgodir, "libgo.h")
	h2 := filepath.Join("pkg", libgodir, "p.h")

	buildcmd := []string{"go", "install", "-i", "-buildmode=c-archive", "libgo"}

	cmd := exec.Command(buildcmd[0], buildcmd[1:]...)
	cmd.Env = gopathEnv
	t.Log(buildcmd)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if _, err := os.Stat(h1); err != nil {
		t.Errorf("libgo.h not installed: %v", err)
	}
	if _, err := os.Stat(h2); err != nil {
		t.Errorf("p.h not installed: %v", err)
	}

	if err := os.Remove(h1); err != nil {
		t.Fatal(err)
	}
	if err := os.Remove(h2); err != nil {
		t.Fatal(err)
	}

	cmd = exec.Command(buildcmd[0], buildcmd[1:]...)
	cmd.Env = gopathEnv
	t.Log(buildcmd)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if _, err := os.Stat(h1); err != nil {
		t.Errorf("libgo.h not installed in second run: %v", err)
	}
	if _, err := os.Stat(h2); err != nil {
		t.Errorf("p.h not installed in second run: %v", err)
	}
}
