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 }