package buildstep import ( "bytes" "io" "os" "path/filepath" "strings" ) // Program is a changed file whose first bytes are an executable binary: a compiled program, which is build output and not // source (novox/hq issue 1577: `go build` in a module's folder writes one beside go.mod, and `git add -A` takes it). type Program struct { Path string `json:"path"` Kind string `json:"kind"` // ELF, Mach-O or PE } // ExecutableKind says what executable format the first bytes of a file are, or "" when they are none. A PE needs its header // ("PE\0\0" at the offset in bytes 0x3c) so a text starting with "MZ" is not one; a fat Mach-O (0xCAFEBABE) is told from a // Java class by its architecture count (bytes 4 to 7, fewer than 0x20; a class has its version there). func ExecutableKind(head []byte) string { switch { case bytes.HasPrefix(head, []byte("\x7fELF")): return "ELF" case bytes.HasPrefix(head, []byte("MZ")): if len(head) >= 0x40 { off := int(head[0x3c]) | int(head[0x3d])<<8 | int(head[0x3e])<<16 | int(head[0x3f])<<24 if off >= 0 && off+4 <= len(head) && bytes.Equal(head[off:off+4], []byte("PE\x00\x00")) { return "PE" } } case bytes.HasPrefix(head, []byte{0xcf, 0xfa, 0xed, 0xfe}), bytes.HasPrefix(head, []byte{0xce, 0xfa, 0xed, 0xfe}), bytes.HasPrefix(head, []byte{0xfe, 0xed, 0xfa, 0xcf}), bytes.HasPrefix(head, []byte{0xfe, 0xed, 0xfa, 0xce}), bytes.HasPrefix(head, []byte{0xca, 0xfe, 0xba, 0xbe}) && len(head) >= 8 && head[4] == 0 && head[5] == 0 && head[6] == 0 && head[7] < 0x20: return "Mach-O" } return "" } // programHeadSize is how many bytes of a file are read to tell its format: enough for a PE header at its usual offsets. const programHeadSize = 0x100 // programsIn reads the output of `git diff --raw -z --no-renames --diff-filter=AM` (": \0\0" per file) and names each added or modified regular file of the tree dir that is a compiled // program, from its first bytes read locally. A symlink (mode 120000) and a submodule (160000) are not read. It never // answers nil, so a caller can tell no programs from none reported. func programsIn(dir, raw string) []Program { out := []Program{} f := strings.Split(raw, "\x00") for i := 0; i+1 < len(f); i += 2 { meta := strings.Fields(strings.TrimPrefix(f[i], ":")) if len(meta) < 2 || (meta[1] != "100644" && meta[1] != "100755") { continue } p := f[i+1] full := filepath.Join(dir, filepath.FromSlash(p)) if st, err := os.Lstat(full); err != nil || !st.Mode().IsRegular() { continue } fh, err := os.Open(full) if err != nil { continue } head := make([]byte, programHeadSize) n, _ := io.ReadFull(fh, head) fh.Close() if k := ExecutableKind(head[:n]); k != "" { out = append(out, Program{Path: p, Kind: k}) } } return out }