// Copyright 2014 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 main

import (
	"bytes"
	"debug/macho"
	"encoding/binary"
	"fmt"
	"io/ioutil"
	"strings"
	"testing"
)

// Test macho writing by checking that each generated prog can be written
// and then read back using debug/macho to get the same prog.
// Also check against golden testdata file.
var machoWriteTests = []struct {
	name   string
	golden bool
	prog   *Prog
}{
	// amd64 exit 9
	{
		name:   "exit9",
		golden: true,
		prog: &Prog{
			GOARCH:       "amd64",
			GOOS:         "darwin",
			UnmappedSize: 0x1000,
			Entry:        0x1000,
			Segments: []*Segment{
				{
					Name:       "text",
					VirtAddr:   0x1000,
					VirtSize:   13,
					FileOffset: 0,
					FileSize:   13,
					Data: []byte{
						0xb8, 0x01, 0x00, 0x00, 0x02, // MOVL $0x2000001, AX
						0xbf, 0x09, 0x00, 0x00, 0x00, // MOVL $9, DI
						0x0f, 0x05, // SYSCALL
						0xf4, // HLT
					},
					Sections: []*Section{
						{
							Name:     "text",
							VirtAddr: 0x1000,
							Size:     13,
							Align:    64,
						},
					},
				},
			},
		},
	},

	// amd64 write hello world & exit 9
	{
		name:   "hello",
		golden: true,
		prog: &Prog{
			GOARCH:       "amd64",
			GOOS:         "darwin",
			UnmappedSize: 0x1000,
			Entry:        0x1000,
			Segments: []*Segment{
				{
					Name:       "text",
					VirtAddr:   0x1000,
					VirtSize:   35,
					FileOffset: 0,
					FileSize:   35,
					Data: []byte{
						0xb8, 0x04, 0x00, 0x00, 0x02, // MOVL $0x2000001, AX
						0xbf, 0x01, 0x00, 0x00, 0x00, // MOVL $1, DI
						0xbe, 0x00, 0x30, 0x00, 0x00, // MOVL $0x3000, SI
						0xba, 0x0c, 0x00, 0x00, 0x00, // MOVL $12, DX
						0x0f, 0x05, // SYSCALL
						0xb8, 0x01, 0x00, 0x00, 0x02, // MOVL $0x2000001, AX
						0xbf, 0x09, 0x00, 0x00, 0x00, // MOVL $9, DI
						0x0f, 0x05, // SYSCALL
						0xf4, // HLT
					},
					Sections: []*Section{
						{
							Name:     "text",
							VirtAddr: 0x1000,
							Size:     35,
							Align:    64,
						},
					},
				},
				{
					Name:       "data",
					VirtAddr:   0x2000,
					VirtSize:   12,
					FileOffset: 0x1000,
					FileSize:   12,
					Data:       []byte("hello world\n"),
					Sections: []*Section{
						{
							Name:     "data",
							VirtAddr: 0x2000,
							Size:     12,
							Align:    64,
						},
					},
				},
			},
		},
	},

	// amd64 write hello world from rodata & exit 0
	{
		name:   "helloro",
		golden: true,
		prog: &Prog{
			GOARCH:       "amd64",
			GOOS:         "darwin",
			UnmappedSize: 0x1000,
			Entry:        0x1000,
			Segments: []*Segment{
				{
					Name:       "text",
					VirtAddr:   0x1000,
					VirtSize:   0x100c,
					FileOffset: 0,
					FileSize:   0x100c,
					Data: concat(
						[]byte{
							0xb8, 0x04, 0x00, 0x00, 0x02, // MOVL $0x2000001, AX
							0xbf, 0x01, 0x00, 0x00, 0x00, // MOVL $1, DI
							0xbe, 0x00, 0x30, 0x00, 0x00, // MOVL $0x3000, SI
							0xba, 0x0c, 0x00, 0x00, 0x00, // MOVL $12, DX
							0x0f, 0x05, // SYSCALL
							0xb8, 0x01, 0x00, 0x00, 0x02, // MOVL $0x2000001, AX
							0xbf, 0x00, 0x00, 0x00, 0x00, // MOVL $0, DI
							0x0f, 0x05, // SYSCALL
							0xf4, // HLT
						},
						make([]byte, 0x1000-35),
						[]byte("hello world\n"),
					),
					Sections: []*Section{
						{
							Name:     "text",
							VirtAddr: 0x1000,
							Size:     35,
							Align:    64,
						},
						{
							Name:     "rodata",
							VirtAddr: 0x2000,
							Size:     12,
							Align:    64,
						},
					},
				},
			},
		},
	},
}

