debug/buildinfo: new package with Read and ReadFile

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>
This commit is contained in:
Jay Conrod 2021-09-30 10:25:49 -07:00
parent 765c9116be
commit 434cdd0337
5 changed files with 645 additions and 48 deletions

View 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

View File

@ -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
}

View File

@ -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)
}
}
}
})
}
})
}
}

View 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

View 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
}