diff --git a/api/next.txt b/api/next.txt index cb729ea72f..0a976d7b19 100644 --- a/api/next.txt +++ b/api/next.txt @@ -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 index 0000000000..8def2eae6e --- /dev/null +++ b/src/debug/buildinfo/buildinfo.go @@ -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 index 0000000000..765bf24627 --- /dev/null +++ b/src/debug/buildinfo/buildinfo_test.go @@ -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) + } + } + } + }) + } + }) + } +} diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index 07fbc8b023..a92bb3893b 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -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 diff --git a/src/runtime/debug/mod.go b/src/runtime/debug/mod.go index 11f995ba75..8c6c48089b 100644 --- a/src/runtime/debug/mod.go +++ b/src/runtime/debug/mod.go @@ -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 }