func concat(xs ...[]byte) []byte {
	var out []byte
	for _, x := range xs {
		out = append(out, x...)
	}
	return out
}

func TestMachoWrite(t *testing.T) {
	for _, tt := range machoWriteTests {
		name := tt.prog.GOARCH + "." + tt.name
		prog := cloneProg(tt.prog)
		prog.init()
		var f machoFormat
		vsize, fsize := f.headerSize(prog)
		shiftProg(prog, vsize, fsize)
		var buf bytes.Buffer
		f.write(&buf, prog)
		if false { // enable to debug
			ioutil.WriteFile("a.out", buf.Bytes(), 0777)
		}
		read, err := machoRead(machoArches[tt.prog.GOARCH], buf.Bytes())
		if err != nil {
			t.Errorf("%s: reading mach-o output:\n\t%v", name, err)
			continue
		}
		diffs := diffProg(read, prog)
		if diffs != nil {
			t.Errorf("%s: mismatched prog:\n\t%s", name, strings.Join(diffs, "\n\t"))
			continue
		}
		if !tt.golden {
			continue
		}
		checkGolden(t, buf.Bytes(), "testdata/macho."+name)
	}
}

// machoRead reads the mach-o file in data and returns a corresponding prog.
func machoRead(arch machoArch, data []byte) (*Prog, error) {
	f, err := macho.NewFile(bytes.NewReader(data))
	if err != nil {
		return nil, err
	}

	var errors []string
	errorf := func(format string, args ...interface{}) {
		errors = append(errors, fmt.Sprintf(format, args...))
	}

	magic := uint32(0xFEEDFACE)
	if arch.CPU&macho64Bit != 0 {
		magic |= 1
	}
	if f.Magic != magic {
		errorf("header: Magic = %#x, want %#x", f.Magic, magic)
	}
	if f.Cpu != macho.CpuAmd64 {
		errorf("header: CPU = %#x, want %#x", f.Cpu, macho.CpuAmd64)
	}
	if f.SubCpu != 3 {
		errorf("header: SubCPU = %#x, want %#x", f.SubCpu, 3)
	}
	if f.Type != 2 {
		errorf("header: FileType = %d, want %d", f.Type, 2)
	}
	if f.Flags != 1 {
		errorf("header: Flags = %d, want %d", f.Flags, 1)
	}

	msects := f.Sections
	var limit uint64
	prog := new(Prog)
	for _, load := range f.Loads {
		switch load := load.(type) {
		default:
			errorf("unexpected macho load %T %x", load, load.Raw())

		case macho.LoadBytes:
			if len(load) < 8 || len(load)%4 != 0 {
				errorf("unexpected load length %d", len(load))
				continue
			}
			cmd := f.ByteOrder.Uint32(load)
			switch macho.LoadCmd(cmd) {
			default:
				errorf("unexpected macho load cmd %s", macho.LoadCmd(cmd))
			case macho.LoadCmdUnixThread:
				data := make([]uint32, len(load[8:])/4)
				binary.Read(bytes.NewReader(load[8:]), f.ByteOrder, data)
				if len(data) != 44 {
					errorf("macho thread len(data) = %d, want 42", len(data))
					continue
				}
				if data[0] != 4 {
					errorf("macho thread type = %d, want 4", data[0])
				}
				if data[1] != uint32(len(data))-2 {
					errorf("macho thread desc len = %d, want %d", data[1], uint32(len(data))-2)
					continue
				}
				for i, val := range data[2:] {
					switch i {
					default:
						if val != 0 {
							errorf("macho thread data[%d] = %#x, want 0", i, val)
						}
					case 32:
						prog.Entry = Addr(val)
					case 33:
						prog.Entry |= Addr(val) << 32
					}
				}
			}

		case *macho.Segment:
			if load.Addr < limit {
				errorf("segments out of order: %q at %#x after %#x", load.Name, load.Addr, limit)
			}
			limit = load.Addr + load.Memsz
			if load.Name == "__PAGEZERO" || load.Addr == 0 && load.Filesz == 0 {
				if load.Name != "__PAGEZERO" {
					errorf("segment with Addr=0, Filesz=0 is named %q, want %q", load.Name, "__PAGEZERO")
				} else if load.Addr != 0 || load.Filesz != 0 {
					errorf("segment %q has Addr=%#x, Filesz=%d, want Addr=%#x, Filesz=%d", load.Name, load.Addr, load.Filesz, 0, 0)
				}
				prog.UnmappedSize = Addr(load.Memsz)
				continue
			}

			if !strings.HasPrefix(load.Name, "__") {
				errorf("segment name %q does not begin with %q", load.Name, "__")
			}
			if strings.ToUpper(load.Name) != load.Name {
				errorf("segment name %q is not all upper case", load.Name)
			}

			seg := &Segment{
				Name:       strings.ToLower(strings.TrimPrefix(load.Name, "__")),
				VirtAddr:   Addr(load.Addr),
				VirtSize:   Addr(load.Memsz),
				FileOffset: Addr(load.Offset),
				FileSize:   Addr(load.Filesz),
			}
			prog.Segments = append(prog.Segments, seg)

			data, err := load.Data()
			if err != nil {
				errorf("loading data from %q: %v", load.Name, err)
			}
			seg.Data = data

			var maxprot, prot uint32
			if load.Name == "__TEXT" {
				maxprot, prot = 7, 5
			} else {
				maxprot, prot = 3, 3
			}
			if load.Maxprot != maxprot || load.Prot != prot {
				errorf("segment %q protection is %d, %d, want %d, %d",
					load.Name, load.Maxprot, load.Prot, maxprot, prot)
			}

			for len(msects) > 0 && msects[0].Addr < load.Addr+load.Memsz {
				msect := msects[0]
				msects = msects[1:]

				if msect.Offset > 0 && prog.HeaderSize == 0 {
					prog.HeaderSize = Addr(msect.Offset)
					if seg.FileOffset != 0 {
						errorf("initial segment %q does not map header", load.Name)
					}
					seg.VirtAddr += prog.HeaderSize
					seg.VirtSize -= prog.HeaderSize
					seg.FileOffset += prog.HeaderSize
					seg.FileSize -= prog.HeaderSize
					seg.Data = seg.Data[prog.HeaderSize:]
				}

				if msect.Addr < load.Addr {
					errorf("section %q at address %#x is missing segment", msect.Name, msect.Addr)
					continue
				}

				if !strings.HasPrefix(msect.Name, "__") {
					errorf("section name %q does not begin with %q", msect.Name, "__")
				}
				if strings.ToLower(msect.Name) != msect.Name {
					errorf("section name %q is not all lower case", msect.Name)
				}
				if msect.Seg != load.Name {
					errorf("section %q is lists segment name %q, want %q",
						msect.Name, msect.Seg, load.Name)
				}
				if uint64(msect.Offset) != uint64(load.Offset)+msect.Addr-load.Addr {
					errorf("section %q file offset is %#x, want %#x",
						msect.Name, msect.Offset, load.Offset+msect.Addr-load.Addr)
				}
				if msect.Reloff != 0 || msect.Nreloc != 0 {
					errorf("section %q has reloff %d,%d, want %d,%d",
						msect.Name, msect.Reloff, msect.Nreloc, 0, 0)
				}
				flags := uint32(0)
				if msect.Name == "__text" {
					flags = 0x400
				}
				if msect.Offset == 0 {
					flags = 1
				}
				if msect.Flags != flags {
					errorf("section %q flags = %#x, want %#x", msect.Name, msect.Flags, flags)
				}
				sect := &Section{
					Name:     strings.ToLower(strings.TrimPrefix(msect.Name, "__")),
					VirtAddr: Addr(msect.Addr),
					Size:     Addr(msect.Size),
					Align:    1 << msect.Align,
				}
				seg.Sections = append(seg.Sections, sect)
			}
		}
	}

	for _, msect := range msects {
		errorf("section %q has no segment", msect.Name)
	}

	limit = 0
	for _, msect := range f.Sections {
		if msect.Addr < limit {
			errorf("sections out of order: %q at %#x after %#x", msect.Name, msect.Addr, limit)
		}
		limit = msect.Addr + msect.Size
	}

	err = nil
	if errors != nil {
		err = fmt.Errorf("%s", strings.Join(errors, "\n\t"))
	}
	return prog, err
}
