]> Cypherpunks repositories - gostls13.git/commitdiff
debug/buildinfo: new package with Read and ReadFile
authorJay Conrod <jayconrod@google.com>
Thu, 30 Sep 2021 17:25:49 +0000 (10:25 -0700)
committerJay Conrod <jayconrod@google.com>
Thu, 14 Oct 2021 18:44:08 +0000 (18:44 +0000)
These functions provide access to module information stamped into Go
binaries. In the future, they'll provide access to other information
(like VCS info).

These functions are added in a new package instead of runtime/debug
since they use binary parsing packages like debug/elf, which would
make runtime/debug an unacceptably heavy dependency. The types in
runtime/debug are still used; debug/buildinfo uses them via type
aliases.

This information is already available for the running binary through
debug.ReadBuildInfo and for other binaries with 'go version -m', but
until now, there hasn't been a way to get it for other binaries
without installing cmd/go.

This change copies most of the code in cmd/go/internal/version. A
later CL will migrate 'go version -m' to use this package.

For #37475
Fixes #39301

Change-Id: I0fbe0896e04f12ef81c6d79fb61b20daede86159
Reviewed-on: https://go-review.googlesource.com/c/go/+/353887
Trust: Jay Conrod <jayconrod@google.com>
Run-TryBot: Jay Conrod <jayconrod@google.com>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
api/next.txt
src/debug/buildinfo/buildinfo.go [new file with mode: 0644]
src/debug/buildinfo/buildinfo_test.go [new file with mode: 0644]
src/go/build/deps_test.go
src/runtime/debug/mod.go

index cb729ea72f92e5e7450d9a3ea0b2c63d9e755d8f..0a976d7b19fa8478be747325fc4de8a9141d8b4b 100644 (file)
@@ -1,4 +1,8 @@
+pkg debug/buildinfo, func Read(io.ReaderAt) (*debug.BuildInfo, error)
+pkg debug/buildinfo, func ReadFile(string) (*debug.BuildInfo, error)
+pkg debug/buildinfo, type BuildInfo = debug.BuildInfo
 pkg runtime/debug, method (*BuildInfo) MarshalText() ([]byte, error)
+pkg runtime/debug, method (*BuildInfo) UnmarshalText() ([]byte, error)
 pkg syscall (darwin-amd64), func RecvfromInet4(int, []uint8, int, *SockaddrInet4) (int, error)
 pkg syscall (darwin-amd64), func RecvfromInet6(int, []uint8, int, *SockaddrInet6) (int, error)
 pkg syscall (darwin-amd64), func SendtoInet4(int, []uint8, int, SockaddrInet4) error
