package executable

import (
	"debug/macho"

	"github.com/anchore/syft/internal"
	"github.com/anchore/syft/syft/file"
	"github.com/anchore/syft/syft/internal/unionreader"
)

// source http://www.cilinder.be/docs/next/NeXTStep/3.3/nd/DevTools/14_MachO/MachO.htmld/index.html
const (
	machoNPExt uint8 = 0x10 /* N_PEXT: private external symbol bit */
	machoNExt  uint8 = 0x01 /* N_EXT: external symbol bit, set for external symbols */
	// > #define LC_REQ_DYLD 0x80000000
	// > #define LC_MAIN (0x28|LC_REQ_DYLD) /* replacement for LC_UNIXTHREAD */
	lcMain = 0x28 | 0x80000000
)

func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) error {
	// TODO: support security features

	// a universal binary may have multiple architectures, so we need to check each one
	readers, err := unionreader.GetReaders(reader)
	if err != nil {
		return err
	}

	var libs []string
	for _, r := range readers {
		f, err := macho.NewFile(r)
		if err != nil {
			return err
		}

		rLibs, err := f.ImportedLibraries()
		if err != nil {
			return err
		}
		libs = append(libs, rLibs...)

		// TODO handle only some having entrypoints/exports? If that is even practical
		// only check for entrypoint if we don't already have one
		if !data.HasEntrypoint {
			data.HasEntrypoint = machoHasEntrypoint(f)
		}
		// only check for exports if we don't already have them
		if !data.HasExports {
			data.HasExports = machoHasExports(f)
		}
	}

	// de-duplicate libraries
	data.ImportedLibraries = internal.NewSet(libs...).ToSlice()

	return nil
}

func machoHasEntrypoint(f *macho.File) bool {
	// derived from struct entry_point_command found from which explicitly calls out LC_MAIN:
	// https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h
	// we need to look for both LC_MAIN and LC_UNIXTHREAD commands to determine if the file is an executable
	//
	// this is akin to:
	//    otool -l ./path/to/bin | grep -A4 LC_MAIN
	//    otool -l ./path/to/bin | grep -A4 LC_UNIXTHREAD
	for _, l := range f.Loads {
		data := l.Raw()
		cmd := f.ByteOrder.Uint32(data)

		if macho.LoadCmd(cmd) == macho.LoadCmdUnixThread || macho.LoadCmd(cmd) == lcMain {
			return true
		}
	}
	return false
}

func machoHasExports(f *macho.File) bool {
	if f == nil || f.Symtab == nil {
		return false
	}
	for _, sym := range f.Symtab.Syms {
		// look for symbols that are:
		//  - not private and are external
		//  - do not have an N_TYPE value of N_UNDF (undefined symbol)
		//
		// here's the bit layout for the n_type field:
		// 0000 0000
		// ─┬─│ ─┬─│
		//  │ │  │ └─ N_EXT (external symbol)
		//  │ │  └─ N_TYPE (N_UNDF, N_ABS, N_SECT, N_PBUD, N_INDR)
		//  │ └─ N_PEXT (private external symbol)
		//  └─ N_STAB (debugging symbol)
		//
		isExternal := sym.Type&machoNExt == machoNExt
		isPrivate := sym.Type&machoNPExt == machoNPExt
		nTypeIsUndefined := sym.Type&0x0e == 0

		if isExternal && !isPrivate {
			if sym.Name == "_main" || sym.Name == "__mh_execute_header" {
				// ...however there are some symbols that are not exported but are still important
				// for debugging or as an entrypoint, so we need to explicitly check for them
				continue
			}
			if nTypeIsUndefined {
				continue
			}
			// we have a symbol that is not private and is external
			// and is not undefined, so it is an export
			return true
		}
	}
	return false
}
