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 // Commit is set for a program a commit of the branch added and a later one removed (or replaced): the merge's tree is clean, // but the merge commit carries the blob into the trunk's history for good. Empty for a program the merge's tree holds. Commit string `json:"commit,omitempty"` } // 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 // historyPrograms names the compiled programs that a commit of base..head added or modified and the merge's tree no longer // holds at that path (those it holds are in final already): each with the commit that added it. git is the merge's git runner. func historyPrograms(git func(args ...string) (string, error), base, head string, final []Program) ([]Program, error) { revs, err := git("rev-list", "--no-merges", "--reverse", base+".."+head) if err != nil { return nil, err } held := map[string]bool{} for _, p := range final { held[p.Path] = true } out, seen := []Program{}, map[string]bool{} for _, c := range strings.Fields(revs) { raw, err := git("diff-tree", "-r", "-z", "--no-commit-id", "--no-renames", "--root", "--diff-filter=AM", c) if err != nil { return nil, err } f := strings.Split(raw, "\x00") for i := 0; i+1 < len(f); i += 2 { meta := strings.Fields(strings.TrimPrefix(f[i], ":")) if len(meta) < 4 || (meta[1] != "100644" && meta[1] != "100755") || held[f[i+1]] || seen[meta[3]] { continue } seen[meta[3]] = true blob, err := git("cat-file", "blob", meta[3]) if err != nil { return nil, err } head := []byte(blob) if len(head) > programHeadSize { head = head[:programHeadSize] } if k := ExecutableKind(head); k != "" { out = append(out, Program{Path: f[i+1], Kind: k, Commit: c}) } } } return out, nil } // 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 }