diff --git a/src/debug/buildinfo/buildinfo.go b/src/debug/buildinfo/buildinfo.go
new file mode 100644 (file)
index 0000000..8def2ea
--- /dev/null
@@ -0,0 +1,374 @@
+// Copyright 2021 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 buildinfo provides access to information embedded in a Go binary
+// about how it was built. This includes the Go toolchain version, and the
+// set of modules used (for binaries built in module mode).
+//
+// Build information is available for the currently running binary in
+// runtime/debug.ReadBuildInfo.
+package buildinfo
+
+import (
+       "bytes"
+       "debug/elf"
+       "debug/macho"
+       "debug/pe"
+       "encoding/binary"
+       "errors"
+       "fmt"
+       "internal/xcoff"
+       "io"
+       "io/fs"
+       "os"
+       "runtime/debug"
+)
+
+// Type alias for build info. We cannot move the types here, since
+// runtime/debug would need to import this package, which would make it
+// a much larger dependency.
+type BuildInfo = debug.BuildInfo
+
+var (
+       // errUnrecognizedFormat is returned when a given executable file doesn't
+       // appear to be in a known format, or it breaks the rules of that format,
+       // or when there are I/O errors reading the file.
+       errUnrecognizedFormat = errors.New("unrecognized file format")
+
+       // errNotGoExe is returned when a given executable file is valid but does
+       // not contain Go build information.
+       errNotGoExe = errors.New("not a Go executable")
+
+       // The build info blob left by the linker is identified by
+       // a 16-byte header, consisting of buildInfoMagic (14 bytes),
+       // the binary's pointer size (1 byte),
+       // and whether the binary is big endian (1 byte).
+       buildInfoMagic = []byte("\xff Go buildinf:")
+)
+
+// ReadFile returns build information embedded in a Go binary
+// file at the given path. Most information is only available for binaries built
+// with module support.
+func ReadFile(name string) (info *BuildInfo, err error) {
+       defer func() {
+               if pathErr := (*fs.PathError)(nil); errors.As(err, &pathErr) {
+                       err = fmt.Errorf("could not read Go build info: %w", err)
+               } else if err != nil {
+                       err = fmt.Errorf("could not read Go build info from %s: %w", name, err)
+               }
+       }()
+
+       f, err := os.Open(name)
+       if err != nil {
+               return nil, err
+       }
+       defer f.Close()
+       return Read(f)
+}
+
+// Read returns build information embedded in a Go binary file
+// accessed through the given ReaderAt. Most information is only available for
+// binaries built with module support.
+func Read(r io.ReaderAt) (*BuildInfo, error) {
+       _, mod, err := readRawBuildInfo(r)
+       if err != nil {
+               return nil, err
+       }
+       bi := &BuildInfo{}
+       if err := bi.UnmarshalText([]byte(mod)); err != nil {
+               return nil, err
+       }
+       return bi, nil
+}
+
+type exe interface {
+       // ReadData reads and returns up to size bytes starting at virtual address addr.
+       ReadData(addr, size uint64) ([]byte, error)
+
+       // DataStart returns the virtual address of the segment or section that
+       // should contain build information. This is either a specially named section
+       // or the first writable non-zero data segment.
+       DataStart() uint64
+}
+
+// readRawBuildInfo extracts the Go toolchain version and module information
+// strings from a Go binary. On success, vers should be non-empty. mod
+// is empty if the binary was not built with modules enabled.
+func readRawBuildInfo(r io.ReaderAt) (vers, mod string, err error) {
+       // Read the first bytes of the file to identify the format, then delegate to
+       // a format-specific function to load segment and section headers.
+       ident := make([]byte, 16)
+       if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil {
+               return "", "", errUnrecognizedFormat
+       }
+
+       var x exe
+       switch {
+       case bytes.HasPrefix(ident, []byte("\x7FELF")):
+               f, err := elf.NewFile(r)
+               if err != nil {
+                       return "", "", errUnrecognizedFormat
+               }
+               x = &elfExe{f}
+       case bytes.HasPrefix(ident, []byte("MZ")):
+               f, err := pe.NewFile(r)
+               if err != nil {
+                       return "", "", errUnrecognizedFormat
+               }
+               x = &peExe{f}
+       case bytes.HasPrefix(ident, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(ident[1:], []byte("\xFA\xED\xFE")):
+               f, err := macho.NewFile(r)
+               if err != nil {
+                       return "", "", errUnrecognizedFormat
+               }
+               x = &machoExe{f}
+       case bytes.HasPrefix(ident, []byte{0x01, 0xDF}) || bytes.HasPrefix(ident, []byte{0x01, 0xF7}):
+               f, err := xcoff.NewFile(r)
+               if err != nil {
+                       return "", "", errUnrecognizedFormat
+               }
+               x = &xcoffExe{f}
+       default:
+               return "", "", errUnrecognizedFormat
+       }
+
+       // Read the first 64kB of dataAddr to find the build info blob.
+       // On some platforms, the blob will be in its own section, and DataStart
+       // returns the address of that section. On others, it's somewhere in the
+       // data segment; the linker puts it near the beginning.
+       // See cmd/link/internal/ld.Link.buildinfo.
+       dataAddr := x.DataStart()
+       data, err := x.ReadData(dataAddr, 64*1024)
+       if err != nil {
+               return "", "", err
+       }
+       const (
+               buildInfoAlign = 16
+               buildinfoSize  = 32
+       )
+       for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[buildInfoAlign:] {
+               if len(data) < 32 {
+                       return "", "", errNotGoExe
+               }
+       }
+
+       // Decode the blob.
+       // The first 14 bytes are buildInfoMagic.
+       // The next two bytes indicate pointer size in bytes (4 or 8) and endianness
+       // (0 for little, 1 for big).
+       // Two virtual addresses to Go strings follow that: runtime.buildVersion,
+       // and runtime.modinfo.
+       // On 32-bit platforms, the last 8 bytes are unused.
+       ptrSize := int(data[14])
+       bigEndian := data[15] != 0
+       var bo binary.ByteOrder
+       if bigEndian {
+               bo = binary.BigEndian
+       } else {
+               bo = binary.LittleEndian
+       }
+       var readPtr func([]byte) uint64
+       if ptrSize == 4 {
+               readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }
+       } else {
+               readPtr = bo.Uint64
+       }
+       vers = readString(x, ptrSize, readPtr, readPtr(data[16:]))
+       if vers == "" {
+               return "", "", errNotGoExe
+       }
+       mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))
+       if len(mod) >= 33 && mod[len(mod)-17] == '\n' {
+               // Strip module framing: sentinel strings delimiting the module info.
+               // These are cmd/go/internal/modload.infoStart and infoEnd.
+               mod = mod[16 : len(mod)-16]
+       } else {
+               mod = ""
+       }
+
+       return vers, mod, nil
+}
+
+// readString returns the string at address addr in the executable x.
+func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string {
+       hdr, err := x.ReadData(addr, uint64(2*ptrSize))
+       if err != nil || len(hdr) < 2*ptrSize {
+               return ""
+       }
+       dataAddr := readPtr(hdr)
+       dataLen := readPtr(hdr[ptrSize:])
+       data, err := x.ReadData(dataAddr, dataLen)
+       if err != nil || uint64(len(data)) < dataLen {
+               return ""
+       }
+       return string(data)
+}
+
+// elfExe is the ELF implementation of the exe interface.
+type elfExe struct {
+       f *elf.File
+}
+
+func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) {
+       for _, prog := range x.f.Progs {
+               if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 {
+                       n := prog.Vaddr + prog.Filesz - addr
+                       if n > size {
+                               n = size
+                       }
+                       data := make([]byte, n)
+                       _, err := prog.ReadAt(data, int64(addr-prog.Vaddr))
+                       if err != nil {
+                               return nil, err
+                       }
+                       return data, nil
+               }
+       }
+       return nil, errUnrecognizedFormat
+}
+
+func (x *elfExe) DataStart() uint64 {
+       for _, s := range x.f.Sections {
+               if s.Name == ".go.buildinfo" {
+                       return s.Addr
+               }
+       }
+       for _, p := range x.f.Progs {
+               if p.Type == elf.PT_LOAD && p.Flags&(elf.PF_X|elf.PF_W) == elf.PF_W {
+                       return p.Vaddr
+               }
+       }
+       return 0
+}
+
+// peExe is the PE (Windows Portable Executable) implementation of the exe interface.
+type peExe struct {
+       f *pe.File
+}
+
+func (x *peExe) imageBase() uint64 {
+       switch oh := x.f.OptionalHeader.(type) {
+       case *pe.OptionalHeader32:
+               return uint64(oh.ImageBase)
+       case *pe.OptionalHeader64:
+               return oh.ImageBase
+       }
+       return 0
+}
+
+func (x *peExe) ReadData(addr, size uint64) ([]byte, error) {
+       addr -= x.imageBase()
+       for _, sect := range x.f.Sections {
+               if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) {
+                       n := uint64(sect.VirtualAddress+sect.Size) - addr
+                       if n > size {
+                               n = size
+                       }
+                       data := make([]byte, n)
+                       _, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress)))
+                       if err != nil {
+                               return nil, errUnrecognizedFormat
+                       }
+                       return data, nil
+               }
+       }
+       return nil, errUnrecognizedFormat
+}
+
+func (x *peExe) DataStart() uint64 {
+       // Assume data is first writable section.
+       const (
+               IMAGE_SCN_CNT_CODE               = 0x00000020
+               IMAGE_SCN_CNT_INITIALIZED_DATA   = 0x00000040
+               IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080
+               IMAGE_SCN_MEM_EXECUTE            = 0x20000000
+               IMAGE_SCN_MEM_READ               = 0x40000000
+               IMAGE_SCN_MEM_WRITE              = 0x80000000
+               IMAGE_SCN_MEM_DISCARDABLE        = 0x2000000
+               IMAGE_SCN_LNK_NRELOC_OVFL        = 0x1000000
+               IMAGE_SCN_ALIGN_32BYTES          = 0x600000
+       )
+       for _, sect := range x.f.Sections {
+               if sect.VirtualAddress != 0 && sect.Size != 0 &&
+                       sect.Characteristics&^IMAGE_SCN_ALIGN_32BYTES == IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE {
+                       return uint64(sect.VirtualAddress) + x.imageBase()
+               }
+       }
+       return 0
+}
+
+// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface.
+type machoExe struct {
+       f *macho.File
+}
+
+func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) {
+       for _, load := range x.f.Loads {
+               seg, ok := load.(*macho.Segment)
+               if !ok {
+                       continue
+               }
+               if seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 {
+                       if seg.Name == "__PAGEZERO" {
+                               continue
+                       }
+                       n := seg.Addr + seg.Filesz - addr
+                       if n > size {
+                               n = size
+                       }
+                       data := make([]byte, n)
+                       _, err := seg.ReadAt(data, int64(addr-seg.Addr))
+                       if err != nil {
+                               return nil, err
+                       }
+                       return data, nil
+               }
+       }
+       return nil, errUnrecognizedFormat
+}
+
+func (x *machoExe) DataStart() uint64 {
+       // Look for section named "__go_buildinfo".
+       for _, sec := range x.f.Sections {
+               if sec.Name == "__go_buildinfo" {
+                       return sec.Addr
+               }
+       }
+       // Try the first non-empty writable segment.
+       const RW = 3
+       for _, load := range x.f.Loads {
+               seg, ok := load.(*macho.Segment)
+               if ok && seg.Addr != 0 && seg.Filesz != 0 && seg.Prot == RW && seg.Maxprot == RW {
+                       return seg.Addr
+               }
+       }
+       return 0
+}
+
+// xcoffExe is the XCOFF (AIX eXtended COFF) implementation of the exe interface.
+type xcoffExe struct {
+       f *xcoff.File
+}
+
+func (x *xcoffExe) ReadData(addr, size uint64) ([]byte, error) {
+       for _, sect := range x.f.Sections {
+               if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) {
+                       n := uint64(sect.VirtualAddress+sect.Size) - addr
+                       if n > size {
+                               n = size
+                       }
+                       data := make([]byte, n)
+                       _, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress)))
+                       if err != nil {
+                               return nil, err
+                       }
+                       return data, nil
+               }
+       }
+       return nil, fmt.Errorf("address not mapped")
+}
+
+func (x *xcoffExe) DataStart() uint64 {
+       return x.f.SectionByType(xcoff.STYP_DATA).VirtualAddress
+}
diff --git a/src/debug/buildinfo/buildinfo_test.go b/src/debug/buildinfo/buildinfo_test.go
new file mode 100644 (file)
index 0000000..765bf24
--- /dev/null
@@ -0,0 +1,206 @@
+// Copyright 2021 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 buildinfo_test
+
+import (
+       "bytes"
+       "debug/buildinfo"
+       "flag"
+       "internal/testenv"
+       "os"
+       "os/exec"
+       "path"
+       "path/filepath"
+       "runtime"
+       "strings"
+       "testing"
+)
+
+var flagAll = flag.Bool("all", false, "test all supported GOOS/GOARCH platforms, instead of only the current platform")
+
+// TestReadFile confirms that ReadFile can read build information from binaries
+// on supported target platforms. It builds a trivial binary on the current
+// platforms (or all platforms if -all is set) in various configurations and
+// checks that build information can or cannot be read.
+func TestReadFile(t *testing.T) {
+       if testing.Short() {
+               t.Skip("test requires compiling and linking, which may be slow")
+       }
+       testenv.MustHaveGoBuild(t)
+
+       type platform struct{ goos, goarch string }
+       platforms := []platform{
+               {"aix", "ppc64"},
+               {"darwin", "amd64"},
+               {"darwin", "arm64"},
+               {"linux", "386"},
+               {"linux", "amd64"},
+               {"windows", "386"},
+               {"windows", "amd64"},
+       }
+       runtimePlatform := platform{runtime.GOOS, runtime.GOARCH}
+       haveRuntimePlatform := false
+       for _, p := range platforms {
+               if p == runtimePlatform {
+                       haveRuntimePlatform = true
+                       break
+               }
+       }
+       if !haveRuntimePlatform {
+               platforms = append(platforms, runtimePlatform)
+       }
+
+       buildWithModules := func(t *testing.T, goos, goarch string) string {
+               dir := t.TempDir()
+               gomodPath := filepath.Join(dir, "go.mod")
+               gomodData := []byte("module example.com/m\ngo 1.18\n")
+               if err := os.WriteFile(gomodPath, gomodData, 0666); err != nil {
+                       t.Fatal(err)
+               }
+               helloPath := filepath.Join(dir, "hello.go")
+               helloData := []byte("package main\nfunc main() {}\n")
+               if err := os.WriteFile(helloPath, helloData, 0666); err != nil {
+                       t.Fatal(err)
+               }
+               outPath := filepath.Join(dir, path.Base(t.Name()))
+               cmd := exec.Command("go", "build", "-o="+outPath)
+               cmd.Dir = dir
+               cmd.Env = append(os.Environ(), "GO111MODULE=on", "GOOS="+goos, "GOARCH="+goarch)
+               stderr := &bytes.Buffer{}
+               cmd.Stderr = stderr
+               if err := cmd.Run(); err != nil {
+                       t.Fatalf("failed building test file: %v\n%s", err, stderr.Bytes())
+               }
+               return outPath
+       }
+
+       buildWithGOPATH := func(t *testing.T, goos, goarch string) string {
+               gopathDir := t.TempDir()
+               pkgDir := filepath.Join(gopathDir, "src/example.com/m")
+               if err := os.MkdirAll(pkgDir, 0777); err != nil {
+                       t.Fatal(err)
+               }
+               helloPath := filepath.Join(pkgDir, "hello.go")
+               helloData := []byte("package main\nfunc main() {}\n")
+               if err := os.WriteFile(helloPath, helloData, 0666); err != nil {
+                       t.Fatal(err)
+               }
+               outPath := filepath.Join(gopathDir, path.Base(t.Name()))
+               cmd := exec.Command("go", "build", "-o="+outPath)
+               cmd.Dir = pkgDir
+               cmd.Env = append(os.Environ(), "GO111MODULE=off", "GOPATH="+gopathDir, "GOOS="+goos, "GOARCH="+goarch)
+               stderr := &bytes.Buffer{}
+               cmd.Stderr = stderr
+               if err := cmd.Run(); err != nil {
+                       t.Fatalf("failed building test file: %v\n%s", err, stderr.Bytes())
+               }
+               return outPath
+       }
+
+       damageBuildInfo := func(t *testing.T, name string) {
+               data, err := os.ReadFile(name)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               i := bytes.Index(data, []byte("\xff Go buildinf:"))
+               if i < 0 {
+                       t.Fatal("Go buildinf not found")
+               }
+               data[i+2] = 'N'
+               if err := os.WriteFile(name, data, 0666); err != nil {
+                       t.Fatal(err)
+               }
+       }
+
+       cases := []struct {
+               name    string
+               build   func(t *testing.T, goos, goarch string) string
+               want    string
+               wantErr string
+       }{
+               {
+                       name: "doesnotexist",
+                       build: func(t *testing.T, goos, goarch string) string {
+                               return "doesnotexist.txt"
+                       },
+                       wantErr: "doesnotexist",
+               },
+               {
+                       name: "empty",
+                       build: func(t *testing.T, _, _ string) string {
+                               dir := t.TempDir()
+                               name := filepath.Join(dir, "empty")
+                               if err := os.WriteFile(name, nil, 0666); err != nil {
+                                       t.Fatal(err)
+                               }
+                               return name
+                       },
+                       wantErr: "unrecognized file format",
+               },
+               {
+                       name:  "valid_modules",
+                       build: buildWithModules,
+                       want: "path\texample.com/m\n" +
+                               "mod\texample.com/m\t(devel)\t\n",
+               },
+               {
+                       name: "invalid_modules",
+                       build: func(t *testing.T, goos, goarch string) string {
+                               name := buildWithModules(t, goos, goarch)
+                               damageBuildInfo(t, name)
+                               return name
+                       },
+                       wantErr: "not a Go executable",
+               },
+               {
+                       name:  "valid_gopath",
+                       build: buildWithGOPATH,
+                       want:  "",
+               },
+               {
+                       name: "invalid_gopath",
+                       build: func(t *testing.T, goos, goarch string) string {
+                               name := buildWithGOPATH(t, goos, goarch)
+                               damageBuildInfo(t, name)
+                               return name
+                       },
+                       wantErr: "not a Go executable",
+               },
+       }
+
+       for _, p := range platforms {
+               p := p
+               t.Run(p.goos+"_"+p.goarch, func(t *testing.T) {
+                       if p != runtimePlatform && !*flagAll {
+                               t.Skipf("skipping platforms other than %s_%s because -all was not set", runtimePlatform.goos, runtimePlatform.goarch)
+                       }
+                       for _, tc := range cases {
+                               tc := tc
+                               t.Run(tc.name, func(t *testing.T) {
+                                       t.Parallel()
+                                       name := tc.build(t, p.goos, p.goarch)
+                                       if info, err := buildinfo.ReadFile(name); err != nil {
+                                               if tc.wantErr == "" {
+                                                       t.Fatalf("unexpected error: %v", err)
+                                               } else if errMsg := err.Error(); !strings.Contains(errMsg, tc.wantErr) {
+                                                       t.Fatalf("got error %q; want error containing %q", errMsg, tc.wantErr)
+                                               }
+                                       } else {
+                                               if tc.wantErr != "" {
+                                                       t.Fatalf("unexpected success; want error containing %q", tc.wantErr)
+                                               } else if got, err := info.MarshalText(); err != nil {
+                                                       t.Fatalf("unexpected error marshaling BuildInfo: %v", err)
+                                               } else {
+                                                       got := string(got)
+                                                       if got != tc.want {
+                                                               t.Fatalf("got:\n%s\nwant:\n%s", got, tc.want)
+                                                       }
+                                               }
+                                       }
+                               })
+                       }
+               })
+       }
+}
index 07fbc8b023859be8d9996a5fec4226304d71396c..a92bb3893b07453b367b9cd8dd70a509c88537e6 100644 (file)
@@ -214,7 +214,6 @@ var depsRules = `
          mime/quotedprintable,
          net/internal/socktest,
          net/url,
-         runtime/debug,
          runtime/trace,
          text/scanner,
          text/tabwriter;
@@ -271,8 +270,10 @@ var depsRules = `
 
        # executable parsing
        FMT, encoding/binary, compress/zlib
