src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

index.go (9003B)


      1 // Copyright 2024 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package modindex
      6 
      7 import (
      8 	"bufio"
      9 	"crypto/sha256"
     10 	"encoding/csv"
     11 	"fmt"
     12 	"io"
     13 	"os"
     14 	"path/filepath"
     15 	"strconv"
     16 	"strings"
     17 	"testing"
     18 	"time"
     19 )
     20 
     21 /*
     22 The on-disk index ("payload") is a text file.
     23 The first 3 lines are header information containing CurrentVersion,
     24 the value of GOMODCACHE, and the validity date of the index.
     25 (This is when the code started building the index.)
     26 Following the header are sections of lines, one section for each
     27 import path. These sections are sorted by package name.
     28 The first line of each section, marked by a leading :, contains
     29 the package name, the import path, the name of the directory relative
     30 to GOMODCACHE, and its semantic version.
     31 The rest of each section consists of one line per exported symbol.
     32 The lines are sorted by the symbol's name and contain the name,
     33 an indication of its lexical type (C, T, V, F), and if it is the
     34 name of a function, information about the signature.
     35 
     36 The fields in the section header lines are separated by commas, and
     37 in the unlikely event this would be confusing, the csv package is used
     38 to write (and read) them.
     39 
     40 In the lines containing exported names, C=const, V=var, T=type, F=func.
     41 If it is a func, the next field is the number of returned values,
     42 followed by pairs consisting of formal parameter names and types.
     43 All these fields are separated by spaces. Any spaces in a type
     44 (e.g., chan struct{}) are replaced by $s on the disk. The $s are
     45 turned back into spaces when read.
     46 
     47 Here is an index header (the comments are not part of the index):
     48 0                                      // version (of the index format)
     49 /usr/local/google/home/pjw/go/pkg/mod  // GOMODCACHE
     50 2024-09-11 18:55:09                    // validity date of the index
     51 
     52 Here is an index section:
     53 :yaml,gopkg.in/yaml.v1,gopkg.in/yaml.v1@v1.0.0-20140924161607-9f9df34309c0,v1.0.0-20140924161607-9f9df34309c0
     54 Getter T
     55 Marshal F 2 in interface{}
     56 Setter T
     57 Unmarshal F 1 in []byte out interface{}
     58 
     59 The package name is yaml, the import path is gopkg.in/yaml.v1.
     60 Getter and Setter are types, and Marshal and Unmarshal are functions.
     61 The latter returns one value and has two arguments, 'in' and 'out'
     62 whose types are []byte and interface{}.
     63 */
     64 
     65 // CurrentVersion tells readers about the format of the index.
     66 const CurrentVersion int = 0
     67 
     68 // Index is returned by [Read].
     69 type Index struct {
     70 	Version    int
     71 	GOMODCACHE string    // absolute path of Go module cache dir
     72 	ValidAt    time.Time // moment at which the index was up to date
     73 	Entries    []Entry
     74 }
     75 
     76 func (ix *Index) String() string {
     77 	return fmt.Sprintf("Index(%s v%d has %d entries at %v)",
     78 		ix.GOMODCACHE, ix.Version, len(ix.Entries), ix.ValidAt)
     79 }
     80 
     81 // An Entry contains information for an import path.
     82 type Entry struct {
     83 	Dir        string // package directory relative to GOMODCACHE; uses OS path separator
     84 	ImportPath string
     85 	PkgName    string
     86 	Version    string
     87 	Names      []string // exported names and information
     88 }
     89 
     90 // IndexDir is where the module index is stored.
     91 // Each logical index entry consists of a pair of files:
     92 //
     93 //   - the "payload" (index-VERSION-XXX), whose name is
     94 //     randomized, holds the actual index; and
     95 //   - the "link" (index-name-VERSION-HASH),
     96 //     whose name is predictable, contains the
     97 //     name of the payload file.
     98 //
     99 // Since the link file is small (<512B),
    100 // reads and writes to it may be assumed atomic.
    101 var IndexDir string = func() string {
    102 	var dir string
    103 	if testing.Testing() {
    104 		dir = os.TempDir()
    105 	} else {
    106 		var err error
    107 		dir, err = os.UserCacheDir()
    108 		// shouldn't happen, but TempDir is better than
    109 		// creating ./goimports
    110 		if err != nil {
    111 			dir = os.TempDir()
    112 		}
    113 	}
    114 	dir = filepath.Join(dir, "goimports")
    115 	if err := os.MkdirAll(dir, 0777); err != nil {
    116 		dir = "" // #75505, people complain about the error message
    117 	}
    118 	return dir
    119 }()
    120 
    121 // Read reads the latest version of the on-disk index
    122 // for the specified Go module cache directory.
    123 // If there is no index, it returns a nil Index and an fs.ErrNotExist error.
    124 func Read(gomodcache string) (*Index, error) {
    125 	gomodcache, err := filepath.Abs(gomodcache)
    126 	if err != nil {
    127 		return nil, err
    128 	}
    129 	if IndexDir == "" {
    130 		return nil, os.ErrNotExist
    131 	}
    132 
    133 	// Read the "link" file for the specified gomodcache directory.
    134 	// It names the payload file.
    135 	content, err := os.ReadFile(filepath.Join(IndexDir, linkFileBasename(gomodcache)))
    136 	if err != nil {
    137 		return nil, err
    138 	}
    139 	payloadFile := filepath.Join(IndexDir, string(content))
    140 
    141 	// Read the index out of the payload file.
    142 	f, err := os.Open(payloadFile)
    143 	if err != nil {
    144 		return nil, err
    145 	}
    146 	defer f.Close()
    147 	return readIndexFrom(gomodcache, bufio.NewReader(f))
    148 }
    149 
    150 func readIndexFrom(gomodcache string, r io.Reader) (*Index, error) {
    151 	scan := bufio.NewScanner(r)
    152 
    153 	// version
    154 	if !scan.Scan() {
    155 		return nil, fmt.Errorf("unexpected scan error: %v", scan.Err())
    156 	}
    157 	version, err := strconv.Atoi(scan.Text())
    158 	if err != nil {
    159 		return nil, err
    160 	}
    161 	if version != CurrentVersion {
    162 		return nil, fmt.Errorf("got version %d, expected %d", version, CurrentVersion)
    163 	}
    164 
    165 	// gomodcache
    166 	if !scan.Scan() {
    167 		return nil, fmt.Errorf("scanner error reading module cache dir: %v", scan.Err())
    168 	}
    169 	// TODO(pjw): need to check that this is the expected cache dir
    170 	// so the tag should be passed in to this function
    171 	if dir := string(scan.Text()); dir != gomodcache {
    172 		return nil, fmt.Errorf("index file GOMODCACHE mismatch: got %q, want %q", dir, gomodcache)
    173 	}
    174 
    175 	// changed
    176 	if !scan.Scan() {
    177 		return nil, fmt.Errorf("scanner error reading index creation time: %v", scan.Err())
    178 	}
    179 	changed, err := time.ParseInLocation(time.DateTime, scan.Text(), time.Local)
    180 	if err != nil {
    181 		return nil, err
    182 	}
    183 
    184 	// entries
    185 	var (
    186 		curEntry *Entry
    187 		entries  []Entry
    188 	)
    189 	for scan.Scan() {
    190 		v := scan.Text()
    191 		if len(v) < 2 {
    192 			return nil, fmt.Errorf("malformed line: %q, %d entries", v, len(entries))
    193 		}
    194 		if v[0] == ':' {
    195 			if curEntry != nil {
    196 				entries = append(entries, *curEntry)
    197 			}
    198 			// as directories may contain commas and quotes, they need to be read as csv.
    199 			rdr := strings.NewReader(v[1:])
    200 			cs := csv.NewReader(rdr)
    201 			flds, err := cs.Read()
    202 			if err != nil {
    203 				return nil, err
    204 			}
    205 			if len(flds) != 4 {
    206 				return nil, fmt.Errorf("header contains %d fields, not 4: %q", len(v), v)
    207 			}
    208 			curEntry = &Entry{
    209 				PkgName:    flds[0],
    210 				ImportPath: flds[1],
    211 				Dir:        relative(gomodcache, flds[2]),
    212 				Version:    flds[3],
    213 			}
    214 			continue
    215 		}
    216 		curEntry.Names = append(curEntry.Names, v)
    217 	}
    218 	if err := scan.Err(); err != nil {
    219 		return nil, fmt.Errorf("scanner failed while reading modindex entry: %v", err)
    220 	}
    221 	if curEntry != nil {
    222 		entries = append(entries, *curEntry)
    223 	}
    224 
    225 	return &Index{
    226 		Version:    version,
    227 		GOMODCACHE: gomodcache,
    228 		ValidAt:    changed,
    229 		Entries:    entries,
    230 	}, nil
    231 }
    232 
    233 // write writes the index file and updates the index directory to refer to it.
    234 func write(gomodcache string, ix *Index) error {
    235 	if IndexDir == "" {
    236 		return os.ErrNotExist
    237 	}
    238 	// Write the index into a payload file with a fresh name.
    239 	f, err := os.CreateTemp(IndexDir, fmt.Sprintf("index-%d-*", CurrentVersion))
    240 	if err != nil {
    241 		return err // e.g. disk full, or index dir deleted
    242 	}
    243 	if err := writeIndexToFile(ix, bufio.NewWriter(f)); err != nil {
    244 		_ = f.Close() // ignore error
    245 		return err
    246 	}
    247 	if err := f.Close(); err != nil {
    248 		return err
    249 	}
    250 
    251 	// Write the name of the payload file into a link file.
    252 	indexDirFile := filepath.Join(IndexDir, linkFileBasename(gomodcache))
    253 	content := []byte(filepath.Base(f.Name()))
    254 	return os.WriteFile(indexDirFile, content, 0666)
    255 }
    256 
    257 func writeIndexToFile(x *Index, w *bufio.Writer) error {
    258 	fmt.Fprintf(w, "%d\n", x.Version)
    259 	fmt.Fprintf(w, "%s\n", x.GOMODCACHE)
    260 	tm := x.ValidAt.Truncate(time.Second) // round the time down
    261 	fmt.Fprintf(w, "%s\n", tm.Format(time.DateTime))
    262 	for _, e := range x.Entries {
    263 		if e.ImportPath == "" {
    264 			continue // shouldn't happen
    265 		}
    266 		// PJW: maybe always write these headers as csv?
    267 		if strings.ContainsAny(string(e.Dir), ",\"") {
    268 			cw := csv.NewWriter(w)
    269 			cw.Write([]string{":" + e.PkgName, e.ImportPath, string(e.Dir), e.Version})
    270 			cw.Flush()
    271 		} else {
    272 			fmt.Fprintf(w, ":%s,%s,%s,%s\n", e.PkgName, e.ImportPath, e.Dir, e.Version)
    273 		}
    274 		for _, x := range e.Names {
    275 			fmt.Fprintf(w, "%s\n", x)
    276 		}
    277 	}
    278 	return w.Flush()
    279 }
    280 
    281 // linkFileBasename returns the base name of the link file in the
    282 // index directory that holds the name of the payload file for the
    283 // specified (absolute) Go module cache dir.
    284 func linkFileBasename(gomodcache string) string {
    285 	// Note: coupled to logic in ./gomodindex/cmd.go. TODO: factor.
    286 	h := sha256.Sum256([]byte(gomodcache)) // collision-resistant hash
    287 	return fmt.Sprintf("index-name-%d-%032x", CurrentVersion, h)
    288 }
    289 
    290 func relative(base, file string) string {
    291 	if rel, err := filepath.Rel(base, file); err == nil {
    292 		return rel
    293 	}
    294 	return file
    295 }