+       < runtime/debug
        < debug/dwarf
        < debug/elf, debug/gosym, debug/macho, debug/pe, debug/plan9obj, internal/xcoff
+       < debug/buildinfo
        < DEBUG;
 
        # go parser and friends.
@@ -510,7 +511,7 @@ var depsRules = `
        FMT, flag, math/rand
        < testing/quick;
 
-       FMT, flag, runtime/debug, runtime/trace, internal/sysinfo, math/rand
+       FMT, DEBUG, flag, runtime/trace, internal/sysinfo, math/rand
        < testing;
 
        FMT, crypto/sha256, encoding/json, go/ast, go/parser, go/token, math/rand, encoding/hex, crypto/sha256
index 11f995ba758f2888d0cbf7b7cc2c9c2bdd4a93d9..8c6c48089b4091151d28cc990723b0cfb7820c33 100644 (file)
@@ -7,7 +7,6 @@ package debug
 import (
        "bytes"
        "fmt"
-       "strings"
 )
 
 // exported from runtime
@@ -17,11 +16,19 @@ func modinfo() string
 // in the running binary. The information is available only
 // in binaries built with module support.
 func ReadBuildInfo() (info *BuildInfo, ok bool) {
-       return readBuildInfo(modinfo())
+       data := modinfo()
+       if len(data) < 32 {
+               return nil, false
+       }
+       data = data[16 : len(data)-16]
+       bi := &BuildInfo{}
+       if err := bi.UnmarshalText([]byte(data)); err != nil {
+               return nil, false
+       }
+       return bi, true
 }
 
-// BuildInfo represents the build information read from
-// the running binary.
+// BuildInfo represents the build information read from a Go binary.
 type BuildInfo struct {
        Path string    // The main package path
        Main Module    // The module containing the main package
@@ -71,80 +78,85 @@ func (bi *BuildInfo) MarshalText() ([]byte, error) {
        return buf.Bytes(), nil
 }
 
-func readBuildInfo(data string) (*BuildInfo, bool) {
-       if len(data) < 32 {
-               return nil, false
-       }
-       data = data[16 : len(data)-16]
+func (bi *BuildInfo) UnmarshalText(data []byte) (err error) {
+       *bi = BuildInfo{}
+       lineNum := 1
+       defer func() {
+               if err != nil {
+                       err = fmt.Errorf("could not parse Go build info: line %d: %w", lineNum, err)
+               }
+       }()
 
-       const (
-               pathLine = "path\t"
-               modLine  = "mod\t"
-               depLine  = "dep\t"
-               repLine  = "=>\t"
+       var (
+               pathLine = []byte("path\t")
+               modLine  = []byte("mod\t")
+               depLine  = []byte("dep\t")
+               repLine  = []byte("=>\t")
+               newline  = []byte("\n")
+               tab      = []byte("\t")
        )
 
-       readEntryFirstLine := func(elem []string) (Module, bool) {
+       readModuleLine := func(elem [][]byte) (Module, error) {
                if len(elem) != 2 && len(elem) != 3 {
-                       return Module{}, false
+                       return Module{}, fmt.Errorf("expected 2 or 3 columns; got %d", len(elem))
                }
                sum := ""
                if len(elem) == 3 {
-                       sum = elem[2]
+                       sum = string(elem[2])
                }
                return Module{
-                       Path:    elem[0],
-                       Version: elem[1],
+                       Path:    string(elem[0]),
+                       Version: string(elem[1]),
                        Sum:     sum,
-               }, true
+               }, nil
        }
 
        var (
-               info = &BuildInfo{}
                last *Module
-               line string
+               line []byte
                ok   bool
        )
-       // Reverse of cmd/go/internal/modload.PackageBuildInfo
+       // Reverse of BuildInfo.String()
        for len(data) > 0 {
-               line, data, ok = strings.Cut(data, "\n")
+               line, data, ok = bytes.Cut(data, newline)
                if !ok {
                        break
                }
                switch {
-               case strings.HasPrefix(line, pathLine):
+               case bytes.HasPrefix(line, pathLine):
                        elem := line[len(pathLine):]
-                       info.Path = elem
-               case strings.HasPrefix(line, modLine):
-                       elem := strings.Split(line[len(modLine):], "\t")
-                       last = &info.Main
-                       *last, ok = readEntryFirstLine(elem)
-                       if !ok {
-                               return nil, false
+                       bi.Path = string(elem)
+               case bytes.HasPrefix(line, modLine):
+                       elem := bytes.Split(line[len(modLine):], tab)
+                       last = &bi.Main
+                       *last, err = readModuleLine(elem)
+                       if err != nil {
+                               return err
                        }
-               case strings.HasPrefix(line, depLine):
-                       elem := strings.Split(line[len(depLine):], "\t")
+               case bytes.HasPrefix(line, depLine):
+                       elem := bytes.Split(line[len(depLine):], tab)
                        last = new(Module)
-                       info.Deps = append(info.Deps, last)
-                       *last, ok = readEntryFirstLine(elem)
-                       if !ok {
-                               return nil, false
+                       bi.Deps = append(bi.Deps, last)
+                       *last, err = readModuleLine(elem)
+                       if err != nil {
+                               return err
                        }
-               case strings.HasPrefix(line, repLine):
-                       elem := strings.Split(line[len(repLine):], "\t")
+               case bytes.HasPrefix(line, repLine):
+                       elem := bytes.Split(line[len(repLine):], tab)
                        if len(elem) != 3 {
-                               return nil, false
+                               return fmt.Errorf("expected 3 columns for replacement; got %d", len(elem))
                        }
                        if last == nil {
-                               return nil, false
+                               return fmt.Errorf("replacement with no module on previous line")
                        }
                        last.Replace = &Module{
-                               Path:    elem[0],
-                               Version: elem[1],
-                               Sum:     elem[2],
+                               Path:    string(elem[0]),
+                               Version: string(elem[1]),
+                               Sum:     string(elem[2]),
                        }
                        last = nil
                }
+               lineNum++
        }
-       return info, true
+       return nil
 }